Skip to content

Commit b09ef2e

Browse files
committed
feat: HTTP loader, embeddable widget, compare mode, API playground, status dashboard, use cases
Six high-impact surfaces built on top of the new HTTP loader strategy. All designed in the established scholarly-ink + brand-teal system. 1. HTTP loader (in interscript-ts#fix/megaregexp-parallel-99-percent) - New httpStrategy fetches map IR on demand from any URL - Two-tier cache: in-memory + optional localStorage (30-day TTL) - MapLoader now supports async strategies via loadAsync() - transliterateAsync() public API - Foundation for everything below 2. Embeddable widget (/embed) - Standalone page designed for iframe embedding on third-party sites - URL params: ?system=<code>&input=<text> - Custom element with Shadow DOM (no style leakage) - Uses httpStrategy — no map bundle, ~50KB initial payload - Verified: Антон → Anton end-to-end 3. Compare mode (/compare) - Killer demo for "why every authority matters" - Same input piped through 4-5 systems simultaneously - Presets for Cyrillic, Arabic, Greek, Hindi names - Live example: Щербакова surfaces as Shcherbakova (BGN/PCGN), Ŝerbakova (ISO 9), Ščerbakova (UN), etc. 4. API playground (/api) - Interactive try-it-now with system picker + input field - Generates live curl/JavaScript/Ruby snippets for the current call - Copy-to-clipboard for each - Embed widget preview iframe + install cards for npm/gem/CDN 5. Status dashboard (/status) - Public transparency: every number computed at build time - 6 KPIs: Ruby parity (100%), total systems, authorities, scripts, corpus size, last sync date - Ranked breakdowns by authority and source script (with bars) - Documents exactly how each number is reproducible 6. Use cases (/use-cases) - 6 real-world domains with worked examples: Library catalogs (MARC), Passports (ICAO 9303), Newsrooms, Academic citations, Geographic names (UNGEGN), Genealogy - Each links to the systems that commonly serve it - CTA back to /api for trying the playground Navigation: expanded primary nav from 7 to 10 items (added Compare, Use cases, API, Status). /blog still accessible but de-prioritized since it's mostly historical Opal-era content. Tests: 24 new tests in test/new-features.test.ts covering each new page + cross-page navigation + partner attribution. All 153 tests pass.
1 parent ffefd42 commit b09ef2e

12 files changed

Lines changed: 2209 additions & 18 deletions

File tree

src/components/CompareMode.vue

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
<script setup lang="ts">
2+
/**
3+
* CompareMode — show the same input through N transliteration systems
4+
* side-by-side. This is the killer demo for "why every authority
5+
* matters": the same name surfaces differently across BGN/PCGN, ISO,
6+
* UN, ALA-LC, ODNI.
7+
*
8+
* Uses httpStrategy so map IR loads on demand.
9+
*
10+
* Script display names are passed in as a prop (resolved at build time
11+
* from @iso24229/iso15924-data) so this component stays browser-safe.
12+
*/
13+
import { ref, computed, onMounted, watch } from "vue"
14+
15+
interface System {
16+
code: string
17+
authority: string
18+
note: string
19+
scriptName: string
20+
}
21+
22+
interface Preset {
23+
id: string
24+
label: string
25+
input: string
26+
systems: System[]
27+
}
28+
29+
interface Props {
30+
presets: Preset[]
31+
inputLabel: string
32+
}
33+
34+
const props = defineProps<Props>()
35+
36+
const presetId = ref(props.presets[0]?.id ?? "")
37+
const input = ref(props.presets[0]?.input ?? "")
38+
const outputs = ref<Record<string, string>>({})
39+
const errors = ref<Record<string, string>>({})
40+
const loading = ref(false)
41+
42+
const currentPreset = computed(() =>
43+
props.presets.find((p) => p.id === presetId.value) ?? props.presets[0]!,
44+
)
45+
46+
let transliterateFn: ((code: string, input: string) => Promise<string>) | null = null
47+
48+
async function ensureEngine() {
49+
if (transliterateFn) return
50+
const mod = await import("interscript-ts")
51+
mod.reset()
52+
mod.configure({
53+
strategies: [
54+
mod.httpStrategy({
55+
baseUrl: "/maps",
56+
cacheKeyPrefix: "isx-compare:",
57+
}),
58+
],
59+
})
60+
transliterateFn = mod.transliterateAsync
61+
}
62+
63+
async function run() {
64+
if (!transliterateFn) return
65+
loading.value = true
66+
outputs.value = {}
67+
errors.value = {}
68+
for (const sys of currentPreset.value.systems) {
69+
try {
70+
const result = await transliterateFn(sys.code, input.value)
71+
outputs.value = { ...outputs.value, [sys.code]: result }
72+
} catch (e) {
73+
errors.value = { ...errors.value, [sys.code]: (e as Error).message }
74+
}
75+
}
76+
loading.value = false
77+
}
78+
79+
function selectPreset(id: string) {
80+
presetId.value = id
81+
const preset = props.presets.find((p) => p.id === id)
82+
if (preset) input.value = preset.input
83+
}
84+
85+
onMounted(async () => {
86+
await ensureEngine()
87+
await run()
88+
})
89+
90+
watch([input, presetId], () => {
91+
void run()
92+
})
93+
</script>
94+
95+
<template>
96+
<div class="compare">
97+
<div class="compare-controls">
98+
<div class="preset-rail">
99+
<p class="rail-label">Try a preset</p>
100+
<div class="preset-row">
101+
<button
102+
v-for="preset in presets"
103+
:key="preset.id"
104+
:class="['preset-pill', { active: preset.id === presetId }]"
105+
@click="selectPreset(preset.id)"
106+
>
107+
{{ preset.label }}
108+
</button>
109+
</div>
110+
</div>
111+
112+
<div class="input-row">
113+
<label class="input-label" for="compare-input">
114+
{{ currentPreset.systems[0]?.scriptName ?? 'Source' }} input
115+
</label>
116+
<input
117+
id="compare-input"
118+
v-model="input"
119+
class="compare-input"
120+
type="text"
121+
spellcheck="false"
122+
:placeholder="'Type or edit…'"
123+
/>
124+
</div>
125+
</div>
126+
127+
<ul class="result-list">
128+
<li v-for="sys in currentPreset.systems" :key="sys.code" class="result-row">
129+
<div class="result-meta">
130+
<span class="result-authority">{{ sys.authority }}</span>
131+
<span class="result-note">{{ sys.note }}</span>
132+
<a class="result-link" :href="`/maps/${sys.code}`" title="View system detail">↗</a>
133+
</div>
134+
<div class="result-output" :class="{ error: errors[sys.code], loading: loading && !outputs[sys.code] && !errors[sys.code] }">
135+
<span v-if="errors[sys.code]" class="err-msg">⚠ {{ errors[sys.code] }}</span>
136+
<span v-else-if="loading && !outputs[sys.code]">Loading…</span>
137+
<span v-else>{{ outputs[sys.code] }}</span>
138+
</div>
139+
<code class="result-code">{{ sys.code }}</code>
140+
</li>
141+
</ul>
142+
143+
<p class="compare-deck">
144+
Same input, different romanization systems. Each authority publishes
145+
its own rules — Interscript encodes them as comparable, runnable maps
146+
so you can see the differences at a glance.
147+
</p>
148+
</div>
149+
</template>
150+
151+
<style scoped>
152+
.compare {
153+
display: grid;
154+
gap: 1.75rem;
155+
}
156+
157+
.compare-controls {
158+
display: grid;
159+
gap: 1.25rem;
160+
}
161+
162+
.preset-rail .rail-label {
163+
font-family: var(--font-mono);
164+
font-size: var(--text-micro);
165+
letter-spacing: 0.15em;
166+
text-transform: uppercase;
167+
color: var(--color-stone);
168+
margin: 0 0 0.625rem;
169+
}
170+
.preset-row {
171+
display: flex;
172+
flex-wrap: wrap;
173+
gap: 0.5rem;
174+
}
175+
.preset-pill {
176+
font-family: var(--font-sans);
177+
font-size: 0.875rem;
178+
padding: 0.45rem 0.95rem;
179+
border: 1.5px solid var(--color-rule);
180+
background: var(--color-vellum);
181+
color: var(--color-ink);
182+
border-radius: 1px;
183+
cursor: pointer;
184+
transition: all 0.15s ease;
185+
}
186+
.preset-pill:hover {
187+
border-color: var(--color-brand);
188+
}
189+
.preset-pill.active {
190+
background: var(--color-brand);
191+
border-color: var(--color-brand);
192+
color: var(--color-vellum);
193+
}
194+
195+
.input-row {
196+
display: grid;
197+
gap: 0.4rem;
198+
}
199+
.input-label {
200+
font-family: var(--font-mono);
201+
font-size: var(--text-micro);
202+
letter-spacing: 0.15em;
203+
text-transform: uppercase;
204+
color: var(--color-stone);
205+
}
206+
.compare-input {
207+
font-family: var(--font-display);
208+
font-size: 1.5rem;
209+
padding: 0.875rem 1rem;
210+
border: 1.5px solid var(--color-rule);
211+
background: var(--color-vellum);
212+
color: var(--color-ink);
213+
border-radius: 1px;
214+
outline: none;
215+
transition: border-color 0.15s ease;
216+
}
217+
.compare-input:focus {
218+
border-color: var(--color-brand);
219+
}
220+
221+
.result-list {
222+
list-style: none;
223+
padding: 0;
224+
margin: 0;
225+
border-top: 2px solid var(--color-ink);
226+
}
227+
.result-row {
228+
display: grid;
229+
grid-template-columns: 200px 1fr 1fr;
230+
gap: 1.5rem;
231+
align-items: center;
232+
padding: 1.125rem 0;
233+
border-bottom: 1px solid var(--color-rule);
234+
}
235+
@media (max-width: 768px) {
236+
.result-row {
237+
grid-template-columns: 1fr;
238+
gap: 0.5rem;
239+
}
240+
}
241+
.result-meta {
242+
display: flex;
243+
align-items: center;
244+
gap: 0.625rem;
245+
flex-wrap: wrap;
246+
}
247+
.result-authority {
248+
font-family: var(--font-mono);
249+
font-size: 0.75rem;
250+
letter-spacing: 0.12em;
251+
text-transform: uppercase;
252+
color: var(--color-brand-deep);
253+
font-weight: 500;
254+
}
255+
.result-note {
256+
font-family: var(--font-mono);
257+
font-size: 0.65rem;
258+
color: var(--color-stone-light);
259+
letter-spacing: 0.05em;
260+
}
261+
.result-link {
262+
color: var(--color-stone-light);
263+
text-decoration: none;
264+
font-size: 0.85rem;
265+
}
266+
.result-link:hover {
267+
color: var(--color-highlight);
268+
}
269+
.result-output {
270+
font-family: var(--font-display);
271+
font-size: 1.5rem;
272+
font-weight: 500;
273+
color: var(--color-highlight);
274+
font-style: italic;
275+
letter-spacing: -0.01em;
276+
min-height: 2rem;
277+
display: flex;
278+
align-items: center;
279+
}
280+
.result-output.error {
281+
color: var(--color-highlight-deep);
282+
font-style: normal;
283+
font-size: 0.875rem;
284+
}
285+
.result-output.loading {
286+
color: var(--color-stone-light);
287+
font-style: normal;
288+
font-size: 0.875rem;
289+
}
290+
.result-code {
291+
font-family: var(--font-mono);
292+
font-size: 0.7rem;
293+
color: var(--color-stone);
294+
background: transparent;
295+
padding: 0;
296+
letter-spacing: 0.02em;
297+
}
298+
299+
.compare-deck {
300+
font-size: 0.9375rem;
301+
color: var(--color-stone);
302+
line-height: 1.6;
303+
margin: 0;
304+
padding-top: 1rem;
305+
border-top: 1px solid var(--color-rule);
306+
}
307+
</style>

src/layouts/Base.astro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ const {
1818
const navItems = [
1919
{ href: "/", label: "Home" },
2020
{ href: "/demo", label: "Demo" },
21+
{ href: "/compare", label: "Compare" },
2122
{ href: "/maps", label: "Maps" },
23+
{ href: "/use-cases", label: "Use cases" },
24+
{ href: "/api", label: "API" },
25+
{ href: "/status", label: "Status" },
2226
{ href: "/authorities", label: "Authorities" },
2327
{ href: "/docs", label: "Docs" },
24-
{ href: "/blog", label: "Blog" },
2528
{ href: "/about", label: "About" },
2629
]
2730

0 commit comments

Comments
 (0)