<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ネタ神AI Pro - アイデアメーカー</title>
<style>
:root {
--bg: #070914;
--bg2: #111831;
--card: rgba(255, 255, 255, 0.08);
--card2: rgba(255, 255, 255, 0.13);
--text: #f5f7ff;
--muted: #aeb8df;
--line: rgba(255, 255, 255, 0.16);
--primary: #7c5cff;
--cyan: #00d4ff;
--green: #38ffad;
--yellow: #ffd35c;
--red: #ff5c7c;
--shadow: 0 22px 55px rgba(0, 0, 0, 0.35);
--radius: 22px;
}
body.light {
--bg: #eef2ff;
--bg2: #ffffff;
--card: rgba(255, 255, 255, 0.8);
--card2: rgba(255, 255, 255, 0.95);
--text: #151829;
--muted: #566179;
--line: rgba(20, 30, 60, 0.14);
--shadow: 0 18px 45px rgba(40, 60, 110, 0.16);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 20% 10%, rgba(124, 92, 255, 0.32), transparent 28%),
radial-gradient(circle at 90% 20%, rgba(0, 212, 255, 0.24), transparent 28%),
radial-gradient(circle at 50% 100%, rgba(255, 92, 124, 0.15), transparent 32%),
linear-gradient(135deg, var(--bg), var(--bg2));
transition: 0.25s;
}
button,
input,
select,
textarea {
font-family: inherit;
}
button {
border: 1px solid var(--line);
border-radius: 14px;
background: var(--card2);
color: var(--text);
padding: 11px 14px;
font-weight: 800;
cursor: pointer;
transition: 0.2s;
backdrop-filter: blur(12px);
}
button:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.btn-main {
border: none;
background: linear-gradient(135deg, var(--primary), var(--cyan));
color: white;
box-shadow: 0 16px 35px rgba(0, 212, 255, 0.2);
}
.btn-green {
border: none;
background: linear-gradient(135deg, #13bf84, var(--green));
color: #06120d;
}
.btn-red {
border-color: rgba(255, 92, 124, 0.4);
background: rgba(255, 92, 124, 0.14);
}
.app {
width: min(1380px, 94%);
margin: 0 auto;
padding: 28px 0 70px;
}
header {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: center;
margin-bottom: 22px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.logo {
width: 58px;
height: 58px;
border-radius: 20px;
background: linear-gradient(135deg, var(--primary), var(--cyan));
display: grid;
place-items: center;
font-size: 30px;
box-shadow: 0 20px 45px rgba(124, 92, 255, 0.35);
}
h1, h2, h3, h4, p {
margin-top: 0;
}
.brand h1 {
margin: 0;
font-size: clamp(27px, 4vw, 46px);
letter-spacing: 0.03em;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 14px;
}
.header-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.hero {
border: 1px solid var(--line);
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
padding: 26px;
margin-bottom: 22px;
overflow: hidden;
position: relative;
}
.hero::after {
content: "";
position: absolute;
width: 320px;
height: 320px;
right: -120px;
bottom: -160px;
border-radius: 50%;
background: rgba(0, 212, 255, 0.13);
filter: blur(8px);
}
.hero h2 {
font-size: clamp(24px, 3vw, 40px);
margin-bottom: 10px;
line-height: 1.35;
}
.hero p {
color: var(--muted);
line-height: 1.8;
max-width: 900px;
margin-bottom: 0;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.stat {
border: 1px solid var(--line);
background: var(--card);
border-radius: 18px;
padding: 16px;
box-shadow: var(--shadow);
}
.stat strong {
display: block;
font-size: 24px;
margin-bottom: 3px;
}
.stat span {
color: var(--muted);
font-size: 13px;
}
.layout {
display: grid;
grid-template-columns: 420px 1fr;
gap: 22px;
align-items: start;
}
.panel {
border: 1px solid var(--line);
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
overflow: hidden;
}
.panel-header {
padding: 18px 20px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 19px;
}
.panel-body {
padding: 20px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background: linear-gradient(135deg, var(--green), var(--cyan));
color: #06121c;
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
color: var(--muted);
font-size: 13px;
font-weight: 800;
margin-bottom: 8px;
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.22);
color: var(--text);
border-radius: 14px;
padding: 12px 13px;
outline: none;
font-size: 15px;
transition: 0.2s;
}
body.light input,
body.light select,
body.light textarea {
background: rgba(255, 255, 255, 0.85);
}
select option {
background: #10162a;
color: white;
}
input:focus,
select:focus,
textarea:focus {
border-color: rgba(0, 212, 255, 0.85);
box-shadow: 0 0 0 4px rgba(0, 212, 255, 0.12);
}
textarea {
min-height: 105px;
resize: vertical;
line-height: 1.7;
}
.two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.chip {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.08);
color: var(--muted);
border-radius: 999px;
padding: 8px 10px;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: 0.2s;
}
.chip:hover {
color: var(--text);
border-color: rgba(0, 212, 255, 0.7);
transform: translateY(-1px);
}
.button-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.button-grid .wide-btn {
grid-column: 1 / -1;
}
.result-tools {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.empty {
text-align: center;
padding: 70px 24px;
color: var(--muted);
}
.empty .icon {
font-size: 64px;
margin-bottom: 12px;
}
.idea-list {
display: grid;
gap: 16px;
}
.idea-card {
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.18);
border-radius: 20px;
overflow: hidden;
}
body.light .idea-card {
background: rgba(255, 255, 255, 0.78);
}
.idea-top {
padding: 20px;
border-bottom: 1px solid var(--line);
display: grid;
grid-template-columns: 1fr auto;
gap: 15px;
align-items: start;
}
.idea-title {
margin: 0;
font-size: clamp(24px, 3vw, 36px);
line-height: 1.25;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.pill {
font-size: 12px;
font-weight: 900;
padding: 6px 9px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
background: rgba(255,255,255,0.07);
}
.score-box {
width: 96px;
text-align: center;
padding: 12px;
border-radius: 18px;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.35), rgba(0, 212, 255, 0.24));
border: 1px solid var(--line);
}
.score-box strong {
display: block;
font-size: 26px;
}
.score-box span {
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.catch {
padding: 16px 20px;
font-size: 17px;
line-height: 1.7;
background: rgba(255, 255, 255, 0.07);
border-bottom: 1px solid var(--line);
}
.idea-body {
padding: 20px;
}
.sections {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.section {
border: 1px solid var(--line);
border-radius: 17px;
padding: 16px;
background: rgba(0, 0, 0, 0.18);
}
body.light .section {
background: rgba(255, 255, 255, 0.62);
}
.section.wide {
grid-column: 1 / -1;
}
.section h4 {
margin: 0 0 10px;
font-size: 15px;
}
.section p,
.section li {
color: var(--muted);
line-height: 1.75;
font-size: 14px;
}
.section p {
margin-bottom: 0;
}
.section ul,
.section ol {
margin: 0;
padding-left: 22px;
}
.idea-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 20px 20px;
}
.history-controls {
display: grid;
grid-template-columns: 1fr 180px;
gap: 10px;
margin-bottom: 14px;
}
.history-list {
display: grid;
gap: 10px;
}
.history-item {
border: 1px solid var(--line);
border-radius: 16px;
padding: 13px;
cursor: pointer;
background: rgba(0, 0, 0, 0.15);
transition: 0.2s;
}
body.light .history-item {
background: rgba(255, 255, 255, 0.7);
}
.history-item:hover {
transform: translateY(-2px);
border-color: rgba(0, 212, 255, 0.6);
}
.history-item strong {
display: block;
margin-bottom: 5px;
}
.history-item small {
color: var(--muted);
}
.toast {
position: fixed;
right: 20px;
bottom: 20px;
background: rgba(10, 15, 30, 0.94);
color: white;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 14px 18px;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(20px);
pointer-events: none;
transition: 0.25s;
z-index: 100;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.footer {
margin-top: 28px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1050px) {
header {
flex-direction: column;
align-items: flex-start;
}
.layout {
grid-template-columns: 1fr;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.sections {
grid-template-columns: 1fr;
}
.idea-top {
grid-template-columns: 1fr;
}
.score-box {
width: 100%;
}
}
@media (max-width: 620px) {
.two,
.button-grid,
.history-controls,
.stats {
grid-template-columns: 1fr;
}
.header-actions {
width: 100%;
}
.header-actions button {
flex: 1;
}
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="brand">
<div class="logo">💡</div>
<div>
<h1>ネタ神AI Pro</h1>
<p>API不要。ブラウザだけで動く創作・Webサービス企画メーカー</p>
</div>
</div>
<div class="header-actions">
<button onclick="toggleTheme()">テーマ切替</button>
<button onclick="downloadText()">TXT出力</button>
<button onclick="downloadJSON()">JSON出力</button>
<button class="btn-red" onclick="clearAll()">全削除</button>
</div>
</header>
<section class="hero">
<h2>APIなしでも、かなり使える「企画書生成ツール」にする。</h2>
<p>
外部AIに接続せず、ローカルのテンプレート・ランダム生成・条件分岐だけで、Webサービス、AIツール、ゲーム、小説、SNSなどの企画案を作ります。
API料金もキー管理も不要です。まず作品として公開しやすい形です。
</p>
</section>
<section class="stats">
<div class="stat">
<strong id="statIdeas">0</strong>
<span>今回生成した案</span>
</div>
<div class="stat">
<strong id="statSaved">0</strong>
<span>保存済みアイデア</span>
</div>
<div class="stat">
<strong>0円</strong>
<span>API利用料</span>
</div>
<div class="stat">
<strong>100%</strong>
<span>ローカル動作</span>
</div>
</section>
<main class="layout">
<section class="panel">
<div class="panel-header">
<h3>生成条件</h3>
<span class="badge">NO API</span>
</div>
<div class="panel-body">
<div class="two">
<div class="form-group">
<label for="genre">ジャンル</label>
<select id="genre">
<option>Webサービス</option>
<option>AIツール</option>
<option>ゲーム</option>
<option>小説</option>
<option>SNS</option>
<option>動画サイト</option>
<option>ポートフォリオ</option>
<option>便利ツール</option>
<option>学習サービス</option>
<option>創作支援</option>
</select>
</div>
<div class="form-group">
<label for="mood">雰囲気</label>
<select id="mood">
<option>かっこいい</option>
<option>やさしい</option>
<option>近未来</option>
<option>ファンタジー</option>
<option>シンプル</option>
<option>高級感</option>
<option>かわいい</option>
<option>ダーク</option>
<option>実用的</option>
<option>ゲーム風</option>
</select>
</div>
</div>
<div class="two">
<div class="form-group">
<label for="level">開発難易度</label>
<select id="level">
<option>簡単</option>
<option>普通</option>
<option>本格</option>
<option>超本格</option>
</select>
</div>
<div class="form-group">
<label for="target">ターゲット</label>
<select id="target">
<option>個人クリエイター</option>
<option>学生</option>
<option>社会人</option>
<option>在宅ワーカー</option>
<option>ゲーム制作者</option>
<option>小説家志望</option>
<option>配信者</option>
<option>初心者</option>
<option>副業したい人</option>
</select>
</div>
</div>
<div class="two">
<div class="form-group">
<label for="amount">生成数</label>
<select id="amount">
<option value="1">1個</option>
<option value="3" selected>3個</option>
<option value="5">5個</option>
</select>
</div>
<div class="form-group">
<label for="style">出力スタイル</label>
<select id="style">
<option>企画書風</option>
<option>サービス紹介風</option>
<option>開発メモ風</option>
<option>ピッチ資料風</option>
</select>
</div>
</div>
<div class="form-group">
<label for="keywords">キーワード</label>
<textarea id="keywords" placeholder="例:AI / RPG / SNS / メモ / 仕事 / 創作 / ポートフォリオ"></textarea>
<div class="chips">
<span class="chip" onclick="addKeyword('AI')">AI</span>
<span class="chip" onclick="addKeyword('RPG')">RPG</span>
<span class="chip" onclick="addKeyword('SNS')">SNS</span>
<span class="chip" onclick="addKeyword('小説')">小説</span>
<span class="chip" onclick="addKeyword('仕事')">仕事</span>
<span class="chip" onclick="addKeyword('メモ')">メモ</span>
<span class="chip" onclick="addKeyword('ポートフォリオ')">ポートフォリオ</span>
<span class="chip" onclick="addKeyword('動画')">動画</span>
<span class="chip" onclick="addKeyword('学習')">学習</span>
<span class="chip" onclick="addKeyword('ゲーム開発')">ゲーム開発</span>
</div>
</div>
<div class="form-group">
<label for="problem">解決したい悩み</label>
<textarea id="problem" placeholder="例:何を作ればいいかわからない。作業が続かない。アイデアを整理できない。"></textarea>
</div>
<div class="button-grid">
<button class="btn-main wide-btn" onclick="generateIdeas()">アイデア生成</button>
<button onclick="randomSet()">ランダム条件</button>
<button onclick="makePractical()">現実的にする</button>
<button onclick="makeFantasy()">派手にする</button>
<button onclick="clearForm()">入力クリア</button>
</div>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h3>生成結果</h3>
<div class="result-tools">
<button onclick="copyAll()">コピー</button>
<button class="btn-green" onclick="saveAll()">全部保存</button>
</div>
</div>
<div class="panel-body">
<div id="result">
<div class="empty">
<div class="icon">🧠</div>
<h2>まだアイデアはありません</h2>
<p>左の条件を入れて「アイデア生成」を押してください。</p>
</div>
</div>
</div>
</section>
</main>
<section class="panel" style="margin-top:22px;">
<div class="panel-header">
<h3>保存したアイデア</h3>
<span class="badge" id="savedCount">0件</span>
</div>
<div class="panel-body">
<div class="history-controls">
<input id="historySearch" placeholder="保存アイデアを検索" oninput="renderHistory()" />
<select id="historyGenre" onchange="renderHistory()">
<option value="all">全ジャンル</option>
<option>Webサービス</option>
<option>AIツール</option>
<option>ゲーム</option>
<option>小説</option>
<option>SNS</option>
<option>動画サイト</option>
<option>ポートフォリオ</option>
<option>便利ツール</option>
<option>学習サービス</option>
<option>創作支援</option>
</select>
</div>
<div class="history-list" id="historyList"></div>
</div>
</section>
<div class="footer">
ネタ神AI Pro / APIなしローカル版 / HTML・CSS・JavaScriptのみ
</div>
</div>
<div class="toast" id="toast">完了しました</div>
<script>
const DATA = {
titleHeads: [
"Nova", "Idea", "Neta", "Mira", "Chrono", "Elder", "Prompt", "Vision",
"Craft", "Yume", "Neo", "Astra", "Luna", "Meta", "Spark", "Quest"
],
titleTails: {
"Webサービス": ["Hub", "Works", "Base", "Cloud", "Studio", "Panel", "Link", "Board"],
"AIツール": ["AI", "Brain", "Agent", "Prompt", "Copilot", "Mind", "Assist", "Genius"],
"ゲーム": ["Quest", "Chronicle", "Saga", "Blade", "Dungeon", "Legend", "Arc", "World"],
"小説": ["Novel", "Story", "Tale", "Script", "Lore", "Ink", "Scene", "Dream"],
"SNS": ["Verse", "Circle", "Post", "Talk", "Room", "Link", "Wave", "Nest"],
"動画サイト": ["Tube", "Stream", "Clip", "Vision", "Cast", "Channel", "View", "Media"],
"ポートフォリオ": ["Portfolio", "Gallery", "Works", "Profile", "Card", "Showcase", "Archive", "Page"],
"便利ツール": ["Tool", "Memo", "Desk", "Kit", "Task", "Quick", "Utility", "Simple"],
"学習サービス": ["Learn", "Study", "Lesson", "Skill", "Academy", "Trainer", "Coach", "Note"],
"創作支援": ["Create", "Maker", "Muse", "Seed", "Craft", "Atelier", "Generator", "Factory"]
},
moodDesc: {
"かっこいい": "鋭く洗練された印象で、使うだけで制作意欲が上がる",
"やさしい": "初心者でも迷わない、安心感のある",
"近未来": "AI時代らしい自動化と先進性を感じる",
"ファンタジー": "クエストやギルドのような世界観を活かした",
"シンプル": "余計な機能を削り、すぐ使えることに集中した",
"高級感": "プロ向けツールのように落ち着いた印象の",
"かわいい": "親しみやすく、毎日開きたくなる",
"ダーク": "深い世界観と中二感を活かした",
"実用的": "仕事や制作の効率化に直結する",
"ゲーム風": "レベル、経験値、クエストのような要素を持つ"
},
features: {
"Webサービス": ["ユーザー投稿", "検索", "タグ分類", "お気に入り", "ランキング", "管理画面", "コメント", "カテゴリ管理", "共有リンク", "レスポンシブUI"],
"AIツール": ["文章生成", "テンプレート選択", "プロンプト保存", "履歴管理", "自動分類", "要約", "言い換え", "コピー", "お気に入り", "出力形式変更"],
"ゲーム": ["キャラクター管理", "クエスト", "ステージ選択", "スキル", "装備", "敵図鑑", "ストーリー分岐", "進行度保存", "称号", "実績"],
"小説": ["キャラ設定", "世界観管理", "章立て", "プロット", "セリフ案", "伏線メモ", "用語集", "文体変換", "シーン整理", "年表"],
"SNS": ["タイムライン", "投稿", "いいね", "フォロー", "通知", "プロフィール", "ハッシュタグ", "DM風UI", "おすすめ投稿", "AI投稿提案"],
"動画サイト": ["動画カード", "検索", "カテゴリ", "ランキング", "チャンネル", "視聴履歴", "コメント", "お気に入り", "おすすめ", "タグ"],
"ポートフォリオ": ["作品カード", "リンク管理", "カテゴリ分類", "紹介文生成", "スキル表示", "実績一覧", "検索", "テーマ変更", "外部リンク", "更新履歴"],
"便利ツール": ["メモ", "ToDo", "検索", "タグ", "自動整形", "コピー", "履歴", "エクスポート", "チェックリスト", "通知風表示"],
"学習サービス": ["学習記録", "復習リスト", "クイズ", "用語集", "進捗", "AI風解説", "弱点メモ", "計画作成", "達成バッジ", "問題生成"],
"創作支援": ["アイデア生成", "タイトル案", "キャッチコピー", "キャラ案", "世界観案", "企画書化", "画像プロンプト", "構成案", "メモ保存", "ネタ帳"]
},
monetization: [
"無料版+Pro版",
"広告表示",
"買い切り版",
"テンプレート販売",
"月額プレミアム",
"法人向けプラン",
"追加保存枠の課金",
"作品公開ページの有料カスタム",
"支援・投げ銭",
"素材パック販売"
],
risks: [
"機能を増やしすぎると完成しにくくなる",
"最初からログインや課金を入れると開発が重くなる",
"ターゲットが広すぎると特徴が薄くなる",
"保存機能の設計を後回しにすると作り直しが出やすい",
"見た目だけ作って実用性が弱いと使われにくい",
"スマホ対応を忘れると使い勝手が落ちる"
],
firstSteps: {
"簡単": ["1画面UIを作る", "入力欄と生成ボタンを作る", "結果表示を作る", "コピー機能を付ける", "ローカル保存を付ける"],
"普通": ["基本UIを作る", "複数パターン生成を作る", "履歴保存を作る", "検索と絞り込みを作る", "テキスト出力を作る"],
"本格": ["MVPを作る", "保存データ構造を決める", "ログインなし版を完成させる", "ユーザー登録版を検討する", "公開ページを整える"],
"超本格": ["小さいMVPを先に作る", "フロントとバックエンドを分ける", "DB設計をする", "課金やログインを後から追加する", "運用コストを確認する"]
}
};
let currentIdeas = [];
let generatedCount = 0;
function $(id) {
return document.getElementById(id);
}
function val(id) {
return $(id).value.trim();
}
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function shuffle(arr) {
return [...arr].sort(() => Math.random() - 0.5);
}
function escapeHTML(str) {
return String(str)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function addKeyword(word) {
const box = $("keywords");
if (!box.value.includes(word)) {
box.value = box.value ? box.value + " / " + word : word;
}
}
function randomSet() {
randomSelect("genre");
randomSelect("mood");
randomSelect("level");
randomSelect("target");
randomSelect("style");
const sets = [
"AI / メモ / 作業効率",
"RPG / クエスト / 進捗管理",
"SNS / 投稿 / AI風返信",
"小説 / 世界観 / キャラクター",
"在宅ワーク / 日報 / 整理",
"ポートフォリオ / 作品 / 自動紹介",
"動画 / まとめ / ランキング",
"学習 / 復習 / クイズ",
"ゲーム開発 / アイデア / 仕様書",
"創作 / ネタ帳 / 企画書"
];
const problems = [
"何を作ればいいかわからない",
"作業が続かない",
"アイデアが散らばって整理できない",
"作品紹介文を書くのが難しい",
"学習した内容を忘れやすい",
"毎日の進捗を見える化したい",
"企画を作っても途中で止まりやすい"
];
$("keywords").value = pick(sets);
$("problem").value = pick(problems);
showToast("ランダム条件を入れました");
}
function randomSelect(id) {
const el = $(id);
el.selectedIndex = Math.floor(Math.random() * el.options.length);
}
function makePractical() {
$("mood").value = "実用的";
$("level").value = "簡単";
$("style").value = "開発メモ風";
if (!$("problem").value) {
$("problem").value = "毎日の作業やアイデアを整理して、次にやることを明確にしたい";
}
showToast("現実的な条件に寄せました");
}
function makeFantasy() {
$("mood").value = "ファンタジー";
$("style").value = "サービス紹介風";
if (!$("keywords").value.includes("ギルド")) {
addKeyword("ギルド");
addKeyword("クエスト");
}
showToast("派手な条件に寄せました");
}
function clearForm() {
$("keywords").value = "";
$("problem").value = "";
showToast("入力をクリアしました");
}
function generateIdeas() {
const amount = Number(val("amount"));
currentIdeas = [];
for (let i = 0; i < amount; i++) {
currentIdeas.push(createIdea(i));
}
generatedCount += amount;
$("statIdeas").textContent = generatedCount;
renderIdeas();
showToast(`${amount}個のアイデアを生成しました`);
}
function createIdea(index) {
const genre = val("genre");
const mood = val("mood");
const level = val("level");
const target = val("target");
const style = val("style");
const keywords = val("keywords") || "AI / 創作 / アイデア";
const problem = val("problem") || "アイデアを整理して、作り始めやすくしたい";
const title = makeTitle(genre, keywords, index);
const features = shuffle(DATA.features[genre]).slice(0, 6);
const mvp = features.slice(0, 3);
const money = shuffle(DATA.monetization).slice(0, 3);
const risks = shuffle(DATA.risks).slice(0, 3);
const steps = DATA.firstSteps[level];
const score = calcScore(genre, level);
const keywordMain = splitKeywords(keywords)[0] || "アイデア";
return {
id: Date.now() + Math.random(),
createdAt: new Date().toLocaleString("ja-JP"),
title,
genre,
mood,
level,
target,
style,
keywords,
problem,
score,
catchcopy: makeCatch(target, mood, genre, keywordMain),
overview: makeOverview(genre, mood, target, keywords, problem, style),
unique: makeUnique(genre, mood, keywordMain),
features,
mvp,
money,
risks,
steps,
devTime: makeDevTime(level),
nextAction: makeNextAction(level, genre),
design: makeDesign(mood),
pitch: makePitch(title, target, genre, problem)
};
}
function splitKeywords(text) {
return text.split(/[\/、,\s]+/).map(x => x.trim()).filter(Boolean);
}
function makeTitle(genre, keywords, index) {
const keys = splitKeywords(keywords);
const key = keys[index % Math.max(keys.length, 1)] || "Idea";
const head = pick(DATA.titleHeads);
const tail = pick(DATA.titleTails[genre] || DATA.titleTails["創作支援"]);
const patterns = [
`${head}${tail}`,
`${key}${tail}`,
`${head} ${tail}`,
`${key}メーカー`,
`${key}ギルド`,
`${key}Forge`,
`${head}ノート`,
`${key}ラボ`,
`${head}Factory`,
`${key}クエスト`
];
return pick(patterns);
}
function makeCatch(target, mood, genre, key) {
const desc = DATA.moodDesc[mood];
const patterns = [
`${target}の「作りたい」を形にする、${desc}${genre}。`,
`${key}を起点に、企画・整理・実行まで支える${genre}。`,
`思いつきを企画に変える、${target}向けの${desc}サービス。`,
`迷っている時間を減らし、制作を前に進める${genre}。`
];
return pick(patterns);
}
function makeOverview(genre, mood, target, keywords, problem, style) {
const desc = DATA.moodDesc[mood];
if (style === "ピッチ資料風") {
return `${problem}という悩みを持つ${target}に向けて、${keywords}を軸にした${genre}を提供します。${desc}体験により、ユーザーはアイデア出しから整理、実行までを短時間で進められます。`;
}
if (style === "開発メモ風") {
return `${keywords}をテーマにした${genre}。まずは小さく作る。${target}が抱える「${problem}」を解決するため、生成、保存、検索、コピーの流れを重視する。`;
}
if (style === "サービス紹介風") {
return `この${genre}は、${target}が${keywords}に関するアイデアをすばやく整理できるサービスです。${desc}デザインで、毎日開きたくなる使い心地を目指します。`;
}
return `${keywords}をテーマにした${genre}です。${target}が抱える「${problem}」を解決するため、アイデア出し、情報整理、保存、次の行動提案をまとめて行える企画にします。`;
}
function makeUnique(genre, mood, key) {
return `${key}をただ生成するだけでなく、MVP、開発手順、収益化、注意点まで同時に出せる点が特徴です。${mood}な方向性を明確にすることで、似たような${genre}との差別化もしやすくなります。`;
}
function calcScore(genre, level) {
let score = 82;
if (level === "簡単") score += 10;
if (level === "普通") score += 5;
if (level === "本格") score -= 2;
if (level === "超本格") score -= 9;
if (genre === "便利ツール") score += 4;
if (genre === "AIツール") score += 3;
if (genre === "ゲーム") score -= 4;
if (genre === "SNS") score -= 3;
score += Math.floor(Math.random() * 9) - 4;
return Math.max(55, Math.min(98, score));
}
function makeDevTime(level) {
if (level === "簡単") return "1日〜3日";
if (level === "普通") return "1週間〜2週間";
if (level === "本格") return "1か月〜3か月";
return "3か月以上";
}
function makeNextAction(level, genre) {
if (level === "簡単") {
return `まずは${genre}の1画面版を作ります。入力欄、生成ボタン、結果表示、保存だけで完成扱いにするのが安全です。`;
}
if (level === "普通") {
return `最初にUIを作り、そのあと保存・検索・出力機能を追加します。ログイン機能は後回しで大丈夫です。`;
}
if (level === "本格") {
return `MVPを公開できる状態まで作ってから、ユーザー登録やデータベースを検討します。最初から全部入れない方が完成します。`;
}
return `超本格版は重いので、まずはプロトタイプを完成させてください。完成後にサーバー、DB、課金、ログインを分割して追加する流れが安全です。`;
}
function makeDesign(mood) {
const map = {
"かっこいい": "黒背景、青紫グラデーション、カード型UI、シャープなボタン",
"やさしい": "白背景、淡い青や緑、角丸カード、大きめ文字",
"近未来": "ダーク背景、ネオン、水色アクセント、ガラス風UI",
"ファンタジー": "羊皮紙風、ギルドカード、クエストボード風UI",
"シンプル": "白背景、余白多め、入力欄と結果表示を中心にする",
"高級感": "黒と金、細い罫線、落ち着いたカードUI",
"かわいい": "パステルカラー、丸いボタン、アイコン多め",
"ダーク": "黒、赤紫、重厚な影、世界観重視",
"実用的": "管理画面風、見出し明確、コピー・保存ボタンを目立たせる",
"ゲーム風": "ステータス画面、経験値バー、クエストカード風"
};
return map[mood] || "カード型で見やすいUI";
}
function makePitch(title, target, genre, problem) {
return `${title}は、${target}が抱える「${problem}」を解決する${genre}です。複雑な作業を整理し、次にやることを明確にすることで、制作や仕事を止めずに進められるようにします。`;
}
function renderIdeas() {
const result = $("result");
if (currentIdeas.length === 0) {
result.innerHTML = `
<div class="empty">
<div class="icon">🧠</div>
<h2>まだアイデアはありません</h2>
<p>左の条件を入れて「アイデア生成」を押してください。</p>
</div>
`;
return;
}
result.innerHTML = `
<div class="idea-list">
${currentIdeas.map(renderIdeaCard).join("")}
</div>
`;
}
function renderIdeaCard(idea, index) {
return `
<article class="idea-card">
<div class="idea-top">
<div>
<h2 class="idea-title">${escapeHTML(idea.title)}</h2>
<div class="meta">
<span class="pill">${escapeHTML(idea.genre)}</span>
<span class="pill">${escapeHTML(idea.mood)}</span>
<span class="pill">${escapeHTML(idea.level)}</span>
<span class="pill">${escapeHTML(idea.target)}</span>
<span class="pill">開発目安 ${escapeHTML(idea.devTime)}</span>
</div>
</div>
<div class="score-box">
<strong>${idea.score}</strong>
<span>実現度</span>
</div>
</div>
<div class="catch">
${escapeHTML(idea.catchcopy)}
</div>
<div class="idea-body">
<div class="sections">
<div class="section wide">
<h4>📝 概要</h4>
<p>${escapeHTML(idea.overview)}</p>
</div>
<div class="section">
<h4>🎯 解決する悩み</h4>
<p>${escapeHTML(idea.problem)}</p>
</div>
<div class="section">
<h4>🎨 デザイン方針</h4>
<p>${escapeHTML(idea.design)}</p>
</div>
<div class="section">
<h4>⚙️ 主な機能</h4>
<ul>
${idea.features.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section">
<h4>🚀 MVP機能</h4>
<ol>
${idea.mvp.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ol>
</div>
<div class="section">
<h4>💰 収益化案</h4>
<ul>
${idea.money.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section">
<h4>⚠️ リスク</h4>
<ul>
${idea.risks.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section wide">
<h4>✅ 開発ステップ</h4>
<ol>
${idea.steps.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ol>
</div>
<div class="section wide">
<h4>✨ 差別化ポイント</h4>
<p>${escapeHTML(idea.unique)}</p>
</div>
<div class="section wide">
<h4>📣 紹介文</h4>
<p>${escapeHTML(idea.pitch)}</p>
</div>
<div class="section wide">
<h4>👉 次にやること</h4>
<p>${escapeHTML(idea.nextAction)}</p>
</div>
</div>
</div>
<div class="idea-actions">
<button onclick="copyOne(${index})">この案をコピー</button>
<button onclick="saveOne(${index})">保存</button>
<button onclick="regenerateOne(${index})">この案だけ再生成</button>
</div>
</article>
`;
}
function ideaToText(idea) {
return `
【タイトル】
${idea.title}
【ジャンル】
${idea.genre}
【雰囲気】
${idea.mood}
【ターゲット】
${idea.target}
【開発難易度】
${idea.level}
【開発目安】
${idea.devTime}
【実現度】
${idea.score}点
【キャッチコピー】
${idea.catchcopy}
【解決する悩み】
${idea.problem}
【概要】
${idea.overview}
【主な機能】
${idea.features.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【MVP機能】
${idea.mvp.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【収益化案】
${idea.money.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【リスク】
${idea.risks.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【開発ステップ】
${idea.steps.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【デザイン方針】
${idea.design}
【差別化ポイント】
${idea.unique}
【紹介文】
${idea.pitch}
【次にやること】
${idea.nextAction}
`.trim();
}
function copyOne(index) {
const idea = currentIdeas[index];
if (!idea) return;
copyText(ideaToText(idea));
}
function copyAll() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
copyText(currentIdeas.map(ideaToText).join("\n\n====================\n\n"));
}
function copyText(text) {
navigator.clipboard.writeText(text)
.then(() => showToast("コピーしました"))
.catch(() => showToast("コピーに失敗しました"));
}
function regenerateOne(index) {
currentIdeas[index] = createIdea(index);
renderIdeas();
showToast("再生成しました");
}
function getSaved() {
try {
return JSON.parse(localStorage.getItem("netagami_saved")) || [];
} catch {
return [];
}
}
function setSaved(data) {
localStorage.setItem("netagami_saved", JSON.stringify(data));
updateStats();
}
function saveOne(index) {
const idea = currentIdeas[index];
if (!idea) return;
const saved = getSaved();
saved.unshift(idea);
setSaved(saved.slice(0, 100));
renderHistory();
showToast("保存しました");
}
function saveAll() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
const saved = getSaved();
setSaved([...currentIdeas, ...saved].slice(0, 100));
renderHistory();
showToast("全部保存しました");
}
function renderHistory() {
const list = $("historyList");
const saved = getSaved();
const q = $("historySearch").value.trim().toLowerCase();
const genre = $("historyGenre").value;
let filtered = saved;
if (genre !== "all") {
filtered = filtered.filter(x => x.genre === genre);
}
if (q) {
filtered = filtered.filter(x => {
return JSON.stringify(x).toLowerCase().includes(q);
});
}
$("savedCount").textContent = `${saved.length}件`;
if (filtered.length === 0) {
list.innerHTML = `<p style="color:var(--muted);">保存アイデアはありません。</p>`;
return;
}
list.innerHTML = filtered.map(item => `
<div class="history-item" onclick="loadSaved('${item.id}')">
<strong>${escapeHTML(item.title)}</strong>
<small>${escapeHTML(item.genre)} / ${escapeHTML(item.level)} / ${escapeHTML(item.createdAt)}</small>
</div>
`).join("");
}
function loadSaved(id) {
const saved = getSaved();
const idea = saved.find(x => String(x.id) === String(id));
if (!idea) return;
currentIdeas = [idea];
renderIdeas();
window.scrollTo({ top: 0, behavior: "smooth" });
showToast("保存アイデアを表示しました");
}
function downloadText() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
const text = currentIdeas.map(ideaToText).join("\n\n====================\n\n");
downloadFile("netagami-ideas.txt", text, "text/plain");
showToast("TXT出力しました");
}
function downloadJSON() {
const data = currentIdeas.length ? currentIdeas : getSaved();
if (data.length === 0) {
showToast("出力するデータがありません");
return;
}
downloadFile("netagami-ideas.json", JSON.stringify(data, null, 2), "application/json");
showToast("JSON出力しました");
}
function downloadFile(filename, content, type) {
const blob = new Blob([content], { type: type + ";charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function clearAll() {
if (!confirm("生成結果と保存履歴を削除しますか?")) return;
currentIdeas = [];
localStorage.removeItem("netagami_saved");
renderIdeas();
renderHistory();
updateStats();
showToast("削除しました");
}
function toggleTheme() {
document.body.classList.toggle("light");
localStorage.setItem("netagami_theme", document.body.classList.contains("light") ? "light" : "dark");
}
function updateStats() {
$("statSaved").textContent = getSaved().length;
}
function showToast(message) {
const toast = $("toast");
toast.textContent = message;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
}, 1800);
}
function init() {
if (localStorage.getItem("netagami_theme") === "light") {
document.body.classList.add("light");
}
$("keywords").value = "AI / 創作 / Webサービス / メモ";
$("problem").value = "何を作ればいいかわからない。アイデアを企画書レベルまで整理したい。";
renderHistory();
updateStats();
}
init();
</script>
</body>
</html>
カテゴリー: programming
C++ 静的メンバ
main.cpp
#include "rat.h"
#include <iostream>
using namespace std;
int main() {
CRat *r1, *r2, *r3;
r1 = new CRat(); // 一匹目のネズミ生成
r1->squeak();
CRat::showNum(); // ネズミの数を表示
r2 = new CRat(); // 二匹目のネズミ生成
r3 = new CRat(); // 三匹目のネズミ生成
r2->squeak();
r3->squeak();
delete r1; // 一匹目のネズミ消去
delete r2; // 二匹目のネズミ消去
CRat::showNum(); // ネズミの数を表示
delete r3; // 三匹目のネズミ消去
CRat::showNum(); // ネズミの数を表示
return 0;
}
rat.cpp
#include "rat.h"
#include <iostream>
using namespace std;
// ネズミの数の初期値を0に設定
int CRat::m_count = 0;
// コンストラクタ
CRat::CRat() : m_id(0) {
m_id = m_count; // ネズミの数を、IDとする。
m_count++; // ネズミの数を一つ増やす
}
// デストラクタ
CRat::~CRat() {
cout << "ネズミ:" << m_id << "消去" << endl;
m_count--; // ネズミの数を一つ減らす
}
// ネズミの数の出力
void CRat::showNum()
{
cout << "現在のネズミの数は、" << m_count << " 匹です。" << endl;
}
// ネズミが鳴く
void CRat::squeak()
{
cout << m_id << ":" << "チューチュー" << endl;
}
rat.h
#ifndef _RAT_H_
#define _RAT_H_
class CRat {
public:
// コンストラクタ
CRat();
// デストラクタ
~CRat();
// ネズミの数の出力
static void showNum();
// ネズミが鳴く
void squeak();
private:
// ネズミの番号
int m_id;
// ネズミの数
static int m_count;
};
#endif /* _RAT_H_ */
C++ string
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
s = "This is a"; // 最初の文字列
s.append(" pen."); // 文字列の追加
cout << s << endl;
cout << "文字列の長さ:" << s.length() << endl;
// printfで表示
printf("char*:%s\n", s.c_str());
return 0;
}
C++ コンソールから入力
#include <iostream>
using namespace std;
int main() {
int a;
cin >> a;
cout << "a=" << a << endl;
return 0;
}
C言語 printf
include
void main()
{
printf(“こんにちは。私の名前は%sです。\n年齢は%d歳です。\n”, “長留裕平”, 32);
printf(“イニシャルは、%cです。\n”, ‘T’);
printf(“%f + %f = %f\n”, 1.2, 2.7, 1.2 + 2.7);
}
C言語 HelloWorld
include<stdio.h>
void main() {
printf(“HelloWorld.\n”);
printf(“ABC\n”);
printf(“日本語でも大丈夫\n”);
}
CSSの基礎
CSSって?
- HTMLの見た目(色・余白・レイアウト・アニメ)を指定するスタイル言語。
- 重要キーワード:Cascade(優先順位の連なり)、Specificity(詳細度)、Inheritance(継承)。
1) CSSの書き方・読み込み方
<!-- 外部ファイル(推奨) -->
<link rel="stylesheet" href="styles.css">
<!-- ページ内(学習用) -->
<style>
p { color: #333; }
</style>
<!-- インライン(基本非推奨) -->
<p style="color:#333;">テキスト</p>
2) セレクタの基本
/* タイプ(要素) */ h1 { ... }
/* クラス */ .btn { ... }
/* ID */ #header { ... } /* 乱用しない */
/* 子孫・子・隣接 */ nav a { ... } nav > a { ... } h2 + p { ... }
/* 属性 */ input[type="email"] { ... }
/* 擬似クラス */ a:hover, li:nth-child(2) { ... }
/* 擬似要素 */ p::first-line, a::after { content:"→"; }
3) カスケード&優先度(Specificity)
- 計算イメージ:
ID(100) > class/属性/擬似クラス(10) > 要素/擬似要素(1) - 競合したら:後勝ち(後から書いた方が有効)
!importantは最終手段(設計悪化のもと)
4) ボックスモデル(超重要)
margin ─ 外側の余白 border ─ 枠線 padding ─ 内側の余白 content ─ 中身
* { box-sizing: border-box; } /* 幅計算が直感的になる定番 */
5) 単位&色
- 単位:
px(固定)/%(相対)/em(親のfont-size基準)/rem(ルート基準)/vw,vh(ビューポート)
→ レスポンシブは rem と%を多用。 - 色:
#222/rgb(34 34 34)/hsl(210 10% 20%)(HSLは調整しやすい) - 変数:
--brand: #5865f2;→color: var(--brand);
6) 文字・余白の基本
html { font-size: 16px; } /* remの基準 */
body { line-height: 1.7; }
h1 { font-size: clamp(1.5rem, 3vw, 2.5rem); } /* 可変サイズ */
p { margin: 0 0 1rem; }
7) レイアウト:display / Flex / Grid
display
.block { display: block; } /* 幅いっぱい */
.inline { display: inline; } /* 行内 */
.inline-block { display:inline-block; }
Flex(1次元レイアウト:横並び・縦中央寄せが得意)
.container {
display: flex;
gap: 1rem;
align-items: center; /* 交差軸整列(縦) */
justify-content: space-between; /* 主軸整列(横) */
}
.center {
display:flex; align-items:center; justify-content:center;
}
Grid(2次元レイアウト:段組が得意)
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3列 */
gap: 1rem;
}
/* レスポンシブな自動詰め */
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1rem;
}
8) 位置指定
.box { position: relative; }
.badge {
position: absolute; top: .5rem; right: .5rem;
}
header { position: sticky; top: 0; } /* スクロール追従 */
9) レスポンシブ(メディアクエリ・コンテナクエリ)
/* 画面幅が768px以上で適用(モバイル優先) */
@media (min-width: 768px) {
.nav { display: flex; }
}
/* コンテナクエリ(対応ブラウザ増) */
.card { container-type: inline-size; }
@container (min-width: 500px) {
.card__side-by-side { display:flex; }
}
10) トランジション&トランスフォーム
.btn {
transition: transform .2s ease, background-color .2s;
}
.btn:hover {
transform: translateY(-2px) scale(1.02);
background: #111;
color: #fff;
}
11) リセットとベース
/* まずはこれでOKな最小ベース */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
img, video { max-width: 100%; height: auto; display:block; }
button, input, select, textarea { font: inherit; }
:root {
--bg: #fff; --fg:#222; --muted:#6b7280; --brand:#3b82f6;
}
@media (prefers-color-scheme: dark) {
:root { --bg:#0b0b0f; --fg:#e5e7eb; --muted:#9ca3af; }
}
body { background: var(--bg); color: var(--fg); line-height:1.7; }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
12) すぐ試せるミニページ(HTML+CSS)
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS基礎デモ</title>
<style>
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, sans-serif; line-height:1.7; }
header, footer { padding:16px; background:#f5f5f5; }
.wrap { max-width:960px; margin:24px auto; padding:0 16px; }
.hero {
padding: 48px 16px; text-align:center;
background: linear-gradient(120deg, #e0f2fe, #fde68a);
border-radius:16px;
}
.btn {
display:inline-block; padding:.75rem 1rem; border-radius:9999px;
background:#111; color:#fff; transition: transform .2s ease;
}
.btn:hover { transform: translateY(-2px); }
.grid {
display:grid; gap:1rem;
grid-template-columns: repeat(auto-fit, minmax(220px,1fr));
margin-top:24px;
}
.card { border:1px solid #e5e7eb; border-radius:12px; padding:16px; }
.card h3 { margin:.25rem 0 .5rem; }
</style>
</head>
<body>
<header><div class="wrap"><strong>CSS基礎デモ</strong></div></header>
<main class="wrap">
<section class="hero">
<h1>CSSの基本を掴もう</h1>
<p>セレクタ / ボックスモデル / Flex / Grid / レスポンシブ</p>
<a class="btn" href="#cards">カードを見る</a>
</section>
<section id="cards" class="grid">
<article class="card">
<h3>セレクタ</h3>
<p>`.class` / `#id` / `a:hover` / `input[type="text"]` …</p>
</article>
<article class="card">
<h3>Flex</h3>
<p>横並び・中央寄せが簡単。`display:flex; gap:1rem;`</p>
</article>
<article class="card">
<h3>Grid</h3>
<p>2次元レイアウト。`auto-fit`×`minmax()`が実用的。</p>
</article>
</section>
</main>
<footer><div class="wrap"><small>© 2025 CSS Demo</small></div></footer>
</body>
</html>
13) つまずきポイント
- 高さが合わない:親に
align-items: stretchやheight:auto、画像にはdisplay:block。 - 中央寄せできない:インラインは
text-align:center、ブロックはmargin: 0 auto、Flexならcenter。 - 崩れる:
box-sizing:border-boxにして、余白はgap優先、width指定は最小限。 - 優先順位に勝てない:セレクタを少しだけ強くする(親クラスを1段増やすなど)。
!importantは避ける。
次に進むなら
- レイアウト設計:BEM・Utility First(Tailwind的考え方)
- モダン機能:
subgrid、container queries、logical properties(margin-inlineなど) - パフォーマンス:未使用CSSの削減、
content-visibility、will-changeの慎重な活用
HTMLの基礎
HTMLってなに?
- Webページの骨組みを作る言語(見出し・段落・画像・リンクなどの構造)。
- 見た目はCSS、動きはJSが担当。HTMLは“意味と構造”。
まずは雛形(コピペOK)
<!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>はじめてのHTML</title> <meta name="description" content="このページの説明文" /> </head> <body> <h1>こんにちは!</h1> <p>これは最小構成のHTML5ページです。</p> </body> </html>
よく使う要素(超基本)
- 見出し:
<h1>~<h6>(ページに基本はh1は1つ) - 段落:
<p> - リンク:
<a href="https://example.com">リンク</a> - 画像:
<img src="img.png" alt="画像の説明">(alt必須) - リスト:
<ul><li>…</li></ul>/<ol>…</ol> - 強調:
<strong>(重要) /<em>(強調) - 区切り:
<br>(改行は最小限)、<hr>(区切り線) - まとまり:
<div>(汎用ブロック)、<span>(汎用インライン)
セマンティック要素(構造をわかりやすく)
header(ヘッダー)nav(ナビ)main(主内容は1ページ1つ)section(章)article(単体で完結する記事)aside(補足)footer(フッター)
属性のキホン
id(一意な識別子)/class(グループ化)href(リンク先)/src(画像・スクリプト元)alt(画像代替文)/title(補足ヒント)target="_blank"はrel="noopener noreferrer"とセットで
フォーム最小例
<form action="/search" method="get"> <label for="q">検索:</label> <input id="q" name="q" type="search" required> <button type="submit">送信</button> </form>
テーブル最小例(表)
<table>
<thead><tr><th>商品</th><th>価格</th></tr></thead>
<tbody>
<tr><td>りんご</td><td>120</td></tr>
<tr><td>みかん</td><td>100</td></tr>
</tbody>
</table>
CSS / JS の読み込み
<link rel="stylesheet" href="styles.css"> <script src="app.js" defer></script>
deferはHTML解析後に実行(推奨)。
ちょっとだけ“実践的”なサンプル
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ミニサイト</title>
<style>
body { font-family: system-ui, sans-serif; line-height: 1.7; margin: 0; }
header, footer { padding: 16px; background: #f5f5f5; }
nav a { margin-right: 12px; }
main { max-width: 920px; margin: 24px auto; padding: 0 16px; }
img { max-width: 100%; height: auto; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
</style>
</head>
<body>
<header>
<h1>ミニサイト</h1>
<nav>
<a href="#about">概要</a>
<a href="#gallery">ギャラリー</a>
<a href="#contact">お問い合わせ</a>
</nav>
</header>
<main id="content">
<section id="about">
<h2>概要</h2>
<p>これはHTMLの基本で作ったミニページです。</p>
</section>
<section id="gallery">
<h2>ギャラリー</h2>
<div class="card">
<img src="sample.jpg" alt="サンプル画像">
<p>レスポンシブに画像が縮みます。</p>
</div>
<ul>
<li>箇条書き1</li>
<li>箇条書き2</li>
</ul>
</section>
<section id="contact">
<h2>お問い合わせ</h2>
<form>
<label for="name">お名前</label><br>
<input id="name" name="name" required><br><br>
<label for="msg">メッセージ</label><br>
<textarea id="msg" name="msg" rows="4"></textarea><br><br>
<button type="submit">送信</button>
</form>
</section>
</main>
<footer>
<small>© 2025 MiniSite</small>
</footer>
<script>
// ごく簡単なJS:ナビをクリックしたらスムーズスクロール
document.querySelectorAll('nav a').forEach(a => {
a.addEventListener('click', e => {
const id = a.getAttribute('href');
if (id.startsWith('#')) {
e.preventDefault();
document.querySelector(id)?.scrollIntoView({ behavior: 'smooth' });
}
});
});
</script>
</body>
</html>
初心者がつまずきやすいポイント
- 文字化け→
<meta charset="utf-8">を必ず入れる。 - スマホで拡大縮小が変→
<meta name="viewport" …>を入れる。 - 画像が大きすぎる→CSSで
img { max-width: 100%; height: auto; } - 見出し乱用→
h1はページの主題に1回、階層は順序を守る。 - altなし→スクリーンリーダー/SEO的にマイナス。必ず書く。
もっと深掘り(フォームのバリデーション、SEO、アクセシビリティ、Flex/Gridレイアウト、コンポーネント化など)もまとめられます。どこから強化したい?(例:フォームをしっかり、レイアウトを学ぶ、CSS設計、JS連携 など)
X風サイト.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>X-like UI – Polished (Left-aligned)</title>
<style>
/* ===== Design Tokens ===== */
:root{
/* colors */
--bg: #0b0d10;
--surface: #0e1116;
--panel: rgba(16,18,24,.75);
--card: #0f1319;
--line: #1c2230; /* single-pixel separators */
--text: #e7ecf3;
--muted: #9aa7ba;
--accent: #1da1f2;
--accent-2: #7dd3fc;
--accent-3: #60a5fa;
/* radius & shadow */
--r: 14px;
--shadow-sm: 0 1px 0 rgba(255,255,255,.02) inset, 0 8px 24px rgba(0,0,0,.35);
--shadow-card: 0 10px 30px rgba(0,0,0,.25);
/* layout */
--col-left: 84px;
--col-center-min: 360px;
--col-center-max: 720px;
--col-right-min: 280px;
--col-right-max: 420px;
/* NEW: tighter left gutter for center column */
--gutter-x: 10px;
/* motion */
--e1: cubic-bezier(.2,.8,.2,1);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
background:
radial-gradient(1200px 600px at 10% -10%, rgba(29,161,242,.15), transparent 40%),
radial-gradient(800px 500px at 110% -10%, rgba(96,165,250,.12), transparent 40%),
var(--bg);
color:var(--text);
font:14px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", "Hiragino Kaku Gothic ProN", "Helvetica Neue", Arial;
}
/* focus ring */
:where(a,button,[role="button"],input,textarea,.sb-btn,.ic-btn,.plus,.btn,.btn-ghost,.btn-primary){outline:none}
:where(a,button,[role="button"],input,textarea,.sb-btn,.ic-btn,.plus,.btn,.btn-ghost,.btn-primary):focus-visible{
box-shadow:0 0 0 3px rgba(29,161,242,.35);
border-radius:10px;
}
/* ===== Layout ===== */
.app{
display:grid; gap:0;
grid-template-columns: var(--col-left) minmax(var(--col-center-min),var(--col-center-max)) minmax(var(--col-right-min),var(--col-right-max));
height:100%;
max-width:1280px;
margin:0 auto;
}
.sidebar{
position:sticky; top:0; height:100vh;
backdrop-filter: blur(10px);
background:linear-gradient(180deg, rgba(16,18,24,.7), rgba(16,18,24,.3));
/* NOTE: borders are drawn via ::after to avoid subpixel drift */
padding:10px 8px; display:flex; flex-direction:column; gap:6px;
}
.sb-btn{
border-radius:999px; padding:12px 14px; cursor:pointer;
display:flex; align-items:center; gap:14px; transition:background .2s var(--e1), transform .12s var(--e1);
}
.sb-btn:hover{background:rgba(255,255,255,.04)}
.sb-btn:active{transform:translateY(1px)}
.sb-icon{width:28px;height:28px;display:grid;place-items:center;border-radius:999px}
.sb-btn .label{font-weight:700;letter-spacing:.02em}
.compose-fab{
position:fixed; left:18px; bottom:18px; z-index:50; cursor:pointer;
border:none; color:#fff; font-weight:800; letter-spacing:.02em;
border-radius:999px; padding:12px 18px;
background:linear-gradient(135deg, var(--accent), var(--accent-3));
box-shadow:0 10px 30px rgba(29,161,242,.35);
transition: transform .12s var(--e1), filter .2s var(--e1);
}
.compose-fab:hover{filter:brightness(1.05)}
.compose-fab:active{transform:translateY(1px)}
.you-chip{
position:fixed; left:18px; bottom:84px; width:48px;height:48px;border-radius:999px;
display:grid;place-items:center;font-weight:900; background:conic-gradient(from 180deg at 50% 50%, #0f629e, #0c3c68 70%, #0f629e);
color:#fff; border:1px solid rgba(255,255,255,.08);
}
.main{
position:relative; /* for ::after separator */
min-height:100vh; background:rgba(0,0,0,.25);
backdrop-filter: blur(6px);
}
.rightcol{
padding:14px; position:sticky; top:0; height:100vh; overflow:auto;
}
.searchbar{
display:flex; align-items:center; gap:10px; padding:10px 14px;
background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.06);
border-radius:999px;
}
/* ===== Single-pixel separators (no drift) ===== */
.sidebar::after,
.main::after{
content:"";
position:absolute; top:0; bottom:0; width:1px; background:var(--line);
pointer-events:none; z-index:10;
}
.sidebar::after{ right:0; } /* Sidebar ↔ Main */
.main::after{ right:0; } /* Main ↔ Right column */
/* ===== Header Tabs ===== */
.header-title{padding:10px var(--gutter-x); font-size:20px; font-weight:900; border-bottom:1px solid var(--line)}
.tabs{
position:sticky; top:0; z-index:6;
display:flex; gap:24px; padding:0 var(--gutter-x); /* <<< tightened left padding */
background:linear-gradient(180deg, rgba(14,17,22,.8), rgba(14,17,22,.55));
backdrop-filter: saturate(120%) blur(8px);
border-bottom:1px solid var(--line);
}
.tab{
padding:16px 6px; cursor:pointer; color:var(--muted); font-weight:800; border-bottom:3px solid transparent;
transition:color .2s var(--e1), border-color .2s var(--e1);
}
.tab.active{color:var(--text); border-color:var(--accent)}
/* ===== Composer ===== */
.composer{
padding:14px var(--gutter-x); /* <<< tightened */
border-bottom:1px solid var(--line); display:flex; gap:12px;
background:linear-gradient(180deg, rgba(17,22,29,.8), rgba(17,22,29,.4));
}
.avatar{
width:42px; height:42px; border-radius:999px; display:grid; place-items:center; font-weight:900; letter-spacing:.02em;
color:#fff; border:1px solid rgba(255,255,255,.08);
background:radial-gradient(120% 120% at 20% 15%, #1e81c5, #0f2a43 70%);
}
.composer-box{flex:1}
.composer textarea{
width:100%; min-height:76px; resize:vertical; background:transparent; border:none; color:var(--text);
outline:none; font-size:18px; caret-color:var(--accent);
}
.reply-scope{color:var(--accent);font-weight:700;font-size:13px}
.row{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:6px}
.icons{display:flex; gap:8px}
.ic-btn{width:32px;height:32px;display:grid;place-items:center;border-radius:10px;cursor:pointer;transition:background .2s}
.ic-btn:hover{background:rgba(255,255,255,.06)}
.post-btn{
border:none; color:#0b1220; font-weight:900; letter-spacing:.02em;
padding:9px 18px; border-radius:999px; cursor:not-allowed;
background:linear-gradient(135deg, #6b7280 0%, #9aa7ba 100%);
filter:saturate(.7); opacity:.7; transition:filter .2s, transform .12s;
}
.post-btn.enabled{
cursor:pointer; opacity:1; color:#fff; filter:none;
background:linear-gradient(135deg, var(--accent), var(--accent-3));
box-shadow:0 12px 30px rgba(29,161,242,.3);
}
.post-btn.enabled:hover{filter:brightness(1.05)}
.post-btn.enabled:active{transform:translateY(1px)}
/* ===== Cards ===== */
.card{
border-bottom:1px solid var(--line); display:flex; gap:12px;
padding:14px var(--gutter-x); /* <<< tightened */
background:linear-gradient(180deg, rgba(16,20,27,.5), rgba(16,20,27,.25));
}
.meta{display:flex; gap:6px; color:var(--muted)}
.name{font-weight:900}
.handle{color:var(--muted)}
.hash a{color:var(--accent)}
.post-img{
border-radius:18px; border:1px solid rgba(255,255,255,.06); width:100%; margin-top:10px;
box-shadow:var(--shadow-card)
}
.actions{display:flex; gap:26px; margin-top:8px; color:var(--muted)}
.action{display:flex; gap:6px; align-items:center; cursor:pointer; transition:color .15s}
.action:hover{color:var(--accent)}
/* ===== Right column ===== */
.rightcol .rc-card{
background:linear-gradient(180deg, rgba(17,22,29,.65), rgba(17,22,29,.35));
border:1px solid rgba(255,255,255,.08); border-radius:18px; overflow:hidden; margin-top:12px; box-shadow:var(--shadow-sm)
}
.rc-title{font-weight:900; padding:12px 16px; border-bottom:1px solid rgba(255,255,255,.06)}
.rc-item{padding:12px 16px; border-top:1px solid rgba(255,255,255,.04)}
.chip{background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border:1px solid rgba(255,255,255,.08);
padding:4px 10px; border-radius:999px; color:var(--muted); font-size:12px}
/* ===== Subtabs / Explore ===== */
.subtabs{display:flex; gap:18px; padding:10px var(--gutter-x); border-bottom:1px solid var(--line); background:rgba(255,255,255,.02)} /* <<< tightened */
.subtab{padding:10px 2px; color:var(--muted); font-weight:800; cursor:pointer; border-bottom:3px solid transparent}
.subtab.active{color:var(--text); border-color:var(--accent)}
/* ===== Empties / Profile / Lists ===== */
.empty{display:grid; place-items:center; padding:72px var(--gutter-x); color:var(--muted); text-align:center}
.empty h2{margin:0 0 6px; color:var(--text)}
.cover{height:170px; background:
radial-gradient(70% 120% at 10% 0%, rgba(29,161,242,.25), transparent 40%),
radial-gradient(60% 120% at 100% 0%, rgba(96,165,250,.2), transparent 40%),
linear-gradient(180deg,#121722,#0b0f17)}
.prof-wrap{padding:0 var(--gutter-x) 16px}
.prof-row{display:flex; justify-content:space-between; align-items:end; margin-top:-36px}
.pfp{width:96px;height:96px;border-radius:999px;border:4px solid var(--surface);background:radial-gradient(120% 120% at 20% 15%, #1e81c5, #0f2a43 70%);display:grid;place-items:center;color:#fff;font-weight:900}
.btn{background:transparent; border:1px solid rgba(255,255,255,.18); color:#fff; border-radius:999px; padding:8px 14px; cursor:pointer; transition:background .2s}
.btn:hover{background:rgba(255,255,255,.05)}
.alert{background:rgba(18,42,24,.65); border:1px solid #165c36; color:#a9f2b7; border-radius:14px; padding:12px 14px; margin:12px var(--gutter-x); display:flex; gap:10px; align-items:center}
.check{width:18px;height:18px;border-radius:4px;border:2px solid #a9f2b7;display:grid;place-items:center}
.list-row{display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 16px}
.list-pill{width:44px;height:44px;border-radius:12px; background:linear-gradient(135deg,#374151,#1f2937)}
.plus{width:28px;height:28px;border-radius:999px;border:1px solid rgba(255,255,255,.18);display:grid;place-items:center}
/* ===== Grok ===== */
.grok{display:grid; place-items:center; padding:44px var(--gutter-x)}
.grok .logo{font-size:28px; font-weight:1000; display:flex; align-items:center; gap:10px}
.grok .input{margin-top:18px; display:flex; gap:8px; width:min(680px,90vw)}
.grok .input input{flex:1; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.18); background:#0c1118; color:#fff; transition:border .2s}
.grok .input input:focus{border-color:rgba(125,211,252,.6)}
.grok .input button{padding:12px 16px; border-radius:12px; border:1px solid rgba(255,255,255,.18); background:linear-gradient(135deg,#10151f,#0e131b); color:#fff}
.grok .tools{display:flex; gap:8px; margin-top:12px}
/* ===== E2EE Page ===== */
.e2ee{padding:56px var(--gutter-x); text-align:center}
.e2ee h1{font-size:26px; margin:0 0 12px}
.bullet{display:flex; gap:10px; align-items:flex-start; justify-content:center; color:#cfd9ea}
.e2ee .cta{display:flex; gap:12px; justify-content:center; margin-top:18px}
.btn-ghost{border:1px solid rgba(255,255,255,.18); background:transparent; color:#fff; padding:10px 14px; border-radius:999px}
.btn-primary{border:1px solid rgba(29,161,242,.55); background:linear-gradient(135deg, var(--accent), var(--accent-2)); color:#001e33; font-weight:900; padding:10px 16px; border-radius:999px}
/* ===== Modal ===== */
.modal-backdrop{
position:fixed; inset:0; background:rgba(4,8,12,.6); display:none; align-items:center; justify-content:center; z-index:100;
animation:fadeIn .2s var(--e1);
}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.modal{
width:min(640px,92vw); background:linear-gradient(180deg, rgba(14,18,24,.95), rgba(14,18,24,.85));
border:1px solid rgba(255,255,255,.12); border-radius:20px; overflow:hidden; box-shadow:var(--shadow-card)
}
.modal .top{display:flex; align-items:center; justify-content:space-between; padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.06)}
.close-x{width:32px;height:32px;border-radius:999px;display:grid;place-items:center;cursor:pointer;transition:background .2s}
.close-x:hover{background:rgba(255,255,255,.06)}
.small{font-size:12px;color:var(--muted)}
/* ===== Toast ===== */
.toast{
position:fixed; left:50%; transform:translateX(-50%) translateY(20px);
bottom:20px; background:rgba(18,22,28,.92); border:1px solid rgba(255,255,255,.12);
padding:10px 14px; border-radius:999px; display:none; gap:10px; align-items:center; z-index:120;
box-shadow:var(--shadow-card); animation:slideUp .25s var(--e1);
}
.toast .view{background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12); padding:6px 10px; border-radius:999px}
@keyframes slideUp{from{opacity:0; transform:translateX(-50%) translateY(40px)} to{opacity:1; transform:translateX(-50%) translateY(0)}}
/* ===== Utilities ===== */
.hide{display:none !important}
/* ===== Responsive ===== */
@media (max-width:1100px){
.app{grid-template-columns: 72px 1fr}
.rightcol{display:none}
}
@media (max-width:520px){
.sb-btn .label{display:none}
.sidebar{align-items:center}
}
</style>
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Sidebar">
<div class="sb-btn" data-nav="home"><div class="sb-icon">🏠</div><div class="label">Home</div></div>
<div class="sb-btn" data-nav="explore"><div class="sb-icon">🔎</div><div class="label">Explore</div></div>
<div class="sb-btn" data-nav="notifications"><div class="sb-icon">🔔</div><div class="label">Notifications</div></div>
<div class="sb-btn" data-nav="messages"><div class="sb-icon">✉️</div><div class="label">Messages</div></div>
<div class="sb-btn" data-nav="grok"><div class="sb-icon">⚡</div><div class="label">Grok</div></div>
<div class="sb-btn" data-nav="lists"><div class="sb-icon">🗂️</div><div class="label">Lists</div></div>
<div class="sb-btn" data-nav="profile"><div class="sb-icon">👤</div><div class="label">Profile</div></div>
<div class="sb-btn" data-nav="encrypted"><div class="sb-icon">🔒</div><div class="label">Chat</div></div>
</aside>
<!-- Main -->
<main class="main" id="main">
<!-- Home -->
<section id="view-home" role="region" aria-label="Home timeline">
<div class="tabs">
<div class="tab active" data-home-tab="foryou">For you</div>
<div class="tab" data-home-tab="following">Following</div>
</div>
<div class="composer">
<div class="avatar">裕平</div>
<div class="composer-box">
<div class="reply-scope">Everyone can reply</div>
<textarea id="compose-input" placeholder="What’s happening?"></textarea>
<div class="row">
<div class="icons" aria-label="Composer actions">
<div class="ic-btn" title="Media">🖼️</div>
<div class="ic-btn" title="GIF">🌀</div>
<div class="ic-btn" title="Poll">📊</div>
<div class="ic-btn" title="Emoji">😊</div>
<div class="ic-btn" title="Schedule">🗓️</div>
<div class="ic-btn" title="Location">📍</div>
</div>
<button id="post-btn" class="post-btn" disabled>Post</button>
</div>
</div>
</div>
<article class="card">
<div class="avatar">H</div>
<div style="flex:1">
<div class="meta"><span class="name">HANA</span><span class="handle">@HANA__BRAVE · 9h</span></div>
<div>🎂 <span class="hash"><a href="#">#HAPPYJISOODAY</a></span><br/>Happy Birthday JISOO 🤍<br/><span class="hash"><a href="#">#HANA</a> <a href="#">#JISOO</a></span></div>
<img class="post-img" alt="sample" src="https://picsum.photos/seed/jisoo/720/380" />
<div class="actions">
<div class="action">💬 <span>24</span></div>
<div class="action">🔁 <span>10</span></div>
<div class="action">❤️ <span>128</span></div>
<div class="action">↗️</div>
</div>
</div>
</article>
<article class="card">
<div class="avatar">X</div>
<div style="flex:1">
<div class="meta"><span class="name">MTV VMA · LIVE</span><span class="handle"> · now</span></div>
<div class="hash"><a href="#">#VMAs</a></div>
<img class="post-img" alt="vmas" src="https://picsum.photos/seed/vma/720/300" />
<div class="actions">
<div class="action">💬 <span>8</span></div>
<div class="action">🔁 <span>3</span></div>
<div class="action">❤️ <span>42</span></div>
<div class="action">↗️</div>
</div>
</div>
</article>
<div id="feed-anchor"></div>
</section>
<!-- Explore -->
<section id="view-explore" class="hide" role="region" aria-label="Explore">
<div class="header-title">
<div class="searchbar"><span>🔎</span><input style="flex:1;background:transparent;border:none;color:#fff;outline:none" placeholder="Search" /></div>
</div>
<div class="subtabs">
<div class="subtab active">For You</div>
<div class="subtab">Trending</div>
<div class="subtab">News</div>
<div class="subtab">Sports</div>
<div class="subtab">Entertainment</div>
</div>
<article class="card">
<div class="avatar">T</div>
<div style="flex:1">
<div class="meta"><span class="name">Tokyo 2025</span><span class="handle"> · promoted</span></div>
<img class="post-img" alt="tokyo" src="https://picsum.photos/seed/tokyo2025/720/260" />
</div>
</article>
</section>
<!-- Notifications -->
<section id="view-notifications" class="hide" role="region" aria-label="Notifications">
<div class="tabs">
<div class="tab active">All</div>
<div class="tab">Verified</div>
<div class="tab">Mentions</div>
</div>
<div class="empty">
<div>
<h2>Nothing to see here — yet</h2>
<div>From likes to reposts and a whole lot more, this is where all the action happens.</div>
</div>
</div>
</section>
<!-- Messages -->
<section id="view-messages" class="hide" role="region" aria-label="Messages">
<div class="inbox" style="min-height:60vh">
<div class="dm-left" style="border-right:1px solid var(--line)">
<div style="padding:16px">
<h3 style="margin:4px 0">Welcome to your inbox!</h3>
<div class="muted">Drop a line, share posts and more with private conversations between you and others on X.</div>
<div style="height:10px"></div>
<button class="btn">Write a message</button>
</div>
</div>
<div>
<div class="empty">
<div>
<h2>Select a message</h2>
<div class="muted">Choose from your existing conversations, start a new one, or just keep swimming.</div>
<div style="height:10px"></div>
<button class="btn">New message</button>
</div>
</div>
</div>
</div>
</section>
<!-- Lists -->
<section id="view-lists" class="hide" role="region" aria-label="Lists">
<div class="header-title">Lists</div>
<div style="padding:16px">
<div class="muted" style="padding:8px 16px">Discover new Lists</div>
<div class="rc-card" role="list">
<div class="list-row">
<div style="display:flex;gap:12px;align-items:center">
<div class="list-pill"></div>
<div>
<div>J.League · <span class="muted">60 members</span></div>
<div class="muted">2K followers including @sascha348</div>
</div>
</div>
<div class="plus">+</div>
</div>
<div class="list-row">
<div style="display:flex;gap:12px;align-items:center">
<div class="list-pill"></div>
<div>
<div>Official Accounts · <span class="muted">83 members</span></div>
<div class="muted">263 followers including @dencetuno</div>
</div>
</div>
<div class="plus">+</div>
</div>
<div class="list-row">
<div style="display:flex;gap:12px;align-items:center">
<div class="list-pill"></div>
<div>
<div>kitchen · <span class="muted">52 members</span></div>
<div class="muted">181 followers including @Carolina_3254</div>
</div>
</div>
<div class="plus">+</div>
</div>
</div>
<div class="muted" style="padding:22px 16px 8px">Your Lists</div>
<div class="empty" style="opacity:.75"><div>You haven't created or followed any Lists. When you do, they'll show up here.</div></div>
</div>
</section>
<!-- Profile -->
<section id="view-profile" class="hide" role="region" aria-label="Profile">
<div class="cover"></div>
<div class="prof-wrap">
<div class="prof-row">
<div class="pfp">裕平</div>
<button class="btn">Edit profile</button>
</div>
<h2 style="margin:10px 0 0">長留裕平</h2>
<div class="muted">@PingZhang89719 · Joined September 2025</div>
<div style="height:8px"></div>
<div class="muted">0 Following · 0 Followers</div>
</div>
<div class="alert">
<div class="check">✔</div>
<div>
<div class="name" style="font-weight:900">You aren’t verified yet</div>
<div class="muted">Get verified for boosted replies, analytics, ad-free browsing, and more.</div>
</div>
</div>
<article class="card">
<div class="avatar">裕平</div>
<div style="flex:1">
<div class="meta"><span class="name">長留裕平</span><span class="handle"> · 1m</span></div>
<div>はじめました。</div>
</div>
</article>
</section>
<!-- Grok -->
<section id="view-grok" class="hide" role="region" aria-label="Grok">
<div class="grok">
<div class="logo">⚡ <span>Grok</span></div>
<div class="muted">Ask anything</div>
<div class="input">
<input placeholder="Ask anything" />
<button>➤</button>
</div>
<div class="tools">
<button class="btn">Create Images</button>
<button class="btn">Edit Image</button>
</div>
<div class="muted" style="margin-top:16px">Fast ▾ · History</div>
</div>
</section>
<!-- Encrypted Chat -->
<section id="view-encrypted" class="hide" role="region" aria-label="Encrypted Chat">
<div class="e2ee">
<h1>Meet new Chat, now fully encrypted.</h1>
<div class="muted">X Chat are now protected with end-to-end encryption on all your devices.</div>
<div style="height:12px"></div>
<div class="bullet">🔒 <div><b>End-to-End Encryption</b><br/>Your messages are protected across devices.</div></div>
<div class="bullet">🛡️ <div><b>Uncompromising Privacy</b><br/>No one — not even X — can access or read your messages.</div></div>
<div class="cta">
<button class="btn-ghost">Maybe later</button>
<button class="btn-primary">Set up now</button>
</div>
</div>
</section>
</main>
<!-- Right -->
<aside class="rightcol" aria-label="Right column">
<div class="rc-card">
<div class="rc-title">What’s happening</div>
<div class="rc-item"><b>MTV Video Music Awards 2025</b><div class="muted">LIVE</div></div>
<div class="rc-item"><b>東京2025 世界陸上</b><div class="muted">Trending · 8,724 posts</div></div>
<div class="rc-item"><b>JISOO</b><div class="chip">K-POP · Trending</div></div>
</div>
<div class="rc-card">
<div class="rc-title">Who to follow</div>
<div class="rc-item">🅿️ <b>Product Dev</b> · <span class="muted">@buildhub</span> <button class="btn" style="float:right">Follow</button></div>
<div class="rc-item">🧠 <b>AI Lab</b> · <span class="muted">@ailab</span> <button class="btn" style="float:right">Follow</button></div>
</div>
</aside>
</div>
<!-- Floating -->
<button class="compose-fab" id="open-compose">Post</button>
<div class="you-chip">裕平</div>
<!-- Modal -->
<div class="modal-backdrop" id="composer-modal" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-label="New post">
<div class="top">
<div class="close-x" id="close-compose">✕</div>
<a class="small" href="#" id="drafts-link">Drafts</a>
</div>
<div class="composer" style="border:none">
<div class="avatar">裕平</div>
<div class="composer-box">
<div class="reply-scope">Everyone can reply</div>
<textarea id="modal-input" placeholder="What’s happening?"></textarea>
<div class="row">
<div class="icons">
<div class="ic-btn">🖼️</div><div class="ic-btn">🌀</div><div class="ic-btn">📊</div><div class="ic-btn">😊</div><div class="ic-btn">🗓️</div><div class="ic-btn">📍</div>
</div>
<button id="modal-post" class="post-btn" disabled>Post</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">
<div>✅ Your post was sent.</div>
<a class="view" href="#feed-anchor">View</a>
</div>
<script>
/* ---------- Router ---------- */
const views = {
home: document.getElementById('view-home'),
explore: document.getElementById('view-explore'),
notifications: document.getElementById('view-notifications'),
messages: document.getElementById('view-messages'),
lists: document.getElementById('view-lists'),
profile: document.getElementById('view-profile'),
grok: document.getElementById('view-grok'),
encrypted: document.getElementById('view-encrypted'),
};
function show(view){
for(const k in views){ views[k].classList.add('hide'); }
(views[view]||views.home).classList.remove('hide');
window.location.hash = view;
}
document.querySelectorAll('.sb-btn').forEach(btn=>{
btn.addEventListener('click', ()=>show(btn.dataset.nav));
});
window.addEventListener('load', ()=>{
const v = location.hash.replace('#','');
if(v && views[v]) show(v);
});
/* ---------- Composer (inline) ---------- */
const composeInput = document.getElementById('compose-input');
const postBtn = document.getElementById('post-btn');
composeInput.addEventListener('input',()=>{
const on = composeInput.value.trim().length>0;
postBtn.disabled = !on; postBtn.classList.toggle('enabled', on);
});
postBtn.addEventListener('click', ()=>{
addPost(composeInput.value.trim());
composeInput.value=''; postBtn.disabled=true; postBtn.classList.remove('enabled');
showToast();
});
/* ---------- Composer (modal) ---------- */
const modal = document.getElementById('composer-modal');
const openCompose = document.getElementById('open-compose');
const closeCompose = document.getElementById('close-compose');
const modalInput = document.getElementById('modal-input');
const modalPost = document.getElementById('modal-post');
openCompose.addEventListener('click', ()=>{ modal.style.display='flex'; modalInput.focus(); });
closeCompose.addEventListener('click', ()=>{ modal.style.display='none'; });
modalInput.addEventListener('input', ()=>{
const on = modalInput.value.trim().length>0;
modalPost.disabled = !on; modalPost.classList.toggle('enabled', on);
});
modalPost.addEventListener('click', ()=>{
addPost(modalInput.value.trim());
modalInput.value=''; modalPost.disabled=true; modalPost.classList.remove('enabled');
modal.style.display='none'; showToast();
});
modal.addEventListener('click', (e)=>{ if(e.target===modal) modal.style.display='none'; });
/* ---------- Add Post ---------- */
function addPost(text){
if(!text) return;
const card = document.createElement('article');
card.className='card';
card.innerHTML = `
<div class="avatar">裕平</div>
<div style="flex:1">
<div class="meta"><span class="name">長留裕平</span><span class="handle"> · now</span></div>
<div>${escapeHTML(text)}</div>
<div class="actions">
<div class="action">💬 <span>0</span></div>
<div class="action">🔁 <span>0</span></div>
<div class="action">❤️ <span>0</span></div>
<div class="action">↗️</div>
</div>
</div>`;
const anchor = document.getElementById('feed-anchor');
anchor.parentNode.insertBefore(card, anchor);
}
function escapeHTML(s){return s.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>')}
/* ---------- Toast ---------- */
const toast = document.getElementById('toast');
function showToast(){
toast.style.display='flex';
clearTimeout(showToast._t);
showToast._t = setTimeout(()=> toast.style.display='none', 2600);
}
/* ---------- Home tab visual only ---------- */
document.querySelectorAll('[data-home-tab]').forEach(t=>{
t.addEventListener('click', ()=>{
document.querySelectorAll('[data-home-tab]').forEach(x=>x.classList.remove('active'));
t.classList.add('active');
});
});
</script>
</body>
</html>
MailLite — シンプルWebメール
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MailLite — シンプルWebメール</title>
<style>
:root{
--bg:#f6f7fb;
--panel:#ffffff;
--text:#1f2937;
--muted:#6b7280;
--primary:#4f46e5;
--primary-weak:#eef2ff;
--border:#e5e7eb;
--danger:#ef4444;
--success:#10b981;
--warning:#f59e0b;
}
.dark{
--bg:#0b0e15;
--panel:#0f1623;
--text:#e5e7eb;
--muted:#9ca3af;
--primary:#8b5cf6;
--primary-weak:#221a36;
--border:#1f2937;
--danger:#f87171;
--success:#34d399;
--warning:#fbbf24;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;background:var(--bg);color:var(--text);
font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,"Noto Sans JP",sans-serif;
}
.app{
display:grid;grid-template-rows:56px 1fr;height:100%;
}
/* Topbar */
.topbar{
display:flex;align-items:center;gap:12px;
padding:0 16px;border-bottom:1px solid var(--border);background:var(--panel);
position:sticky;top:0;z-index:5;
}
.logo{
display:flex;align-items:center;gap:10px;font-weight:700;
letter-spacing:.2px;
}
.badge{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--primary-weak);color:var(--primary)}
.search{
margin-left:auto;display:flex;align-items:center;gap:8px;background:var(--bg);
padding:6px 10px;border-radius:10px;border:1px solid var(--border);min-width:220px;max-width:460px;flex:1;
}
.search input{border:none;background:transparent;outline:none;color:var(--text);width:100%}
.icon{width:18px;height:18px;display:inline-block;flex:0 0 18px}
.btn{
display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:10px;
border:1px solid var(--border);background:var(--panel);cursor:pointer;color:var(--text);
}
.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
.btn.ghost{background:transparent}
.btn:disabled{opacity:.6;cursor:not-allowed}
/* Layout */
.layout{
display:grid;grid-template-columns:260px 360px 1fr;gap:12px;padding:12px;height:calc(100vh - 56px);
}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;overflow:hidden;display:flex;flex-direction:column;min-height:0}
.sidebar{padding:12px}
.compose-block{padding:12px}
.compose-button{width:100%;justify-content:center}
.nav-group{margin-top:8px}
.nav-item{
display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:10px;color:var(--text);text-decoration:none;cursor:pointer;
}
.nav-item:hover{background:var(--primary-weak)}
.nav-item.active{background:var(--primary);color:#fff}
.nav-item .count{margin-left:auto;opacity:.8}
/* List */
.list-toolbar{display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid var(--border)}
.list{overflow:auto}
.msg{
display:grid;grid-template-columns:24px 1fr auto;gap:10px;padding:12px;border-bottom:1px solid var(--border);cursor:pointer;
}
.msg:hover{background:var(--primary-weak)}
.msg.unread{background:linear-gradient(0deg,transparent,transparent), var(--panel);font-weight:600}
.msg .from{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.msg .subject{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.msg .snippet{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.msg .meta{display:flex;flex-direction:column;align-items:end;gap:4px;color:var(--muted)}
.chip{display:inline-flex;align-items:center;gap:6px;padding:2px 8px;border-radius:999px;border:1px solid var(--border);font-size:11px}
.star{cursor:pointer;opacity:.7}
.star.active{opacity:1}
.avatar{
width:24px;height:24px;border-radius:50%;background:linear-gradient(135deg,var(--primary),#22c1c3);
display:grid;place-items:center;color:#fff;font-size:12px;font-weight:700;
}
/* Reader */
.reader-head{
padding:12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-wrap:wrap;
}
.reader-title{font-size:18px;font-weight:700}
.reader-meta{color:var(--muted);font-size:12px}
.reader-actions{margin-left:auto;display:flex;gap:8px}
.reader-body{padding:16px;overflow:auto}
.empty{display:grid;place-items:center;height:100%;color:var(--muted)}
/* Modal */
.modal{
position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;align-items:center;justify-content:center;z-index:20;
}
.modal.open{display:flex}
.modal-card{
width:min(920px,94vw);max-height:88vh;overflow:auto;background:var(--panel);border:1px solid var(--border);
border-radius:16px;
}
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border)}
.modal-body{padding:16px;display:grid;gap:10px}
.field{display:grid;gap:6px}
.input, textarea{
width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);
background:transparent;color:var(--text);outline:none;
}
textarea{min-height:220px;resize:vertical}
.row{display:flex;gap:10px;flex-wrap:wrap}
.grow{flex:1}
/* Responsive */
@media (max-width: 1100px){
.layout{grid-template-columns:220px 1fr}
.reader{display:none}
.layout.show-reader .list{display:none}
.layout.show-reader .reader{display:flex}
}
@media (max-width: 640px){
.layout{grid-template-columns:1fr}
.sidebar{display:none}
}
</style>
</head>
<body>
<div class="app">
<!-- Topbar -->
<div class="topbar">
<div class="logo">
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M3 7.5 12 13l9-5.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5v-9Z" stroke="currentColor" stroke-width="1.5"/>
<path d="m3 7.5 9-5 9 5" stroke="currentColor" stroke-width="1.5"/>
</svg>
MailLite <span class="badge">beta</span>
</div>
<div class="search">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="m21 21-4.2-4.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="1.5"/></svg>
<input id="search" placeholder="メールを検索(差出人・件名・本文・ラベル)" />
<button class="btn ghost" id="clearSearch" title="検索クリア">クリア</button>
</div>
<button class="btn" id="toggleDark" title="ダークモード">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9Z" stroke="currentColor" stroke-width="1.5"/></svg>
主题
</button>
<button class="btn primary" id="composeBtn">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M4 20h16M6 14l9.5-9.5a2.1 2.1 0 1 1 3 3L9 17l-5 1 2-4Z" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>
新規作成
</button>
</div>
<!-- Main layout -->
<div class="layout" id="layout">
<!-- Sidebar -->
<aside class="panel sidebar">
<div class="compose-block">
<button class="btn primary compose-button" id="composeBtn2">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M4 20h16M6 14l9.5-9.5a2.1 2.1 0 1 1 3 3L9 17l-5 1 2-4Z" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>
新規メール
</button>
</div>
<nav class="nav-group" id="folders"></nav>
</aside>
<!-- List -->
<section class="panel">
<div class="list-toolbar">
<button class="btn" id="markReadBtn" title="既読にする">既読</button>
<button class="btn" id="markUnreadBtn" title="未読にする">未読</button>
<button class="btn" id="archiveBtn" title="アーカイブ">アーカイブ</button>
<button class="btn" id="deleteBtn" title="削除">削除</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:6px;">
<span class="chip"><span class="dot" style="width:8px;height:8px;border-radius:50%;background:var(--primary)"></span> ラベル</span>
</div>
</div>
<div class="list" id="list"></div>
</section>
<!-- Reader -->
<section class="panel reader" id="reader">
<div class="empty" id="emptyState">メールを選択してください</div>
<div style="display:none;flex-direction:column;height:100%;" id="readerWrap">
<div class="reader-head">
<div style="display:flex;align-items:center;gap:10px;min-width:0">
<div class="avatar" id="readerAvatar">Y</div>
<div style="min-width:0">
<div class="reader-title" id="readerSubject">件名</div>
<div class="reader-meta" id="readerMeta">From – To ・ 日付</div>
</div>
</div>
<div class="reader-actions">
<button class="btn" id="replyBtn" title="返信">返信</button>
<button class="btn" id="starBtn" title="スター">
<span id="starIcon">☆</span> スター
</button>
<button class="btn" id="archBtn" title="アーカイブ">アーカイブ</button>
<button class="btn" id="trashBtn" title="削除" style="color:var(--danger)">削除</button>
</div>
</div>
<div class="reader-body">
<div id="readerBody"></div>
</div>
</div>
</section>
</div>
</div>
<!-- Compose Modal -->
<div class="modal" id="composeModal" aria-hidden="true">
<div class="modal-card">
<div class="modal-head">
<strong>新規メッセージ</strong>
<div class="row">
<button class="btn" id="saveDraftBtn">下書き保存</button>
<button class="btn primary" id="sendBtn">送信</button>
<button class="btn" id="closeModalBtn">閉じる</button>
</div>
</div>
<div class="modal-body">
<div class="row">
<div class="field grow">
<label for="to">宛先(カンマ区切り)</label>
<input class="input" id="to" placeholder="example@example.com, someone@domain.jp" />
</div>
<div class="field" style="min-width:160px;">
<label for="label">ラベル</label>
<input class="input" id="label" placeholder="work, personal など" />
</div>
</div>
<div class="field">
<label for="subject">件名</label>
<input class="input" id="subject" placeholder="件名を入力" />
</div>
<div class="field">
<label for="body">本文</label>
<textarea id="body" placeholder="本文を入力"></textarea>
</div>
</div>
</div>
</div>
<script>
/** ======= Simple Mail App (no backend) ======= */
const DB_KEY = "maillite-db-v1";
const QS = sel => document.querySelector(sel);
const QSA = sel => [...document.querySelectorAll(sel)];
const state = {
currentFolder: "inbox",
query: "",
selectedIds: new Set(),
currentId: null,
db: { messages: [] }
};
const FOLDERS = [
{id:"inbox", name:"受信箱", icon:"📥"},
{id:"starred", name:"スター", icon:"⭐"},
{id:"sent", name:"送信済み", icon:"📤"},
{id:"drafts", name:"下書き", icon:"📝"},
{id:"archive", name:"アーカイブ",icon:"🗄️"},
{id:"spam", name:"迷惑", icon:"🚫"},
{id:"trash", name:"ゴミ箱", icon:"🗑️"},
];
function initDB(){
const saved = localStorage.getItem(DB_KEY);
if(saved){
state.db = JSON.parse(saved);
return;
}
// Seed sample messages
const now = Date.now();
const demo = [
mkMsg("suzuki@example.com","ようこそ MailLite へ","MailLite をお試しいただきありがとうございます!\n\nこのメールはデモです。",["welcome"], now-3600_000),
mkMsg("shop@ec.example.com","【お知らせ】サマーセール開催!","最大 50%OFF。今すぐチェック!",["promo"], now-7200_000),
mkMsg("boss@company.jp","明日の打合せ議題","・リリース計画\n・障害対応\n・コスト見直し",["work"], now-86400_000, true),
mkMsg("friend@chat.jp","週末の予定どう?","映画かカラオケ行かない?",["personal"], now-5400_000),
mkMsg("security@service.jp","ログイン通知","新しい端末からログインがありました。",["security"], now-9600_000),
];
demo[2].unread = false;
state.db.messages = demo;
persist();
}
function mkMsg(from, subject, body, labels=[], time=Date.now(), starred=false){
const id = crypto.randomUUID();
const initials = (from.split("@")[0][0]||"U").toUpperCase();
return {
id, box:"inbox", from, to:[], subject, body, labels, starred, unread:true,
date: time, initials
};
}
function persist(){ localStorage.setItem(DB_KEY, JSON.stringify(state.db)); }
function formatDate(ts){
const d = new Date(ts);
const pad = n => String(n).padStart(2,"0");
return `${d.getFullYear()}/${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// Render folders
function renderFolders(){
const el = QS("#folders");
el.innerHTML = "";
for(const f of FOLDERS){
const count = countFolder(f.id);
const a = document.createElement("a");
a.className = "nav-item" + (state.currentFolder===f.id?" active":"");
a.dataset.id = f.id;
a.innerHTML = `<span>${f.icon}</span><span>${f.name}</span><span class="count">${count||""}</span>`;
a.addEventListener("click", ()=>{
state.currentFolder = f.id;
state.selectedIds.clear();
state.currentId = null;
render();
});
el.appendChild(a);
}
}
function countFolder(folder){
return filterByFolder(state.db.messages, folder).length;
}
function filterByFolder(list, folder){
switch(folder){
case "inbox": return list.filter(m=>m.box==="inbox");
case "starred": return list.filter(m=>m.starred && m.box!=="trash");
case "sent": return list.filter(m=>m.box==="sent");
case "drafts": return list.filter(m=>m.box==="drafts");
case "archive": return list.filter(m=>m.box==="archive");
case "spam": return list.filter(m=>m.box==="spam");
case "trash": return list.filter(m=>m.box==="trash");
default: return list;
}
}
// Search
function matchesQuery(m,q){
if(!q) return true;
const s = q.toLowerCase();
const hay = [
m.from, (m.to||[]).join(","), m.subject, m.body, (m.labels||[]).join(",")
].join("\n").toLowerCase();
return hay.includes(s);
}
// Render list
function renderList(){
const wrap = QS("#list");
const items = filterByFolder(state.db.messages, state.currentFolder)
.filter(m=>matchesQuery(m, state.query))
.sort((a,b)=>b.date-a.date);
wrap.innerHTML = "";
if(items.length===0){
wrap.innerHTML = `<div class="empty" style="height:100%;">${state.query? "検索結果がありません":"このフォルダは空です"}</div>`;
return;
}
for(const m of items){
const row = document.createElement("div");
row.className = "msg" + (m.unread?" unread":"");
row.dataset.id = m.id;
const starClass = m.starred? "active":"";
row.innerHTML = `
<div class="avatar" title="${m.from}">${m.initials}</div>
<div style="min-width:0">
<div class="from">${m.from}</div>
<div class="subject">${m.subject}</div>
<div class="snippet">${(m.labels?.length? m.labels.map(l=>"#"+l).join(" ")+" · ":"")}${m.body.replace(/\n/g," ").slice(0,120)}</div>
</div>
<div class="meta">
<div>${formatDate(m.date)}</div>
<div class="star ${starClass}" data-star-id="${m.id}" title="スター">${m.starred?"★":"☆"}</div>
</div>
`;
row.addEventListener("click", (e)=>{
// If star clicked, don't open
if(e.target && e.target.dataset.starId){ return; }
openMessage(m.id);
});
row.querySelector(".star").addEventListener("click",(e)=>{
e.stopPropagation();
toggleStar(m.id);
});
wrap.appendChild(row);
}
}
// Reader
function openMessage(id){
const m = state.db.messages.find(x=>x.id===id);
if(!m) return;
state.currentId = id;
m.unread = false;
persist();
QS("#emptyState").style.display = "none";
QS("#readerWrap").style.display = "flex";
QS("#readerSubject").textContent = m.subject || "(件名なし)";
QS("#readerMeta").textContent = `From: ${m.from} / To: ${(m.to||[]).join(", ")||"(なし)"} ・ ${formatDate(m.date)}`;
QS("#readerBody").innerHTML = safeHtml(m.body).replace(/\n/g,"<br>");
QS("#readerAvatar").textContent = m.initials || "U";
QS("#starIcon").textContent = m.starred ? "★" : "☆";
// Mobile: show reader
QS("#layout").classList.add("show-reader");
render();
}
function safeHtml(s=""){
return s.replace(/[&<>"']/g, ch => ({
"&":"&","<":"<",">":">",'"':""","'":"'"
}[ch]));
}
// Actions
function getSelectedOrCurrentIds(){
if(state.selectedIds.size>0) return [...state.selectedIds];
if(state.currentId) return [state.currentId];
return [];
}
function markRead(read=true){
for(const id of getSelectedOrCurrentIds()){
const m = state.db.messages.find(x=>x.id===id);
if(m) m.unread = !read;
}
persist(); render();
}
function moveTo(box){
for(const id of getSelectedOrCurrentIds()){
const m = state.db.messages.find(x=>x.id===id);
if(m) m.box = box;
}
// If current message was moved out of view, clear reader
if(state.currentId){
const cm = state.db.messages.find(x=>x.id===state.currentId);
const visible = filterByFolder([cm], state.currentFolder).length>0;
if(!visible){ state.currentId = null; QS("#readerWrap").style.display="none"; QS("#emptyState").style.display="grid"; }
}
persist(); render();
}
function toggleStar(id){
const m = state.db.messages.find(x=>x.id===id);
if(m){ m.starred = !m.starred; persist(); render(); if(state.currentId===id) QS("#starIcon").textContent=m.starred?"★":"☆"; }
}
// Compose
function openCompose(prefill={}){
QS("#composeModal").classList.add("open");
QS("#to").value = prefill.to?.join(", ") || "";
QS("#subject").value = prefill.subject || "";
QS("#body").value = prefill.body || "";
QS("#label").value = (prefill.labels||[]).join(", ");
}
function closeCompose(){ QS("#composeModal").classList.remove("open"); }
function saveDraft(){
const draft = collectForm();
draft.box = "drafts";
draft.unread = false;
draft.from = "me@local";
draft.id = crypto.randomUUID();
state.db.messages.push(draft);
persist(); closeCompose(); render();
alert("下書きを保存しました。");
}
function sendMail(){
const msg = collectForm();
if(!msg.to.length){ alert("宛先を入力してください"); return; }
msg.box = "sent";
msg.unread = false;
msg.from = "me@local";
msg.id = crypto.randomUUID();
state.db.messages.push(msg);
persist(); closeCompose(); render();
alert("送信しました(デモ動作:実送信なし)");
}
function collectForm(){
const to = QS("#to").value.split(",").map(s=>s.trim()).filter(Boolean);
const subject = QS("#subject").value.trim() || "(件名なし)";
const body = QS("#body").value;
const labels = QS("#label").value.split(",").map(s=>s.trim()).filter(Boolean);
return { to, subject, body, labels, date: Date.now(), starred:false, unread:true, initials:"M" };
}
// Selection (click+Ctrl/Shift optional simplified)
QS("#list").addEventListener("click",(e)=>{
const row = e.target.closest(".msg");
if(!row) return;
if(e.ctrlKey || e.metaKey){
const id = row.dataset.id;
if(state.selectedIds.has(id)) state.selectedIds.delete(id); else state.selectedIds.add(id);
row.classList.toggle("selected");
}
});
// Topbar controls
QS("#composeBtn").onclick = ()=>openCompose();
QS("#composeBtn2").onclick = ()=>openCompose();
QS("#closeModalBtn").onclick = closeCompose;
QS("#saveDraftBtn").onclick = saveDraft;
QS("#sendBtn").onclick = sendMail;
QS("#search").addEventListener("input",(e)=>{ state.query = e.target.value.trim(); renderList(); });
QS("#clearSearch").onclick = ()=>{ QS("#search").value=""; state.query=""; renderList(); };
QS("#toggleDark").onclick = ()=>{
document.body.classList.toggle("dark");
localStorage.setItem("maillite-theme", document.body.classList.contains("dark")? "dark":"light");
};
// List toolbar actions
QS("#markReadBtn").onclick = ()=>markRead(true);
QS("#markUnreadBtn").onclick = ()=>markRead(false);
QS("#archiveBtn").onclick = ()=>moveTo("archive");
QS("#deleteBtn").onclick = ()=>moveTo("trash");
// Reader actions
QS("#replyBtn").onclick = ()=>{
const m = state.db.messages.find(x=>x.id===state.currentId);
if(!m) return;
openCompose({ to:[m.from], subject:"Re: "+m.subject, body:`\n\n--- ${m.from} さんのメッセージ ---\n${m.body}`, labels:["reply"] });
};
QS("#starBtn").onclick = ()=>{ if(state.currentId) toggleStar(state.currentId); };
QS("#archBtn").onclick = ()=>moveTo("archive");
QS("#trashBtn").onclick = ()=>moveTo("trash");
// Clicking outside modal to close
QS("#composeModal").addEventListener("click",(e)=>{ if(e.target===QS("#composeModal")) closeCompose(); });
// Mobile back from reader by clicking empty area (or press Escape)
document.addEventListener("keydown",(e)=>{
if(e.key==="Escape"){
if(QS("#composeModal").classList.contains("open")) closeCompose();
else QS("#layout").classList.remove("show-reader");
}
});
// Initial render
function render(){
renderFolders();
renderList();
}
(function boot(){
initDB();
render();
// Theme
if(localStorage.getItem("maillite-theme")==="dark"){ document.body.classList.add("dark"); }
})();
</script>
</body>
</html>
