mirror of
https://github.com/tiennm99/try-gstack.git
synced 2026-05-23 16:25:53 +00:00
v0.0.1.0 chore: scaffold Hình Học Sống (Astro + GitHub Pages) (#1)
* chore: add gstack skill routing rules to CLAUDE.md Append a "## Skill routing" section so future Claude Code sessions in this repo route each task through the matching gstack skill (e.g., /investigate for bugs, /ship for PRs, /office-hours for product brainstorming). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: scaffold Astro 5 + TypeScript strict with base /try-gstack/ Initialize the project per the autoplan-locked decisions: - Astro 5 SSG, output static, base path /try-gstack/ for GitHub Pages subdirectory hosting - TypeScript strict (Astro's strict tsconfig + path alias ~/* -> src/*) - @astrojs/sitemap for SEO meta - bun as the package manager (lockfile committed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: Vietnamese landing page with Be Vietnam Pro + i18n stub Implement the locked design decisions D2 (typography) and D3 (palette) plus the i18n discipline from autoplan eng review: - Be Vietnam Pro single family (woff2, weights 400/500/700, vietnamese subset) self-hosted via @fontsource so KaTeX/CDN-blocking ISPs cannot break the page - font-feature-settings: "kern", "locl" for proper VN diacritic positioning - 17px body / 1.6 line-height / max-w-prose 56ch (denser than English) - 3-color SGK-aligned palette in tailwind config: pair1 #D7263D, pair2 #1B998B, pair3 #F46036 - BaseLayout with lang="vi", canonical URL, OpenGraph (vi_VN), X-Frame-Options DENY - src/i18n/vi.ts holds every user-facing string; t() helper resolves the active locale. Adding English later means adding en.ts; no string churn through templates. - Landing page lists three grade cards with "Sắp ra mắt" status (modules ship later) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: bootstrap Vitest with pure geom-engine vec module Establish the math-engine boundary and test discipline from autoplan eng decision E2: - src/geom-engine/ is pure (no DOM imports allowed); first module is vec.ts - Vec2 helpers: vec, add, sub, scale, dot, len, dist, normalize, approxEqualLen - EPSILON_LEN=0.5 viewBox units justified vs ~4px human drag precision - scale() normalizes IEEE-754 -0 back to +0 so consumers don't see signed-zero ghosts - Vitest config gates the module at 95% line/function/statement, 90% branch - 16 unit tests covering commutativity, mutation safety, orthogonality, normalization, and EPSILON tolerance behavior Property tests via fast-check come with the first canvas module; this commit establishes the test scaffold so adding them later is one dependency away. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: GitHub Actions test workflow + Pages deploy via deploy-pages@v4 Wire the autoplan-locked CI/CD pipeline (decision E3) for GitHub Pages hosting: - ci.yml: typecheck + tests + build on every PR and push to main - deploy.yml: build + actions/deploy-pages@v4 on push to main, concurrency-grouped so a force-push retry doesn't abort an in-flight rollback - Build env pins SITE_URL=https://tiennm99.github.io and SITE_BASE=/try-gstack so astro.config.mjs produces correct canonical/OG URLs against the subdirectory host - bun 1.3.13 pinned for both workflows Lighthouse-CI + size-limit gates deferred until the first canvas module ships (no JS bundle to budget yet — current pages are zero-JS). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: VERSION 0.0.1.0 + CHANGELOG + RUNBOOK + README rewrite Initial 4-digit gstack version (matches package.json), changelog entry for the scaffold, operations runbook covering rollback and the deferred .vn domain trigger (500 sessions/30d OR 1 organic teacher share OR 5+ modules shipped). README rewritten as the project README for Hình Học Sống with the locked architectural decisions visible to teammates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: TODOS.md tracking deferred autoplan items Capture the deferred work surfaced by the /office-hours + /autoplan reviews so the backlog is visible to teammates and bisectable from the source-of-truth design doc. Organized by component (Phase 0 distribution, TheoremCanvas, Module 3/1/2, testing infrastructure, retention, domain) then priority P0–P4. Per the gstack TODOS.md format conventions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Unit tests
|
||||
run: bun run test
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
env:
|
||||
SITE_URL: https://tiennm99.github.io
|
||||
SITE_BASE: /try-gstack
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# build output
|
||||
dist/
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
bun-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# editor
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# tooling
|
||||
.eslintcache
|
||||
*.tsbuildinfo
|
||||
|
||||
# test output
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -0,0 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to **Hình Học Sống** are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: 4-digit MAJOR.MINOR.PATCH.MICRO per gstack.
|
||||
|
||||
## [0.0.1.0] - 2026-04-29
|
||||
|
||||
### Added
|
||||
|
||||
- Astro project scaffold with TypeScript strict mode and `base: '/try-gstack/'` for GitHub Pages subdirectory hosting.
|
||||
- Vietnamese-first BaseLayout with `lang="vi"`, canonical URL, OpenGraph, robots meta, `X-Frame-Options: DENY`.
|
||||
- Be Vietnam Pro typography (self-hosted via `@fontsource/be-vietnam-pro`, Vietnamese subset, weights 400/500/700) with locked sizes/line-heights and `font-feature-settings: kern, locl`.
|
||||
- Tailwind 3 with the locked SGK 3-color palette (#D7263D / #1B998B / #F46036) and `max-w-prose: 56ch`.
|
||||
- Landing page (hub) with three placeholder grade cards (lớp 7 / 8 / 9), each marked "Sắp ra mắt".
|
||||
- `src/i18n/vi.ts` + `t()` helper — every user-facing string routed through i18n from day 1 so English can ship later by adding `en.ts`.
|
||||
- `src/geom-engine/` pure module (no DOM imports) with `Vec2`, `add/sub/scale/dot/len/dist/normalize`, `EPSILON_LEN = 0.5`, and `approxEqualLen` helper.
|
||||
- Vitest with v8 coverage, 95% line/function/statement and 90% branch threshold against the geom-engine module.
|
||||
- GitHub Actions: `ci.yml` runs typecheck + tests + build on PRs and pushes to `main`; `deploy.yml` deploys to GitHub Pages from `main` via `actions/deploy-pages@v4`.
|
||||
- `RUNBOOK.md` with rollback procedure and domain-purchase trigger thresholds (500 sessions / 30d, 1 organic teacher share, or ≥5 modules shipped).
|
||||
@@ -57,3 +57,21 @@ For **all** web browsing, use the `/browse` skill from gstack.
|
||||
- `/unfreeze`
|
||||
- `/gstack-upgrade`
|
||||
- `/learn`
|
||||
|
||||
## Skill routing
|
||||
|
||||
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
|
||||
|
||||
Key routing rules:
|
||||
- Product ideas/brainstorming → invoke /office-hours
|
||||
- Strategy/scope → invoke /plan-ceo-review
|
||||
- Architecture → invoke /plan-eng-review
|
||||
- Design system/plan review → invoke /design-consultation or /plan-design-review
|
||||
- Full review pipeline → invoke /autoplan
|
||||
- Bugs/errors → invoke /investigate
|
||||
- QA/testing site behavior → invoke /qa or /qa-only
|
||||
- Code review/diff check → invoke /review
|
||||
- Visual polish → invoke /design-review
|
||||
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
||||
- Save progress → invoke /context-save
|
||||
- Resume context → invoke /context-restore
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
# try-gstack
|
||||
# Hình Học Sống
|
||||
|
||||
A sandbox repo for trying out [gstack](https://github.com/garrytan/gstack) — a collection of slash-command skills for Claude Code covering planning, code review, deploys, design, browser automation, and more.
|
||||
Interactive Vietnamese THCS (lớp 7–9) geometry visualizer. Drag the points; watch the theorems hold. Aligned to the SGK curriculum.
|
||||
|
||||
## Purpose
|
||||
> Sandbox repo currently doubling as a [gstack](https://github.com/garrytan/gstack) trial — see `CLAUDE.md`.
|
||||
|
||||
This repo exists to experiment with gstack workflows in isolation, without polluting other projects. Use it to:
|
||||
## Status
|
||||
|
||||
- Explore the available gstack slash commands
|
||||
- Test setup steps (`/setup-deploy`, `/setup-browser-cookies`, `/setup-gbrain`)
|
||||
- Try planning and review flows (`/plan-eng-review`, `/plan-design-review`, `/review`, `/ship`)
|
||||
- Practice with the `/browse` skill instead of `mcp__claude-in-chrome__*` tools
|
||||
Scaffold only (v0.0.1.0). Modules are placeholders — landing page lists three grades with "Sắp ra mắt" badges. See the design doc at `~/.gstack/projects/tiennm99-try-gstack/` for the full plan.
|
||||
|
||||
## Installing gstack
|
||||
## Develop
|
||||
|
||||
```sh
|
||||
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
|
||||
cd ~/.claude/skills/gstack && ./setup
|
||||
bun install
|
||||
bun run dev # http://localhost:4321/try-gstack/
|
||||
bun run test # Vitest (geom-engine unit tests)
|
||||
bun run typecheck # Astro check + TS strict
|
||||
bun run build # Static output to dist/
|
||||
```
|
||||
|
||||
See `CLAUDE.md` for the project-level convention on which gstack skills are in scope here.
|
||||
## Deploy
|
||||
|
||||
Auto-deploys to GitHub Pages from `main` via `actions/deploy-pages@v4`. See `RUNBOOK.md` for rollback + domain-migration procedure.
|
||||
|
||||
## Architecture (locked decisions, per autoplan review)
|
||||
|
||||
- **Static**: Astro SSG, `base: '/try-gstack/'`, output `dist/`
|
||||
- **Typography**: Be Vietnam Pro single family, weights 400/500/700, woff2 only, Vietnamese subset
|
||||
- **Math engine**: pure `src/geom-engine/` module (no DOM imports), Vitest unit tests; property tests with `fast-check` to be added with first canvas module
|
||||
- **Canvas**: vanilla SVG + Pointer Events + `setPointerCapture` (no Konva) — to be added with first canvas module
|
||||
- **Math rendering**: KaTeX, bundled locally + SSR-rendered — to be added with first canvas module
|
||||
- **Animation**: Web Animations API + `requestAnimationFrame` (no GSAP / Motion One / Lottie)
|
||||
- **Analytics**: Cloudflare Web Analytics, deferred until 50+ daily sessions
|
||||
- **i18n**: every string via `t()` from `src/i18n/vi.ts`; English added by adding `en.ts`
|
||||
|
||||
## License
|
||||
|
||||
TBD.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Runbook — Hình Học Sống
|
||||
|
||||
## Deployment
|
||||
|
||||
Production deploys from `main` via GitHub Actions (`.github/workflows/deploy.yml`).
|
||||
|
||||
- Live URL: `https://tiennm99.github.io/try-gstack/`
|
||||
- Build: `bun run build` (Astro static, output to `dist/`)
|
||||
- Deploy mechanism: `actions/upload-pages-artifact@v3` + `actions/deploy-pages@v4`
|
||||
- Concurrency: `pages-deploy` group, cancel-in-progress disabled (so a force-pushed retry doesn't abort an in-flight rollback)
|
||||
|
||||
## Rollback
|
||||
|
||||
GitHub Pages keeps the last successful deployment. To roll back:
|
||||
|
||||
1. Find the offending commit on `main`: `git log --oneline main`
|
||||
2. Revert it: `git revert <sha>` and push to `main`
|
||||
3. The next workflow run rebuilds and re-deploys the prior good state
|
||||
4. Verify the live site
|
||||
|
||||
If the workflow itself is broken (e.g., bad `deploy.yml` change), use the GitHub UI to re-run a previous successful deployment from the **Deployments** tab.
|
||||
|
||||
## CI
|
||||
|
||||
- `.github/workflows/ci.yml` runs typecheck + Vitest + build on every PR and push to `main`. Build failure on `main` blocks the deploy job.
|
||||
- Local equivalent: `bun install && bun run typecheck && bun run test && bun run build`
|
||||
|
||||
## Domain migration trigger
|
||||
|
||||
Per autoplan plan, buy a `.vn` / `.com.vn` domain only when ANY of:
|
||||
|
||||
1. **500 unique sessions** in any rolling 30-day window post-launch, OR
|
||||
2. **1 organic teacher share** (Facebook group, Zalo, or school chat — verified, not founder-initiated), OR
|
||||
3. **≥5 modules shipped** (signals content sustainability and amortizes the domain cost)
|
||||
|
||||
If none hit within 90 days of soft launch, stay on `tiennm99.github.io/try-gstack/`.
|
||||
|
||||
When the trigger fires:
|
||||
|
||||
1. Register the domain (VN registration: passport scan + address proof + MIC filing, 7–14 days)
|
||||
2. Add `CNAME` file at repo root with the new domain (e.g. `hinhhocsong.vn`)
|
||||
3. Configure custom domain in GitHub Pages settings (Settings → Pages → Custom domain)
|
||||
4. Update `astro.config.mjs`: change `SITE_BASE` env from `/try-gstack` to `/`, point `SITE_URL` at the new domain. Or update the workflow `env:` block.
|
||||
5. Update OpenGraph + canonical URLs (handled automatically once `astro.config.mjs` reflects the new domain)
|
||||
6. Wait 24h, then update sitemap submission in Google Search Console
|
||||
7. Set up 301 redirects (GitHub Pages handles this automatically once the custom domain is the primary)
|
||||
|
||||
## Things to NOT do
|
||||
|
||||
- Never `git push --force` to `main`.
|
||||
- Never edit `VERSION` or `package.json.version` independently — they must agree (`/ship` enforces this).
|
||||
- Never change `astro.config.mjs` `base` without simultaneously updating the deploy workflow `env:` and any hardcoded internal links.
|
||||
@@ -0,0 +1,145 @@
|
||||
# TODOS
|
||||
|
||||
Tracked work, organized by component then priority (P0 highest → P4 lowest). See `~/.gstack/projects/tiennm99-try-gstack/tiennm99-main-design-20260429-160610.md` for the full plan.
|
||||
|
||||
## Distribution & Validation (Phase 0, pre-build)
|
||||
|
||||
- **Phase 0 SERP audit**
|
||||
**Priority:** P0
|
||||
**What:** 2-hour Google.com.vn / YouTube VN / TikTok VN audit on `góc nội tiếp`, `tam giác bằng nhau`, `tam giác đồng dạng`. Document positions 1–10 + dominant content type.
|
||||
**Why:** Determines whether text page can win SERP or whether we pivot to video-first.
|
||||
**Output:** `competitive-landscape.md` companion file.
|
||||
|
||||
- **Phase 0 TikTok / Shorts validation**
|
||||
**Priority:** P0
|
||||
**What:** 5 short videos (10–30s) of mock drag interactions (Figma + Lottie + screen-record). Post to test the visual-wow thesis BEFORE writing module code.
|
||||
**Why:** If videos flop, the entire visualizer wedge is dead before a weekend is invested.
|
||||
|
||||
- **Vetted Facebook group entry**
|
||||
**Priority:** P1
|
||||
**What:** Join "Hỏi đáp Toán THCS", "Toán học vui", "Giáo viên Toán". Post one helpful, non-self-promotional answer in each. Earn standing.
|
||||
**Why:** Distribution plan depends on these groups; founder must have presence before the launch post.
|
||||
|
||||
## TheoremCanvas primitive (weekend 1, before any module)
|
||||
|
||||
- **Build TheoremCanvas as single HTML file first**
|
||||
**Priority:** P1
|
||||
**What:** `<TheoremCanvas mode="..." initial={...} client:visible />` API. SVG canvas + Pointer Events drag + setPointerCapture per vertex. AbortController teardown. Per autoplan eng decision E1.
|
||||
**Where:** prototype as standalone `prototype.html`, then port into `src/components/TheoremCanvas.astro` once API feels right.
|
||||
|
||||
- **Vertex drag affordance + 48px hit-target**
|
||||
**Priority:** P1
|
||||
**What:** 14px filled circle, 2px white stroke, 4px drop shadow, 48px transparent square hit-target, `tabindex=0`, focused = 3px #D7263D ring. Per Design Decision D1.
|
||||
|
||||
- **Keyboard navigation for vertices**
|
||||
**Priority:** P1
|
||||
**What:** Tab cycles vertices; arrows ±4px; shift+arrow ±16px; Enter snaps to nearest interesting position. Per Design F6.1.
|
||||
|
||||
## Module 3 — Lớp 9 Góc nội tiếp (weekend 2, hero module)
|
||||
|
||||
- **Drag M around circle, project to circle constraint**
|
||||
**Priority:** P1
|
||||
**What:** `M = center + r·normalize(pointer − center)` every `pointermove`. No free movement. Per Eng failure mode #4.
|
||||
|
||||
- **Theorem panel + 3 worked SGK-style examples**
|
||||
**Priority:** P1
|
||||
|
||||
- **First-load coaching state** (per Design F2.2)
|
||||
**Priority:** P2
|
||||
**What:** Animated ghost-finger gesture for 1.5s, "Kéo điểm M để khám phá" caption, dismissible, persisted in localStorage.
|
||||
|
||||
- **ARIA-live announcements on dragend**
|
||||
**Priority:** P2
|
||||
**What:** "Góc AMB bằng 47 độ" announced via `aria-live="polite"`. Per Design F6.4.
|
||||
|
||||
## Module 1 — Lớp 7 Tam giác bằng nhau (weekend 3)
|
||||
|
||||
- **SSS / SAS / ASA / AAS / cạnh huyền-góc nhọn / cạnh huyền-cạnh góc vuông detectors**
|
||||
**Priority:** P1
|
||||
**What:** Pure geometry functions in `src/geom-engine/congruence.ts`. Use `EPSILON_LEN = 0.5`. Color-pair highlighting + green "Hai tam giác bằng nhau" badge.
|
||||
**Note:** Rigid-motion overlay animation EXPLICITLY DROPPED per autoplan (was scope creep).
|
||||
|
||||
- **SGK tick-mark encoding**
|
||||
**Priority:** P1
|
||||
**What:** 1/2/3 ticks for matching sides, 1/2/3 arcs for matching angles. Always paired with the 3-color palette (#D7263D / #1B998B / #F46036). Per Design Decision D3 — single largest a11y + pedagogical unlock.
|
||||
|
||||
## Module 2 — Lớp 8 Tam giác đồng dạng (weekend 4)
|
||||
|
||||
- **Similarity ratio + AA/SAS/SSS-similar detectors**
|
||||
**Priority:** P1
|
||||
**What:** `src/geom-engine/similarity.ts`. Live ratio display via numeric text-node updates only (KaTeX template rendered at build, never re-parsed during drag).
|
||||
|
||||
- **Scale slider clamped to [0.5, 2.0]**
|
||||
**Priority:** P1
|
||||
**What:** `min=0.5, max=2, step=0.05`. Cannot reach 0. Per Eng failure mode #3.
|
||||
|
||||
## Testing & CI infrastructure (with first canvas module)
|
||||
|
||||
- **fast-check property tests**
|
||||
**Priority:** P1
|
||||
**What:** `∀ M on arc, |∠AMB − ∠AM₀B| < 0.5°` (inscribed-angle invariance). `∀ pointer, dist(projectToCircle(p, c), c.center) = c.r ± ε`.
|
||||
|
||||
- **Playwright e2e + multi-touch tests**
|
||||
**Priority:** P1
|
||||
**What:** Per-module happy-path + keyboard-path + multi-touch-path. Use `device: 'Pixel 5'` for touch profiles. Per autoplan test plan.
|
||||
|
||||
- **size-limit bundle gate**
|
||||
**Priority:** P2
|
||||
**What:** 200KB gz JS / 50KB gz CSS / 80KB woff2 fonts. CI fail-fast.
|
||||
|
||||
- **Lighthouse-CI gate**
|
||||
**Priority:** P2
|
||||
**What:** Mobile preset, ≥90 Perf + A11y. Run against `bun preview`.
|
||||
|
||||
- **axe-playwright a11y tests**
|
||||
**Priority:** P2
|
||||
**What:** Zero axe-violations on every module page.
|
||||
|
||||
- **Visual regression for tick-marks + Vietnamese diacritics**
|
||||
**Priority:** P2
|
||||
**What:** Argos / Playwright snapshot baselines.
|
||||
|
||||
## Retention & Distribution (post-MVP)
|
||||
|
||||
- **Zalo / Telegram subscribe CTA on every module**
|
||||
**Priority:** P2
|
||||
**What:** "Nhận theorem mới mỗi tuần" — turns hub into sequence. Per CEO finding (theme 4).
|
||||
|
||||
- **Lớp-9 course-spine "next theorem" footer card**
|
||||
**Priority:** P2
|
||||
**What:** Each lớp-9 module links forward to next theorem. Per Design F3.5.
|
||||
|
||||
- **OpenGraph image generation per module**
|
||||
**Priority:** P2
|
||||
**What:** Pre-rendered SVG-to-PNG of canvas hero state. Critical for FB / Zalo shares.
|
||||
|
||||
- **Schema.org `LearningResource` meta**
|
||||
**Priority:** P3
|
||||
**What:** Per-module structured data for Google rich results.
|
||||
|
||||
## Domain & infra (post-validation trigger)
|
||||
|
||||
- **Buy `.vn` / `.com.vn` domain**
|
||||
**Priority:** P1
|
||||
**What:** TRIGGER: 500 sessions/30d OR 1 organic teacher share OR ≥5 modules shipped (per RUNBOOK). VN registration takes 7–14 days.
|
||||
|
||||
- **Cloudflare Web Analytics**
|
||||
**Priority:** P2
|
||||
**What:** JS-injected, no cookies, no consent banner. Activate after first 50 daily sessions.
|
||||
|
||||
- **`PUBLIC_VERSION` env wiring**
|
||||
**Priority:** P3
|
||||
**What:** Currently `index.astro` reads `import.meta.env.PUBLIC_VERSION ?? '0.0.1.0'` — env never set. Wire it from CI build (read VERSION file) so footer doesn't rot on bump.
|
||||
|
||||
## Out of scope (deferred indefinitely or to v2)
|
||||
|
||||
- English / bilingual support
|
||||
- Embed-as-iframe API (CSP frame-ancestors work)
|
||||
- Comments via Giscus
|
||||
- Account / auth / email capture
|
||||
- Browser extension overlay (out of static scope)
|
||||
- Anki-style spaced-repetition deck (different product shape)
|
||||
- Print → digital QR bridge (distribution experiment, not engineering)
|
||||
- Teacher-tool / B2B2C demo mode (build after Module 3 ships)
|
||||
|
||||
## Completed
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
const siteUrl = process.env.SITE_URL ?? 'https://tiennm99.github.io';
|
||||
const basePath = process.env.SITE_BASE ?? '/try-gstack';
|
||||
|
||||
export default defineConfig({
|
||||
site: siteUrl,
|
||||
base: basePath,
|
||||
trailingSlash: 'always',
|
||||
output: 'static',
|
||||
integrations: [tailwind({ applyBaseStyles: false }), sitemap()],
|
||||
build: {
|
||||
assets: 'assets',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "try-gstack",
|
||||
"version": "0.0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Hình Học Sống — Interactive Vietnamese THCS geometry visualizer (lớp 7-9). Static site, drag-to-explore theorems.",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"typecheck": "astro check",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.4.0",
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"@fontsource/be-vietnam-pro": "^5.2.0",
|
||||
"astro": "^5.14.0",
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"@types/node": "^22.10.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
add,
|
||||
approxEqualLen,
|
||||
dist,
|
||||
dot,
|
||||
EPSILON_LEN,
|
||||
len,
|
||||
normalize,
|
||||
scale,
|
||||
sub,
|
||||
vec,
|
||||
} from './vec';
|
||||
|
||||
describe('add', () => {
|
||||
it('is commutative', () => {
|
||||
const a = vec(2, 3);
|
||||
const b = vec(-1, 4);
|
||||
expect(add(a, b)).toEqual(add(b, a));
|
||||
});
|
||||
|
||||
it('returns a new object (does not mutate)', () => {
|
||||
const a = vec(1, 2);
|
||||
const b = vec(3, 4);
|
||||
const before = { ...a };
|
||||
add(a, b);
|
||||
expect(a).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sub', () => {
|
||||
it('is the inverse of add', () => {
|
||||
const a = vec(5, 7);
|
||||
const b = vec(2, 1);
|
||||
expect(sub(add(a, b), b)).toEqual(a);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scale', () => {
|
||||
it('multiplies both components by k', () => {
|
||||
expect(scale(vec(2, 3), 4)).toEqual(vec(8, 12));
|
||||
});
|
||||
|
||||
it('handles k = 0', () => {
|
||||
expect(scale(vec(7, -3), 0)).toEqual(vec(0, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('dot', () => {
|
||||
it('computes the standard inner product', () => {
|
||||
expect(dot(vec(1, 2), vec(3, 4))).toBe(11);
|
||||
});
|
||||
|
||||
it('returns 0 for orthogonal vectors', () => {
|
||||
expect(dot(vec(1, 0), vec(0, 1))).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('len and dist', () => {
|
||||
it('len of (3,4) is 5', () => {
|
||||
expect(len(vec(3, 4))).toBe(5);
|
||||
});
|
||||
|
||||
it('dist is symmetric', () => {
|
||||
const a = vec(1, 2);
|
||||
const b = vec(4, 6);
|
||||
expect(dist(a, b)).toBeCloseTo(dist(b, a));
|
||||
});
|
||||
|
||||
it('dist of a point with itself is 0', () => {
|
||||
expect(dist(vec(7, -3), vec(7, -3))).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalize', () => {
|
||||
it('returns a unit vector for non-zero input', () => {
|
||||
const n = normalize(vec(3, 4));
|
||||
expect(len(n)).toBeCloseTo(1, 10);
|
||||
});
|
||||
|
||||
it('returns the zero vector for the zero vector', () => {
|
||||
expect(normalize(vec(0, 0))).toEqual(vec(0, 0));
|
||||
});
|
||||
|
||||
it('preserves direction', () => {
|
||||
const n = normalize(vec(2, 0));
|
||||
expect(n).toEqual(vec(1, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('approxEqualLen with EPSILON_LEN', () => {
|
||||
it('treats values within epsilon as equal', () => {
|
||||
expect(approxEqualLen(10, 10 + EPSILON_LEN / 2)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects values outside epsilon', () => {
|
||||
expect(approxEqualLen(10, 10 + EPSILON_LEN * 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses 0.5 as the default tolerance (per autoplan eng decision)', () => {
|
||||
expect(EPSILON_LEN).toBe(0.5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
export interface Vec2 {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
|
||||
export const EPSILON_LEN = 0.5;
|
||||
export const EPSILON_ANGLE_DEG = 0.5;
|
||||
|
||||
export function vec(x: number, y: number): Vec2 {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function add(a: Vec2, b: Vec2): Vec2 {
|
||||
return { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
|
||||
export function sub(a: Vec2, b: Vec2): Vec2 {
|
||||
return { x: a.x - b.x, y: a.y - b.y };
|
||||
}
|
||||
|
||||
export function scale(a: Vec2, k: number): Vec2 {
|
||||
// `+ 0` normalizes IEEE-754 -0 back to +0 so consumers comparing coordinates
|
||||
// with === or Object.is don't see a signed-zero ghost from k=0 paths.
|
||||
return { x: a.x * k + 0, y: a.y * k + 0 };
|
||||
}
|
||||
|
||||
export function dot(a: Vec2, b: Vec2): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
export function len(a: Vec2): number {
|
||||
return Math.hypot(a.x, a.y);
|
||||
}
|
||||
|
||||
export function dist(a: Vec2, b: Vec2): number {
|
||||
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
export function normalize(a: Vec2): Vec2 {
|
||||
const l = len(a);
|
||||
if (l === 0) return { x: 0, y: 0 };
|
||||
return { x: a.x / l, y: a.y / l };
|
||||
}
|
||||
|
||||
export function approxEqualLen(a: number, b: number, eps = EPSILON_LEN): boolean {
|
||||
return Math.abs(a - b) < eps;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { vi } from './vi';
|
||||
|
||||
const locales = { vi } as const;
|
||||
|
||||
export type LocaleKey = keyof typeof locales;
|
||||
|
||||
const defaultLocale: LocaleKey = 'vi';
|
||||
|
||||
export function t(): typeof vi {
|
||||
return locales[defaultLocale];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export const vi = {
|
||||
site: {
|
||||
title: 'Hình Học Sống',
|
||||
tagline: 'Học hình bằng cách kéo',
|
||||
description:
|
||||
'Trang web tương tác giúp học sinh THCS (lớp 7–9) hiểu hình học bằng cách kéo và quan sát các định lý sống động.',
|
||||
},
|
||||
hub: {
|
||||
intro:
|
||||
'Mỗi định lý là một hình động. Kéo các điểm và xem định lý vẫn đúng. Đã chuẩn theo sách giáo khoa.',
|
||||
chooseGrade: 'Chọn lớp của bạn:',
|
||||
},
|
||||
grade: {
|
||||
'lop-7': {
|
||||
title: 'Lớp 7',
|
||||
hero: 'Tam giác bằng nhau',
|
||||
blurb: 'SSS, SAS, ASA — kéo hai tam giác để xem khi nào chúng bằng nhau.',
|
||||
status: 'sap-ra-mat',
|
||||
},
|
||||
'lop-8': {
|
||||
title: 'Lớp 8',
|
||||
hero: 'Tam giác đồng dạng',
|
||||
blurb:
|
||||
'AA, SAS, SSS đồng dạng. Kéo để xem tỉ số cạnh giữ nguyên khi tam giác phóng to.',
|
||||
status: 'sap-ra-mat',
|
||||
},
|
||||
'lop-9': {
|
||||
title: 'Lớp 9',
|
||||
hero: 'Góc nội tiếp',
|
||||
blurb:
|
||||
'Kéo điểm M trên đường tròn — góc nội tiếp giữ nguyên khi M ở cùng cung.',
|
||||
status: 'sap-ra-mat',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
'sap-ra-mat': 'Sắp ra mắt',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Locale = typeof vi;
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import '~/styles/global.css';
|
||||
import { t } from '~/i18n';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const copy = t();
|
||||
const { title = copy.site.title, description = copy.site.description } = Astro.props;
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site).href;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:locale" content="vi_VN" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta http-equiv="X-Frame-Options" content="DENY" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import BaseLayout from '~/layouts/BaseLayout.astro';
|
||||
import { t } from '~/i18n';
|
||||
|
||||
const copy = t();
|
||||
const grades = [
|
||||
{ key: 'lop-7' as const, ...copy.grade['lop-7'] },
|
||||
{ key: 'lop-8' as const, ...copy.grade['lop-8'] },
|
||||
{ key: 'lop-9' as const, ...copy.grade['lop-9'] },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<main class="mx-auto max-w-prose px-5 py-10">
|
||||
<header class="mb-10">
|
||||
<h1 class="mb-2">{copy.site.title}</h1>
|
||||
<p class="text-lg" style="color:#444">{copy.site.tagline}</p>
|
||||
</header>
|
||||
|
||||
<section class="mb-10">
|
||||
<p>{copy.hub.intro}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-4">{copy.hub.chooseGrade}</h2>
|
||||
<ul class="space-y-3">
|
||||
{
|
||||
grades.map((grade) => (
|
||||
<li class="rounded-lg border border-neutral-300 p-4">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<strong>{grade.title}</strong>
|
||||
<span class="text-sm uppercase tracking-wide" style="color:#888">
|
||||
{copy.status[grade.status]}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 font-medium">{grade.hero}</div>
|
||||
<p class="mt-2 text-base">{grade.blurb}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer class="mt-16 border-t border-neutral-200 pt-6 text-sm" style="color:#888">
|
||||
Phiên bản v{import.meta.env.PUBLIC_VERSION ?? '0.0.1.0'}
|
||||
</footer>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,59 @@
|
||||
@import '@fontsource/be-vietnam-pro/400.css';
|
||||
@import '@fontsource/be-vietnam-pro/500.css';
|
||||
@import '@fontsource/be-vietnam-pro/700.css';
|
||||
@import '@fontsource/be-vietnam-pro/vietnamese-400.css';
|
||||
@import '@fontsource/be-vietnam-pro/vietnamese-500.css';
|
||||
@import '@fontsource/be-vietnam-pro/vietnamese-700.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-feature-settings: 'kern', 'locl';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'Be Vietnam Pro', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #111;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 56ch;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1B998B;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 3px solid #D7263D;
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Be Vietnam Pro"', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
// Locked palette per autoplan Decision D3 (a11y + SGK tick-marks).
|
||||
pair1: '#D7263D',
|
||||
pair2: '#1B998B',
|
||||
pair3: '#F46036',
|
||||
},
|
||||
maxWidth: {
|
||||
prose: '56ch',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
},
|
||||
"verbatimModuleSyntax": false
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/geom-engine/**/*.ts'],
|
||||
exclude: ['src/geom-engine/**/*.test.ts'],
|
||||
thresholds: {
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 90,
|
||||
statements: 95,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user