diff --git a/docs/content-guide.md b/docs/content-guide.md index 674c2e6..b3a8e4e 100644 --- a/docs/content-guide.md +++ b/docs/content-guide.md @@ -145,8 +145,11 @@ and for the home hiking hero (`group-hiking` photo path). Do not add Life to the site nav or sitemap. The homepage shows a **mission** blurb (`home.{zh,en}.missionParagraphs` in -`src/data/page-copy.mjs`, rendered in `HomePage.astro` as `#lab-introduction`) -and the **NEWS & AWARDS** band (award records with external `source` links). Other +`src/data/page-copy.mjs`, rendered in `HomePage.astro` as `#lab-introduction`), +a **lab culture typewriter** (`home.{zh,en}.cultureMottos` — CRA English mottos on +both locales; `src/scripts/home-culture-typewriter.mjs`, `prefers-reduced-motion` +cycles text without typing), and the **NEWS & AWARDS** band (award records with +external `source` links). Other page UI labels also live in `src/data/page-copy.mjs`. Visual styling (gradients, liquid-glass controls, fonts) is documented in `docs/architecture.md` under **CRA visual system**. diff --git a/scripts/smoke-build.mjs b/scripts/smoke-build.mjs index ea5f380..fc80530 100644 --- a/scripts/smoke-build.mjs +++ b/scripts/smoke-build.mjs @@ -71,11 +71,11 @@ const unexpectedMissingCmsVars = ['CMS_GITHUB_REPO', 'CMS_OAUTH_BASE_URL', 'PUBL const checks = [ { file: 'dist/index.html', - includes: ['DVLab', 'NEWS & AWARDS', '設計驗證實驗室'], + includes: ['DVLab', 'NEWS & AWARDS', '設計驗證實驗室', 'We research because we are curious'], }, { file: 'dist/en/index.html', - includes: ['DVLab', 'NEWS & AWARDS', 'Design Verification Lab'], + includes: ['DVLab', 'NEWS & AWARDS', 'Design Verification Lab', 'We research because we are curious'], }, { file: 'dist/host/index.html', diff --git a/src/components/HomePage.astro b/src/components/HomePage.astro index 1d56377..acce5be 100644 --- a/src/components/HomePage.astro +++ b/src/components/HomePage.astro @@ -9,6 +9,8 @@ import { pickI18nText } from '../utils/i18n-text'; const { lang } = Astro.props as { lang: 'zh' | 'en' }; const labels = pageCopy.home[lang]; +const cultureMottosJson = JSON.stringify(labels.cultureMottos); +const cultureMottoInitial = labels.cultureMottos[0] ?? ''; const awardLabels = pageCopy.awards[lang]; const pathPrefix = lang === 'en' ? '/en' : ''; const t = lang === 'en' ? siteEn : siteZh; @@ -34,6 +36,16 @@ function awardAttendants(award: { students: string[]; advisors?: string[] }) { {labels.missionParagraphs.map((paragraph) => (

{paragraph}

))} +

+ + {cultureMottoInitial} + +

@@ -71,4 +83,5 @@ function awardAttendants(award: { students: string[]; advisors?: string[] }) { diff --git a/src/data/page-copy.mjs b/src/data/page-copy.mjs index 9f2bc76..d0ffa38 100644 --- a/src/data/page-copy.mjs +++ b/src/data/page-copy.mjs @@ -6,6 +6,11 @@ export const pageCopy = { "在 DVLab,我們聚焦三大研究軸線:AI 形式方法、EDA 與 3DIC、以及量子設計自動化。面對系統層級設計的驗證瓶頸,我們以自動抽象降低問題複雜度,並以學習與引導式精化捕捉關鍵設計意圖,朝向可重現、可分享的開源驗證與設計流程努力。", "我們也關注量子計算如何重塑設計堆疊——從合成、映射、最佳化到在實際裝置上的驗證;同時在 3DIC 與異質整合場景延伸時序、簽核與系統級 EDA 方法。無論是哪一條路線,我們都希望在嚴謹理論、實用工具與探索未知的好奇心之間取得平衡。" ], + "cultureMottos": [ + "We research because we are curious", + "We publish because we love to share", + "We exercise because we work too hard" + ], "newsAwards": "NEWS & AWARDS", "allAwards": "所有獲獎紀錄", "awardSeparator": "、", @@ -19,6 +24,11 @@ export const pageCopy = { "At DVLab, we advance three connected directions—AI Formal Methods, EDA for 3DIC, and Quantum Design Automation. To ease system-level verification bottlenecks, we combine automatic abstraction that tames complexity with learning-guided refinement that captures essential design intent, building open, reproducible frameworks our collaborators can rely on.", "We are equally curious about how quantum techniques reshape the computing stack—from synthesis and optimization to verification on practical devices—and how classical EDA must evolve for heterogeneous 3DIC integration and sign-off. Across AI Formal, EDA 3DIC, and Quantum, we pursue rigorous theory, useful tools, and the curiosity that keeps hard engineering problems worth solving." ], + "cultureMottos": [ + "We research because we are curious", + "We publish because we love to share", + "We exercise because we work too hard" + ], "newsAwards": "NEWS & AWARDS", "allAwards": "All awards", "awardSeparator": ", ", diff --git a/src/scripts/home-culture-typewriter.mjs b/src/scripts/home-culture-typewriter.mjs new file mode 100644 index 0000000..56f0a24 --- /dev/null +++ b/src/scripts/home-culture-typewriter.mjs @@ -0,0 +1,106 @@ +/** CRA About lab-culture typewriter (typewriter-effect options: delay 20, deleteSpeed 30, cursor _). */ + +const TYPE_DELAY_MS = 20; +const DELETE_DELAY_MS = 30; +const PAUSE_TYPED_MS = 2000; +const PAUSE_DELETED_MS = 400; +const REDUCED_ROTATE_MS = 5000; + +function parseStrings(root) { + try { + const raw = root.dataset.strings; + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter((s) => typeof s === 'string' && s.length > 0) : []; + } catch { + return []; + } +} + +function runTypewriter(display, strings) { + let stringIndex = 0; + let charIndex = 0; + let deleting = false; + let cancelled = false; + + const schedule = (fn, ms) => { + const id = window.setTimeout(fn, ms); + return () => { + cancelled = true; + window.clearTimeout(id); + }; + }; + + const step = () => { + if (cancelled) return; + const current = strings[stringIndex]; + if (!deleting) { + charIndex += 1; + display.textContent = current.slice(0, charIndex); + if (charIndex >= current.length) { + schedule(() => { + deleting = true; + step(); + }, PAUSE_TYPED_MS); + return; + } + schedule(step, TYPE_DELAY_MS); + return; + } + + charIndex -= 1; + display.textContent = current.slice(0, charIndex); + if (charIndex <= 0) { + deleting = false; + stringIndex = (stringIndex + 1) % strings.length; + schedule(step, PAUSE_DELETED_MS); + return; + } + schedule(step, DELETE_DELAY_MS); + }; + + step(); + return () => { + cancelled = true; + }; +} + +function runReducedCycle(display, strings) { + let index = 0; + display.textContent = strings[0]; + if (strings.length <= 1) return () => {}; + + const id = window.setInterval(() => { + index = (index + 1) % strings.length; + display.textContent = strings[index]; + }, REDUCED_ROTATE_MS); + + return () => window.clearInterval(id); +} + +export function initHomeCultureTypewriter(root) { + if (!(root instanceof HTMLElement)) return () => {}; + const strings = parseStrings(root); + const display = root.querySelector('[data-typewriter-display]'); + if (!display || strings.length === 0) return () => {}; + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + root.classList.toggle('is-reduced-motion', reduced); + + if (reduced) { + return runReducedCycle(display, strings); + } + + return runTypewriter(display, strings); +} + +export function initHomeCultureTypewriters() { + const roots = document.querySelectorAll('[data-culture-typewriter]'); + const cleanups = []; + for (const root of roots) { + cleanups.push(initHomeCultureTypewriter(root)); + } + return () => cleanups.forEach((fn) => fn()); +} + +initHomeCultureTypewriters(); diff --git a/src/styles/cra-layout.css b/src/styles/cra-layout.css index 9ddcac8..bdfbb81 100644 --- a/src/styles/cra-layout.css +++ b/src/styles/cra-layout.css @@ -210,6 +210,48 @@ margin-bottom: 0; } +/* CRA About lab-culture typewriter (Coolvetica + light gold, blinking _) */ +.home-culture-motto { + margin: clamp(28px, 4vw, 40px) 0 0; + text-align: center; +} + +.home-culture-typewriter { + display: inline-block; + white-space: nowrap; + font-family: var(--font-title); + font-size: clamp(26px, 3.2vw, 44px); + font-weight: var(--fw-latin-strong); + line-height: 1.25; + color: var(--cra-light-gold); + letter-spacing: 0.02em; +} + +[data-theme='light'] .home-culture-typewriter { + color: #4a5e20; +} + +.home-culture-cursor { + display: inline-block; + margin-left: 1px; + animation: home-culture-cursor-blink 0.9s step-end infinite; +} + +.home-culture-motto.is-reduced-motion .home-culture-cursor { + animation: none; + opacity: 0.85; +} + +@keyframes home-culture-cursor-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + [data-theme='light'] .home-mission-text { color: var(--text-soft); } diff --git a/tests/home-logo-browser.mjs b/tests/home-logo-browser.mjs index b795814..deab606 100644 --- a/tests/home-logo-browser.mjs +++ b/tests/home-logo-browser.mjs @@ -19,6 +19,8 @@ export async function testHomeLogo(browser, base) { assert.equal(await logo.locator('img').count(), 6); assert.equal(await page.locator('a[href*="/join/"]').count(), 0); assert.equal(await page.locator('.home-news-item').count(), 7); + assert(await page.locator('[data-culture-typewriter]').isVisible()); + assert((await page.locator('[data-culture-typewriter]').getAttribute('data-strings'))?.includes('We publish because we love to share')); assert((await page.locator('.home-opening').boundingBox()).height >= 844 * 0.8 - 1); assert(await page.locator('h1').isVisible()); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));