diff --git a/app/pages/research/index.vue b/app/pages/research/index.vue index 284a9edc..becfe1f0 100644 --- a/app/pages/research/index.vue +++ b/app/pages/research/index.vue @@ -6,7 +6,7 @@ // (PHP equivalent: `get_query_var('uupg_search')`). // // Below the UupgsList we embed the 5-tab `research-map` IIFE built from -// Map-Framework/apps/DOXA-MAPS/doxa-research-mfe — exposes Engagement / Prayer +// the upstream DOXA-MAPS module — exposes Engagement / Prayer // / Adoption (clones of doxa-simple-map) plus Language Families. The // 1040-maps build now publishes /js/doxa-research-map.js (canonical name). // via the publishToDoxaLife() plugin. @@ -50,7 +50,7 @@ useTextHighlight() + the upstream module on every research-mfe build). --> { }) const legendHideTabs = computed(() => _isFlatTab.value) const legendColumnLabel = computed(() => { - if (activeLegendType.value === 'religion') return 'World Religions' + if (activeLegendType.value === 'religion') return 'UUPG Religions' if (activeLegendType.value === 'prayer') return 'Prayer Status' if (activeLegendType.value === 'engagement') return 'Engagement Status' if (activeLegendType.value === 'adoption') return 'Adoption Status' @@ -714,8 +714,48 @@ provide('getActivePinColor', (properties) => { const strat = getColorStrategy(activeTab.value?.colorStrategy ?? 'languageFamily') return strat?.getColor?.(properties) ?? '#1a1a1a' }) +// Card d1336834 — flat-tab nodes carry a placeholder ['==', ['get','_flat_filter'], key] +// filter that never matches real features, so we synthesize the real match +// expression for the camera here. Tree-tab nodes carry usable Mapbox filters +// already (fam:/lang:/dial: + region:/block:/country: + bloc:/cluster:/pg:/pgic:), +// so this returns null for them and zoomToLegendRow falls back to node.filter. +function _buildFlatLegendMatchExpr(node, legendType) { + if (!node || !legendType) return null + const key = node.id + if (legendType === 'prayer') { + const v = ['to-number', ['get', 'peoplePraying']] + if (key === 'noPrayer') return ['==', v, 0] + if (key === 'fullPrayer') return ['>=', v, FULL_PRAYER_THRESHOLD] + if (key === 'hasPrayer') return ['all', ['>', v, 0], ['<', v, FULL_PRAYER_THRESHOLD]] + } else if (legendType === 'engagement') { + if (key === 'hasEngagement') return ['==', ['get', 'engagementStatus'], true] + if (key === 'notEngaged') return ['==', ['get', 'engagementStatus'], false] + } else if (legendType === 'adoption') { + if (key === 'hasAdoption') return ['==', ['get', 'adoptionStatus'], true] + if (key === 'notAdopted') return ['==', ['get', 'adoptionStatus'], false] + } else if (legendType === 'religion') { + // expectedValue is a family letter (C/M/H/B/E/J/S/N/O/U). Match first char. + return ['==', ['slice', ['coalesce', ['get', 'religion'], ''], 0, 1], key] + } + return null +} function onSemanticTreeSelect(node) { const m = map.value + // Card d1336834 — uniform legend-row camera: any non-null row click moves + // the camera to fit its scope, derived from live pin coords. Flat-tab nodes + // need the real match expression synthesized (their node.filter is a + // _flat_filter placeholder); tree-tab nodes use node.filter directly. + if (node) { + const flatExpr = _isFlatTab.value + ? _buildFlatLegendMatchExpr(node, activeLegendType.value) + : null + // doxa-regions tab → fit polygon (sparse regions like Oceania should + // zoom TO the region, not OUT to span its few scattered pins). + const isRegionsTab = activeTab.value?.id === 'doxa-regions' + const flyOpts = flatExpr ? { matchExpr: flatExpr } : {} + if (isRegionsTab) flyOpts.regionPolygonSource = 'regions' + mapFly.zoomToLegendRow?.(node, Object.keys(flyOpts).length ? flyOpts : undefined) + } if (!node) { // Flat tabs: clear filter directly if (_isFlatTab.value) { @@ -782,8 +822,12 @@ function onSemanticTreeSelect(node) { if (m && m.getLayer('language-family-pins') && node.filter) { // Drop any prior setFilter from a previous tab interaction so all pins render. try { m.setFilter('language-family-pins', null) } catch (_) {} - _syncHitboxFilter(m, null) - // Dim via case expression — matched pins at 1.0, others at 0.06 (matches research-map's + // Mirror the match expression onto the invisible hitbox layer so + // dimmed (opacity-0) pins are not selectable. Without this the + // hitbox stays full-size for every pin and the user can tap an + // apparent empty area to select a hidden pin. + _syncHitboxFilter(m, node.filter) + // Dim via case expression — matched pins at 1.0, others at 0 (matches // applyDimFilter values). m.setPaintProperty('language-family-pins', 'circle-opacity', [ 'case', node.filter, 1, 0 @@ -1129,6 +1173,18 @@ function clearAllHighlights(m) { if (mapStore.selectedFamily) mapStore.selectFamily(null) if (mapStore.selectedLanguage) mapStore.selectLanguage(null) if (mapStore.selectedDialect) mapStore.selectDialect(null) + // Flat-tab rows (prayer/engagement/adoption/religion) and tree rows are + // tracked separately on pplrInstance.selection — clearing the uiStore + // filters above does NOT cascade into pplrInstance, so without this the + // legend row stays visually selected while the map filter clears, leaving + // the UI inconsistent. Reset the legend-row selection directly so a clear + // anywhere (toggle-off, empty-map tap, programmatic) leaves both the map + // and the legend in matching states. + try { + if (pplrInstance && pplrInstance.selection && pplrInstance.selection.value) { + pplrInstance.selection.value = null + } + } catch (_) { /* no-op */ } } // Same-tick dedupe — a single legend-row click fires applyDimFilter TWICE on @@ -1243,9 +1299,10 @@ function applyDimFilter(detail) { } } else { // Family / language / prayer / engagement / adoption: keep all pins - // rendered (no source-filter) but dim the non-matching ones. + // rendered on the visible pins layer (no source-filter so the bucket + // can still be styled / sorted), but dim the non-matching ones via a + // circle-opacity `case` expression. m.setFilter('language-family-pins', null) - _syncHitboxFilter(m, null) // Build the per-kind match expression. Prayer compares against a numeric // bucket (noPrayer=0 / hasPrayer=(0,FULL_PRAYER_THRESHOLD) / fullPrayer @@ -1322,6 +1379,12 @@ function applyDimFilter(detail) { 'case', matchExpr, 1, 0 ]) } + // Restrict the invisible hitbox layer to the matched bucket so dimmed + // (opacity-0) pins are no longer hit-testable. Without this the user + // can tap on apparent empty space and select a hidden pin, and the + // generic map-click handler sees a queryRenderedFeatures hit on the + // hidden pin and skips its own clear path inconsistently. + _syncHitboxFilter(m, matchExpr) mapLayers.syncGlowFilter(matchExpr) if (m.getLayer('regions-fill')) { m.setPaintProperty('regions-fill', 'fill-opacity', 0.20) @@ -1628,30 +1691,10 @@ async function onMapReady(normalizedPeopleGroups) { schedule(() => { void ensureRegionsLoaded() }) } - // Click-on-empty-map clears any active legend-row filter so dimmed pins - // restore (qa: 2026-05-02 iter-12 — user reported clicking a row dimmed - // non-matching pins as expected, but clicking off the row left the dim - // state stuck). Mapbox layer-specific handlers fire BEFORE this generic - // one, so we just check queryRenderedFeatures for the relevant interactive - // layers; if empty, the click landed on empty map. - map.value.on('click', (e) => { - const interactiveLayers = ['language-family-pins', 'regions-fill'] - .filter(id => map.value.getLayer(id)) - const hits = interactiveLayers.length - ? map.value.queryRenderedFeatures(e.point, { layers: interactiveLayers }) - : [] - if (hits.length) return - const hadFilter = - uiStore.prayerFilter || uiStore.engagementFilter || uiStore.adoptionFilter || uiStore.religionFilter || - mapStore.selectedFamily || mapStore.selectedLanguage || mapStore.selectedDialect || - mapStore.selectedRegion - if (!hadFilter) return - if (uiStore.prayerFilter) uiStore.setPrayerFilter?.(null) - if (uiStore.engagementFilter) uiStore.setEngagementFilter?.(null) - if (uiStore.adoptionFilter) uiStore.setAdoptionFilter?.(null) - if (uiStore.religionFilter) uiStore.setReligionFilter?.(null) - clearAllHighlights(map.value) - }) + // Empty-map-click intentionally does nothing. The legend selection is + // sticky — only the same row re-tapped or the legend close button clears + // it. (Spec change from earlier "click-off clears" behavior; users found + // the auto-clear surprising when they tapped the map just to inspect.) mapStore.setMapReady(mapId) appReady.value = true diff --git a/embeddables/micro-frontends/1040-maps/app-profiles/doxa-simple-map/profiles/doxa-simple-map.vue b/embeddables/micro-frontends/1040-maps/app-profiles/doxa-simple-map/profiles/doxa-simple-map.vue index afdcc833..93416c61 100644 --- a/embeddables/micro-frontends/1040-maps/app-profiles/doxa-simple-map/profiles/doxa-simple-map.vue +++ b/embeddables/micro-frontends/1040-maps/app-profiles/doxa-simple-map/profiles/doxa-simple-map.vue @@ -25,6 +25,7 @@ const LegendMobile = defineAsyncComponent(() => import('@map/components/LegendMo import SemanticTreeLegend from '@map/components/SemanticTreeLegend.vue' import { createPplrInstance, provideInstance } from '@map/composables/usePplrInstance.js' import { useLegendData as useFlatLegendData } from '@map/composables/useLegendData.js' +import { useMapFly } from '@map/composables/useMapFly.js' import SideMenuDrawer from '@map/components/SideMenuDrawer.vue' // ─── Map control components ─────────────────────────────────────────────────── // Each button is imported individually so the profile can compose any subset @@ -328,6 +329,15 @@ function onSemanticTreeSelect(node) { if (t === 'prayer') uiStore.setPrayerFilter(node.id) else if (t === 'engagement') uiStore.setEngagementFilter(node.id) else if (t === 'adoption') uiStore.setAdoptionFilter(node.id) + + // Card d1336834 — uniform legend-row camera. node.filter is a _flat_filter + // placeholder for simple-map's prayer/engagement/adoption tabs, so we + // synthesize the real match expression via buildMatchExpr (same one + // applyLegendFilter uses for the dim path). + const matchExpr = buildMatchExpr(t, node.id) + if (matchExpr) { + mapFly.zoomToLegendRow?.(node, { matchExpr }) + } } const mapContainer = ref(null) @@ -405,6 +415,15 @@ const mapLayers = useMapLayers({ getNormalizedPeopleGroups: () => mapData.normalizedPeopleGroups.value || [] }) +// ─── Fly composable (card d1336834 — uniform legend-row camera) ───────────── +const mapFly = useMapFly({ + getMap: () => map.value, + mapId, + mapStore, + defaultCenter: [20, 10], + defaultZoom: 1.8 +}) + // ─── GO marker (selected pin highlight) ─────────────────────────────────────── const selectedPin = useSelectedPin({ getMap: () => map.value }) @@ -721,6 +740,16 @@ function switchTab(tabId) { const ACTIVE_LAYER = 'language-family-pins-active' const PULSE_LAYER = 'language-family-pins-pulse' +const HITBOX_LAYER = 'language-family-pins-hitbox' + +// Mirror a Mapbox filter expression to the invisible hitbox layer so dimmed +// (low-opacity) pins are no longer hit-testable. Pass `null` to restore the +// full-dataset hitbox (every pin selectable again). +function _syncHitboxFilter(m, filter) { + if (m?.getLayer(HITBOX_LAYER)) { + try { m.setFilter(HITBOX_LAYER, filter) } catch (_) {} + } +} let _pulseRaf = null let _pulsePhase = 0 @@ -949,6 +978,7 @@ function applyLegendFilter(filterType, filterKey) { m.setPaintProperty('language-family-pins', 'circle-color', getBaseColorExpr()) m.setPaintProperty('language-family-pins', 'circle-opacity', 0.9) m.setPaintProperty('language-family-pins', 'circle-stroke-opacity', 1) + _syncHitboxFilter(m, null) mapLayers.syncGlowFilter(null) clearActiveLayer() return @@ -960,6 +990,7 @@ function applyLegendFilter(filterType, filterKey) { m.setPaintProperty('language-family-pins', 'circle-color', getBaseColorExpr()) m.setPaintProperty('language-family-pins', 'circle-opacity', 0.9) m.setPaintProperty('language-family-pins', 'circle-stroke-opacity', 1) + _syncHitboxFilter(m, null) mapLayers.syncGlowFilter(null) clearActiveLayer() return @@ -976,6 +1007,10 @@ function applyLegendFilter(filterType, filterKey) { ['case', matchExpr, 1, 0.06]) m.setPaintProperty('language-family-pins', 'circle-stroke-opacity', ['case', matchExpr, 1, 0.06]) + // Restrict the invisible hitbox layer to the matched bucket so dimmed + // (low-opacity) pins are no longer hit-testable. Without this the user + // can tap an apparent empty area to select a near-invisible pin. + _syncHitboxFilter(m, matchExpr) mapLayers.syncGlowFilter(matchExpr) return } @@ -1216,7 +1251,11 @@ onBeforeUnmount(() => { MapToolbar is a layout-only shell (positioned column + slot). Binding :map="map" here guarantees that in a multi-map profile these buttons operate ONLY on their own map instance. --> - + + diff --git a/embeddables/micro-frontends/1040-maps/build-status.md b/embeddables/micro-frontends/1040-maps/build-status.md deleted file mode 100644 index f986d8a0..00000000 --- a/embeddables/micro-frontends/1040-maps/build-status.md +++ /dev/null @@ -1,49 +0,0 @@ -BUILD-READY: 610deb7 - -## Changes - -### Feat: Redesign ShareButton for non-technical users - -**Commit:** 610deb7 -**Files:** -- src/components/map-controls/ShareButton.vue (complete rewrite) - -Complete UX redesign of the share popover modeled on YouTube, Spotify, Canva, -and Google Maps share patterns. Two-tab layout replaces the old single-panel: - -**Tab 1: Share Link (default)** -- Clean URL in a styled read-only display box -- Large "Copy Link" CTA button with clipboard copy + checkmark feedback -- Social share row: email (mailto) and WhatsApp share links - -**Tab 2: Add to Your Website** -- Friendly plain-English description (never uses the word "iframe") -- Small / Medium / Large size-preset pills (400/600/800px height) -- Styled code box that updates live when preset changes -- "Copy Code" button with same Copied! feedback - -**UX polish:** -- Pop-in animation on open -- Header with "Share" title and X close button -- Box-with-arrow-up share icon (iOS/Material standard) -- Full dark/light theme via useShadowStyles -- Mobile-responsive: wider popover below button - -**Build verified:** both doxa-research-map.js and doxa-simple-map.js compile. - -## Test plan -- Click share icon -- popover opens with "Share Link" tab active -- Tab switching: click "Add to Your Website" -- tab content changes smoothly -- Copy Link: click button -- clipboard gets URL, button shows checkmark + "Copied!" for 2s -- Email icon: opens mailto: link with map URL in body -- WhatsApp icon: opens wa.me share with map URL -- Switch to embed tab -- size pills default to "Medium" -- Click "Small" pill -- code box updates to height="400" -- Click "Large" pill -- code box updates to height="800" -- Copy Code: click button -- clipboard gets embed snippet, "Copied!" feedback -- Click outside popover -- closes -- Press Escape -- closes -- Re-click share button -- toggles closed -- Toggle dark/light theme -- all popover styles adapt -- Mobile viewport -- popover wider, positioned below button, no overflow -- Paste embed snippet into test HTML -- map renders at chosen size diff --git a/embeddables/micro-frontends/1040-maps/bun.lock b/embeddables/micro-frontends/1040-maps/bun.lock deleted file mode 100644 index acb6b9fa..00000000 --- a/embeddables/micro-frontends/1040-maps/bun.lock +++ /dev/null @@ -1,264 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "1040-maps", - "dependencies": { - "jspdf": "^4.2.1", - "pinia": "^2.3.1", - "topojson-client": "^3.1.0", - "vue": "^3.5.20", - "vue-i18n": "^9.14.5", - "world-atlas": "^2.0.2", - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.0.4", - "terser": "^5.46.0", - "vite": "^5.4.21", - "vite-plugin-css-injected-by-js": "^3.5.2", - }, - }, - }, - "packages": { - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - - "@intlify/core-base": ["@intlify/core-base@9.14.5", "", { "dependencies": { "@intlify/message-compiler": "9.14.5", "@intlify/shared": "9.14.5" } }, "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA=="], - - "@intlify/message-compiler": ["@intlify/message-compiler@9.14.5", "", { "dependencies": { "@intlify/shared": "9.14.5", "source-map-js": "^1.0.2" } }, "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ=="], - - "@intlify/shared": ["@intlify/shared@9.14.5", "", {}, "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], - - "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], - - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - - "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="], - - "@vue/compiler-core": ["@vue/compiler-core@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], - - "@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], - - "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.35", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.5.35", "@vue/compiler-dom": "3.5.35", "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw=="], - - "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.35", "", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw=="], - - "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], - - "@vue/reactivity": ["@vue/reactivity@3.5.35", "", { "dependencies": { "@vue/shared": "3.5.35" } }, "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ=="], - - "@vue/runtime-core": ["@vue/runtime-core@3.5.35", "", { "dependencies": { "@vue/reactivity": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg=="], - - "@vue/runtime-dom": ["@vue/runtime-dom@3.5.35", "", { "dependencies": { "@vue/reactivity": "3.5.35", "@vue/runtime-core": "3.5.35", "@vue/shared": "3.5.35", "csstype": "^3.2.3" } }, "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA=="], - - "@vue/server-renderer": ["@vue/server-renderer@3.5.35", "", { "dependencies": { "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "vue": "3.5.35" } }, "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA=="], - - "@vue/shared": ["@vue/shared@3.5.35", "", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], - - "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], - - "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "dompurify": ["dompurify@3.4.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="], - - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="], - - "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], - - "iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="], - - "jspdf": ["jspdf@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - - "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], - - "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "pinia": ["pinia@2.3.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug=="], - - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - - "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], - - "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - - "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], - - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="], - - "svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="], - - "terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="], - - "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], - - "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="], - - "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], - - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - - "vite-plugin-css-injected-by-js": ["vite-plugin-css-injected-by-js@3.5.2", "", { "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ=="], - - "vue": ["vue@3.5.35", "", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/compiler-sfc": "3.5.35", "@vue/runtime-dom": "3.5.35", "@vue/server-renderer": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q=="], - - "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], - - "vue-i18n": ["vue-i18n@9.14.5", "", { "dependencies": { "@intlify/core-base": "9.14.5", "@intlify/shared": "9.14.5", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g=="], - - "world-atlas": ["world-atlas@2.0.2", "", {}, "sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ=="], - } -} diff --git a/embeddables/micro-frontends/1040-maps/intel/zoom-out/2026-05-19-legend-is-the-app.md b/embeddables/micro-frontends/1040-maps/intel/zoom-out/2026-05-19-legend-is-the-app.md deleted file mode 100644 index cab59a44..00000000 --- a/embeddables/micro-frontends/1040-maps/intel/zoom-out/2026-05-19-legend-is-the-app.md +++ /dev/null @@ -1,36 +0,0 @@ -# Zoom-Out: The legend IS the app - -**Date:** 2026-05-19 -**Session:** 85e9db765e8dccad (coordinator, 1040-maps) -**Reframing question answered:** Why does every build wave end with legend fixes? - ---- - -## Core tension - -The 1040-maps legend is not a sidebar UI element — it is the primary interaction surface of every map, and it is being treated as a secondary concern that gets patched after each feature wave. - -## What the local work keeps doing about it - -Every build wave follows the same arc: land a major feature (research map port, modern styling, prayer ripple, geo-steward migration), then spend 5-15 iterative commits fixing legend behavior — height, collapse state, mobile parity, tab memory, caret alignment, scroll overflow, pill styling. The commit history on `maps` tells the story: "Research map iter-1" through "iter-21" are overwhelmingly legend fixes. The current session immediately surfaced three more legend issues from a single tablet test. The predecessor session's geo-steward migration landed 65 files in one night, but the *next* session's first QA pass is... legend height on tablet. - -## The reframe - -The legend is not a component that accompanies the map. It is the app's primary navigation, selection, filtering, and detail-display surface. On mobile/tablet it occupies 30-90% of viewport. It has a tri-state height machine, a mode switcher (prayer/detail), per-tab tier memory, collapsed-pill mode, semantic tree rendering, flat-row rendering, and a detail panel that swaps in-place. It is, by complexity and interaction surface area, a larger application than the Mapbox canvas it sits beside. - -Treating it as "the legend" — a secondary annotation layer — causes two downstream failures: - -1. **No acceptance criteria before build.** Features land without specifying legend behavior, so legend bugs are discovered post-hoc via QA rather than prevented by spec. -2. **No tablet/mobile-first design.** The legend is designed desktop-first and then reactively patched for smaller viewports, despite mobile/tablet being the primary consumption context for maps serving disciples in high-security countries. - -## What changes if we accept the reframe - -- **Legend behavior becomes a first-class section in every feature spec.** Before any builder spawns, the card must answer: what does the legend do when this feature activates? What height state? What mode? What happens on mobile? -- **Tablet is the reference viewport, not desktop.** The Driver just tested on tablet and found it "works pretty good" — that's the target device. Desktop is the secondary viewport. Design legend from tablet up, not desktop down. -- **The legend gets its own QA checklist.** A standing 5-item gate: (1) content-fit height, (2) no bottom underlap, (3) detail-mode collapses legend, (4) touch targets >= 44px, (5) collapsed pill accessible on all tabs. -- **The geo-steward migration (sitting unpushed in Map-Framework) must include legend integration spec before merging.** 65 files of backend + components without a legend spec = another round of post-hoc legend fixes. -- **Consider extracting legend into its own composable with explicit viewport contracts** rather than patching CSS per-map. The current approach (SemanticTreeLegend.vue at 838 lines + uiStore legend state machine) is one component doing too many jobs across too many viewport assumptions. - -## Reframing question for the next session - -If the legend is the app, should it have its own store, its own viewport-contract tests, and its own QA gate — or is the current "patch after each wave" cost acceptable given the shipping velocity it enables? diff --git a/embeddables/micro-frontends/1040-maps/src/components/LegendDesktop.vue b/embeddables/micro-frontends/1040-maps/src/components/LegendDesktop.vue index 63c0364b..20cf4375 100644 --- a/embeddables/micro-frontends/1040-maps/src/components/LegendDesktop.vue +++ b/embeddables/micro-frontends/1040-maps/src/components/LegendDesktop.vue @@ -62,9 +62,23 @@ useShadowStyles(` .legend-container:not(.legend-dark) .legend-collapse-caret:hover{background:rgba(59,70,61,0.12);border-color:#3b463d;color:#3b463d;} /* Detail-mode close button — top-right of the card */ -.detail-close-btn{position:absolute;top:10px;right:10px;background:none;border:none;padding:4px;cursor:pointer;color:#888;display:flex;align-items:center;justify-content:center;z-index:2;transition:color 0.2s ease;} +.detail-close-btn{position:absolute;top:10px;right:10px;background:none;border:none;padding:4px;cursor:pointer;color:#888;display:flex;align-items:center;justify-content:center;z-index:20;transition:color 0.2s ease;} .detail-close-btn:hover{color:#333;} +/* Detail-mode collapse caret — top-left of the card, mirrors the inline + .legend-collapse-caret used by the data-view title row. Sits at z-index + above the PeopleGroupDetail sticky header (.detail-header z-index:10) so + it is always tappable even when the detail panel scrolls. The button is + only rendered in the detail-mode template branch (see