diff --git a/.DS_Store b/.DS_Store
index e8276eb..dbea4da 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index ddce69b..083f7d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
node_modules/
dist/
.astro/
+.claude/
+.vscode/
+smartinput/downloads/
diff --git a/about.html b/about.html
new file mode 100644
index 0000000..cfa9314
--- /dev/null
+++ b/about.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+ 关于我们|智花 · 五道题
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
智花 · 五道题
+
关于我们
+
五道题(北京)科技有限公司开发学习、输入与效率类软件。智花覆盖中文学习与输入;MassiveEdit 面向需要处理超大文本的用户。
+
+
+
+
+
+ 企业愿景
+ 把学习、表达与日常工作里真正费力的环节做成可用的软件。我们希望工具足够清楚、稳定,也愿意长期打磨细节。
+
+
+ 我们的使命
+ 持续交付可落地的产品:学生能完成作业与开口练习,输入更自然,识字写字有路径可循,大文件也能流畅打开与编辑。
+
+
+ 品牌故事
+ 「智花」寓意智慧与文化的共同绽放。五道题是公司主体,「智花」是面向中文学习与输入的产品品牌。我们把语言学、工程与产品迭代放在同一条工作线上。
+
+
+ 团队
+ 团队服务产品研发与持续迭代:做功能、修问题、听反馈。我们不招生、不运营师资加盟,把精力放在软件本身。
+
+
+
+
+
+
+
+
+
+
diff --git a/astro.config.mjs b/astro.config.mjs
deleted file mode 100644
index 2f6e023..0000000
--- a/astro.config.mjs
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig } from 'astro/config';
-import icon from 'astro-icon';
-
-export default defineConfig({
- integrations: [icon()],
- compressHTML: true,
- build: {
- inlineStylesheets: 'auto',
- },
- vite: {
- build: {
- cssMinify: 'esbuild',
- },
- },
-});
diff --git a/css/site.css b/css/site.css
new file mode 100644
index 0000000..93b3e81
--- /dev/null
+++ b/css/site.css
@@ -0,0 +1,745 @@
+@font-face {
+ font-family: "Manrope";
+ src: url("../fonts/manrope-latin-400-normal.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Manrope";
+ src: url("../fonts/manrope-latin-700-normal.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Manrope";
+ src: url("../fonts/manrope-latin-800-normal.woff2") format("woff2");
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+}
+
+:root {
+ --bg: #fffdfb;
+ --surface: #ffffff;
+ --surface-soft: #f8f9fd;
+ --line: #ece7e2;
+ --line-strong: #d8dde8;
+ --text: #2d2a28;
+ --text-heading: #1f2430;
+ --muted: #8e8a87;
+ --muted-strong: #686461;
+ --brand: #7aa0ff;
+ --brand-strong: #4b76eb;
+ --brand-soft: #eef3ff;
+ --footer: #f4f1ec;
+ --footer-heading: #1f2430;
+ --footer-muted: #6f6b68;
+ --footer-faint: #948f8b;
+ --footer-line: #e6e1db;
+ --header-bg: rgba(255, 253, 251, 0.9);
+ --nav-link: #44423f;
+ --shadow-card: 0 14px 48px rgba(43, 53, 84, 0.08);
+ --radius-lg: 24px;
+ --content: 1320px;
+ --nav-h: 104px;
+ color-scheme: light;
+}
+
+[data-theme="dark"] {
+ --bg: #0f1115;
+ --surface: #171a21;
+ --surface-soft: #1c2028;
+ --line: #2a2f3a;
+ --line-strong: #3d4454;
+ --text: #e8eaef;
+ --text-heading: #f1f3f7;
+ --muted: #8b95a5;
+ --muted-strong: #aeb6c3;
+ --brand: #9bb4ff;
+ --brand-strong: #b8c8ff;
+ --brand-soft: #252a38;
+ --footer: #0c0e12;
+ --footer-heading: #f1f3f7;
+ --footer-muted: #8a91a0;
+ --footer-faint: #6d7584;
+ --footer-line: #2a2f3a;
+ --header-bg: rgba(15, 17, 21, 0.88);
+ --nav-link: #c9d0db;
+ --shadow-card: 0 14px 48px rgba(0, 0, 0, 0.4);
+ color-scheme: dark;
+}
+
+.site-lang-en body,
+.site-lang-fr body {
+ font-family: Manrope, "Segoe UI", system-ui, sans-serif;
+}
+
+.site-lang-ja body {
+ font-family:
+ Manrope,
+ "Hiragino Sans",
+ "Hiragino Kaku Gothic ProN",
+ "Noto Sans JP",
+ "PingFang SC",
+ "Microsoft YaHei",
+ sans-serif;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ background: var(--bg);
+ color: var(--text);
+ font-family: Manrope, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
+ text-rendering: optimizeLegibility;
+}
+
+img {
+ display: block;
+ max-width: 100%;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+button {
+ font: inherit;
+}
+
+.page-shell {
+ position: relative;
+ overflow-x: clip;
+}
+
+.container {
+ width: min(calc(100% - 48px), var(--content));
+ margin: 0 auto;
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: var(--header-bg);
+ backdrop-filter: blur(16px);
+}
+
+.site-header::after {
+ content: "";
+ position: absolute;
+ inset: auto 0 0;
+ height: 1px;
+ background: var(--line);
+}
+
+.site-nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ min-height: var(--nav-h);
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 14px;
+ min-width: 0;
+}
+
+.brand-mark {
+ width: 34px;
+ height: 34px;
+ flex: none;
+}
+
+.brand-copy {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ white-space: nowrap;
+ font-size: 24px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ color: var(--text-heading);
+}
+
+.nav-links {
+ display: inline-flex;
+ align-items: center;
+ gap: 34px;
+ flex-wrap: wrap;
+}
+
+.nav-link {
+ position: relative;
+ padding: 8px 0;
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--nav-link);
+ letter-spacing: 0.04em;
+}
+
+.nav-link.current {
+ color: var(--brand);
+}
+
+.nav-link.current::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -10px;
+ margin: auto;
+ width: 18px;
+ height: 2px;
+ border-radius: 999px;
+ background: currentColor;
+}
+
+.header-tools {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.lang-switch {
+ position: relative;
+}
+
+.lang-switch summary {
+ list-style: none;
+ cursor: pointer;
+}
+
+.lang-switch summary::-webkit-details-marker {
+ display: none;
+}
+
+.lang-switch-panel {
+ position: absolute;
+ right: 0;
+ top: calc(100% + 10px);
+ min-width: 168px;
+ padding: 8px;
+ border-radius: 16px;
+ border: 1px solid var(--line);
+ background: var(--surface);
+ box-shadow: var(--shadow-card);
+ z-index: 120;
+}
+
+.lang-switch-panel a {
+ display: block;
+ padding: 10px 12px;
+ border-radius: 12px;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text);
+}
+
+.lang-switch-panel a:hover,
+.lang-switch-panel a:focus-visible {
+ background: var(--brand-soft);
+ color: var(--brand-strong);
+}
+
+.lang-switch-panel a.current {
+ color: var(--brand-strong);
+}
+
+.theme-toggle-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 44px;
+ height: 44px;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ background: var(--surface);
+ color: var(--text-heading);
+ cursor: pointer;
+}
+
+.theme-toggle-btn:hover {
+ border-color: var(--line-strong);
+}
+
+.theme-toggle-btn .icon-dark {
+ display: none;
+}
+
+[data-theme="dark"] .theme-toggle-btn .icon-light {
+ display: none;
+}
+
+[data-theme="dark"] .theme-toggle-btn .icon-dark {
+ display: block;
+}
+
+.locale-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ background: var(--surface);
+ color: var(--muted-strong);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.locale-pill svg {
+ width: 10px;
+ height: 10px;
+ flex-shrink: 0;
+}
+
+.menu-toggle {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 46px;
+ height: 46px;
+ border: 0;
+ border-radius: 14px;
+ background: var(--surface);
+ box-shadow: 0 8px 24px rgba(49, 50, 56, 0.08);
+ color: var(--text);
+ cursor: pointer;
+}
+
+.mobile-menu {
+ display: none;
+ padding: 0 0 24px;
+}
+
+.mobile-menu.is-open {
+ display: block;
+}
+
+.mobile-menu a {
+ display: block;
+ padding: 14px 18px;
+ border-radius: 16px;
+ font-size: 15px;
+ font-weight: 700;
+}
+
+.mobile-menu a.current {
+ background: var(--brand-soft);
+ color: var(--brand-strong);
+}
+
+.hero {
+ padding: 92px 0 60px;
+}
+
+.home-hero {
+ text-align: center;
+ padding: 72px 0 36px;
+}
+
+.home-kicker {
+ margin: 0 0 16px;
+ color: var(--brand);
+ font-size: 13px;
+ font-weight: 800;
+ letter-spacing: 0.16em;
+}
+
+.home-lead {
+ max-width: 36em;
+ margin: 16px auto 0;
+ color: var(--muted-strong);
+ font-size: 18px;
+ line-height: 1.7;
+}
+
+.home-hero .hero-actions {
+ margin-top: 28px;
+}
+
+.about-hero {
+ padding-bottom: 12px;
+}
+
+.hero-title {
+ margin: 0;
+ color: var(--text-heading);
+ font-size: clamp(40px, 5vw, 72px);
+ line-height: 1.15;
+ font-weight: 800;
+ letter-spacing: -0.03em;
+}
+
+.hero-actions {
+ display: flex;
+ justify-content: center;
+ gap: 18px;
+ margin-top: 44px;
+ flex-wrap: wrap;
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ min-height: 48px;
+ padding: 0 24px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ font-size: 13px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ cursor: pointer;
+}
+
+.btn-outline {
+ border-color: rgba(122, 160, 255, 0.4);
+ color: var(--brand);
+ background: rgba(255, 255, 255, 0.72);
+}
+
+[data-theme="dark"] .btn-outline {
+ background: rgba(23, 26, 33, 0.72);
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #7aa0ff, #5f84ef);
+ color: #fff;
+ box-shadow: 0 14px 34px rgba(95, 132, 239, 0.28);
+}
+
+.section {
+ padding: 122px 0;
+}
+
+.section.compact {
+ padding-top: 88px;
+ padding-bottom: 88px;
+}
+
+.section-head {
+ margin: 0 auto 58px;
+ text-align: center;
+}
+
+.section-title {
+ margin: 0;
+ color: var(--text-heading);
+ font-size: clamp(26px, 3.3vw, 48px);
+ line-height: 1.18;
+ letter-spacing: -0.02em;
+}
+
+.section-copy {
+ max-width: 780px;
+ margin: 22px auto 0;
+ color: var(--muted-strong);
+ font-size: 15px;
+ line-height: 1.95;
+}
+
+.grid-two {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 30px;
+}
+
+.feature-card {
+ position: relative;
+ min-height: 100%;
+ padding: 34px;
+ border-radius: var(--radius-lg);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ box-shadow: var(--shadow-card);
+}
+
+.feature-card h3 {
+ margin: 0 0 16px;
+ color: var(--text-heading);
+ font-size: 28px;
+ line-height: 1.22;
+ letter-spacing: -0.02em;
+}
+
+.feature-card p {
+ margin: 0;
+ color: var(--muted-strong);
+ font-size: 15px;
+ line-height: 1.9;
+}
+
+.mini-link {
+ display: inline-flex;
+ margin-top: 26px;
+ color: var(--brand);
+ font-size: 13px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+}
+
+.product-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 30px;
+}
+
+.product-kicker {
+ margin: 0 0 8px;
+ color: var(--brand);
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.product-card .mini-link {
+ margin-top: 18px;
+}
+
+.about-stack {
+ display: grid;
+ gap: 18px;
+}
+
+.about-stack h2 {
+ margin: 0 0 10px;
+ color: var(--text-heading);
+ font-size: 26px;
+}
+
+.about-stack p {
+ margin: 0;
+ color: var(--muted-strong);
+ line-height: 1.8;
+}
+
+.about-products {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 10px 18px;
+}
+
+.about-products a {
+ color: var(--brand-strong);
+ font-weight: 700;
+}
+
+.about-products a:hover {
+ text-decoration: underline;
+}
+
+.site-footer {
+ margin-top: 120px;
+ background: var(--footer);
+ color: var(--footer-heading);
+ border-top: 1px solid var(--footer-line);
+}
+
+.footer-main {
+ display: grid;
+ grid-template-columns: 1.35fr 0.8fr 0.8fr;
+ gap: 48px;
+ padding: 60px 0 40px;
+}
+
+.site-footer h3 {
+ margin: 0 0 18px;
+ color: var(--footer-heading);
+ font-size: 18px;
+ letter-spacing: -0.01em;
+}
+
+.site-footer p,
+.site-footer a {
+ color: var(--footer-muted);
+ font-size: 14px;
+ line-height: 2;
+}
+
+.site-footer a:hover,
+.site-footer a:focus-visible {
+ color: var(--footer-heading);
+}
+
+.footer-links {
+ display: grid;
+ gap: 6px;
+}
+
+.footer-bottom {
+ border-top: 1px solid var(--footer-line);
+ padding: 22px 0 30px;
+ color: var(--footer-faint);
+ font-size: 12px;
+ text-align: center;
+}
+
+.footer-legal-line {
+ margin: 0;
+ line-height: 1.65;
+}
+
+.site-footer .footer-icp-link {
+ color: var(--footer-muted);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.site-footer .footer-icp-link:hover {
+ color: var(--footer-heading);
+}
+
+.footer-trademark {
+ margin: 14px auto 0;
+ max-width: 720px;
+ color: var(--footer-faint);
+ font-size: 11px;
+ line-height: 1.7;
+}
+
+.bg-wash {
+ position: relative;
+}
+
+.bg-wash::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(circle at 12% 12%, rgba(122, 160, 255, 0.08), transparent 24%),
+ radial-gradient(circle at 88% 18%, rgba(242, 136, 119, 0.08), transparent 22%),
+ radial-gradient(circle at 78% 86%, rgba(122, 160, 255, 0.05), transparent 24%);
+ pointer-events: none;
+}
+
+.bg-wash > * {
+ position: relative;
+}
+
+@media (max-width: 1180px) {
+ .site-nav {
+ min-height: 88px;
+ }
+
+ .nav-links,
+ .lang-switch {
+ display: none;
+ }
+
+ .mobile-lang {
+ padding: 8px 0 4px;
+ margin-top: 8px;
+ border-top: 1px solid var(--line);
+ }
+
+ .mobile-lang p {
+ margin: 0 0 8px;
+ padding: 0 18px;
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--muted);
+ }
+
+ .mobile-lang a {
+ display: block;
+ padding: 10px 18px;
+ border-radius: 12px;
+ font-size: 14px;
+ font-weight: 700;
+ }
+
+ .mobile-lang a.current {
+ background: var(--brand-soft);
+ color: var(--brand-strong);
+ }
+
+ .mobile-menu .theme-toggle-btn {
+ margin: 12px 18px 0;
+ width: calc(100% - 36px);
+ }
+
+ .menu-toggle {
+ display: inline-flex;
+ }
+
+ .product-grid,
+ .footer-main,
+ .grid-two {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 760px) {
+ :root {
+ --nav-h: 84px;
+ }
+
+ .container {
+ width: min(calc(100% - 32px), var(--content));
+ }
+
+ .hero {
+ padding: 64px 0 32px;
+ }
+
+ .section {
+ padding: 82px 0;
+ }
+
+ .feature-card {
+ padding: 26px;
+ }
+
+ .brand-copy {
+ gap: 6px;
+ font-size: 24px;
+ }
+
+ .hero-title {
+ font-size: clamp(28px, 8.4vw, 40px);
+ overflow-wrap: break-word;
+ }
+
+ .home-lead {
+ font-size: 17px;
+ line-height: 1.65;
+ }
+
+ .hero-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .btn {
+ width: 100%;
+ }
+
+ .site-footer {
+ margin-top: 82px;
+ }
+}
diff --git a/docs/.DS_Store b/docs/.DS_Store
new file mode 100644
index 0000000..9f40b0d
Binary files /dev/null and b/docs/.DS_Store differ
diff --git a/设计稿/about.png b/docs/about.png
similarity index 100%
rename from 设计稿/about.png
rename to docs/about.png
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
new file mode 100644
index 0000000..26373ab
--- /dev/null
+++ b/docs/astro.config.mjs
@@ -0,0 +1,30 @@
+import { defineConfig } from 'astro/config';
+import icon from 'astro-icon';
+import { productSites } from './scripts/product-sites.mjs';
+
+export default defineConfig({
+ integrations: [icon(), productSites()],
+ compressHTML: true,
+ build: {
+ inlineStylesheets: 'auto',
+ },
+ redirects: {
+ '/teaching-philosophy': '/',
+ '/curriculum': '/',
+ '/faculty': '/',
+ '/en/teaching-philosophy': '/en',
+ '/en/curriculum': '/en',
+ '/en/faculty': '/en',
+ '/ja/teaching-philosophy': '/ja',
+ '/ja/curriculum': '/ja',
+ '/ja/faculty': '/ja',
+ '/fr/teaching-philosophy': '/fr',
+ '/fr/curriculum': '/fr',
+ '/fr/faculty': '/fr',
+ },
+ vite: {
+ build: {
+ cssMinify: 'esbuild',
+ },
+ },
+});
diff --git a/设计稿/curriculum.png b/docs/curriculum.png
similarity index 100%
rename from 设计稿/curriculum.png
rename to docs/curriculum.png
diff --git a/设计稿/faculty.png b/docs/faculty.png
similarity index 100%
rename from 设计稿/faculty.png
rename to docs/faculty.png
diff --git a/设计稿/index.png b/docs/index.png
similarity index 100%
rename from 设计稿/index.png
rename to docs/index.png
diff --git a/package-lock.json b/docs/package-lock.json
similarity index 100%
rename from package-lock.json
rename to docs/package-lock.json
diff --git a/package.json b/docs/package.json
similarity index 100%
rename from package.json
rename to docs/package.json
diff --git a/public/_headers b/docs/public/_headers
similarity index 88%
rename from public/_headers
rename to docs/public/_headers
index 12bbe46..690cc6a 100644
--- a/public/_headers
+++ b/docs/public/_headers
@@ -17,15 +17,6 @@
/about/*
Cache-Control: no-cache
-/curriculum/*
- Cache-Control: no-cache
-
-/faculty/*
- Cache-Control: no-cache
-
-/teaching-philosophy/*
- Cache-Control: no-cache
-
/en/*
Cache-Control: no-cache
@@ -34,3 +25,15 @@
/fr/*
Cache-Control: no-cache
+
+/smartbuddy/*
+ Cache-Control: no-cache
+
+/smartinput/*
+ Cache-Control: no-cache
+
+/smarthanzi/*
+ Cache-Control: no-cache
+
+/massiveedit/*
+ Cache-Control: no-cache
diff --git a/public/fonts/manrope-latin-400-normal.woff2 b/docs/public/fonts/manrope-latin-400-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-400-normal.woff2
rename to docs/public/fonts/manrope-latin-400-normal.woff2
diff --git a/public/fonts/manrope-latin-600-normal.woff2 b/docs/public/fonts/manrope-latin-600-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-600-normal.woff2
rename to docs/public/fonts/manrope-latin-600-normal.woff2
diff --git a/public/fonts/manrope-latin-700-normal.woff2 b/docs/public/fonts/manrope-latin-700-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-700-normal.woff2
rename to docs/public/fonts/manrope-latin-700-normal.woff2
diff --git a/public/fonts/manrope-latin-800-normal.woff2 b/docs/public/fonts/manrope-latin-800-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-800-normal.woff2
rename to docs/public/fonts/manrope-latin-800-normal.woff2
diff --git a/public/fonts/manrope-latin-ext-400-normal.woff2 b/docs/public/fonts/manrope-latin-ext-400-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-ext-400-normal.woff2
rename to docs/public/fonts/manrope-latin-ext-400-normal.woff2
diff --git a/public/fonts/manrope-latin-ext-600-normal.woff2 b/docs/public/fonts/manrope-latin-ext-600-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-ext-600-normal.woff2
rename to docs/public/fonts/manrope-latin-ext-600-normal.woff2
diff --git a/public/fonts/manrope-latin-ext-700-normal.woff2 b/docs/public/fonts/manrope-latin-ext-700-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-ext-700-normal.woff2
rename to docs/public/fonts/manrope-latin-ext-700-normal.woff2
diff --git a/public/fonts/manrope-latin-ext-800-normal.woff2 b/docs/public/fonts/manrope-latin-ext-800-normal.woff2
similarity index 100%
rename from public/fonts/manrope-latin-ext-800-normal.woff2
rename to docs/public/fonts/manrope-latin-ext-800-normal.woff2
diff --git a/public/fonts/manrope.css b/docs/public/fonts/manrope.css
similarity index 100%
rename from public/fonts/manrope.css
rename to docs/public/fonts/manrope.css
diff --git a/public/images/index-hero-bg.webp b/docs/public/images/index-hero-bg.webp
similarity index 100%
rename from public/images/index-hero-bg.webp
rename to docs/public/images/index-hero-bg.webp
diff --git a/public/images/index-left.webp b/docs/public/images/index-left.webp
similarity index 100%
rename from public/images/index-left.webp
rename to docs/public/images/index-left.webp
diff --git a/public/images/logo-mark.svg b/docs/public/images/logo-mark.svg
similarity index 100%
rename from public/images/logo-mark.svg
rename to docs/public/images/logo-mark.svg
diff --git a/public/images/logo.svg b/docs/public/images/logo.svg
similarity index 100%
rename from public/images/logo.svg
rename to docs/public/images/logo.svg
diff --git a/public/images/logo.webp b/docs/public/images/logo.webp
similarity index 100%
rename from public/images/logo.webp
rename to docs/public/images/logo.webp
diff --git a/public/images/sketch/165dc7c938317262993013e2bf7108b0d8429d1e.webp b/docs/public/images/sketch/165dc7c938317262993013e2bf7108b0d8429d1e.webp
similarity index 100%
rename from public/images/sketch/165dc7c938317262993013e2bf7108b0d8429d1e.webp
rename to docs/public/images/sketch/165dc7c938317262993013e2bf7108b0d8429d1e.webp
diff --git a/public/images/sketch/1abbe19f0bf338ca5914e4918c80e2a0166fb3c6.webp b/docs/public/images/sketch/1abbe19f0bf338ca5914e4918c80e2a0166fb3c6.webp
similarity index 100%
rename from public/images/sketch/1abbe19f0bf338ca5914e4918c80e2a0166fb3c6.webp
rename to docs/public/images/sketch/1abbe19f0bf338ca5914e4918c80e2a0166fb3c6.webp
diff --git a/public/images/sketch/3cfbe88194be43bcc0ab7a8e987c4d7eaad93ef5.webp b/docs/public/images/sketch/3cfbe88194be43bcc0ab7a8e987c4d7eaad93ef5.webp
similarity index 100%
rename from public/images/sketch/3cfbe88194be43bcc0ab7a8e987c4d7eaad93ef5.webp
rename to docs/public/images/sketch/3cfbe88194be43bcc0ab7a8e987c4d7eaad93ef5.webp
diff --git a/public/images/sketch/3fb13b6b14a9c9c38c19f57b131e9dbb84e9d2a8.webp b/docs/public/images/sketch/3fb13b6b14a9c9c38c19f57b131e9dbb84e9d2a8.webp
similarity index 100%
rename from public/images/sketch/3fb13b6b14a9c9c38c19f57b131e9dbb84e9d2a8.webp
rename to docs/public/images/sketch/3fb13b6b14a9c9c38c19f57b131e9dbb84e9d2a8.webp
diff --git a/public/images/sketch/4f0c158d4abd3ded7bc18fb50aedeedc77691076.webp b/docs/public/images/sketch/4f0c158d4abd3ded7bc18fb50aedeedc77691076.webp
similarity index 100%
rename from public/images/sketch/4f0c158d4abd3ded7bc18fb50aedeedc77691076.webp
rename to docs/public/images/sketch/4f0c158d4abd3ded7bc18fb50aedeedc77691076.webp
diff --git a/public/images/sketch/53f5e9c4751757b91531fd260cf01f6eff8e1278.webp b/docs/public/images/sketch/53f5e9c4751757b91531fd260cf01f6eff8e1278.webp
similarity index 100%
rename from public/images/sketch/53f5e9c4751757b91531fd260cf01f6eff8e1278.webp
rename to docs/public/images/sketch/53f5e9c4751757b91531fd260cf01f6eff8e1278.webp
diff --git a/public/images/sketch/67a807fec1b784684e1a026f0ee01f5072a571c7.webp b/docs/public/images/sketch/67a807fec1b784684e1a026f0ee01f5072a571c7.webp
similarity index 100%
rename from public/images/sketch/67a807fec1b784684e1a026f0ee01f5072a571c7.webp
rename to docs/public/images/sketch/67a807fec1b784684e1a026f0ee01f5072a571c7.webp
diff --git a/public/images/sketch/6de9aa9e5d23da3009d3776b55c3b84c02a5aaa2.webp b/docs/public/images/sketch/6de9aa9e5d23da3009d3776b55c3b84c02a5aaa2.webp
similarity index 100%
rename from public/images/sketch/6de9aa9e5d23da3009d3776b55c3b84c02a5aaa2.webp
rename to docs/public/images/sketch/6de9aa9e5d23da3009d3776b55c3b84c02a5aaa2.webp
diff --git a/public/images/sketch/7cdc25ddd57dfe8b8394e8b3ed7ffc28225b6471.webp b/docs/public/images/sketch/7cdc25ddd57dfe8b8394e8b3ed7ffc28225b6471.webp
similarity index 100%
rename from public/images/sketch/7cdc25ddd57dfe8b8394e8b3ed7ffc28225b6471.webp
rename to docs/public/images/sketch/7cdc25ddd57dfe8b8394e8b3ed7ffc28225b6471.webp
diff --git a/public/images/sketch/8f23517eccd3e10f8cdc63887d23cd0aaaa75181.webp b/docs/public/images/sketch/8f23517eccd3e10f8cdc63887d23cd0aaaa75181.webp
similarity index 100%
rename from public/images/sketch/8f23517eccd3e10f8cdc63887d23cd0aaaa75181.webp
rename to docs/public/images/sketch/8f23517eccd3e10f8cdc63887d23cd0aaaa75181.webp
diff --git a/public/images/sketch/9286e6b5a91d38938547ebf3909e5d6886e79899.webp b/docs/public/images/sketch/9286e6b5a91d38938547ebf3909e5d6886e79899.webp
similarity index 100%
rename from public/images/sketch/9286e6b5a91d38938547ebf3909e5d6886e79899.webp
rename to docs/public/images/sketch/9286e6b5a91d38938547ebf3909e5d6886e79899.webp
diff --git a/public/images/sketch/abfcce4aed0c6b426ced3e4a00ca599ddb0bc080.webp b/docs/public/images/sketch/abfcce4aed0c6b426ced3e4a00ca599ddb0bc080.webp
similarity index 100%
rename from public/images/sketch/abfcce4aed0c6b426ced3e4a00ca599ddb0bc080.webp
rename to docs/public/images/sketch/abfcce4aed0c6b426ced3e4a00ca599ddb0bc080.webp
diff --git a/public/images/sketch/bcc2546bb5ac5489aaf0a6cd2501e09da4de3570.webp b/docs/public/images/sketch/bcc2546bb5ac5489aaf0a6cd2501e09da4de3570.webp
similarity index 100%
rename from public/images/sketch/bcc2546bb5ac5489aaf0a6cd2501e09da4de3570.webp
rename to docs/public/images/sketch/bcc2546bb5ac5489aaf0a6cd2501e09da4de3570.webp
diff --git a/public/images/sketch/d47cf2285609fda9339d16a5dae947ac4f22416e.webp b/docs/public/images/sketch/d47cf2285609fda9339d16a5dae947ac4f22416e.webp
similarity index 100%
rename from public/images/sketch/d47cf2285609fda9339d16a5dae947ac4f22416e.webp
rename to docs/public/images/sketch/d47cf2285609fda9339d16a5dae947ac4f22416e.webp
diff --git a/public/images/sketch/d5c638f4b093f233101f28a2872f10f3172a3ac4.webp b/docs/public/images/sketch/d5c638f4b093f233101f28a2872f10f3172a3ac4.webp
similarity index 100%
rename from public/images/sketch/d5c638f4b093f233101f28a2872f10f3172a3ac4.webp
rename to docs/public/images/sketch/d5c638f4b093f233101f28a2872f10f3172a3ac4.webp
diff --git a/public/images/sketch/e008645eb4e8bd0b78d8a748abedf9702a1944c0.webp b/docs/public/images/sketch/e008645eb4e8bd0b78d8a748abedf9702a1944c0.webp
similarity index 100%
rename from public/images/sketch/e008645eb4e8bd0b78d8a748abedf9702a1944c0.webp
rename to docs/public/images/sketch/e008645eb4e8bd0b78d8a748abedf9702a1944c0.webp
diff --git a/public/images/sketch/eb55da7436f1cf69f9ce2a44234033426361d80d.webp b/docs/public/images/sketch/eb55da7436f1cf69f9ce2a44234033426361d80d.webp
similarity index 100%
rename from public/images/sketch/eb55da7436f1cf69f9ce2a44234033426361d80d.webp
rename to docs/public/images/sketch/eb55da7436f1cf69f9ce2a44234033426361d80d.webp
diff --git a/public/images/sketch/efa4288e0fad73637720bfd9ac094a67a39ed465.webp b/docs/public/images/sketch/efa4288e0fad73637720bfd9ac094a67a39ed465.webp
similarity index 100%
rename from public/images/sketch/efa4288e0fad73637720bfd9ac094a67a39ed465.webp
rename to docs/public/images/sketch/efa4288e0fad73637720bfd9ac094a67a39ed465.webp
diff --git a/public/images/sketch/f1b84ab9a9a9c47ffa390d0bea93ab79d019625c.webp b/docs/public/images/sketch/f1b84ab9a9a9c47ffa390d0bea93ab79d019625c.webp
similarity index 100%
rename from public/images/sketch/f1b84ab9a9a9c47ffa390d0bea93ab79d019625c.webp
rename to docs/public/images/sketch/f1b84ab9a9a9c47ffa390d0bea93ab79d019625c.webp
diff --git a/public/images/study.webp b/docs/public/images/study.webp
similarity index 100%
rename from public/images/study.webp
rename to docs/public/images/study.webp
diff --git a/public/images/teacher.webp b/docs/public/images/teacher.webp
similarity index 100%
rename from public/images/teacher.webp
rename to docs/public/images/teacher.webp
diff --git a/readme.md b/docs/readme.md
similarity index 100%
rename from readme.md
rename to docs/readme.md
diff --git a/docs/scripts/build-html-portal.mjs b/docs/scripts/build-html-portal.mjs
new file mode 100644
index 0000000..250d461
--- /dev/null
+++ b/docs/scripts/build-html-portal.mjs
@@ -0,0 +1,359 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { en } from "../src/i18n/en.ts";
+import { fr } from "../src/i18n/fr.ts";
+import { ja } from "../src/i18n/ja.ts";
+import { zh } from "../src/i18n/zh.ts";
+
+const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
+const outDir = path.join(root, "html");
+
+const copies = { zh, en, ja, fr };
+const LANGS = ["zh", "en", "ja", "fr"];
+const htmlLang = { zh: "zh-CN", en: "en", ja: "ja", fr: "fr" };
+const langLabels = { zh: "中文", en: "English", ja: "日本語", fr: "Français" };
+
+function esc(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+function assetPrefix(lang) {
+ return lang === "zh" ? "" : "../";
+}
+
+function pageHref(fromLang, toLang, page) {
+ const file = page === "home" ? "index.html" : "about.html";
+ if (fromLang === toLang) return file;
+ if (fromLang === "zh") return `${toLang}/${file}`;
+ if (toLang === "zh") return `../${file}`;
+ return `../${toLang}/${file}`;
+}
+
+function productHref(key, lang) {
+ if (key === "massiveedit" && (lang === "en" || lang === "fr")) {
+ return "/massiveedit/en/";
+ }
+ return `/${key}/`;
+}
+
+const ICONS = {
+ chevron: ``,
+ moon: ``,
+ sun: ``,
+ menu: ``,
+};
+
+function header(lang, page) {
+ const L = copies[lang];
+ const prefix = assetPrefix(lang);
+ const home = pageHref(lang, lang, "home");
+ const about = pageHref(lang, lang, "about");
+ const products = `${home}#products`;
+
+ const nav = [
+ { label: L.nav.home, href: home, current: page === "home" },
+ { label: L.nav.products, href: products, current: false },
+ { label: L.nav.about, href: about, current: page === "about" },
+ ];
+
+ return ``;
+}
+
+function footer(lang) {
+ const L = copies[lang];
+ return ``;
+}
+
+function homeMain(lang) {
+ const L = copies[lang];
+ const products = [
+ {
+ key: "smartbuddy",
+ name: L.home.productBuddyName,
+ en: L.home.productBuddyEn,
+ body: L.home.productBuddyBody,
+ cta: L.home.productBuddyCta,
+ },
+ {
+ key: "smartinput",
+ name: L.home.productInputName,
+ en: L.home.productInputEn,
+ body: L.home.productInputBody,
+ cta: L.home.productInputCta,
+ },
+ {
+ key: "smarthanzi",
+ name: L.home.productHanziName,
+ en: L.home.productHanziEn,
+ body: L.home.productHanziBody,
+ cta: L.home.productHanziCta,
+ },
+ {
+ key: "massiveedit",
+ name: L.home.productEditName,
+ en: L.home.productEditEn,
+ body: L.home.productEditBody,
+ cta: L.home.productEditCta,
+ },
+ ];
+
+ return `
+
+
${esc(L.brand.strong)} · ${esc(L.brand.suffix)}
+
${esc(L.home.heroTitle)}
+
${esc(L.home.heroSubtitle)}
+
+
+
+
+
+
+
${esc(L.home.productsTitle)}
+
${esc(L.home.productsLead)}
+
+
+ ${products
+ .map(
+ (product) => `
+ ${esc(product.en)}
+ ${esc(product.name)}
+ ${esc(product.body)}
+ ${esc(product.cta)}
+ `,
+ )
+ .join("\n ")}
+
+
+
+ `;
+}
+
+function aboutMain(lang) {
+ const L = copies[lang];
+ return `
+
+
${esc(L.brand.strong)} · ${esc(L.brand.suffix)}
+
${esc(L.about.heroTitle)}
+
${esc(L.about.heroLead)}
+
+
+
+
+
+ ${esc(L.about.visionTitle)}
+ ${esc(L.about.visionBody)}
+
+
+ ${esc(L.about.missionTitle)}
+ ${esc(L.about.missionBody)}
+
+
+ ${esc(L.about.storyTitle)}
+ ${esc(L.about.storyBody)}
+
+
+ ${esc(L.about.teamTitle)}
+ ${esc(L.about.teamBody)}
+
+
+
+ `;
+}
+
+function documentFor(lang, page) {
+ const L = copies[lang];
+ const prefix = assetPrefix(lang);
+ const meta = page === "home" ? L.home : L.about;
+ const alternates = LANGS.map((code) => {
+ const href =
+ code === "zh"
+ ? lang === "zh"
+ ? page === "home"
+ ? "index.html"
+ : "about.html"
+ : `../${page === "home" ? "index.html" : "about.html"}`
+ : lang === "zh"
+ ? `${code}/${page === "home" ? "index.html" : "about.html"}`
+ : code === lang
+ ? page === "home"
+ ? "index.html"
+ : "about.html"
+ : `../${code}/${page === "home" ? "index.html" : "about.html"}`;
+ return ``;
+ }).join("\n ");
+
+ const defaultHref =
+ lang === "zh"
+ ? page === "home"
+ ? "index.html"
+ : "about.html"
+ : `../${page === "home" ? "index.html" : "about.html"}`;
+
+ return `
+
+
+
+
+ ${esc(meta.metaTitle)}
+
+
+ ${alternates}
+
+
+
+
+
+
+ ${header(lang, page)}
+
+ ${page === "home" ? homeMain(lang) : aboutMain(lang)}
+
+ ${footer(lang)}
+
+
+
+
+`;
+}
+
+function writePage(lang, page) {
+ const dir = lang === "zh" ? outDir : path.join(outDir, lang);
+ fs.mkdirSync(dir, { recursive: true });
+ const file = page === "home" ? "index.html" : "about.html";
+ fs.writeFileSync(path.join(dir, file), documentFor(lang, page));
+}
+
+for (const lang of LANGS) {
+ writePage(lang, "home");
+ writePage(lang, "about");
+}
+
+console.log("Wrote static portal pages to html/");
diff --git a/docs/scripts/product-sites.mjs b/docs/scripts/product-sites.mjs
new file mode 100644
index 0000000..c7b9e73
--- /dev/null
+++ b/docs/scripts/product-sites.mjs
@@ -0,0 +1,115 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const PRODUCT_DIRS = ['smartbuddy', 'smartinput', 'smarthanzi', 'massiveedit', 'html'];
+
+const SKIP_NAMES = new Set([
+ '.DS_Store',
+ '.texpadtmp',
+ 'smartbuddy_readme',
+]);
+
+const SKIP_EXT = new Set([
+ '.tex',
+ '.aux',
+ '.log',
+ '.toc',
+ '.out',
+ '.synctex.gz',
+]);
+
+const MIME = {
+ '.html': 'text/html; charset=utf-8',
+ '.css': 'text/css; charset=utf-8',
+ '.js': 'text/javascript; charset=utf-8',
+ '.mjs': 'text/javascript; charset=utf-8',
+ '.json': 'application/json; charset=utf-8',
+ '.svg': 'image/svg+xml',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.webp': 'image/webp',
+ '.gif': 'image/gif',
+ '.ico': 'image/x-icon',
+ '.woff2': 'font/woff2',
+ '.txt': 'text/plain; charset=utf-8',
+ '.md': 'text/plain; charset=utf-8',
+ '.apk': 'application/vnd.android.package-archive',
+ '.deb': 'application/vnd.debian.binary-package',
+ '.pkg': 'application/octet-stream',
+ '.exe': 'application/octet-stream',
+ '.zip': 'application/zip',
+};
+
+export function shouldCopy(src) {
+ const base = path.basename(src);
+ if (SKIP_NAMES.has(base)) return false;
+ if (base.startsWith('.')) return false;
+ if (SKIP_EXT.has(path.extname(base))) return false;
+ return true;
+}
+
+export function copyProductSites(destRoot, sourceRoot = process.cwd()) {
+ for (const dir of PRODUCT_DIRS) {
+ const from = path.join(sourceRoot, dir);
+ if (!fs.existsSync(from)) continue;
+ fs.cpSync(from, path.join(destRoot, dir), {
+ recursive: true,
+ filter: shouldCopy,
+ });
+ }
+}
+
+function resolveProductFile(root, urlPath) {
+ const clean = urlPath.split('?')[0];
+ const product = PRODUCT_DIRS.find(
+ (name) => clean === `/${name}` || clean === `/${name}/` || clean.startsWith(`/${name}/`),
+ );
+ if (!product) return null;
+
+ let rel = clean.slice(product.length + 2);
+ if (!rel || rel.endsWith('/')) rel = `${rel}index.html`;
+
+ const file = path.join(root, product, rel);
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
+
+ const asIndex = path.join(root, product, rel, 'index.html');
+ if (fs.existsSync(asIndex) && fs.statSync(asIndex).isFile()) return asIndex;
+
+ return null;
+}
+
+function serveProduct(root, req, res, next) {
+ const file = resolveProductFile(root, req.url ?? '');
+ if (!file) return next();
+ res.statusCode = 200;
+ res.setHeader('Content-Type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
+ fs.createReadStream(file).pipe(res);
+}
+
+export function productSites() {
+ const root = process.cwd();
+ return {
+ name: 'product-sites',
+ hooks: {
+ 'astro:config:setup'({ updateConfig }) {
+ updateConfig({
+ vite: {
+ plugins: [
+ {
+ name: 'serve-product-sites',
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => serveProduct(root, req, res, next));
+ },
+ },
+ ],
+ },
+ });
+ },
+ 'astro:build:done'({ dir }) {
+ copyProductSites(fileURLToPath(dir));
+ },
+ },
+ };
+}
diff --git a/docs/src/components/Footer.astro b/docs/src/components/Footer.astro
new file mode 100644
index 0000000..6ce0f6a
--- /dev/null
+++ b/docs/src/components/Footer.astro
@@ -0,0 +1,50 @@
+---
+import { hrefFor, hrefForProduct, type Lang, type SiteCopy } from '../i18n';
+
+interface Props {
+ lang: Lang;
+ copy: SiteCopy;
+}
+
+const { lang, copy } = Astro.props;
+---
+
+
diff --git a/src/components/Header.astro b/docs/src/components/Header.astro
similarity index 75%
rename from src/components/Header.astro
rename to docs/src/components/Header.astro
index 5a46aed..1f53ff4 100644
--- a/src/components/Header.astro
+++ b/docs/src/components/Header.astro
@@ -1,6 +1,14 @@
---
import { Icon } from 'astro-icon/components';
-import { LANGS, hrefFor, langLabels, type Lang, type RouteKey, type SiteCopy } from '../i18n';
+import {
+ LANGS,
+ hrefFor,
+ hrefForHomeHash,
+ langLabels,
+ type Lang,
+ type RouteKey,
+ type SiteCopy,
+} from '../i18n';
interface Props {
lang: Lang;
@@ -10,12 +18,10 @@ interface Props {
const { lang, copy, currentSegment } = Astro.props;
-const navItems: { label: string; segment: RouteKey }[] = [
- { label: copy.nav.home, segment: '' },
- { label: copy.nav.philosophy, segment: 'teaching-philosophy' },
- { label: copy.nav.curriculum, segment: 'curriculum' },
- { label: copy.nav.faculty, segment: 'faculty' },
- { label: copy.nav.about, segment: 'about' },
+const navItems: { label: string; href: string; current: boolean }[] = [
+ { label: copy.nav.home, href: hrefFor(lang, ''), current: currentSegment === '' },
+ { label: copy.nav.products, href: hrefForHomeHash(lang, 'products'), current: false },
+ { label: copy.nav.about, href: hrefFor(lang, 'about'), current: currentSegment === 'about' },
];
---
@@ -30,10 +36,7 @@ const navItems: { label: string; segment: RouteKey }[] = [