diff --git a/package.json b/package.json
index 2f33a1b..09b9243 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"type": "module",
"workspaces": [
"theme",
- "bot"
+ "bot",
+ "web"
],
"scripts": {
"build": "npm run build --workspace theme",
@@ -16,6 +17,11 @@
"bot:typecheck": "npm run typecheck -w bot",
"bot:dev": "npm run dev -w bot",
"bot:deploy": "npm run deploy -w bot",
- "bot:commands": "npm run commands -w bot"
+ "bot:commands": "npm run commands -w bot",
+ "web:test": "npm test -w web",
+ "web:typecheck": "npm run typecheck -w web",
+ "web:dev": "npm run dev -w web",
+ "web:build": "npm run build -w web",
+ "web:capture": "npm run capture -w web"
}
}
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..7336934
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ araharah
+
+
+
+
+
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..5d8f052
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@araharah/web",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "node test/runner.mjs",
+ "typecheck": "tsc --noEmit",
+ "dev": "vite",
+ "build": "vite build",
+ "capture": "node scripts/capture.mjs"
+ },
+ "dependencies": {
+ "maplibre-gl": "^6.15.0",
+ "@araharah/theme": "*"
+ },
+ "devDependencies": {
+ "vite": "^8.3.5",
+ "typescript": "^5.9.3",
+ "playwright": "^1.50.0"
+ }
+}
diff --git a/web/src/main.ts b/web/src/main.ts
new file mode 100644
index 0000000..686efa1
--- /dev/null
+++ b/web/src/main.ts
@@ -0,0 +1,10 @@
+/*
+ * main.ts — the Mini App entry module (D-01, D-02, D-11).
+ * --------------------------------------------------------
+ * The one-and-only DOM entry: web/index.html loads it as `type="module"`
+ * into ``. This plan (03-01) scaffolds the workspace; the
+ * Telegram WebApp binding behind the narrow TelegramWebAppLike interface
+ * and the map shell mount here in plans 03-02 and 03-03. Kept
+ * dependency-free until those plans introduce their modules.
+ */
+export {};
diff --git a/web/test/harness.mjs b/web/test/harness.mjs
new file mode 100644
index 0000000..bebcfdc
--- /dev/null
+++ b/web/test/harness.mjs
@@ -0,0 +1,10 @@
+/*
+ * harness.mjs — zero-dependency test registry (D-12).
+ * Suites import { test } and register async cases; runner.mjs drains the registry.
+ * Same hand-rolled shape as theme/scripts/audit.test.mjs (Phase 1 precedent).
+ */
+const tests = [];
+export function test(name, fn) {
+ tests.push({ name, fn });
+}
+export const registry = tests;
diff --git a/web/test/runner.mjs b/web/test/runner.mjs
new file mode 100644
index 0000000..333fe2f
--- /dev/null
+++ b/web/test/runner.mjs
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+/*
+ * runner.mjs — hand-rolled zero-dependency test runner (D-02, D-11).
+ *
+ * Auto-discovers *.test.mjs in this directory (sorted), dynamic-imports
+ * them, and drains the harness registry. A suite that fails to LOAD is
+ * recorded as one failing case — the run never crashes mid-way.
+ * Same hand-rolled shape as theme/scripts/audit.test.mjs (Phase 1)
+ * and bot/test/ (Phase 2) — copied verbatim, workspace-agnostic.
+ */
+import { readdirSync } from "node:fs";
+import { registry } from "./harness.mjs";
+
+const dir = import.meta.dirname;
+const suites = readdirSync(dir)
+ .filter((f) => f.endsWith(".test.mjs"))
+ .sort();
+
+for (const file of suites) {
+ try {
+ await import(`./${file}`);
+ } catch (loadErr) {
+ registry.push({
+ name: `suite load failed: ${file}`,
+ fn: () => {
+ throw loadErr;
+ },
+ });
+ }
+}
+
+let passedCount = 0;
+let failedCount = 0;
+for (const { name, fn } of registry) {
+ try {
+ await fn();
+ passedCount++;
+ console.log(`PASS ${name}`);
+ } catch (err) {
+ failedCount++;
+ console.log(`FAIL ${name}`);
+ console.error(` ${err && err.stack ? err.stack : String(err)}`);
+ }
+}
+
+console.log(`[runner] ${passedCount} passed, ${failedCount} failed`);
+process.exitCode = failedCount > 0 ? 1 : 0;
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..44e869e
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2023",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "types": ["vite/client"],
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "verbatimModuleSyntax": true,
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src"]
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..65f294e
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ base: "/",
+ build: {
+ outDir: "dist",
+ emptyOutDir: true,
+ target: "es2022",
+ },
+ server: {
+ port: 5173,
+ strictPort: true,
+ },
+});