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) => ``, + ) + .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 }[] = [