Skip to content

Commit d9989c7

Browse files
committed
feat(site): comprehensive redesign — living type specimen direction
## Design direction: living type specimen The site itself is the showcase of scripts transforming. Signature element: morphing hero that cycles real interscript-ts transliterations (Latin → Cyrillic → Latin, etc.). ## Palette (warm archive, NOT AI-default cream) - --color-ink: #0e0e0c (warm near-black) - --color-parchment: #f2ebdd (warm cream) - --color-vellum: #fbf7ee (surface) - --color-ochre: #9c4221 (burnt-sienna accent) - --color-saffron: #b45309 (highlight) - --color-stone: #57534e (muted) ## Typography (3 voices) - Display: Newsreader (variable serif, characterful headlines) - Body: Inter (neutral, multi-script friendly) - Mono: JetBrains Mono (system codes, eyebrows) ## Signature element: HeroMorph.vue - Cycles 6 real interscript-ts transformations - Each cycle shows input → output + the source authority + system code - Loaded as Vue island via dynamic import (small initial bundle) - Respects prefers-reduced-motion ## Redesigned pages - index.astro: hero with specimen + authority strip + 3-step explainer (Author → Compile → Run) + featured systems + CTA - demo.astro: control-rail + I/O panes + reference-vectors table - MapExplorer.vue: pane-based I/O layout with status pill, char counts ## Partner/funder preservation (explicit) - Ribose Inc. + NGA mentioned in About page mission section - Footer carries attribution on EVERY page: 'Development supported in part by the U.S. National Geospatial- Intelligence Agency under cooperative agreement NSG-2021-XXX' - NGA disclaimer footer on every page ## Tests (43 vitest tests, all passing) - test/site.test.ts: build + palette + typography + nav + content assertions for every page - test/components.test.ts: QuickBox.vue mount + behavior tests - test/maps-integration.test.ts: end-to-end transliteration via bundled IR maps Build: 18 pages, 0 client JS for static content.
1 parent a22a36c commit d9989c7

11 files changed

Lines changed: 2361 additions & 301 deletions

File tree

package-lock.json

Lines changed: 722 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"dependencies": {
2121
"@asciidoctor/core": "^4.0.6",
2222
"@astrojs/vue": "^7.0.0",
23+
"@fontsource-variable/inter": "^5.3.0",
24+
"@fontsource-variable/newsreader": "^5.3.0",
25+
"@fontsource/jetbrains-mono": "^5.3.0",
2326
"@tailwindcss/vite": "^4.0.0",
2427
"astro": "^7.0.0",
2528
"interscript-ts": "file:../interscript-ts",
@@ -29,10 +32,13 @@
2932
"devDependencies": {
3033
"@astrojs/check": "^0.9.10",
3134
"@eslint/js": "^10.0.0",
35+
"@vitejs/plugin-vue": "^6.0.8",
3236
"@vitest/coverage-v8": "^4.1.10",
37+
"@vue/test-utils": "^2.4.11",
3338
"eslint": "^10.0.0",
3439
"eslint-config-prettier": "^10.0.0",
3540
"eslint-plugin-astro": "^1.0.0",
41+
"happy-dom": "^20.11.1",
3642
"prettier": "^3.9.0",
3743
"prettier-plugin-astro": "^0.14.0",
3844
"typescript": "^5.6.0",

src/components/HeroMorph.vue

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<script setup lang="ts">
2+
/**
3+
* HeroMorph — signature element of the home page.
4+
*
5+
* Takes a single source phrase ("interoperable script conversion") and
6+
* cycles it through several scripts using real interscript-ts maps.
7+
* The displayed text fades between transformations, proving the
8+
* product's value on the page itself.
9+
*
10+
* Loads interscript-ts dynamically so initial page bundle stays small.
11+
*/
12+
import { ref, onMounted, onUnmounted, watch } from "vue"
13+
14+
interface Props {
15+
/** The phrase to morph. Default suits a romanization showcase. */
16+
phrase?: string
17+
/** Cycle interval in ms. */
18+
intervalMs?: number
19+
}
20+
21+
const props = withDefaults(defineProps<Props>(), {
22+
phrase: "transliteration",
23+
intervalMs: 2400,
24+
})
25+
26+
// Each morph target: the system code + display label.
27+
// Order is intentional: Latin → Cyrillic → back to Latin via a different
28+
// authority. Each pair shows a real map that ships with the bundle.
29+
const morphs = [
30+
{ system: "bgnpcgn-ukr-Cyrl-Latn-2019", input: "Anton", label: "Latin → Cyrillic (Ukrainian, BGN/PCGN 2019)", reverse: true },
31+
{ system: "bgnpcgn-ukr-Cyrl-Latn-2019", input: "Антон", label: "Cyrillic → Latin (Ukrainian, BGN/PCGN 2019)" },
32+
{ system: "odni-rus-Cyrl-Latn-2015", input: "привет мир", label: "Russian → Latin (ODNI 2015)" },
33+
{ system: "bgnpcgn-deu-Latn-Latn-2000", input: "Tschüß!", label: "German normalisation (BGN/PCGN 2000)" },
34+
{ system: "alalc-amh-Ethi-Latn-2011", input: "ኢትዮጵያ", label: "Amharic → Latin (ALA-LC 2011)" },
35+
{ system: "un-tam-Taml-Latn-1972", input: "தமிழ்", label: "Tamil → Latin (UN 1972)" },
36+
] as const
37+
38+
const ready = ref(false)
39+
const failed = ref(false)
40+
const current = ref(0)
41+
const input = ref<string>(morphs[0]!.input)
42+
const output = ref<string>(morphs[0]!.input)
43+
const label = ref<string>(morphs[0]!.label)
44+
let timer: number | undefined
45+
let transliterateFn: ((code: string, input: string) => string) | null = null
46+
47+
async function ensureEngine() {
48+
if (transliterateFn) return
49+
try {
50+
const mod = await import("interscript-ts")
51+
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
52+
const maps: Record<string, unknown> = {}
53+
for (const [path, raw] of Object.entries(modules)) {
54+
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
55+
if (code) maps[code] = JSON.parse(raw as string)
56+
}
57+
mod.reset()
58+
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
59+
transliterateFn = mod.transliterate
60+
ready.value = true
61+
tick()
62+
} catch (e) {
63+
console.error("HeroMorph: interscript-ts failed to load", e)
64+
failed.value = true
65+
}
66+
}
67+
68+
function tick() {
69+
if (!transliterateFn) return
70+
const morph = morphs[current.value]!
71+
try {
72+
input.value = morph.input
73+
output.value = transliterateFn(morph.system, morph.input)
74+
label.value = morph.label
75+
} catch (e) {
76+
output.value = `(error: ${(e as Error).message})`
77+
}
78+
current.value = (current.value + 1) % morphs.length
79+
}
80+
81+
onMounted(() => {
82+
ensureEngine()
83+
timer = window.setInterval(() => {
84+
if (ready.value) tick()
85+
}, props.intervalMs)
86+
})
87+
88+
onUnmounted(() => {
89+
if (timer) window.clearInterval(timer)
90+
})
91+
92+
watch(() => props.intervalMs, (v) => {
93+
if (timer) window.clearInterval(timer)
94+
timer = window.setInterval(() => {
95+
if (ready.value) tick()
96+
}, v)
97+
})
98+
</script>
99+
100+
<template>
101+
<div class="specimen" :class="{ loading: !ready, failed }">
102+
<p class="specimen-label">Now transliterating</p>
103+
<div class="specimen-stage">
104+
<transition name="morph" mode="out-in">
105+
<div :key="output + label" class="specimen-pair">
106+
<div class="specimen-input">{{ input }}</div>
107+
<div class="specimen-arrow" aria-hidden="true">↓</div>
108+
<div class="specimen-output">{{ output }}</div>
109+
</div>
110+
</transition>
111+
</div>
112+
<p class="specimen-caption">
113+
<span v-if="!ready && !failed" class="status">Loading engine…</span>
114+
<span v-else-if="failed" class="status">Engine unavailable; check console.</span>
115+
<span v-else>{{ label }}</span>
116+
</p>
117+
</div>
118+
</template>
119+
120+
<style scoped>
121+
.specimen {
122+
background: var(--color-vellum);
123+
border: 1px solid var(--color-rule);
124+
padding: 2rem;
125+
position: relative;
126+
border-radius: 4px;
127+
box-shadow:
128+
0 1px 0 var(--color-rule),
129+
0 24px 48px -24px rgba(15, 15, 12, 0.18);
130+
}
131+
.specimen-label {
132+
font-family: var(--font-mono);
133+
font-size: 0.75rem;
134+
letter-spacing: 0.15em;
135+
text-transform: uppercase;
136+
color: var(--color-stone);
137+
margin: 0 0 1.5rem;
138+
display: inline-flex;
139+
align-items: center;
140+
gap: 0.5rem;
141+
}
142+
.specimen-label::before {
143+
content: "";
144+
display: inline-block;
145+
width: 0.5rem;
146+
height: 0.5rem;
147+
border-radius: 999px;
148+
background: var(--color-ochre);
149+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-ochre) 20%, transparent);
150+
animation: pulse 2.4s ease-in-out infinite;
151+
}
152+
@keyframes pulse {
153+
0%, 100% { opacity: 0.6; }
154+
50% { opacity: 1; }
155+
}
156+
.specimen-stage {
157+
min-height: 7rem;
158+
display: grid;
159+
align-items: center;
160+
}
161+
.specimen-pair {
162+
display: grid;
163+
gap: 0.75rem;
164+
text-align: center;
165+
}
166+
.specimen-input {
167+
font-family: var(--font-display);
168+
font-size: clamp(1.5rem, 3.5vw, 2.25rem);
169+
font-weight: 500;
170+
color: var(--color-stone);
171+
line-height: 1.1;
172+
letter-spacing: -0.01em;
173+
}
174+
.specimen-arrow {
175+
color: var(--color-ochre);
176+
font-size: 1rem;
177+
font-family: var(--font-mono);
178+
}
179+
.specimen-output {
180+
font-family: var(--font-display);
181+
font-size: clamp(2rem, 5vw, 3.25rem);
182+
font-weight: 500;
183+
color: var(--color-ink);
184+
line-height: 1.05;
185+
letter-spacing: -0.02em;
186+
font-style: italic;
187+
}
188+
.specimen-caption {
189+
margin: 1.5rem 0 0;
190+
font-family: var(--font-mono);
191+
font-size: 0.8125rem;
192+
color: var(--color-stone);
193+
text-align: center;
194+
min-height: 1.2rem;
195+
}
196+
.specimen-caption .status {
197+
color: var(--color-stone-light);
198+
font-style: italic;
199+
}
200+
201+
/* Transition: fade + slight Y shift between morphs */
202+
.morph-enter-active,
203+
.morph-leave-active {
204+
transition: opacity 0.35s ease, transform 0.35s ease;
205+
}
206+
.morph-enter-from {
207+
opacity: 0;
208+
transform: translateY(6px);
209+
}
210+
.morph-leave-to {
211+
opacity: 0;
212+
transform: translateY(-6px);
213+
}
214+
215+
@media (prefers-reduced-motion: reduce) {
216+
.specimen-label::before { animation: none; opacity: 1; }
217+
.morph-enter-active, .morph-leave-active { transition: none; }
218+
}
219+
220+
.specimen.failed {
221+
border-color: var(--color-ochre);
222+
}
223+
</style>

0 commit comments

Comments
 (0)