-
- ✈️ たびたび
-
-
旅のしおりを、サクッと作成
-
-
-
-
-
-
-
-
- {#if activeTab === "create"}
-
-
-
- {#if titleError}
-
{titleError}
- {/if}
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/apps/web/src/routes/home/CreateForm.svelte b/apps/web/src/routes/home/CreateForm.svelte
new file mode 100644
index 0000000..582886b
--- /dev/null
+++ b/apps/web/src/routes/home/CreateForm.svelte
@@ -0,0 +1,350 @@
+
+
+
+
+
diff --git a/apps/web/src/routes/home/FeatureCard.svelte b/apps/web/src/routes/home/FeatureCard.svelte
new file mode 100644
index 0000000..29d6dc0
--- /dev/null
+++ b/apps/web/src/routes/home/FeatureCard.svelte
@@ -0,0 +1,200 @@
+
+
+
+
+
{title}
+
{description}
+
+
+
diff --git a/apps/web/src/routes/home/FlyingAirplane.svelte b/apps/web/src/routes/home/FlyingAirplane.svelte
new file mode 100644
index 0000000..3a9257f
--- /dev/null
+++ b/apps/web/src/routes/home/FlyingAirplane.svelte
@@ -0,0 +1,290 @@
+
+
+
+ {#each trail as point (point.id)}
+
+ {/each}
+
+
+
+
+
+
+
diff --git a/apps/web/src/routes/home/Footer.svelte b/apps/web/src/routes/home/Footer.svelte
new file mode 100644
index 0000000..400dbb3
--- /dev/null
+++ b/apps/web/src/routes/home/Footer.svelte
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/apps/web/src/routes/home/PreviewCarousel.svelte b/apps/web/src/routes/home/PreviewCarousel.svelte
new file mode 100644
index 0000000..39a8307
--- /dev/null
+++ b/apps/web/src/routes/home/PreviewCarousel.svelte
@@ -0,0 +1,558 @@
+
+
+
+
+ {#each previews as preview, i}
+
+
+
+
+ {#if preview.layout === "list"}
+
+
{preview.title}
+
+
+ {#each preview.steps as step, j}
+
+ {step.time}
+ {step.label}
+
+ {/each}
+
+
+ {:else if preview.layout === "timeline"}
+
+
{preview.title}
+
+
+
+
+ {#each preview.steps as step, j}
+
+
+
{step.time}
+
+ {#if j < preview.steps.length - 1}
+
+ {/if}
+
+
+
+ {step.label}
+ {#if step.location}
+ 📍 {step.location}
+ {/if}
+
+
+
+ {/each}
+
+
+
+ {:else}
+
+
{preview.title}
+
+
+ {#each preview.steps as step, j}
+
+
{step.time}
+
+ {step.label}
+ {#if step.location}
+ 📍 {step.location}
+ {/if}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+ {#each preview.features.slice(0, 3) as feature}
+ {feature}
+ {/each}
+ {#if preview.features.length > 3}
+ +{preview.features.length - 3}
+ {/if}
+
+
+ {/each}
+
+
+ {#each previews as _, i}
+
+ {/each}
+
+
+
+
diff --git a/apps/web/src/routes/home/RecentItineraries.svelte b/apps/web/src/routes/home/RecentItineraries.svelte
new file mode 100644
index 0000000..688f12f
--- /dev/null
+++ b/apps/web/src/routes/home/RecentItineraries.svelte
@@ -0,0 +1,119 @@
+
+
+{#if items.length > 0}
+
+
最近のしおり
+
+ {#each items as item}
+
+
+
+
+ {/each}
+
+
+{/if}
+
+
diff --git a/apps/web/src/routes/home/ScrollTopButton.svelte b/apps/web/src/routes/home/ScrollTopButton.svelte
new file mode 100644
index 0000000..42a0fd0
--- /dev/null
+++ b/apps/web/src/routes/home/ScrollTopButton.svelte
@@ -0,0 +1,87 @@
+
+
+{#if visible}
+
+{/if}
+
+
diff --git a/apps/web/src/routes/home/icons/IconAirplane.svelte b/apps/web/src/routes/home/icons/IconAirplane.svelte
new file mode 100644
index 0000000..1a475d9
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconAirplane.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconBolt.svelte b/apps/web/src/routes/home/icons/IconBolt.svelte
new file mode 100644
index 0000000..2011f59
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconBolt.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconBook.svelte b/apps/web/src/routes/home/icons/IconBook.svelte
new file mode 100644
index 0000000..ae976a1
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconBook.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconGitHub.svelte b/apps/web/src/routes/home/icons/IconGitHub.svelte
new file mode 100644
index 0000000..5eab360
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconGitHub.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconLink.svelte b/apps/web/src/routes/home/icons/IconLink.svelte
new file mode 100644
index 0000000..89cfe48
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconLink.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconPalette.svelte b/apps/web/src/routes/home/icons/IconPalette.svelte
new file mode 100644
index 0000000..a3543ad
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconPalette.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/IconPhone.svelte b/apps/web/src/routes/home/icons/IconPhone.svelte
new file mode 100644
index 0000000..df4c69e
--- /dev/null
+++ b/apps/web/src/routes/home/icons/IconPhone.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/apps/web/src/routes/home/icons/index.ts b/apps/web/src/routes/home/icons/index.ts
new file mode 100644
index 0000000..7745e6e
--- /dev/null
+++ b/apps/web/src/routes/home/icons/index.ts
@@ -0,0 +1,7 @@
+export { default as IconAirplane } from "./IconAirplane.svelte";
+export { default as IconPhone } from "./IconPhone.svelte";
+export { default as IconLink } from "./IconLink.svelte";
+export { default as IconPalette } from "./IconPalette.svelte";
+export { default as IconBolt } from "./IconBolt.svelte";
+export { default as IconBook } from "./IconBook.svelte";
+export { default as IconGitHub } from "./IconGitHub.svelte";
diff --git a/apps/web/src/routes/home/index.ts b/apps/web/src/routes/home/index.ts
new file mode 100644
index 0000000..8320aaf
--- /dev/null
+++ b/apps/web/src/routes/home/index.ts
@@ -0,0 +1,9 @@
+export { default as PreviewCarousel } from "./PreviewCarousel.svelte";
+export { default as FeatureCard } from "./FeatureCard.svelte";
+export { default as CreateForm } from "./CreateForm.svelte";
+export { default as RecentItineraries } from "./RecentItineraries.svelte";
+export { default as Footer } from "./Footer.svelte";
+export { default as ScrollTopButton } from "./ScrollTopButton.svelte";
+export { default as FlyingAirplane } from "./FlyingAirplane.svelte";
+export { previewItineraries } from "./previewData/index";
+export type { PreviewItinerary, PreviewStep, ThemeColors } from "./previewData/index";
diff --git a/apps/web/src/routes/home/previewData/ai-generated.ts b/apps/web/src/routes/home/previewData/ai-generated.ts
new file mode 100644
index 0000000..490f933
--- /dev/null
+++ b/apps/web/src/routes/home/previewData/ai-generated.ts
@@ -0,0 +1,23 @@
+import type { PreviewItinerary } from "./types";
+
+export const aiGeneratedPreview: PreviewItinerary = {
+ title: "沖縄旅行",
+ themeId: "ai-generated",
+ themeName: "AI Generated",
+ description: "開発中",
+ layout: "card",
+ colors: {
+ primary: "#c580d8ff",
+ secondary: "#a855f7",
+ background: "linear-gradient(135deg, #fdf2f8 0%, #faf5ff 50%, #f5f3ff 100%)",
+ text: "#6c27a5ff",
+ accent: "#f472b6",
+ border: "#e9d5ff",
+ },
+ steps: [
+ { time: "10:00", label: "那覇空港", icon: "✈️" },
+ { time: "13:00", label: "ビーチ", icon: "🏖️" },
+ { time: "19:00", label: "地元料理ディナー", icon: "🍣" },
+ ],
+ features: ["タイムライン", "開発中"],
+};
diff --git a/apps/web/src/routes/home/previewData/index.ts b/apps/web/src/routes/home/previewData/index.ts
new file mode 100644
index 0000000..a9d2df6
--- /dev/null
+++ b/apps/web/src/routes/home/previewData/index.ts
@@ -0,0 +1,11 @@
+export type { PreviewItinerary, PreviewStep, ThemeColors } from "./types";
+import { minimalPreview } from "./minimal";
+import { standardAutumnPreview } from "./standard-autumn";
+import { aiGeneratedPreview } from "./ai-generated";
+import type { PreviewItinerary } from "./types";
+
+export const previewItineraries: PreviewItinerary[] = [
+ minimalPreview,
+ standardAutumnPreview,
+ aiGeneratedPreview,
+];
diff --git a/apps/web/src/routes/home/previewData/minimal.ts b/apps/web/src/routes/home/previewData/minimal.ts
new file mode 100644
index 0000000..d6a8cfe
--- /dev/null
+++ b/apps/web/src/routes/home/previewData/minimal.ts
@@ -0,0 +1,23 @@
+import type { PreviewItinerary } from "./types";
+
+export const minimalPreview: PreviewItinerary = {
+ title: "週末おでかけ",
+ themeId: "minimal",
+ themeName: "ミニマル",
+ description: "シンプル・軽量",
+ layout: "list",
+ colors: {
+ primary: "#565656ff",
+ secondary: "#888888",
+ background: "#ffffff",
+ text: "#333333",
+ accent: "#333333",
+ border: "#eeeeee",
+ },
+ steps: [
+ { time: "10:00", label: "駅集合", icon: "" },
+ { time: "11:30", label: "ランチ", icon: "" },
+ { time: "14:00", label: "カフェ", icon: "" },
+ ],
+ features: ["タイムライン"],
+};
diff --git a/apps/web/src/routes/home/previewData/standard-autumn.ts b/apps/web/src/routes/home/previewData/standard-autumn.ts
new file mode 100644
index 0000000..4265855
--- /dev/null
+++ b/apps/web/src/routes/home/previewData/standard-autumn.ts
@@ -0,0 +1,23 @@
+import type { PreviewItinerary } from "./types";
+
+export const standardAutumnPreview: PreviewItinerary = {
+ title: "京都紅葉旅行",
+ themeId: "standard-autumn",
+ themeName: "標準",
+ description: "多機能",
+ layout: "timeline",
+ colors: {
+ primary: "#a93529",
+ secondary: "#e6b422",
+ background: "#fcf9f2",
+ text: "#4a3b32",
+ accent: "#d4762c",
+ border: "#e8e0d0",
+ },
+ steps: [
+ { time: "09:00", label: "清水寺", icon: "⛩️" },
+ { time: "12:00", label: "祇園ランチ", icon: "🍱" },
+ { time: "15:00", label: "★★ Secret ★★", icon: "🔒" },
+ ],
+ features: ["日カード", "secret機能", "walica連携"],
+};
diff --git a/apps/web/src/routes/home/previewData/types.ts b/apps/web/src/routes/home/previewData/types.ts
new file mode 100644
index 0000000..a85096c
--- /dev/null
+++ b/apps/web/src/routes/home/previewData/types.ts
@@ -0,0 +1,26 @@
+export interface PreviewStep {
+ time: string;
+ label: string;
+ icon: string;
+ location?: string;
+}
+
+export interface ThemeColors {
+ primary: string;
+ secondary: string;
+ background: string;
+ text: string;
+ accent: string;
+ border?: string;
+}
+
+export interface PreviewItinerary {
+ title: string;
+ themeId: string;
+ themeName: string;
+ description: string;
+ layout: "list" | "timeline" | "card";
+ colors: ThemeColors;
+ steps: PreviewStep[];
+ features: string[];
+}
diff --git a/tools/README.md b/tools/README.md
new file mode 100644
index 0000000..0496902
--- /dev/null
+++ b/tools/README.md
@@ -0,0 +1,8 @@
+# 単一ページを測定
+node tools/measure-site-weight.js http://localhost:5173
+
+# 詳細表示 (-v)
+node tools/measure-site-weight.js http://localhost:5173 -v
+
+# リンク先も含めて測定 (--depth)
+node tools/measure-site-weight.js http://localhost:5173 --depth 1
diff --git a/tools/measure-site-weight.js b/tools/measure-site-weight.js
new file mode 100644
index 0000000..21c92f6
--- /dev/null
+++ b/tools/measure-site-weight.js
@@ -0,0 +1,424 @@
+#!/usr/bin/env node
+
+const http = require("http");
+const https = require("https");
+const { URL } = require("url");
+const zlib = require("zlib");
+
+const COLORS = {
+ reset: "\x1b[0m",
+ bold: "\x1b[1m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ red: "\x1b[31m",
+ cyan: "\x1b[36m",
+ dim: "\x1b[2m",
+};
+
+function formatBytes(bytes) {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+}
+
+function formatTime(ms) {
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
+ return `${(ms / 1000).toFixed(2)}s`;
+}
+
+function getScoreColor(score) {
+ if (score >= 90) return COLORS.green;
+ if (score >= 50) return COLORS.yellow;
+ return COLORS.red;
+}
+
+function calculateScore(totalSize, requestCount, loadTime) {
+ let score = 100;
+
+ if (totalSize > 500 * 1024) score -= 20;
+ else if (totalSize > 200 * 1024) score -= 10;
+ else if (totalSize > 100 * 1024) score -= 5;
+
+ if (requestCount > 50) score -= 20;
+ else if (requestCount > 20) score -= 10;
+ else if (requestCount > 10) score -= 5;
+
+ if (loadTime > 3000) score -= 20;
+ else if (loadTime > 1000) score -= 10;
+ else if (loadTime > 500) score -= 5;
+
+ return Math.max(0, Math.min(100, score));
+}
+
+function fetchUrl(urlString, options = {}) {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now();
+ const parsedUrl = new URL(urlString);
+ const protocol = parsedUrl.protocol === "https:" ? https : http;
+
+ const requestOptions = {
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port || (parsedUrl.protocol === "https:" ? 443 : 80),
+ path: parsedUrl.pathname + parsedUrl.search,
+ method: "GET",
+ headers: {
+ "User-Agent": "SiteWeightMeasurer/1.0",
+ "Accept-Encoding": "gzip, deflate",
+ Accept: "*/*",
+ ...options.headers,
+ },
+ timeout: options.timeout || 30000,
+ };
+
+ const req = protocol.request(requestOptions, (res) => {
+ const chunks = [];
+ let decodedSize = 0;
+
+ const encoding = res.headers["content-encoding"];
+ let stream = res;
+
+ if (encoding === "gzip") {
+ stream = res.pipe(zlib.createGunzip());
+ } else if (encoding === "deflate") {
+ stream = res.pipe(zlib.createInflate());
+ }
+
+ stream.on("data", (chunk) => {
+ chunks.push(chunk);
+ decodedSize += chunk.length;
+ });
+
+ stream.on("end", () => {
+ const endTime = Date.now();
+ const rawSize = parseInt(res.headers["content-length"] || "0", 10);
+
+ resolve({
+ url: urlString,
+ statusCode: res.statusCode,
+ headers: res.headers,
+ contentType: res.headers["content-type"] || "unknown",
+ transferSize: rawSize || decodedSize,
+ decodedSize,
+ compressed: !!encoding,
+ loadTime: endTime - startTime,
+ body: Buffer.concat(chunks).toString("utf-8"),
+ });
+ });
+
+ stream.on("error", reject);
+ });
+
+ req.on("timeout", () => {
+ req.destroy();
+ reject(new Error(`Request timeout: ${urlString}`));
+ });
+
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+function extractResources(html, baseUrl) {
+ const resources = [];
+ const base = new URL(baseUrl);
+
+ const patterns = [
+ { regex: /
]+href=["']([^"']+)["'][^>]*>/gi, type: "stylesheet" },
+ { regex: /