doxa-maps/plugin-designs/key-cities/data-tables.vue
Doxa Maps 5285574259 Key Cities (gated): the map, list, tables and posters speak the reader's language
The last bundle of the L1 pass: key-cities had no i18n at all though its entry installs it. A new
keyCities area (38 keys, all 11 locales) carries the map title, band legend, caveats, the city
sheet's labels, the list's intro, filters and section lines, the data tables' title, intro, tabs and
loading line, and the poster wall's heading, tag, nouns and "coming soon" message. Unnamed places
keep showing coordinates, now inside a translated phrase. Counts use a "label: n" form outside English
and numbers keep their existing format (numerals held). cities-map's script message goes through a
guarded useI18n with the English catalog as fallback.
check:i18n-parity PASS; placeholders match English in every locale; the bundle builds.
A library catalog change: studio lands it (rule 5B).
needs eyes: key-cities (gated) with ?lang=ar: the map legend, a city sheet, and the list.

(cherry picked from commit 7d4da13721)
2026-09-26 03:34:30 +00:00

112 lines
5.4 KiB
Vue

<script setup>
/**
* data-tables.vue — the CITIES DATA list view: every research table, one tab each.
*
* Four validated Tabulator tables from the flight's research corpora, each with
* sortable/filterable columns, CSV+JSON export, and its caveats rendered IN the UI.
*
* > design note 2026-08-01: "all the tabulator data and trees for all the multiple
* > various tabulator tables for all this data all organized in list format with
* > full granularity."
*
* The tables are produced by library/scripts/research/build-tabulator-tables.mjs
* (the M+V of IEMV) — flat scalar rows, one row per fact, validated before write.
*
* WHY THE CAVEATS RENDER: a list view strips context. Someone reading
* "Internet 12.6%" needs to know it is a NATIONAL figure, and someone reading a
* network operator needs to know an ASN holder set includes banks as well as ISPs.
* DataTable surfaces any meta key describing a limit, so the qualification travels
* with the data instead of living in a file nobody opens.
*/
import { computed, inject, ref, watch } from 'vue'
import DataTable from '@map/components/tables/DataTable.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
const profileConfig = inject('profileConfig', ref(null))
const isDark = computed(() => profileConfig.value?.isDark === true)
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
const TABS = [
{ id: 'cities', label: 'Key Cities', file: 'key-cities' },
// PER-CITY languages: 30 clusters have a bridge language spoken RIGHT THERE
// that already carries a resource while the group's own language does not.
{ id: 'langcity', label: 'Languages Here', file: 'languages-per-city' },
{ id: 'languages', label: 'By Country', file: 'languages-by-country' },
{ id: 'operators', label: 'ISPs & Carriers', file: 'network-operators' },
{ id: 'messaging', label: 'Messaging Apps', file: 'messaging-channels' },
// Field-access/visa data is PRIVATE (kept outside the repo — release governance):
// publishing it could accelerate access closures. Tab removed from the
// public build; the dataset stays available for internal analysis.
]
const active = ref('cities')
const current = computed(() => TABS.find((t) => t.id === active.value) || TABS[0])
// TABLES ARE FETCHED AT RUNTIME, NOT IMPORTED. Static imports inlined 5.1MB of
// JSON into these bundles (2.0MB and 2.9MB) because the build is a single-file
// IIFE with inlineDynamicImports:true — every view then waits on one parse. Same
// trap that took the 100-list to 6MB. Fetching per tab keeps the bundle small AND
// means a user who never opens 'Social Platforms' never downloads it.
const cache = ref({})
const loading = ref(false)
const loadErr = ref(null)
async function ensure(file) {
if (cache.value[file]) return
loading.value = true; loadErr.value = null
try {
const r = await fetch(`${assetsBase.value}/tables/${file}.json`)
if (!r.ok) throw new Error(`table not found (${r.status})`)
cache.value = { ...cache.value, [file]: await r.json() }
} catch (e) { loadErr.value = String(e.message || e) }
finally { loading.value = false }
}
watch(current, (t) => t && ensure(t.file), { immediate: true })
const table = computed(() => cache.value[current.value?.file] || null)
useShadowStyles(
'.ct{position:absolute;inset:0;overflow-y:auto;padding:18px;background:#f6f7f9;color:#0f1419;' +
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}" +
'.ct.is-dark{background:#0a1410;color:#e5e7eb}' +
'.ct-h{margin:0 0 4px;font-size:18px;font-weight:700}' +
'.ct-sub{margin:0 0 14px;font-size:12.5px;opacity:.72;line-height:1.5}' +
'.ct-tabs{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}' +
'.ct-tab{font:600 12px inherit;padding:7px 14px;border-radius:999px;cursor:pointer;' +
'border:1px solid rgba(0,0,0,.14);background:transparent;color:inherit}' +
'.ct-tab.on{background:#10b981;border-color:#10b981;color:#04241a}' +
'.ct-load{padding:22px;text-align:center;font-size:12.5px;opacity:.6}' +
'.ct-err{padding:11px 13px;border-radius:8px;background:rgba(220,38,38,.10);color:#a12020;font-size:12.5px}' +
'.ct-n{font-size:10.5px;opacity:.6;margin-left:5px;font-variant-numeric:tabular-nums}',
'doxa-keycities-tables',
)
</script>
<template>
<div class="ct" :class="{ 'is-dark': isDark }">
<h2 class="ct-h">{{ $t('keyCities.tables.title') }}</h2>
<p class="ct-sub">
{{ $t('keyCities.tables.intro') }}
</p>
<div class="ct-tabs">
<button v-for="tab in TABS" :key="tab.id" type="button"
class="ct-tab" :class="{ on: active === tab.id }" @click="active = tab.id">
{{ $t('keyCities.tables.tabs.' + tab.id) }}<span class="ct-n" v-if="cache[tab.file]">{{ (cache[tab.file].rows || []).length.toLocaleString() }}</span>
</button>
</div>
<!-- :key forces a fresh Tabulator per tab rather than mutating one in place;
swapping both columns AND data on a live instance is where column state
(frozen, widths, filters) desynchronises from the new schema. -->
<div v-if="loadErr" class="ct-err">{{ loadErr }}</div>
<div v-else-if="!table" class="ct-load">{{ $t('keyCities.tables.loading', { name: $t('keyCities.tables.tabs.' + current.id) }) }}</div>
<DataTable
v-else
:key="current.id"
:table="table"
:assets-base="assetsBase"
:is-dark="isDark"
height="600px"
/>
</div>
</template>