docs-site-macos について
docs-site-macosは、ガイドやチュートリアル、製品ドキュメント向けのmacOS風サイトを生成するスキルです。外部リクエストのない自己完結型HTMLを作り、ブラウザー監査の合格後に納品します。
主な機能
- 01単一または複数ファイルのHTMLサイト生成
- 02サイドバー、マインドマップ、テーマ切り替え
- 03ドラッグ可能なSVG図とインタラクティブな試作
- 04ローカルプレビューサーバーとPlaywright監査
- 05監査レポートとWiki草稿の作成
活用シーン
- 01Markdownからドキュメントサイトを作成
- 02製品やポートフォリオの紹介ページを作成
- 03Wikiのトピックを個別のHTMLページに分割
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rheinmir/ setup --skill docs-site-macosインストール後、Claude Code で/docs-site-macosを入力して読み込まれたか確認します。
同梱ファイルvendor/README.md
詳細
macOS ドキュメントサイトビルダー
macOS をイメージしたデザインシステムで、単一ファイルの HTML ドキュメントサイトを構築します。
出力は自己完結型の .html ファイルです(JS ライブラリ不要、ビルド工程なし)。
WHAT
目的と背景
- 目的: macOS のリキッドグラスデザインシステムを使った、自己完結型の HTML ドキュメントサイト(1 ファイル、または index + N ページ)を構築します。ガラス調のサイドバー、マインドマップ、ドラッグ&ドロップ可能な動的 SVG 図、ライト/ダークテーマを備え、
llmwiki/html/に保存します。プレビュー用に自動ホストし、Playwright の監査が PASS になってからのみ「完了」と報告します。 - 使用するタイミング:
- ユーザーがドキュメントサイト、ランディングページ、ショーケース、ポートフォリオ、ガイド、チュートリアルサイト、機能概要、または製品ドキュメントを求めている場合。特に「clean」「modern」「Apple-like」「macOS style」「glass」「frosted」「animated diagrams」「single HTML file」といった表現がある場合。
- ドキュメント/ショーケースページに「liquid glass」「frosted glass」「translucent UI」を求めている場合。
- ユーザーが複数の Markdown ファイルを持ち、セクションを含む視覚的な HTML ページへのレンダリングを求めている場合。
- ユーザーが「6 file」と言う、またはトピックごとに個別ページを求めている場合 → Multi-File Mode(一つの wiki ファイルにつき HTML 1 つ +
DDMMYY-index.html)。1 ページにまとめてはいけません。
- 対象外: CSS/JS のテーマ切り替えを独自に書き直さないでください(唯一の情報源はスキル
dark-mode-makerです)。archify エンジンが描画した図の SVG をコピーして外部に出さないでください(<iframe>で埋め込みます)。CDN/Web フォント/外部スクリプトを読み込まないでください。llmwiki/html/の外にファイルを書き出さないでください。監査用スクリプトやスクリーンショットを保持/コミットしないでください。
メンタルモデル
ソースコンテンツ(markdown · wiki · 説明)→ N セクション(#sec-{i}、accent i % 6)→ ページ構成(skip-link · nav sidebar · main · hero · mind map · section · footer)→ 自己完結型 HTML llmwiki/html/DDMMYY-<slug>.html → 自動ホスト :8765 → Playwright 監査 → 出力レポート draft
入力と出力の仕様
| フィールド | 必須? | 意味 | |
|---|---|---|---|
| In | コンテンツ | はい | Markdown ファイル/wiki ページ/トピックの説明 — 各トピックを 1 つのセクションにします |
| In | ページモード | いいえ(デフォルト = 1 ファイル) | 「6 file」/トピックごとの個別ページ → Multi-File Mode |
| In | archify の既存の図 | いいえ | /diagram → archify が生成した viewer ページ *.html を <iframe> で埋め込みます |
| In | インタラクティブなプロトタイプの要件 | いいえ | 「インタラクティブな試用表を作成」→ Interactive Prototype / Editable Data-Grid |
| Out | HTML ページ | はい | llmwiki/html/DDMMYY-<slug>.html(複数ファイルの場合:DDMMYY-index.html + DDMMYY-<slug>.html)、外部へのリクエスト 0 件 |
| Out | プレビューリンク | はい | http://localhost:8765/llmwiki/html/DDMMYY-<file>.html |
| Out | 監査結果 | はい | Playwright スクリプトによる AUDIT PASS — 「完了」は監査 PASS を意味します |
| Out | 出力レポート | はい(成果物が 0 件の場合を除く) | llmwiki/wiki/sources/draft/DDMMYY-<ten>.md + インデックス行 + ログ |
ルールと機能
- RULE-01 (MUST): 自己完結型 — 重大: 出力 HTML から外部へのリクエストを一切発生させないでください。フォント/CSS/JS の CDN リンク、リモート画像、
@import、<script src>は禁止です(外部へのハイパーリンク<a href>は許可されます)。 - RULE-02 (MUST): 出力先 — 重大: HTML ファイルは必ず、現在のプロジェクトルート内の
llmwiki/html/に書き込んでください。ファイル名には必ず今日の日付DDMMYY-を先頭に付けてください。プロジェクトルートやその他のディレクトリには絶対に書き込まないでください。 - RULE-03 (MUST): ナビゲーション — サイドバーのみ(トップバーは一切使用しないでください)。サイドバーには本物のガラス表現を使い、
.nav-toggle/.nav-closeを含めてください。 - RULE-04 (MUST): マインドマップをデフォルトで表示 — 必ず1つ描画すること。文書自体から生成し、折りたたみ可能にしてください。
- RULE-05 (MUST): ライト/ダークテーマ切り替え必須。特定のモードを強制してはいけません —
dark-mode-makerスキルを読み込み、切り替えボタンを<nav>内に配置してください。 - RULE-06 (MUST): アクセシビリティとドキュメントヘッダー必須 — viewport/meta/favicon をインラインで指定し、
:focus-visibleのフォーカスリング、スキップリンクと<main id="main">、全体的な reduced-motion 設定を用意してください。SVG にはテキストによる代替表現を付けてください。 - RULE-07 (MUST): すべての
.diagram-boxに Node-Draggable を、すべてのpre.code-blockにコピー ボタンを、すべてのインタラクティブなコントロールに Water-Ripple を適用してください(いずれも必須です)。 - RULE-08 (MUST): 自動ホスト — HTML ファイルの書き込み後、必ずローカル HTTP サーバーを起動してください。
- RULE-09 (MUST): Playwright 監査必須 — ユーザーに報告する前に実施してください。失敗した場合は修正して再監査し、赤い状態のページを渡してはいけません。
- RULE-10 (MUST): 13″画面向けの文字サイズはコンパクトにしてください。サイズを大きくせず、小さくしてください。
- RULE-11 (MUST): archify の図は
<iframe>で埋め込んでください(内容に合わせたサイズのフレームと、「図を別ウィンドウで開く ↗」リンクを付けます)。meta.visual_presetは空欄にしてください(ルール R20)。 - RULE-12 (MUST): グラデーションテキスト禁止、片側だけのストライプ状ボーダー禁止 —
background-clip:text(グラデーションで塗った文字)と、カード/ボタン/コールアウトに使うborder-left/right: ≥3px solid <色>は、静的ゲートでブロックされる AI 生成の兆候です。文字の強調にはウェイトまたは単色を使い、コールアウトの分類には色付きドット、ラベル、またはカード全体の淡い背景を使ってください(ボーダーを使う場合は四辺すべてに付けてください)。 - RULE-13 (MUST): 引き渡し前に2つのゲートを実行してください —
python3 fdk/tools/frontend-antipattern.py <ページ>(静的)およびNODE_PATH=$(npm root -g) node fdk/tools/html-visual-gate.mjs <ページ>(実動作:コントラスト比 4.5:1 未満の文字、8px 未満の間隔で密着したブロック、文字に重なるアイコン、トグル、両モードでのガラス表現)。ゲートが赤になったら修正して再実行してください。機械で修正できる箇所にはpython3 fdk/tools/html-slop-fix.py <ページ>を使用してください。ゲートが赤のまま完了を報告してはいけません。 - 機能:ソースコンテンツの読み取り、プロジェクトの出力ディレクトリへの HTML ファイル書き込み、ローカル HTTP サーバーの実行、ヘッドレスブラウザーの操作による DOM/コンソール/スクリーンショットの計測、Wiki のドラフト/インデックス/ログの書き込み。
失敗時の対応範囲
- コンテンツがない、またはどのトピックをセクションにすべきか不明 → ユーザーに確認し、ページは生成しません。
- Playwright 監査が失敗(コンソールエラー、
.nav-toggle/.nav-closeの不足、テーマ切り替えが.theme-row内にない)→ ブロック:修正して再監査し、赤い状態のページを完了として報告してはいけません。 @playwright/testが未インストール →/playwright-verifyに従ってインストールしてから監査します。監査を省略してはいけません。- ポート 8765 が使用中 → サーバーはすでに起動しているものとして扱い、起動手順をスキップします(エラーではありません)。
file://経由で archify の iframe を開くと高さを計測できない → 部分的な対応として許容:フレームの高さを 1000px に維持し、「図を別ウィンドウで開く ↗」リンクを代替手段として使います。ぴったりのサイズにするには Auto-Host 経由で開いてください。- 静的ゲートまたは実動作ゲートが赤のまま → ブロック:機械で修正可能な箇所には
html-slop-fix.pyを実行し、残りは手動で修正してから両方のゲートを再実行してください。指摘事項が残ったページを渡してはいけません。 - ブラウザーに
DecompressionStreamがない → Mermaid エンジンは描画されません(__bmReadyが reject されます)。Mermaid Diagram Engine の項目を参照してください。
方法
デフォルトフォント — 2ファミリーを埋め込む(必須、ユーザー決定 20/09/2026 · 内容変更 21/09 · タイトル変更 22/09/2026)
タイトルには Newsreader 600(新聞風のセリフ体、--font-display、--fw-heading:600、--ls-heading:-.01em。wght 600 / opsz 24 で固定した静的フォントファイルを1つ使用 — 同じベトナム語のサンプルで6種類のフォントを比較した後、ユーザーが選択)。本文には Be Vietnam Pro を使用します。400、太字は600です。静的フォントファイルは400/600/800の3つで、500を指定すると400、700を指定すると800が適用されます — 実際のウェイトです。図/グラフ内の文字(svg · .diagram-box · .mm · .graph)には Lexend Deca:標準は Light、太字は Regular を使用します(ユーザー決定 22/09/2026 — ウェイト範囲ごとに分けた静的フォントファイルを2つ使用、--font-chart)。コード用の --font-mono はそのままです。フォントはページ自体にbase64で埋め込み(約175 KB)、ネットワーク接続なしで file:// から開いても正しいフォントを表示します — Google Fonts の <link> は使用せず、base64文字列を手動で貼り付けないでください。作業は次の2ステップのみです。
- ページのCSSで、
body{font-family:var(--font-text);font-weight:var(--fw-text)}を指定します。タイトルとページ名にはfont-family:var(--font-display);font-weight:var(--fw-heading)を指定します(独自のシステムフォントスタックを定義せず、800をハードコードしないでください)。 - ファイルを書き終えた後、次のコマンドを1つ実行します(冪等で、
✓ … nhúng Be Vietnam Proと出力します)。
python3 fdk/tools/html_font.py --apply <trang.html> [trang-khác.html …] # máy khách: python3 ~/.claude/harness/fdk/tools/html_font.py --apply …
ステップ2を実行していない場合、ページはシステムフォントにフォールバックします → 未完了です。簡易確認:grep -c 'id="ovs-font"' <trang.html> の結果が 1 であること。
ステップ2では、フレームも自動で追加します(PLAN 220926)。サイドバー .logo と4つ以上のアンカー #… があるページが対象です。.ic のないすべての nav a にアイコンタイルを付け(項目名のキーワードに基づいてアイコンを選択し、連番「01 ·」を title に設定)、アクティブな項目を背景のピルと色付きドットで示し、読書進捗バー、スキップリンク、<main id="main">、インラインファビコン、スクロールスパイ、リップル、h2/h3から生成するマインドマップ(ページに .mm がない場合)、.diagram-box のドラッグ&ドロップ用JSを追加します。ソース:fdk/tools/html_shell.py。マインドマップとドラッグ&ドロップのCSS/JSは、このスキルのオリジナル版です(html_shell.py --sync)。.nav-toggle / .nav-close だけは手動で作成する必要があります。ルールR20では、フレームが不足しているページに対し、--apply を含む正確なコマンドを提示します。
間隔と文字のリズム(必須、PLAN 220926-spacing-system — ソース fdk/wiki/sources/220926-spacing-standards.md)
- 単一のスケールをパディング/マージン/ギャップに使用します:2 · 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96 px(IBM Carbon + Tailwind)。基盤レイヤーのトークン
--sp-1…--sp-11を使用します。5、6、9、10、13、14px は使用しません。html_font.py --applyはスケールから外れた値を最も近い段階に自動で丸めます。静的チェックspacing-off-scaleは、この処理を通していないページをブロックします。 - **行の高さ:**本文は1.55~1.6(
--lh-body)で、すべての段落/項目に同じ値を使います。見出しは1.1~1.3(--lh-heading)です。複数行の段落で1.5未満はエラーです(line-height-body)。 - **行の長さ:**段落は80文字以下です(WCAG 1.4.8)。目標は
max-width:var(--measure)= 34em です(Baymard。実際には約68文字。chは平均的な文字より幅の広い数字「0」の幅を基準とするため、70chでは約89文字になります)。 - **近接性:**見出しの上の間隔は下の間隔の1.5倍以上にします(USWDS)。見出しは後続の文章に属します。適切な例の実測値は、上が26px、下が12pxです。
- **サイドバーのラベル階層:**ページ名・グループラベル・項目は、4つの属性(サイズ、太さ、色、大文字/小文字)のうち2つ以上で違いを付けます(ルール
hierarchy-flat)。ナビ項目は13~14px、太さ600、本文色にします。グループラベルは小文字の大文字表記にして字間を広げます。 - クリック領域は24×24px以上にします(WCAG 2.5.8)。
- 大から小への見出しスケール(必須):h1 > h2 > h3 > h4(フォントクラスの既定値:32 · 24 · 20 · 17px)。本文の文字サイズより小さくしてはいけません。ページ名はナビ/タブ項目の1.2倍以上にします(サイドバー:ロゴ18px、項目13px)。ルールは
heading-scale、title-scaleです。 - 文頭を大文字にする表記(必須)を見出し、ラベル、ボタン、タブ、ナビ項目に使用します(
sentence-case。基盤レイヤーは--applyで自動修正します)。固有名詞の小文字表記を維持する場合はdata-case="keep"を指定します。 - カンバンのスタイル統一とカードのサイズ固定(必須):ボード上のすべてのカードで、スタイルと幅/高さを統一します。見出しは
line-clamp:2、補足行はellipsisにし、カードをクリックすると詳細を開きます。ルールはkanban-uniformです。 - サイドバー、グリッド、カンバン、リスト、モーション、チャートなどのサンプルコード:ページ
skills/hallmark/references/design-showcase.html(クライアント:~/.claude/skills/hallmark/references/design-showcase.html)。次のコマンドでブロックを取得します:python3 fdk/tools/build-design-showcase.py --get <id>(クライアント:python3 ~/.claude/harness/fdk/tools/build-design-showcase.py --get <id>。--listは索引を出力します)。 - 目を休めるための余白(必須):最初の画面には要約だけを表示し、詳細はクリックしたときに表示します(
<details>、ポップアップ、要約ボタン)。何十個ものバッジ/チップ/ボタンを横並びにしないでください。ルールはeye-restです。特別な要件により標準から外れる場合は、<meta name="overstack-exempt" content="…" data-reason="…">を使用します。
メインワークフロー
| ステップ | 種類 | 入力 | アクション | 出力/終了条件 | 失敗時/次の処理 |
|---|---|---|---|---|---|
| W01 | 判断 | 内容、ユーザーの依頼 | 内容を N 個のセクションに分割します(トピック/ファイルごとに 1 セクション)。単一ファイルか Multi-File Mode(B01)を選択します | セクション一覧+モード | 内容が不足 → 確認 |
| W02 | 判断 | セクション一覧 | Page Architecture に基づいて骨組みを作成します:skip-link、ナビゲーションサイドバー(ガラス効果、.nav-toggle/.nav-close)、<main id="main">、ヒーロー、マインドマップ、section-bg s-bg{i}、フッター | HTML の骨組み | — |
| W03 | 決定的処理 | N 個のセクション | Design System と CSS Generator Pattern に基づいて CSS を生成します(#sec-{i}、accent i % 6、.tag の dark-mode バリエーション)。オーバーレイスクロールバー、システムフォントスタックも設定します | インライン CSS | — |
| W04 | 判断 | 各セクションの内容 | 図を作成します:SVG インラインのドラッグ可能ノード(≤~5 ノード、線形)または Mermaid engine(≥6 ノード/分岐あり)(B02)。archify で作成済みのアーティファクトを <iframe> で埋め込みます(B03) | .diagram-box | — |
| W05 | 決定的処理 | 骨組み+CSS | 必須コンポーネントを組み込みます:マインドマップ、コピー ボタン、water-ripple、折りたたみ、スクロールスパイ、dark-mode-maker のテーマ切り替え、Accessibility & Document Head | 必要なコンポーネントが揃ったページ | — |
| W06 | 効果 | ページ | llmwiki/html/DDMMYY-<slug>.html に書き込みます(フォルダーがなければ作成) | HTML ファイル | — |
| W07 | 効果 | HTML ファイル | Auto-Host:project root から npx serve -p 8765 を実行します | リンク http://localhost:8765/llmwiki/html/... | ポート使用中 → サーバーはすでに実行中なので続行 |
| W08 | 決定的処理 | ホスト中のページ | node で .mjs スクリプトを実行し、Playwright Audit を行います:コンソールエラー 0 件、サイドバーの往復、.theme-row 内でのテーマ切り替え、3 つの状態を撮影 | AUDIT PASS | FAIL → B04 |
| W09 | 効果 | 結果 | ユーザーにリンクを伝え、Output Report の draft+index+log を作成します | draft+index+log | アーティファクト 0 件 → レポートをスキップ |
各ステップの詳細(W01–W09 の唯一の正しい情報源):以下の Design System、Page Architecture、Mind Map、Section-Bg Pattern、Animated SVG Diagrams、Accessibility & Document Head、Output Path、Auto-Host、Playwright Audit、Multi-File Mode、Output Report の各セクションは、移行前の版からそのまま転記します。
分岐
| ID | 種類 | 条件 | 動作 | スキップ/失敗 | 合流先 |
|---|---|---|---|---|---|
| B01 | user_optional | ユーザーが「6 ファイル」と言う/トピックごとに個別のページを希望する | Multi-File Mode:DDMMYY-index.html(カードグリッド)+各 wiki ファイル用の DDMMYY-{slug}.html。同じ design system を使い、すべてのページへ移動できるナビゲーションを設定します | 要求なし → 1 ファイル | W06 |
| B02 | conditional_required | 図が ≥6 ノード、または分岐/結合がある | 手作業で座標を指定する SVG の代わりに、Mermaid Diagram Engine(beautiful-mermaid を gzip+base64 で埋め込み、glassmorphism で後処理)を使います | ≤~5 ノードで線形 → 手動 SVG。DecompressionStream がない → 描画しない | W05 |
| B03 | conditional_required | archify engine で作成済みの図がある | <iframe class="archify-embed">+自動高さ調整+「図を個別に開く ↗」リンクを埋め込みます。visual_preset は空のままにします | file:// で開く → 1000px のフレームを使用し、リンクから個別に開く | W05 |
| B04 | recovery | Playwright audit が FAIL | audit で報告されたエラーを修正してから、W08 を再実行します | まだ赤 → 完了と報告せず、修正を続行 | W08 |
| B05 | user_optional | ユーザーが「UI がどのように見えるか確認したい」/「インタラクティブな表を試しに作成したい」と希望する | インタラクティブプロトタイプ/編集可能なデータグリッド(vanilla JS、引き続き Self-Contained) | 要求なし → スキップ | W06 |
検証と停止条件
コードによる検証:W08 の Playwright スクリプト(console/pageerror、DOM の .nav-toggle/.nav-close、nav-collapsed の往復、data-theme の変更+コントロールが .theme-row 内にあること)を実行します — exit 1 は失敗です。目視が必要な部分:ライトテーマ/ダークテーマ/サイドバーを閉じた状態のスクリーンショット。audit が PASS になったら停止します。元の版では修正回数に厳密な上限はありません — ただし、失敗が残っている間は絶対に「完了」と報告しません。スクリプトと audit の画像は検証後に破棄し、コミットしません。
例
- 成功例:「
llmwiki/wiki/concepts/内の4ファイルからmacOS向けドキュメントサイトを作成」→ サイドバー、4つの枝を持つマインドマップ、4つのセクション#sec-0..3(i % 6に応じたアクセント)を備えたファイルllmwiki/html/190926-overstack-concepts.htmlを作成し、サーバー:8765を起動、Playwright スクリプトでAUDIT PASSを出力 →http://localhost:8765/llmwiki/html/190926-overstack-concepts.htmlのリンクとドラフトレポートを報告。 - **境界/失敗例:**生成されたページでテーマボタンが右隅の
position:fixedチップになっており、サイドバーに.nav-closeがない → 監査でAUDIT FAIL: ['THIẾU .nav-toggle/.nav-close …', '… KHÔNG nằm trong .theme-row …']を出力し、終了コード 1 → B04 で修正してから再監査。ページが完成したとユーザーに報告してはいけません。 - **境界例:**ユーザーが「6ファイル」と指定 → B01:
DDMMYY-index.htmlと6つのページDDMMYY-<slug>.htmlを作成し、1ページにまとめません。
デザインシステム
カラーパレット
ベース:やわらかなライトブルーのグラデーション上の白いガラス面(下記「背景プレーン」を参照)
テキスト:#0f0f12 / #4a4a55
ボーダー:rgba(30,90,170,.14)(クールなブルーグレー。白地に白は使用しません)
パターンにはリキッドグラスのライトブルー+ホワイトを使用します(サーフェス、背景フィールド、ナビ、ヒーロー、リンク)。コンテンツセクションのアクセントには、Appleのセカンダリパレットを順番に使用します(下記「パレットの考え方」を参照)。タグ、h4、箇条書きに限定することで、多色でも雑然としません。フラットな黒のアクセントや、彩度の高い見出しは使用しません。
背景プレーン(ガラスが映り込むもの)
名称を正確に定めると、このデザインシステムは実用的なCSSガラス(透明度+背景のぼかし)であり、真の屈折ではありません。ぼかしを素材として認識できるようにするには、body の背景を単色にせず、控えめなモノクロームのグラデーションフィールドにする必要があります。
body{
background:
radial-gradient(900px 500px at 12% -10%, rgba(10,132,255,.10), transparent 60%),
radial-gradient(700px 420px at 95% 15%, rgba(90,162,232,.08), transparent 55%),
linear-gradient(180deg, #f7fbff 0%, #eaf2fd 100%);
}
この静かな印象を保ち、ブルー系の色合いだけを使います。強い発光レイヤー(「光害」)は使用しません。セクションごとの .s-bgN::before オーバーレイで、残りの局所的な変化を加えます。
**基本の屈折プレーン(必須 — ガラスの下に何が見えるか):**フラットなグラデーションだけでは、ぼかしが「溶け込む」素材として機能しません。すべてのコンテンツの下に、z-index:-1 の fixed レイヤーを2つ追加します。(1)ORBS — 大きな放射状のぼかしを5〜6個配置します(ブルーを主色とし、Appleのセカンダリカラーを1〜2色加え、アルファ値は .06〜.22)。非常にゆっくりと移動させます(約46sの ease alternate、translate ≤2.5%+scale ≤1.05)。(2)DOT-GRID — 1px/22px の細かいドットグリッドをアルファ値 ~.11 で配置し、縦方向にフェードするマスクを適用します。高周波のディテールが backdrop-filter によって本物のガラスのようなテクスチャに変わります。**ビューポートの左端に沿って、少なくとも1〜2個のオーブを配置します(サイドバーの背後)。**サイドバーはページ内で最大のガラス面です。背後に色がなければ、いくらぼかしても白い板にしかなりません。以下も追加します:@media (prefers-reduced-motion:reduce){animation:none}。
body::before{content:'';position:fixed;inset:-10%;z-index:-1;pointer-events:none;
background:
radial-gradient(640px 440px at 10% 14%,rgba(10,132,255,.22),transparent 65%),
radial-gradient(380px 460px at 4% 52%,rgba(48,176,199,.18),transparent 65%),
radial-gradient(540px 400px at 88% 10%,rgba(88,86,214,.13),transparent 60%),
radial-gradient(720px 500px at 74% 76%,rgba(48,176,199,.13),transparent 65%),
radial-gradient(480px 380px at 16% 86%,rgba(255,149,0,.12),transparent 60%);
animation:orbDrift 46s ease-in-out infinite alternate}
@keyframes orbDrift{100%{transform:translate(2.2%,1.6%) scale(1.045)}}
body::after{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;
background-image:radial-gradient(rgba(30,90,170,.11) 1px,transparent 1.3px);
background-size:22px 22px;
mask-image:linear-gradient(180deg,rgba(0,0,0,.55),rgba(0,0,0,.22))}
リキッドグラスのサーフェスシステム(不透明度の段階+ぼかしの強さ)
ガラスは単一のクラスではなく、奥行きのシステムです。アルファ値とぼかしの強さがそれぞれ異なる3つのサーフェス階層を使い、すべてに同じアルファ値を繰り返し使用しないでください。
:root{
--glass-1: rgba(255,255,255,.55); /* tier 1 — chrome: nav, floating panels */
--glass-2: rgba(255,255,255,.7); /* tier 2 — cards, diagram-box, repo-card */
--glass-3: rgba(255,255,255,.88); /* tier 3 — data: tables, long text (calmer, near-solid) */
--blur-1: 24px; --blur-2: 8px; --blur-3: 4px;
--edge-hi: inset 0 1px 0 rgba(255,255,255,.85); /* top inner highlight — REQUIRED on every glass surface */
--border: rgba(30,90,170,.14);
}
階層の割り当て:nav = tier 1、.card / .diagram-box / .repo-card = tier 2、table / 密度の高いデータ領域 = tier 3。最も強いガラスはクローム用に確保し、コンテンツの密度が高くなるほど素材の強度を下げます。
パレットの考え方:ブルーはパターン、Appleのセカンダリカラーはコンテンツ
ブルーはクローム/パターン専用の色です。サイドバーのナビゲーション、ヒーローのグラデーション、リンク、構造上のアクセントに使います。モダンなiOS調の #0a84ff を使い、暗い色やネイビーから始めないでください。コンテンツセクションにはAppleのセカンダリパレット(ティール/インディゴ/グリーン/オレンジ/ピンク)を順番に使用します。多色でも雑然としなければ問題ありません。アクセントは .tag、.card h4、li::before の箇条書きに限定します。
**見出しは暗色にし、彩度の高い色は使いません。**すべてのセクションで .section-header h2 { color: #1d1d1f } を使用します。彩度の高いブルーの見出しは古くさく見えます。Appleは、色付きの小さなタグを上に置き、その下にほぼ黒の見出しを使用しています。
セクションのアクセントカラーサイクル(Apple のセカンダリカラー)
#sec-0 .tag { background: rgba(10,132,255,.10); color: #0a84ff; } /* blue */
#sec-0 .card h4 { color: #0a84ff; } #sec-0 .card li::before { color: #0a84ff; }
#sec-0 .section-header h2 { color: #1d1d1f; } /* h2 luôn tối, mọi section */
.s-bg0::before { background: linear-gradient(180deg, rgba(10,132,255,.05) 0%, transparent 60%); }
/* then sec-1..5 per the table; repeat #sec-6 = #sec-0, ... */
アクセントカラーの一覧:
| インデックス | アクセントカラー | h4 の色調 | グラデーションオーバーレイ |
|---|---|---|---|
| 0 | #0a84ff ブルー | #0a84ff | rgba(10,132,255,.05) |
| 1 | #30b0c7 ティール | #30b0c7 | rgba(48,176,199,.05) |
| 2 | #5856d6 インディゴ | #5856d6 | rgba(88,86,214,.05) |
| 3 | #34c759 グリーン | #28a745 | rgba(52,199,89,.05) |
| 4 | #ff9500 オレンジ | #f08c00 | rgba(255,149,0,.06) |
| 5 | #ff2d55 ピンク | #e0264b | rgba(255,45,85,.05) |
各セクションでは、次の要素にアクセントカラーを使用します。
.tagの背景(不透明度 10〜12%)とテキスト.card h4の色(テキストの読みやすさを保つため、オレンジ/グリーン/ピンクには濃い h4 の色調を使用).card li::beforeの色(›の箇条書き記号)- ⛔
.section-header h2には使用しないでください — h2 は常に#1d1d1fです
グラスモーフィズムのカード
.card {
background: var(--glass-2);
backdrop-filter: blur(var(--blur-2)) saturate(1.1);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--edge-hi), 0 4px 20px rgba(20,40,90,.08); /* top highlight + lower BLUE-TINTED shadow = directional light; never pure black on a blue field */
padding: 20px;
}
.diagram-box と .repo-card は、この tier-2 のレシピを共有します。table(および密度の高いデータ領域)は tier 3 に下げます。つまり、background: var(--glass-3); backdrop-filter: blur(var(--blur-3)) を使用します。ボーダーとエッジハイライトは同じまま、行を読みやすくするため、より落ち着いた素材にします。
リキッドグラスのルール(liquid-glass-design スキルを要約):
- エッジハイライトは必須です。
--edge-hiのないガラスは、色あせたカードのように見えます。 - 指向性のある光:すべてのペインに、上部のかすかな内側のハイライトと、下部の柔らかな外側のシャドウを付けます。
- テキストを強いぼかしの上に直接置いてはいけません。tier 3(ほぼ不透明)で、長文の読み物とテーブルをすべて支えます。
- 重ねたガラスには、少なくとも 1 段階の tier の違い(アルファ値とぼかしの段階の両方)が必要です。そうでないと、レイヤーが混ざって濁ってしまいます。
- グローはかすかにし、青系の色のみを使います。奥行きは雰囲気ではなく、段階的な素材、エッジ、シャドウで表現します。
- 大きなクロームペイン(サイドバー、高さ全体に広がるパネル)に、均一なアルファ値を絶対に使用しないでください — 均一な白い塗りつぶしは、乳白色の壁のように見えます(「単色だと壁のように見える」)。グラデーションアルファのガラス、スペキュラーハイライト
::before、そして背後に直接配置するカラーオーブが必要です。基本のレシピは Navigation セクションを参照してください。
macOS のクローム要素
リポジトリカードとコンバーターのモックアップには、macOS のウィンドウヘッダーを使用します。
<div class="chrome">
<span style="width:12px;height:12px;border-radius:50%;background:#ff5f57"></span>
<span style="width:12px;height:12px;border-radius:50%;background:#ffbd2e"></span>
<span style="width:12px;height:12px;border-radius:50%;background:#28c840"></span>
<div class="url-bar">...</div>
</div>
⚠️ 全幅の枠付き要素(repo-card、コンバーターのモックアップ、枠線のあるあらゆるパネル)には、ヒーロー/セクションと同じ横方向のガターが必要です(2026/06/13 の教訓 — ユーザーから repo-card が「画面の端に近すぎる。左右にパディングが必要」と指摘):ヒーロー/セクションは、max-width:1100px のコンテナ内部にある padding:…24px によってガターを確保します。しかし、枠付き要素に max-width:1100px;margin:auto だけを設定すると、画面幅が 1100px 未満の場合、ビューポートの両端にぴったり接してしまいます(内部にパディングがなく、ボックス自体がコンテンツだからです)。修正するには、ボックスにヒーローと同じコンテンツ領域の幅を指定します。max-width:1052px(= 1100 − 48 ガター)と width:calc(100% - 48px) を設定します。
.repo-card{ max-width:1052px; width:calc(100% - 48px); margin:8px auto 0; /* …glass tier-2… */ }
広い画面 → 最大幅 1052px で中央揃えし、ヒーロー内のテキストと揃います(1100−24−24)。狭い画面 → 常に左右に 24px の余白を確保します。section の外側で <body> の直下に置くすべての枠付き要素(repo-card、コンバーターのモックアップ、全幅のバナー/コールアウト)に適用します。
⚠️ 枠付き要素の最後の本文には、上部より広い下部パディングが必要です(2026/06/13 の教訓 — ユーザーから「切り替わる部分が途切れて見え、滑らかでない」と指摘):最後のコンテンツブロック(例:.rc-body)は、次のセクションへ直接つながります。上下のパディングが同じ(16px)だと、テキストがボックスの端に寄りすぎて、途中で切れたように見えます。下部にゆとりを持たせます:padding:16px 18px 22px(下部は上部より 6px 以上広くします)。ルール:テキストで終わるペインでは、下部パディングを上部パディング以上にします。
ナビゲーション — サイドバーのみ(トップバーは絶対に使わない)
監視機構(R20、PostToolUse フック):*/html/*.html ページに <section id> または <h2> が 3 個を超えてあり、#anchor リンクを 3 つ以上含む <nav> がない場合、どのスキルがページを生成したかにかかわらずブロックされます。意図的に一段組みにするページでは、理由を添えて <meta name="overstack-nav" content="none"> を宣言します。
あらゆる画面サイズで LEFT SIDEBAR と折りたたみボタンを使います。⛔ トップバー表示はありません — 狭い画面でトップバーにリンクを詰め込むと、文字が折り返されて見栄えが悪くなります。狭い画面(<640px)では、サイドバーはコンテンツに重なる OVERLAY になり(body の padding-left は 0 のまま)、デフォルトで折りたたまれ、ユーザーはトグルボタンで開きます。
⚠️ サイドバーは本物のガラスでなければならず、白いすりガラスの板ではいけません(2026/06/12 の教訓 — ユーザーから「単色だと少し遮って見える」と指摘):明るい背景に --glass-1 のアルファ .55 を平坦に適用すると、不透明な「ミルク色」になり、鏡のようにはなりません。大きなペインのクローム(サイドバー、画面いっぱいの高さのパネル)には、次の 3 つが必須です:(1)グラデーションアルファのガラス — 一定値ではなく、ガラス面に沿ってアルファを変化させます。(2)スペキュラシーン ::before — 上隅のラジアルハイライトと斜めのシーン帯。(3)ペインのすぐ背後に色付きのオーブ(背景プレーンを参照)— 24px のぼかしで混ぜるには、実際の色が必要です。--glass-1 は小さなフローティングパネル専用になりました。
:root{--nav-pad-y:18px} /* nguồn chân lý duy nhất cho khoảng đệm dọc của nav — .theme-row (§Theme Toggle) đọc lại biến này, KHÔNG hard-code số riêng, để 2 chỗ không thể lệch nhau (bài học 200826: bug thật, xem lịch sử) */
nav{position:fixed;top:0;left:0;bottom:0;width:200px;z-index:100;
display:flex;flex-direction:column;align-items:stretch;gap:2px;padding:var(--nav-pad-y) 12px;
background:linear-gradient(165deg,rgba(255,255,255,.46) 0%,rgba(255,255,255,.22) 48%,rgba(240,248,255,.34) 100%);
backdrop-filter:blur(var(--blur-1)) saturate(1.7) brightness(1.04);
-webkit-backdrop-filter:blur(var(--blur-1)) saturate(1.7) brightness(1.04);
border-right:1px solid rgba(255,255,255,.55);
box-shadow:inset 0 1px 0 rgba(255,255,255,.9),inset 1px 0 0 rgba(255,255,255,.5),
inset -1px 0 0 rgba(30,90,170,.10),4px 0 24px rgba(30,90,170,.08)}
/* specular sheen — vệt sáng chéo trên mặt kính; con của nav cần position:relative để nổi trên sheen */
nav::before{content:'';position:absolute;inset:0;pointer-events:none;
background:
radial-gradient(220px 160px at 18% 4%,rgba(255,255,255,.55),transparent 70%),
linear-gradient(115deg,rgba(255,255,255,.28) 0%,transparent 28%,transparent 72%,rgba(255,255,255,.14) 100%)}
nav>*{position:relative}
nav .logo{margin:0 0 12px;padding:8px 12px;
background:linear-gradient(135deg,#0a84ff,#64b5f7);-webkit-background-clip:text;background-clip:text;color:transparent}
nav a{padding:8px 12px;border-radius:10px;font-size:13px;position:relative;overflow:hidden}
nav a.active{color:#0a84ff;background:rgba(10,132,255,.08);font-weight:600}
body{padding-left:192px}
@media(max-width:640px){
body{padding-left:0} /* sidebar overlay, không chiếm column */
nav{box-shadow:0 8px 30px rgba(0,0,0,.14)} /* nổi trên content khi mở */
}
リップル効果(サイドバーボタンとトグルで必須): クリックした位置から円が広がり、ボタン全体をゆっくり覆ってからフェードアウトします(半径 = クリック位置から 4 隅までの最長距離)。親要素には position(relative/fixed/absolute のどれでも ink を含められます)と overflow:hidden が必要です。⚠️ .nav-toggle に position:relative を強制する共通ルールは書かないでください — 同じ specificity では後に書かれたルールが優先されるため、position:fixed が上書きされ、ボタンがページの末尾に落ちてしまいます。
リップルの ink は LIQUID GLASS であり、平坦な色の跡ではありません。角度をずらしたスペキュラハイライト(circle at 35% 30%)、半透明の白い本体、淡い青色の縁、下のコンテンツを水滴のように屈折させる backdrop-filter:blur(2px)、内側のエッジハイライトを使います。
.ripple-ink{position:absolute;border-radius:50%;pointer-events:none;
background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.70) 0%,rgba(255,255,255,.28) 38%,rgba(10,132,255,.20) 72%,transparent 100%);
box-shadow:inset 0 1px 0 rgba(255,255,255,.9),inset 0 -10px 20px rgba(10,132,255,.12),0 0 14px rgba(10,132,255,.10);
backdrop-filter:blur(2px) saturate(1.25);-webkit-backdrop-filter:blur(2px) saturate(1.25);
transform:scale(0);opacity:1;animation:rippleGrow .6s cubic-bezier(.25,.46,.45,.94) forwards}
@keyframes rippleGrow{55%{transform:scale(1);opacity:.75}100%{transform:scale(1.04);opacity:0}}
(function(){
function attach(el){
el.addEventListener('pointerdown', function(e){
const r = el.getBoundingClientRect();
const x = e.clientX - r.left, y = e.clientY - r.top;
const rad = Math.hypot(Math.max(x, r.width - x), Math.max(y, r.height - y));
const ink = document.createElement('span'); ink.className = 'ripple-ink';
ink.style.width = ink.style.height = rad * 2 + 'px';
ink.style.left = (x - rad) + 'px'; ink.style.top = (y - rad) + 'px';
el.appendChild(ink);
ink.addEventListener('animationend', () => ink.remove());
});
}
document.querySelectorAll('nav a, .nav-toggle, .nav-close').forEach(attach);
})();
.nav-toggle を作成するスクリプトの後に実行し、トグルボタンにもリップルを適用します。
関連 §テーマの明暗切り替え(下、約 952 行目): .theme-row はこの nav の直接の子で、上の nav の CSS で定義した --nav-pad-y を共用します。nav の padding を変更すると .theme-row にも自動で反映されるため、2 か所を変更する必要はありません。
- どちらの形式にも Tier-1 ガラスを使用
section[id]を監視する IntersectionObserver によるスクロールスパイ(nav aセレクターは変更しません)
折りたたみ(サイドバーでは必須)— 独立した 2 つのボタン: 閉じるボタン ✕ はサイドバー内(右上隅、26×26、薄い背景)に配置します — クリックするとサイドバーが translateX(-100%) になり、body{padding-left:0} で元の一段組みに戻ります。開くボタン ☰ は 32×32 のガラス製で、左上隅に浮かべます。サイドバーが閉じているときだけ表示します(body:not(.nav-collapsed) .nav-toggle{opacity:0;pointer-events:none})。どちらのボタンにもリキッドガラスのリップルを付けます。状態は localStorage('navCollapsed') に保存します。狭い画面ではデフォルトで折りたたみます(matchMedia 640px)。JS が 2 つのボタンを自動生成するため、マークアップの変更は不要です。
(function(){
const nav = document.querySelector('nav'); if (!nav) return;
const btn = document.createElement('button'); btn.className = 'nav-toggle'; btn.textContent = '☰';
document.body.appendChild(btn);
const close = document.createElement('button'); close.className = 'nav-close'; close.textContent = '✕';
nav.appendChild(close);
const apply = c => { document.body.classList.toggle('nav-collapsed', c);
try { localStorage.setItem('navCollapsed', c ? '1' : '0'); } catch(e){} };
btn.addEventListener('click', () => apply(false));
close.addEventListener('click', () => apply(true));
try { const s = localStorage.getItem('navCollapsed');
if (s === '1' || (s !== '0' && matchMedia('(max-width:640px)').matches)) apply(true);
} catch(e){}
})();
nav{transition:transform .28s cubic-bezier(.4,0,.2,1)}
body{transition:padding-left .28s cubic-bezier(.4,0,.2,1)}
body.nav-collapsed nav{transform:translateX(-100%)}
body.nav-collapsed{padding-left:0}
.nav-toggle{position:fixed;top:12px;left:12px;z-index:120;width:32px;height:32px;border-radius:10px;
display:flex;align-items:center;justify-content:center;font-size:14px;color:#4a4a55;cursor:pointer;
background:linear-gradient(165deg,rgba(255,255,255,.5),rgba(255,255,255,.24));
backdrop-filter:blur(var(--blur-1)) saturate(1.7) brightness(1.04);
-webkit-backdrop-filter:blur(var(--blur-1)) saturate(1.7) brightness(1.04);
border:1px solid transparent;
box-shadow:inset 0 1px 0 rgba(255,255,255,.75),0 0 0 1px rgba(30,90,170,.08),0 2px 10px rgba(30,90,170,.12);
transition:opacity .2s,transform .28s cubic-bezier(.4,0,.2,1)}
.nav-toggle:hover{color:#0a84ff}
body:not(.nav-collapsed) .nav-toggle{opacity:0;pointer-events:none;transform:translateX(-6px)}
.nav-close{position:absolute;top:10px;right:10px;width:26px;height:26px;border-radius:8px;
display:flex;align-items:center;justify-content:center;font-size:12px;color:#4a4a55;cursor:pointer;
background:rgba(0,0,0,.04);border:none;overflow:hidden}
.nav-close:hover{color:#0a84ff;background:rgba(10,132,255,.10)}
/* .nav-toggle hiện ở mọi cỡ màn */
⚠️ 明るい背景に浮かぶガラスチップには、真っ白な実線の枠線を使ってはいけません(2026/06/13 の教訓 — ユーザーから .nav-toggle の枠線が「変に見える」と指摘):トグルボタン(および左上隅に浮かぶすべてのガラスチップ)は、ほぼ白から淡い青の平坦なページ背景に置かれています。背後に暗いものがないため、backdrop-filter:blur でガラスらしく混ぜられず、最も目立つのは rgba(255,255,255,.55) の 1px のくっきりした輪郭です。これが「貼り付けたステッカーの枠」のように見え、ガラスの縁には見えません。修正方法:border:1px solid transparent とし、内側の上部ハイライト、極めて細い冷色のリング、冷色のドロップシャドウで縁を作ります。
border:1px solid transparent;
box-shadow:inset 0 1px 0 rgba(255,255,255,.75),0 0 0 1px rgba(30,90,170,.08),0 2px 10px rgba(30,90,170,.12);
ルール:背後にぼかしのサンプルとなる暗色または多色のコンテンツ(例:セクションに重なるサイドバー)があるペインでのみ、白い実線の枠線を使います。明るい背景に浮かぶチップでは、白い線ではなく冷色のシャドウで縁を作ります。
スクロールバー — 自動で隠れるオーバーレイ(デフォルトのスクロールバーをテーマで置き換え)
ブラウザーのデフォルトのスクロールバーを、オーバーレイ型の**完全に非表示のスクロールバー(トラック/背景/角/ボタンはすべて透明で、ガタースペースは残りません)**に置き換えます。ピル型のつまみ(thumb)だけがフェードインし(アルファ値 0 → 青の色合い)、スクロール中またはスクロールバーの帯にマウスを合わせたとき(つまみのホバー時)に、コンテンツに沿って移動します。ビューポートとサイドバー(nav の overflow-y:auto)の両方に適用します。つまみは background-clip:content-box と border:3px solid transparent を使い、ガターの中央に浮かぶ、細く丸みのあるピル型にします。JS でスクロール中に .scrolling クラスを追加し、アイドル状態が約 900ms 続いた後に削除します。
⚠️ 2026/06/13の教訓(ユーザー:「背景のバーも含めて完全に隠す」): track{background:transparent} だけでは不十分です。track-piece、corner、button がデフォルトの背景や枠線を描画し、かすかなグレーの帯になることがあります。thumb 以外のすべての部分に、background:transparent;border:0;box-shadow:none を指定してください。デフォルトの thumb もアルファ値を 0(不可視)にし、.scrolling/:hover のときだけ background-color でフェードさせます(opacity と同様に滑らかに遷移します)。スクロールバーの疑似要素に opacity を直接指定しないでください(WebKit では安定して動作しません)。background-color のアルファ値を使ってください。
⚠️ スクロールバーを透明にするとガターから CANVAS が見えるため、html に背景を設定します: width:11px のガターはスペースを占有したままです。トラックと thumb が透明なため、その背後の canvas が見えます。グラデーションの背景は通常 body に設定されますが(スクロールバー分だけ狭くなります)、ガターには適用されません。その結果、デフォルトで白い html の canvas が見え、右端に全高の白い縦筋が現れます。対処法として、html にフィールドに合う青系の背景を設定します(html{background:#e9f0fb})。
/* Chromium / Safari */
::-webkit-scrollbar{width:11px;height:11px;background:transparent}
::-webkit-scrollbar-track,
::-webkit-scrollbar-track-piece,
::-webkit-scrollbar-corner,
::-webkit-scrollbar-button{background:transparent;border:0;box-shadow:none}
::-webkit-scrollbar-thumb{background:transparent;border-radius:8px;
border:3px solid transparent;background-clip:content-box;transition:background-color .25s ease}
/* hiện pill khi đang cuộn (JS toggle .scrolling) HOẶC khi trỏ vào dải scrollbar (hover thumb) */
html.scrolling::-webkit-scrollbar-thumb,
nav.scrolling::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover{background-color:rgba(10,132,255,.32)}
::-webkit-scrollbar-thumb:active{background-color:rgba(10,132,255,.6)}
/* set nền html để gutter trong suốt không lộ canvas trắng */
html{background:#e9f0fb;scrollbar-width:thin;scrollbar-color:transparent transparent}
html.scrolling{scrollbar-color:rgba(10,132,255,.32) transparent}
/* hiện khi cuộn, ẩn sau ~900ms idle — gắn cho cả trang lẫn sidebar */
(function(){
const flash = (el) => { let t; return () => { el.classList.add('scrolling'); clearTimeout(t);
t = setTimeout(() => el.classList.remove('scrolling'), 900); }; };
const root = document.documentElement;
window.addEventListener('scroll', flash(root), { passive:true });
const nav = document.querySelector('nav');
if (nav) nav.addEventListener('scroll', flash(nav), { passive:true });
})();
注:パターンに合わせるため、青の色合いには #0a84ff を使います。thumb はホバーからアクティブにかけて濃くなります。Firefox では完全に非表示にできず(幅が確保されます)、フォールバックとして色が変わる thin のスクロールバーを使います。これは許容できます。
ページ構成
<a class="skip-link"> — first focusable, jumps to #main (see Accessibility section)
<nav> — fixed LEFT sidebar with section links (never a top bar)
<main id="main"> — wraps everything below for the landmark + skip-link target
<hero> — gradient title + subtitle
<div class="mm"> — MẶC ĐỊNH: một mind map collapsible (xem § Mind Map) — luôn vẽ
<repo-card> — optional: link to source repo with chrome + collapse
<section id="sec-{i}"> — repeat for each topic (i = 0..N-1):
class="section-bg s-bg{i}"
.section-header — .tag badge ("0{i+1} · Title") + h2 + p
.diagram-box — animated SVG (see below)
.content-grid — 2-column cards (Workflow + Use cases)
.table-wrap — optional comparison table
footer link — "Chi tiết: file.md"
<footer>
マインドマップ(デフォルト — 常に1つ描画する)
すべてのドキュメントサイトには、ページ構成(各 section/エンティティ/主要項目)をまとめた NotebookLM 風の折りたたみ可能なマインドマップを必ず含めます。配置場所はヒーローの直後、または「概要」section 内です。特徴:枝ごとに色分けした曲線(ベジェ曲線)+ガラス風ノードチップ+シェブロン。デフォルトでは閉じており、クリックで開閉します。自己完結型で、CSS と純粋な JS で SVG を描画し、ライブラリは使いません。コンテンツはドキュメント自体から生成します(各 section を1つの枝、子項目を葉にします)。葉の説明は簡潔にするため、約54文字以下に切り詰めます。枝が長すぎる(葉が約8個を超える時点で問題です)場合は、フラットな構造にせず、機能ごとにさらに枝分かれさせます(root → 枝 → グループ → 葉)。各グループは、.cat has-children ノードをネストして表現し、それぞれ異なる色の .g0..g5 クラスを付けます(JS の colorOf が曲線に適切な色を返します)。各グループの葉は約8個以下に保ちます。例:overstack では dev-loop(12 → コード修正/オンボーディング構築/評価)、orchestrate(10 → 調整/評価/運用)、utils(37 → 6グループ)をすべて分割します。fdk/tools/build-overstack-docs.py の LOOP_GROUPS を参照してください。
構造(横向きのツリー:root → 枝 .cat → 葉 .leaf。曲線描画用に .mm-canvas と <svg class="mm-links"> を含みます):
<div class="mm"><div class="mm-canvas"><svg class="mm-links"></svg><div class="tree"><div class="row">
<div class="node root has-children"><span class="nm">Chủ đề</span><span class="ds">phụ đề</span><span class="ct">N</span></div>
<div class="children">
<div class="row">
<div class="node b-0 cat has-children"><span class="nm">Nhánh</span><span class="ds">mô tả</span><span class="ct">3</span></div>
<div class="children">
<div class="row"><div class="node b-0 leaf"><span class="nm">mục</span><span class="ds">mô tả ngắn</span></div></div>
</div>
</div>
</div>
</div></div></div></div>
CSS(<style> 内。デザインシステムの --ink2/--border を使用します。.b-0..b-4 は Apple のセカンダリーカラーを .nm+.ct+枠線で順に使用します。JS では曲線にも同じ色を使用します):
.mm{overflow-x:auto;padding:16px 4px 8px}
.mm-canvas{position:relative;width:max-content}
.mm-links{position:absolute;top:0;left:0;pointer-events:none;overflow:visible;z-index:0}
.mm-links path{fill:none;stroke-width:2.2;opacity:.55;stroke-linecap:round}
.mm .tree{position:relative;z-index:1}
.mm .tree,.mm .children{display:flex;flex-direction:column;gap:8px;justify-content:center}
.mm .row{display:flex;align-items:center;gap:48px;position:relative}
.mm .children{position:relative}.mm .children.collapsed{display:none}
.mm .node{position:relative;display:inline-flex;flex-direction:column;gap:1px;padding:8px 12px;border-radius:13px;cursor:default;white-space:nowrap;background:rgba(255,255,255,.72);backdrop-filter:blur(7px) saturate(1.1);border:1px solid var(--border);box-shadow:inset 0 1px 0 rgba(255,255,255,.85),0 3px 14px rgba(20,40,90,.07);transition:transform .12s}
.mm .node.has-children{cursor:pointer}.mm .node:hover{transform:translateY(-1px)}
.mm .node .nm{font-size:13px;font-weight:700;letter-spacing:-.01em}.mm .node .ds{font-size:10.5px;color:var(--ink2)}
.mm .node .ct{font-size:10px;color:#fff;font-weight:700;padding:1px 8px;border-radius:999px;position:absolute;top:-8px;right:-8px;background:#0058d0}
.mm .node.has-children::after{content:'';position:absolute;right:-7px;top:50%;width:6px;height:6px;border-right:2px solid var(--ink2);border-bottom:2px solid var(--ink2);transform:translateY(-50%) rotate(-45deg);opacity:.5}
.mm .node.collapsed-parent::after{transform:translateY(-50%) rotate(45deg)}
.mm .node.root{background:linear-gradient(135deg,rgba(10,132,255,.16),rgba(88,86,214,.14));border-color:rgba(10,132,255,.4)}
html[data-theme=dark] .mm .node{background:rgba(30,42,64,.72);border-color:rgba(120,160,220,.28);box-shadow:none}
html[data-theme=dark] .mm .node .nm{color:#e6e9f0}
@media (prefers-color-scheme:dark){html:not([data-theme=light]) .mm .node{background:rgba(30,42,64,.72);border-color:rgba(120,160,220,.28);box-shadow:none}html:not([data-theme=light]) .mm .node .nm{color:#e6e9f0}}
Playwright による測定(2026/09/22):以前の .ct の白文字を #0a84ff に重ねた場合、10px でコントラスト比は 3.67:1 でした。また、ノードには暗い背景用のバリエーションがなく、文字と暗い背景のコントラスト比は 1.56:1 でした。そのため、現在は #0058d0 と、上記の暗いブロックを使います。
(直線のコネクター .row::before/.children::before は使いません。接続線は JS が <svg class="mm-links"> 内にベジェ曲線として描画します。)
JS(枝ごとに色分けしたベジェ曲線を描画し、.cat の枝をデフォルトで閉じ、クリックによる切り替えと再描画を行います。<script> 内):
(function(){var mm=document.querySelector('.mm');if(!mm)return;var NS='http://www.w3.org/2000/svg';
function colorOf(n){return n.classList.contains('b-0')?'#30b0c7':n.classList.contains('b-1')?'#5856d6':n.classList.contains('b-2')?'#ff9500':n.classList.contains('b-3')?'#34c759':n.classList.contains('b-4')?'#ff2d55':'#9aa4b2';}
function draw(){var canvas=mm.querySelector('.mm-canvas'),svg=mm.querySelector('.mm-links');if(!canvas||!svg)return;var w=canvas.offsetWidth,h=canvas.offsetHeight;svg.setAttribute('width',w);svg.setAttribute('height',h);svg.setAttribute('viewBox','0 0 '+w+' '+h);while(svg.firstChild)svg.removeChild(svg.firstChild);var cR=canvas.getBoundingClientRect();[].slice.call(canvas.querySelectorAll('.node.has-children')).forEach(function(p){var row=p.parentElement,kids=null,ch=row.children;for(var i=0;i<ch.length;i++){if(ch[i].classList.contains('children'))kids=ch[i];}if(!kids||kids.classList.contains('collapsed'))return;var pr=p.getBoundingClientRect(),px=pr.right-cR.left,py=pr.top+pr.height/2-cR.top;[].slice.call(kids.children).forEach(function(crow){var cn=crow.querySelector(':scope > .node');if(!cn)return;var rr=cn.getBoundingClientRect(),cx=rr.left-cR.left,cy=rr.top+rr.height/2-cR.top,dx=Math.max(22,(cx-px)*0.55);var pa=document.createElementNS(NS,'path');pa.setAttribute('d','M'+px+' '+py+' C'+(px+dx)+' '+py+' '+(cx-dx)+' '+cy+' '+cx+' '+cy);pa.setAttribute('stroke',colorOf(cn));svg.appendChild(pa);});});}
[].slice.call(mm.querySelectorAll('.node.has-children')).forEach(function(n){var row=n.parentElement,kids=null,c=row.children;for(var i=0;i<c.length;i++){if(c[i].classList.contains('children'))kids=c[i];}if(!kids)return;if(n.classList.contains('cat')){kids.classList.add('collapsed');n.classList.add('collapsed-parent');}n.addEventListener('click',function(e){e.stopPropagation();var open=kids.classList.toggle('collapsed');n.classList.toggle('collapsed-parent',open);draw();});});
draw();addEventListener('load',function(){setTimeout(draw,60);});addEventListener('resize',function(){clearTimeout(window.__mmt);window.__mmt=setTimeout(draw,120);},{passive:true});})();
root(.cat ではない .has-children)は最初から開いているため、枝が表示されます。.cat の枝は閉じており、クリックすると葉が展開します。曲線は切り替え時/リサイズ時に自動で再描画されます。colorOf は .b-N クラスと同じ色を返します。実際の動作例:llmwiki/html/overstack.html の「参照」タブ。
セクション背景のパターン
各 <section> には2つのクラスを設定します:section-bg s-bgN(N は section のインデックスを6で割った余り)。
グラデーションのオーバーレイは ::before 疑似要素で作成します:
.section-bg { position: relative; overflow: visible; }
/* FULL-BLEED: dải màu tràn hết viewport (100vw), content vẫn max-width — KHÔNG đóng khung
dải tint trong box section, nhìn như panel rời, mất liền mạch */
.section-bg::before { content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
width: 100vw; transform: translateX(-50%); pointer-events: none; }
/* Generate one .s-bgN::before per section index, cycling through 6 colors */
Section のパディング:padding: 64px 24px 80px; max-width: 1100px; margin: 0 auto;
CSS ジェネレーターのパターン
セクションごとの CSS を動的に生成します。セクション数を N とし、#sec-{i} ID を使って N 個のルールセットを生成します。
For i in 0..N-1:
accent = palette[i % 6]
dark = dark[i % 6]
gradient = overlay[i % 6]
#sec-{i} .tag { background: accent at 12%; color: accent; }
#sec-{i} .card h4 { color: accent; }
#sec-{i} .card li::before { color: accent; }
#sec-{i} .section-header h2 { color: dark; }
.s-bg{i}::before { background: linear-gradient(180deg, gradient 0%, transparent 60%); }
⚠️ .tag にはダークモード専用のバリエーションが必須です(実際に発生したバグ 160926 であり、仮説ではありません)。さらに、背景だけでなく文字色も変更してください。 .tag{background:accent at 12%;color:accent} は明るい背景では十分なコントラストがありますが、同じ式をほぼ黒の背景(#0c0f16)で使うと、ピルがほとんど見えず、文字もかすんでしまいます(実際に Playwright で測定済みです。スクリーンショットを見るだけではなく、getComputedStyle と WCAG コントラスト比の計算による測定です)。誤った修正を 2 回測定して確認しています。同じ間違いを繰り返さないでください。
- 1 回目の間違い: 背景のアルファ値(
.22)と枠線だけを強め、文字色はcolor:accentのままにする → 測定されたコントラストは 1.0–1.4:1(文字が本当に読めない状態です。ピルの枠線に目が留まり、かすんだ文字に気づきにくいため、スクリーンショットでは「問題なさそう」に見えます)。 - 2 回目の間違い: 背景のアルファ値を
.26に上げ、文字色を白にする → 暗いアクセント色(blue/indigo/pink)では PASS しますが、明るいアクセント色(teal/green/orange)では依然として 2.0–2.3:1 で FAIL します。色付きの背景が濃くなるほど、白い文字とのコントラストが失われます。特に G/R チャンネルの値が高いアクセント色で顕著です。 - 正しい方法: 背景は薄く暗いまま(アルファ値
.12、実際のページ背景に近い値)にし、枠線は濃くして(.55)ピルの形をはっきりさせ、文字にはニュートラルな白(アクセント色ではなく#f2f4f8)を使います。これにより、実際に暗い背景の上に文字が表示され、どのアクセント色のチャンネル値にも左右されません。Playwright による測定で、6 色すべてのアクセント色で 11–16:1 を確認しています。
@media (prefers-color-scheme: dark){
html:not([data-theme=light]) #sec-{i} .tag{background:accent tại 12%;border:1px solid accent tại 55%;color:#f2f4f8}
}
html[data-theme=dark] #sec-{i} .tag{background:accent tại 12%;border:1px solid accent tại 55%;color:#f2f4f8}
.card h4/li::before は変更する必要はありません。そこでは暗い背景上の文字に十分なコントラストがあります(仮定ではなく測定済みです)。スクリプトでページを生成する場合は、_theme_css() と同様に「1 つのソースから 2 つのブロックを出力する」という原則を守ってください。fdk/tools/build-overstack-docs.py の accent_css() を参照してください。
アニメーション SVG 図
各図はインライン SVG(外部ファイルではありません)で、レスポンシブ表示のための viewBox を備えています。
主要なアニメーション(<defs><style> 内で再利用可能な CSS)
@keyframes flowArrow { 0% { stroke-dashoffset: 20; } 100% { stroke-dashoffset: 0; } }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
@keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes glow { 0%,100% { filter: drop-shadow(0 0 4px rgba(...)); } 50% { filter: drop-shadow(0 0 12px rgba(...)); } }
@keyframes bounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } }
@keyframes orbit { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes scanLine { 0% { transform: translateX(-40px); } 100% { transform: translateX(320px); } }
@keyframes stack { 0%,100% { opacity:.15; } 50% { opacity:.7; } }
SVG 要素には次のクラスを適用します。.flow(破線の矢印)、.pulse(ノード)、.float(出力バッジ)、.glow(電話アイコン)、.drone(バウンス)、.blink(ドローンのライト)、.orbit-ring(回転するプロダクト)、.cameraFlash(ストロボ)、.scan(スキャン線)、.stackAnim(DICOM スライス)、.center-pulse(軌道の中心)。
SVG ボックスのスタイル
- ノード:
rx="6"またはrx="8"の角丸長方形を使い、fill="rgba(255,255,255,.7)"と色付きのストロークを指定します。 - 矢印:
<marker>定義を使うmarker-end="url(#arrowN)"を指定した<line>に、stroke-width="2"と.flowクラスを適用します。 - テキスト: ラベルには
text-anchor="middle"、font-size="9-11"、font-weight="600"を指定します。 - ページで使われている
font-familyを使用します(var(--font-text)。macOS を優先するスタックについてはフォントのセクションを参照してください)。 <svg>には必ずxmlns="http://www.w3.org/2000/svg"を指定します。
ノードをドラッグできる図(すべての .diagram-box で必須)
すべての .diagram-box は、静的な画像や単にパンできるだけの画像ではなく、インタラクティブなノードグラフでなければなりません。
- 各ボックス(ノード)を個別にドラッグできます。テーブルやステップを1つ動かしても、ほかの要素は動きません。コネクター線はノードを自動的に追従するように経路を変更します。
- 空の背景をドラッグすると、キャンバス全体をパンできます。
- マウスホイールでカーソルに向かってズームします。ダブルクリック/⟲ リセットでノードの位置とキャンバスを元に戻します。
.diagram-boxは垂直方向にサイズ変更できます(CSS のresize)。コンテナーは滑らかに拡張します。
これは記述された SVG から自動検出されるため、SVG のマークアップを変更する必要はありません。JS は 70×30 以上のすべての <rect> をノードとして扱い、その <rect> 内に座標が収まる <text>/小さな <rect> の子要素を取り込み、それらを <g class="dnode"> で囲みます。また、各 <line> の端点を最も近いノードにバインドし、コネクターが移動に追従するようにします。座標変換には getScreenCTM().inverse() を使うため、ズーム/パン中もドラッグの精度が保たれます。
CSS — 古い静的な .diagram-box ルールを次の内容に置き換えます。
.diagram-box{background:var(--glass-2);backdrop-filter:blur(var(--blur-2)) saturate(1.1);
border:1px solid var(--border);border-radius:16px;box-shadow:var(--edge-hi),0 4px 20px rgba(20,40,90,.08);
padding:24px;margin:24px 0;
position:relative;overflow:hidden;display:flex;flex-direction:column;
resize:vertical;min-height:160px;transition:box-shadow .2s ease}
.diagram-box:hover{box-shadow:var(--edge-hi),0 6px 28px rgba(20,40,90,.14)}
.diagram-viewport{position:relative;flex:1 1 auto;width:100%;overflow:hidden;cursor:grab;touch-action:none}
.diagram-viewport.grabbing{cursor:grabbing}
.diagram-viewport svg{width:100%;height:auto;display:block;overflow:visible;transform-origin:0 0;
will-change:transform;user-select:none;-webkit-user-select:none}
.dnode{cursor:move}
.dnode>rect{transition:filter .15s ease}
.dnode:hover>rect:first-of-type{filter:drop-shadow(0 3px 8px rgba(0,0,0,.18))}
.diagram-hint{position:absolute;top:8px;right:12px;z-index:5;font-size:10px;color:#4a4a55;
background:rgba(255,255,255,.75);border:1px solid rgba(0,0,0,.05);border-radius:20px;
padding:4px 12px;white-space:nowrap;opacity:0;transition:opacity .2s;pointer-events:none}
.diagram-box:hover .diagram-hint{opacity:.9}
.diagram-reset{position:absolute;bottom:8px;right:10px;z-index:5;font-size:11px;
background:rgba(255,255,255,.85);border:1px solid rgba(0,0,0,.08);border-radius:8px;
padding:4px 8px;cursor:pointer;color:#4a4a55;opacity:0;transition:opacity .2s}
.diagram-box:hover .diagram-reset{opacity:1}
.diagram-reset:hover{background:#fff;color:#0f0f12}
/* resize grip kiểu macOS: 3 vạch chéo trong tam giác góc — ẨN mặc định, hover mới hiện.
⛔ KHÔNG dùng góc L 2 cạnh đậm luôn-hiện (bài học 12/06/2026 — user chê "luôn hiện mà còn xấu").
Kèm ::-webkit-resizer{display:none} để giấu grip mặc định của browser. */
.diagram-box::after{content:'';position:absolute;right:5px;bottom:5px;width:16px;height:16px;
pointer-events:none;opacity:0;transition:opacity .25s ease;
clip-path:polygon(100% 0,100% 100%,0 100%);
background:repeating-linear-gradient(135deg,transparent 0 3.5px,rgba(10,132,255,.45) 3.5px 5px)}
.diagram-box:hover::after{opacity:.85}
.diagram-box::-webkit-resizer{display:none}
JS — 一度だけ追加し、DOM の解析後(<body> の末尾にあるスクリプト)に呼び出します。
function initDraggableDiagrams() {
const NS = 'http://www.w3.org/2000/svg';
const nearest = (nodes, x, y) => {
let best = null, bd = 1e9;
nodes.forEach(n => {
const cx = Math.max(n.x, Math.min(x, n.x + n.w)), cy = Math.max(n.y, Math.min(y, n.y + n.h));
const d = Math.hypot(x - cx, y - cy);
if (d < bd) { bd = d; best = n; }
});
return bd <= 42 ? best : null;
};
document.querySelectorAll('.diagram-box').forEach(box => {
const svg = box.querySelector('svg');
if (!svg || box.dataset.draggable) return;
box.dataset.draggable = '1';
const vp = document.createElement('div'); vp.className = 'diagram-viewport';
box.insertBefore(vp, svg); vp.appendChild(svg);
const hint = document.createElement('div'); hint.className = 'diagram-hint';
hint.textContent = '✥ kéo từng ô · kéo nền để pan · cuộn để zoom · kéo mép dưới để mở rộng';
box.appendChild(hint);
const reset = document.createElement('button'); reset.className = 'diagram-reset'; reset.textContent = '⟲ reset';
box.appendChild(reset);
// detect node rects + adopt children
const allRects = [...svg.querySelectorAll('rect')];
const nodes = allRects.filter(r => (+r.getAttribute('width')) >= 70 && (+r.getAttribute('height')) >= 30)
.map(r => ({ rect: r, x: +r.getAttribute('x'), y: +r.getAttribute('y'),
w: +r.getAttribute('width'), h: +r.getAttribute('height'), els: [r], tx: 0, ty: 0 }));
const inNode = (px, py) => nodes.find(n => px >= n.x-1 && px <= n.x+n.w+1 && py >= n.y-1 && py <= n.y+n.h+1);
[...svg.querySelectorAll('text')].forEach(t => {
const x = parseFloat(t.getAttribute('x')), y = parseFloat(t.getAttribute('y'));
if (isNaN(x) || isNaN(y)) return; const n = inNode(x, y); if (n) n.els.push(t);
});
allRects.forEach(r => { if (nodes.some(n => n.rect === r)) return;
const x = +r.getAttribute('x'), y = +r.getAttribute('y'), w = +r.getAttribute('width')||0, h = +r.getAttribute('height')||0;
const n = inNode(x+w/2, y+h/2); if (n) n.els.push(r); });
nodes.forEach(n => { const g = document.createElementNS(NS, 'g'); g.setAttribute('class', 'dnode');
n.rect.parentNode.insertBefore(g, n.rect); n.els.forEach(el => g.appendChild(el)); n.g = g; });
// bind connector lines
const links = [...svg.querySelectorAll('line')].filter(l => l.hasAttribute('x1') && l.hasAttribute('x2'));
links.forEach(l => { l._x1=+l.getAttribute('x1'); l._y1=+l.getAttribute('y1'); l._x2=+l.getAttribute('x2'); l._y2=+l.getAttribute('y2');
l._n1 = nearest(nodes, l._x1, l._y1); l._n2 = nearest(nodes, l._x2, l._y2); });
const reroute = () => links.forEach(l => {
if (l._n1) { l.setAttribute('x1', l._x1+l._n1.tx); l.setAttribute('y1', l._y1+l._n1.ty); }
if (l._n2) { l.setAttribute('x2', l._x2+l._n2.tx); l.setAttribute('y2', l._y2+l._n2.ty); } });
// auto-fit: viewBox (→ svg height → box) grows to contain dragged nodes ("sizing cùng")
const vbBase = (svg.getAttribute('viewBox') || '0 0 900 200').split(/\s+/).map(Number);
const fitViewBox = () => {
const pad = 16;
let minX = vbBase[0], minY = vbBase[1], maxX = vbBase[0]+vbBase[2], maxY = vbBase[1]+vbBase[3];
nodes.forEach(n => { minX = Math.min(minX, n.x+n.tx-pad); minY = Math.min(minY, n.y+n.ty-pad);
maxX = Math.max(maxX, n.x+n.w+n.tx+pad); maxY = Math.max(maxY, n.y+n.h+n.ty+pad); });
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX-minX} ${maxY-minY}`);
};
// pan + zoom + per-node drag
let ptx=0, pty=0, scale=1;
const applyCanvas = () => { svg.style.transform = `translate(${ptx}px,${pty}px) scale(${scale})`; };
const toSvg = (cx, cy) => { const p = svg.createSVGPoint(); p.x=cx; p.y=cy; return p.matrixTransform(svg.getScreenCTM().inverse()); };
let mode=null, node=null, start=null, t0=null, p0=null;
vp.addEventListener('pointerdown', e => { vp.setPointerCapture(e.pointerId);
const g = e.target.closest && e.target.closest('g.dnode');
if (g) { mode='node'; node = nodes.find(n => n.g === g); start = toSvg(e.clientX, e.clientY); t0 = { x: node.tx, y: node.ty }; }
else { mode='pan'; p0 = { x: e.clientX-ptx, y: e.clientY-pty }; vp.classList.add('grabbing'); } });
vp.addEventListener('pointermove', e => {
if (mode==='node') { const c = toSvg(e.clientX, e.clientY); node.tx = t0.x+(c.x-start.x); node.ty = t0.y+(c.y-start.y);
node.g.setAttribute('transform', `translate(${node.tx},${node.ty})`); reroute(); }
else if (mode==='pan') { ptx = e.clientX-p0.x; pty = e.clientY-p0.y; applyCanvas(); } });
const end = () => { if (mode==='node') fitViewBox(); mode=null; node=null; vp.classList.remove('grabbing'); };
vp.addEventListener('pointerup', end); vp.addEventListener('pointercancel', end);
vp.addEventListener('wheel', e => { e.preventDefault();
const r = vp.getBoundingClientRect(), mx = e.clientX-r.left, my = e.clientY-r.top;
const ns = Math.min(4, Math.max(0.5, scale*(e.deltaY<0?1.1:0.9)));
ptx = mx-(mx-ptx)*(ns/scale); pty = my-(my-pty)*(ns/scale); scale = ns; applyCanvas(); }, { passive:false });
const doReset = () => { ptx=0; pty=0; scale=1; svg.style.transition='transform .3s cubic-bezier(.4,0,.2,1)'; applyCanvas();
setTimeout(() => { svg.style.transition=''; }, 320);
nodes.forEach(n => { n.tx=0; n.ty=0; n.g.setAttribute('transform', 'translate(0,0)'); }); reroute(); fitViewBox(); };
reset.addEventListener('click', doReset); vp.addEventListener('dblclick', doReset);
});
}
initDraggableDiagrams();
注意事項:
- これまでどおり SVG を記述します(固定の
viewBox、フラットな<rect>/<text>/<line>)。ノードのグループ化と線のバインドは実行時に推測されます。正しく取り込まれるよう、ノードラベルのx/yは<rect>の境界内に配置してください。 - コネクターが自動追従するには、
<line>(x1/y1/x2/y2を指定)である必要があります。<path>のコネクターは静的なままです。ノードに追従させるものには<line>を使ってください。 - 自動フィット:ドラッグを離すと、ドラッグしたノードが収まるよう SVG の
viewBoxが拡張されます。そのため、SVG の高さ(およびボックス)はコンテンツに合わせて調整され、ドラッグを離した後にノードが切り取られることはありません。svg{overflow:visible}により、ドラッグ中もノードが表示されます。fitViewBox()はpointerupとリセット時に実行されます。 - 冪等性を備えています(
dataset.draggableガード)。resize:verticalによりユーザーがコンテナーを拡張でき、フレックスのビューポートが新しい高さに合わせて広がります。
Mermaid 図エンジン(複雑な図向けの自動レイアウト)
上記の手書きパスが機能するのは、LLM が各ノードの x/y を手動で選ぶからです。小さな図(ノード数が約5以下で、ほぼ直線的なもの)なら問題ありませんが、分岐が実際にある図ではレイアウトアルゴリズムがなく、判断に頼るだけなので、座標にばらつきが出ます。ノード数が6以上、または分岐/合流がある図では、代わりにこの Mermaid パスを使ってください。つまり、実際の自動レイアウトエンジンを使う価値がある図に適しています。この基準を下回る場合は、手書き SVG パスを使ってください(段階的に導入する方針:YAGNI — 3つのボックスのフローに1.5MBのエンジンを持ち込まないでください)。
これは beautiful-mermaid(MIT、github.com/lukilabs/beautiful-mermaid)を通じて、本物の Mermaid DSL を描画します。ELK.js を基盤とする同期レンダラーです。公式の Mermaid.js ではなくこちらを選んだのは、軽量で、非同期処理によるちらつきがなく、2色テーマ(bg/fg と、任意の line/accent/muted/surface/border)が <svg> ルート上の CSS カスタムプロパティで定義されるためです。そのため、既存のダーク/ライト切り替えですぐにテーマを変更でき、再描画は不要です。バンドルは vendor/beautiful-mermaid.min.js.gz.b64 にgzip 圧縮済みで同梱されています(出所と正確な再ビルドコマンドについては vendor/README.md を参照してください)。バンドルのビルドは配布処理とは別に一度だけ行われ、ページの生成時にバンドラーが実行されたり、ネットワークに接続したりすることはありません。
圧縮する理由: 生のバンドルは1.56MBです。そのまま埋め込むと、図の数に関係なく、生成されるすべてのページにこの容量が追加されます。gzip 圧縮してから base64 に変換すると625KBになります(実測値:gzip -9 により生データは468,777バイト、インライン埋め込み用に base64 化すると625,037バイトで、元の40%)。クライアント側ではブラウザー標準 API の DecompressionStream('gzip') を使って展開します(Chrome/Edge 80+、2020年、Firefox 113+、2021年、Safari 16.4+、2023年に対応。世界全体で約95%をカバー)。そのため、ライブラリーの容量はまったく増えません。Brotli のほうが圧縮率は高くなります(生データで364KB)が、DecompressionStream("brotli") のブラウザー間のサポートがまだ確実に確認されていないため、採用しませんでした(MDN の情報源によって見解が異なります)。どこでも描画できる必要があるこのスキルでは、広く定着し、明確にサポートされている gzip が適切です。
1. 組み立て — 圧縮されたベンダーの Blob と、解凍して評価するブートストラップを埋め込みます。 出力 HTML ファイルを書き込むときに、vendor/beautiful-mermaid.min.js.gz.b64 の Base64 テキストを JS 文字列定数に挿入し、その後に次のブートストラップを続けます(ファイルを連結して固定スニペットを追加するだけです。ページごとに書き直さないでください)。
<script>
var __BM_GZ_B64 = "PASTE vendor/beautiful-mermaid.min.js.gz.b64 CONTENTS HERE, verbatim, no line breaks inside the string";
window.__bmReady = (typeof DecompressionStream === 'undefined')
? Promise.reject(new Error('DecompressionStream unsupported'))
: (async function(){
var bin = atob(__BM_GZ_B64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
var stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
var code = await new Response(stream).text();
(0, eval)(code); // indirect eval -> runs in global scope, defines window.BeautifulMermaid
})();
</script>
これにより、ベンダーの読み込みは非同期になります(解凍に 1 ティックかかります)。以下の renderMermaidDiagram は最初の行で window.__bmReady を待機するため、すべての呼び出し箇所で await renderMermaidDiagram(...)(または .then())を使う必要があります。同期的な戻り値を期待して呼び出してはいけません。
2. テーマトークン — §Theme Toggle sáng/tối で既に必須とされている同じ :root / ダークテーマ上書きブロックに、これらを追加します(このスキルの実際のパレットに対応させます。テキスト #0f0f12/#4a4a55、境界線 rgba(30,90,170,.14)、アクセント #0a84ff — 28〜29 行目、103 行目)。
§Theme Toggle で定義した 4 つのルールの構成を必ずそのまま使います(「システムがダークで、ユーザーが未選択」の場合は @media 内に 2 つ、「ユーザーが明示的にダークを選択」の場合は外に 2 つ)。@media 内のペアが欠けると、ページ全体がシステム設定に応じて暗くなってもダイアグラムは明るいテーマのままになります。これは、この箇所を最初に書いたときに見つかった小さなバグです。
:root{
--mm-bg:#f7fbff; --mm-fg:#0f0f12; --mm-line:rgba(30,90,170,.45);
--mm-accent:#0a84ff; --mm-muted:#4a4a55; --mm-surface:rgba(255,255,255,.7);
--mm-border:rgba(30,90,170,.28);
}
@media (prefers-color-scheme: dark){
html:not([data-theme=light]){
--mm-bg:#0d1a2c; --mm-fg:#e8f0fb; --mm-line:rgba(120,160,220,.4);
--mm-accent:#5aa8ff; --mm-muted:#9fb4d1; --mm-surface:rgba(30,46,72,.7);
--mm-border:rgba(120,160,220,.32);
}
}
html[data-theme=dark]{ /* CÙNG token như trong @media ở trên */
--mm-bg:#0d1a2c; --mm-fg:#e8f0fb; --mm-line:rgba(120,160,220,.4);
--mm-accent:#5aa8ff; --mm-muted:#9fb4d1; --mm-surface:rgba(30,46,72,.7);
--mm-border:rgba(120,160,220,.32);
}
3. .diagram-box / .diagram-viewport のマークアップは、手描き SVG の方法から変更しません(同じ CSS、457〜491 行目)。Mermaid の方法でも同じコンテナー内に描画します。追加するのは 1 点だけです。innerHTML を受け取るラッパーの div に明示的な高さを指定してください。そうしないと、SVG の height:100% が暗黙に auto として解決されます(PoC 中に実際に発生したバグです。CSS のパーセント高さには、明確な高さを持つ祖先要素が必要です)。
.mm-out{height:100%;display:block}
4. 描画、テーマのマッピング、Self-Contained の修正 — 複雑度のしきい値を超える .diagram-box ごとに 1 回呼び出します。
async function renderMermaidDiagram(container, dsl){
try { await window.__bmReady; }
catch (e){
// Graceful degradation (no DecompressionStream — pre-2020 Chrome/Edge, pre-2021
// Firefox, pre-16.4 Safari): explain instead of a silent blank box or a crash.
container.textContent = 'Sơ đồ này cần trình duyệt mới hơn (Chrome/Edge 80+, Firefox 113+, Safari 16.4+).';
return null;
}
function cssVar(name){ return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
var svg = BeautifulMermaid.renderMermaidSVG(dsl, {
bg: cssVar('--mm-bg'), fg: cssVar('--mm-fg'), accent: cssVar('--mm-accent'),
line: cssVar('--mm-line'), muted: cssVar('--mm-muted'),
surface: cssVar('--mm-surface'), border: cssVar('--mm-border')
});
// Self-Contained fix (CRITICAL, not optional): beautiful-mermaid hard-codes a
// Google Fonts @import into every SVG it returns, with no API to disable it.
// Strip it before inserting — font falls back to the system stack already
// declared right after 'Inter' in the same <style> block, so nothing breaks.
svg = svg.replace(/@import url\('https:\/\/fonts\.googleapis\.com[^;]+;/g, '');
container.innerHTML = svg;
var svgEl = container.querySelector('svg');
applyMermaidGlassStyling(svgEl);
initMermaidNodeDrag(svgEl);
return svgEl;
}
すべての呼び出し箇所を await renderMermaidDiagram(container, dsl) にします。ページのダイアグラム初期化コードを async function(またはトップレベルの (async()=>{...})())で囲み、同期的な SVG 要素が返ることを期待して呼び出してはいけません。
5. Glassmorphism の後処理(必須) — beautiful-mermaid のデフォルト描画はフラットです。ページの他の部分と統一するために、角丸、柔らかな青いドロップシャドウ、鏡面のような光沢を適用します(.diagram-box / .card と同じレシピです。SVG は backdrop-filter を安定してサポートしないため、偽の backdrop-filter は使わないでください)。さらに、動きを出すためにエッジに控えめな流れる破線を追加します。新しい用語を作らず、上で定義した (§Key Animations) flowArrow キーフレームをそのまま再利用してください。ノードやエッジの座標には一切触れません。座標はすでに ELK.js が計算しており、ここではその上に視覚的なレイヤーを追加するだけです。
function applyMermaidGlassStyling(svg){
var NS = 'http://www.w3.org/2000/svg';
var defs = svg.querySelector('defs');
if (!defs){ defs = document.createElementNS(NS,'defs'); svg.insertBefore(defs, svg.firstChild); }
var filter = document.createElementNS(NS,'filter');
filter.setAttribute('id','glass-shadow'); filter.setAttribute('x','-40%'); filter.setAttribute('y','-40%');
filter.setAttribute('width','180%'); filter.setAttribute('height','180%');
filter.innerHTML = '<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#0a2a5c" flood-opacity="0.18"/>';
defs.appendChild(filter);
var sheen = document.createElementNS(NS,'linearGradient');
sheen.setAttribute('id','glass-sheen'); sheen.setAttribute('x1','0'); sheen.setAttribute('y1','0');
sheen.setAttribute('x2','1'); sheen.setAttribute('y2','1');
sheen.innerHTML = '<stop offset="0%" stop-color="#ffffff" stop-opacity="0.55"/>'
+ '<stop offset="45%" stop-color="#ffffff" stop-opacity="0.06"/>'
+ '<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>';
defs.appendChild(sheen);
var style = document.createElementNS(NS,'style');
style.textContent = [
'.edge{stroke-dasharray:9 5;stroke-linecap:round;animation:flowArrow 1.9s linear infinite}',
'@media (prefers-reduced-motion:reduce){.edge{animation:none}}',
'g.node rect,g.node polygon{filter:url(#glass-shadow);transition:filter .18s ease}',
'g.node:hover rect,g.node:hover polygon{filter:url(#glass-shadow) brightness(1.04)}',
'.glass-sheen-ov{pointer-events:none}'
].join('\n');
svg.appendChild(style);
svg.querySelectorAll('g.node').forEach(function(g){
var shape = g.querySelector('rect') || g.querySelector('polygon');
if (!shape) return;
var ov;
if (shape.tagName === 'rect'){
shape.setAttribute('rx','8'); shape.setAttribute('ry','8');
ov = document.createElementNS(NS,'rect');
['x','y','width','height'].forEach(function(a){ ov.setAttribute(a, shape.getAttribute(a)); });
ov.setAttribute('rx','8'); ov.setAttribute('ry','8');
} else if (shape.tagName === 'polygon'){
ov = document.createElementNS(NS,'polygon'); ov.setAttribute('points', shape.getAttribute('points'));
} else { return; }
ov.setAttribute('fill','url(#glass-sheen)'); ov.setAttribute('class','glass-sheen-ov');
shape.insertAdjacentElement('afterend', ov);
});
}
6. インタラクティブレイヤー — エッジの再ルーティングを伴うノード単位のドラッグ、ホイールズーム、背景のパンはなし。 beautiful-mermaid はすべての g.node に data-id を、すべての polyline.edge に data-from / data-to をネイティブに出力します。サイズのヒューリスティックではなく、実際の ID を使って紐付けてください。ユーザーの決定(200826):空のキャンバスをドラッグしても何も起きません — ドラッグできるのは個々のノードだけですが、ホイールズームは引き続き機能します。(上記の手描き SVG の方法では、独自の背景ドラッグによるパンの挙動を変更しません。この決定が適用されるのは Mermaid の方法だけです。)
function initMermaidNodeDrag(svg){
var NS = 'http://www.w3.org/2000/svg';
var nodeState = {}, camera = { ptx:0, pty:0, scale:1 };
var nodeGroups = [].slice.call(svg.querySelectorAll('g.node'));
var edges = [].slice.call(svg.querySelectorAll('polyline.edge'));
nodeGroups.forEach(function(g){ nodeState[g.getAttribute('data-id')] = { tx:0, ty:0 }; g.style.cursor = 'move'; });
edges.forEach(function(e){
e._basePts = e.getAttribute('points').trim().split(/\s+/).map(function(pair){
var xy = pair.split(',').map(Number); return { x:xy[0], y:xy[1] };
});
});
function reroute(){
edges.forEach(function(e){
var pts = e._basePts.map(function(p){ return { x:p.x, y:p.y }; });
var from = nodeState[e.getAttribute('data-from')], to = nodeState[e.getAttribute('data-to')];
if (from){ pts[0].x += from.tx; pts[0].y += from.ty; }
if (to){ var last = pts[pts.length-1]; last.x += to.tx; last.y += to.ty; }
e.setAttribute('points', pts.map(function(p){ return p.x+','+p.y; }).join(' '));
});
}
function toSvgPoint(cx, cy){ var p = svg.createSVGPoint(); p.x = cx; p.y = cy; return p.matrixTransform(svg.getScreenCTM().inverse()); }
function applyCamera(){ svg.style.transform = 'translate('+camera.ptx+'px,'+camera.pty+'px) scale('+camera.scale+')'; }
var vp = svg.closest('.diagram-viewport') || svg.parentElement;
var mode = null, dragId = null, start = null, t0 = null;
vp.onpointerdown = function(e){
var g = e.target.closest && e.target.closest('g.node');
if (!g) return; // nền trống: KHÔNG làm gì (yêu cầu user 200826) — chỉ node mới bắt sự kiện
vp.setPointerCapture(e.pointerId);
mode = 'node'; dragId = g.getAttribute('data-id');
start = toSvgPoint(e.clientX, e.clientY); t0 = { x: nodeState[dragId].tx, y: nodeState[dragId].ty };
};
vp.onpointermove = function(e){
if (mode !== 'node') return;
var p = toSvgPoint(e.clientX, e.clientY), st = nodeState[dragId];
st.tx = t0.x + (p.x - start.x); st.ty = t0.y + (p.y - start.y);
svg.querySelector('g.node[data-id="'+dragId+'"]').setAttribute('transform', 'translate('+st.tx+','+st.ty+')');
reroute();
};
var endDrag = function(){ mode = null; dragId = null; };
vp.onpointerup = endDrag; vp.onpointercancel = endDrag;
vp.onwheel = function(e){
e.preventDefault();
var r = vp.getBoundingClientRect(), mx = e.clientX-r.left, my = e.clientY-r.top;
var ns = Math.min(4, Math.max(0.3, camera.scale*(e.deltaY<0?1.1:0.9)));
camera.ptx = mx-(mx-camera.ptx)*(ns/camera.scale); camera.pty = my-(my-camera.pty)*(ns/camera.scale);
camera.scale = ns; applyCamera();
};
}
エンドツーエンドで検証済み(スクリーンショットだけでなく、Playwright/Chromium で検証):9 ノードの分岐ダイアグラムが正しい ELK.js レイアウトで描画されます。ノード単位のドラッグでは、そのノードだけが移動し、紐付けられたエッジの最も近い端点が正確に追従します(測定した transform の差分 == マウスの差分)。兄弟ノードの transform は null のままです。背景をドラッグしても SVG の transform は変化しません。ホイールズームも引き続き機能します。テーマの切り替えでは、再描画なしで CSS カスタムプロパティを介してダイアグラムの色がリアルタイムに変わります。
コードパネルのコピーボタン(すべての pre.code-block で必須)
すべてのコードパネルに、ホバー時に表示されるコピーボタンを必ず付けてください。ボタンを挿入する前に textContent を取得し(ボタンのラベルがコピーされないようにします)、<pre> を相対配置の .code-wrap で囲み、Clipboard API を使ってコピーします。フォールバックとして execCommand を使用してください。
.code-wrap{position:relative}
.code-copy{position:absolute;top:7px;right:7px;z-index:2;width:28px;height:26px;display:inline-flex;align-items:center;justify-content:center;
background:rgba(255,255,255,.1);color:#cbd5e1;border:1px solid rgba(255,255,255,.15);
border-radius:7px;cursor:pointer;opacity:0;transition:opacity .15s,background .15s,color .15s}
.code-copy svg{width:14px;height:14px;display:block}
.code-wrap:hover .code-copy{opacity:1}
.code-copy:hover{background:rgba(255,255,255,.2);color:#fff}
.code-copy.copied{background:rgba(16,185,129,.25);color:#6ee7b7;border-color:rgba(16,185,129,.45);opacity:1}
@media(max-width:640px){.code-copy{opacity:1}}
function initCodeCopy() {
const COPY = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
const CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
document.querySelectorAll('pre.code-block').forEach(pre => {
if (pre.dataset.copy) return; pre.dataset.copy = '1';
const code = pre.textContent;
const wrap = document.createElement('div'); wrap.className = 'code-wrap';
pre.parentNode.insertBefore(wrap, pre); wrap.appendChild(pre);
const btn = document.createElement('button'); btn.className = 'code-copy';
btn.setAttribute('aria-label', 'Copy'); btn.title = 'Copy'; btn.innerHTML = COPY;
wrap.appendChild(btn);
btn.addEventListener('click', async () => {
try { await navigator.clipboard.writeText(code); }
catch { const ta = document.createElement('textarea'); ta.value = code;
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); }
btn.innerHTML = CHECK; btn.classList.add('copied');
setTimeout(() => { btn.innerHTML = COPY; btn.classList.remove('copied'); }, 1500);
});
});
}
initCodeCopy();
水滴リップルクリック効果(すべてのインタラクティブなコントロールで必須)
クリック可能なすべてのコントロール(任意の <button>、.nav-link、.collapse-toggle、.code-copy、.diagram-reset、チェックリストのラベル)では、ポインターダウン時にリキッドガラス風の水滴リップルを発生させます。クリック位置から水の波紋のように広がって消える、淡い青みを帯びた柔らかな白いしぶきです。グローバルリスナーを1つだけ使い、ボタンごとの設定は不要です。
.ripple{position:absolute;border-radius:50%;pointer-events:none;transform:scale(0);opacity:.9;
background:radial-gradient(circle, rgba(255,255,255,.6) 0%, rgba(10,132,255,.22) 35%, transparent 70%);
box-shadow:0 0 0 1px rgba(255,255,255,.45);
animation:rippleWave .65s cubic-bezier(.2,.6,.3,1) forwards}
@keyframes rippleWave{to{transform:scale(2.8);opacity:0}}
function initRipple() {
document.addEventListener('pointerdown', e => {
const el = e.target.closest('button, .nav-link, .collapse-toggle, .checklist label');
if (!el || el.dataset.noRipple) return;
const r = el.getBoundingClientRect();
const d = Math.max(r.width, r.height) * 1.2;
const s = document.createElement('span');
s.className = 'ripple';
s.style.cssText = `width:${d}px;height:${d}px;left:${e.clientX - r.left - d/2}px;top:${e.clientY - r.top - d/2}px`;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
el.style.overflow = 'hidden';
el.appendChild(s);
s.addEventListener('animationend', () => s.remove());
});
}
initRipple();
注記:
- リップルの起点はコントロールの中央ではなく、実際のクリック位置(コントロールを基準とした
clientX/Y)です。これにより、閃光ではなく水のように見えます。 - 波紋がコントロールの角丸形状に沿って切り取られるよう、ホスト要素には
overflow:hiddenを強制します。 - 切り取りによってレイアウトが崩れるコントロールには
data-no-rippleを付けて除外します(例:図のビューポート自体。パンやドラッグでしぶきを出さないようにします)。 - 配色に合わせて青系の色合い(
rgba(10,132,255,…))を維持します。暗い背景(コードパネル)では、白い中心部分が効果を担います。
折りたたみ/続きを読む
アニメーション付きの展開/折りたたみセクション:
<button class="collapse-toggle" onclick="toggleX()">
<span class="arrow" id="arrowX">▶</span> Label
</button>
<div class="collapse-body" id="bodyX">
<div class="collapse-body-inner">...</div>
</div>
CSS:
.collapse-body {
max-height: 0; overflow: hidden;
transition: max-height .35s cubic-bezier(.4,0,.2,1), opacity .25s ease;
opacity: 0;
}
.collapse-body.open { opacity: 1; } /* open max-height is set by JS = scrollHeight, NOT a magic cap */
.collapse-toggle .arrow { transition: transform .25s cubic-bezier(.4,0,.2,1); }
.collapse-toggle .arrow.open { transform: rotate(90deg); }
⛔ .collapse-body.open{max-height:800px} をハードコードしないでください。上限を超えるコンテンツは気付かないうちに切り取られます。scrollHeight を測定してインラインで max-height を設定し、パネルが常にコンテンツ全体に収まるようにします。閉じるときは 0 に戻して、トランジションがアニメーションするようにします。
JS:
function toggleX() {
const body = document.getElementById('bodyX');
const open = body.classList.toggle('open');
document.getElementById('arrowX').classList.toggle('open');
body.style.maxHeight = open ? body.scrollHeight + 'px' : '0';
}
/* if a panel contains images/late-laid-out content, re-measure on resize for any .open panel */
addEventListener('resize', () => document.querySelectorAll('.collapse-body.open')
.forEach(b => { b.style.maxHeight = b.scrollHeight + 'px'; }));
マスター・ディテールのクリック表示(左のリスト → 右の詳細、1つのスクロールページ内)
各項目に長い詳細コンテンツ(ツアーのステップ、エンドポイント、モジュール、ルール一覧など)があるセクションでは、詳細をすべて表示したり、別ページやタブに分割したりしないでください。マスター・ディテールを使用します。左カラムには項目のリスト(ガラス風チップ、クリック可能)を置き、右カラムには1つの詳細パネルを置きます。クリックするとパネルの内容だけがその場で切り替わります。ページ遷移はせず、ほかのセクションも非表示にしません(連続したスクロールページのままです)。これにより、スクロールスパイを崩さずに「順に読み進め、クリックして詳しく見る」形式を実現できます。
<div class="md-wrap"><ul class="md-list" role="listbox"></ul><div class="card md-detail"></div></div>
.md-wrap{display:grid;grid-template-columns:268px 1fr;gap:20px}
.md-list li{padding:12px 12px;border-radius:13px;cursor:pointer;margin-bottom:8px;background:var(--glass-2);
backdrop-filter:blur(var(--blur-2));border:1px solid var(--border);box-shadow:var(--edge-hi);transition:.16s}
.md-list li:hover{transform:translateX(3px)}
.md-list li[aria-selected=true]{background:linear-gradient(120deg,rgba(255,255,255,.92),rgba(244,242,255,.85));border-color:#cdc4ff}
@media(max-width:760px){.md-wrap{grid-template-columns:1fr}}
- アクセシビリティ:
role="listbox"/role="option"と、選択中の項目のaria-selectedを設定します。最初に選択する項目は index 0 です。 - リップル:必要に応じてリストコントロールにのみ付けます。しぶきが詳細パネルに広がらないようにしてください。
- 実際の実装例:
orca-onboardの「Guided Tour」タブ(skeleton v2)。
サイドバーのアイコンタイル(macOS SF Symbols風)
各 nav a は角丸のグラデーションタイル(macOSの「システム設定」風)とし、インライン SVG で描画した線画アイコン(白いストローク、約14px、丸い線端)を含めます。⛔ Unicodeグリフ(◫ ▸ ▤)は使わないでください。ベースラインがずれて見栄えが悪くなります。アプリのロゴには、グラデーションタイル内に「ディスプレイ/モニター」の SVG アイコンを配置します。SF Symbols は Apple の独自フォントです。フォントを埋め込まず、SVGパスを模して描画してください(自己完結型)。
nav a .ic{width:24px;height:24px;border-radius:7px;display:grid;place-items:center}
nav a .ic svg{width:14px;height:14px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* mỗi tab 1 màu tile (theo section accent) */
推奨マッピング:overview→info.circle · architecture→square.stack.3d · guided-tour→mappin.and.ellipse
· modules→cube.box · run/docker→terminal。実際の実装例:orca-onboard skeleton v2。
レスポンシブ
@media (max-width: 700px) {
.content-grid { grid-template-columns: 1fr; }
}
スクロールスパイ
const observer = new IntersectionObserver(entries => {
let active = '';
for (const entry of entries) {
if (entry.isIntersecting) active = entry.target.id;
}
if (active) links.forEach(a => a.classList.toggle('active', a.getAttribute('href') === '#' + active));
}, { rootMargin: '-40% 0px -55% 0px' });
sections.forEach(s => observer.observe(s));
フォント
システムフォントのみを使用してください。Google Fonts の <link>、@import、Webフォントのダウンロードは禁止です。macOS のフォントセット(San Francisco)を優先します。SF がない環境では Roboto/Segoe UI にフォールバックします。いずれのフォールバックも洗練されたものにし、Arial/Times にフォールバックしないようにしてください。
:root{
--font-text: 'Be Vietnam Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; /* font mặc định của overstack: Be Vietnam Pro 400 — file font được NHÚNG ở bước cuối, xem 'Font mặc định' */
--font-display: var(--font-text); /* tiêu đề cùng họ Be Vietnam Pro; đậm nhạt chỉ dùng 400 / 600 / 800 (weight đã nhúng) */
--font-mono: 'SF Mono', ui-monospace, 'SFMono-Regular', Menlo, 'Roboto Mono', Consolas, monospace;
}
body{font-family:var(--font-text)}
h1,h2,h3,.logo{font-family:var(--font-display);letter-spacing:-.02em} /* SF Pro Display cho cỡ ≥20px */
pre.code-block,.foot-tree{font-family:var(--font-mono)}
-apple-system/BlinkMacSystemFontは macOS では San Francisco に解決されます。SF Pro Text/Displayは、個別にインストールしたマシン向けの明示的な名称です。- Roboto/Segoe UI はシステムのフォールバックフォントです(Android/Linux/Windows に標準搭載)。自己完結型を保つため、Webフォントはダウンロードしないでください。
- 等幅フォントは、macOS の新しい環境で SF Mono が使われるよう、Menlo より先に
ui-monospaceを指定します。
テーマの切り替え(ライト/ダーク)(必須 — ユーザーフィードバック 2026-07-06、モードを強制しないこと)
専用スキル dark-mode-maker に切り出してください(フィードバック 160926:「ダーク/ライトモードのエフェクト作成を丸ごと専用スキルにする」)。 生成するすべてのページで、ユーザーがスイッチを使ってライト/ダークを自分で選べるようにしてください。スイッチはサイドバー/ナビゲーションの下部に固定します。prefers-color-scheme は初期設定としてのみ使用してください。正確な実装方法を確認するには、スキル dark-mode-maker(Skill tool)を読み込んでください。 対象は、スイッチのマークアップ、FOUC 対策、中立的なダークモード用パレット(ネイビーがかった色味にしない)、ポインターを起点に広がるサークルリビール効果(ボタンの境界内に収める)、crest-glow のリキッドグラス効果、Playwright による受け入れテストです。ここに CSS/JS を重複して記載しないでください。情報源を一つにして、修正箇所を一か所にまとめ、SKILL.md と fdk/tools/build-overstack-docs.py の間で過去に発生したずれを防ぎます。
スクリプトでページを生成する場合(fdk/tools/build-overstack-docs.py)、引き続き「一つの情報源から CSS ブロックを二つ生成する」(_theme_css()/_DARK_RULES)原則を守ってください。異なるのは、トークンの内容をここに個別に転記するのではなく、dark-mode-maker § Palette のパレットに合わせる点だけです。
アクセシビリティとドキュメントの head(必須)
忘れやすく、気づかないうちに不具合につながりやすい項目です。すべてのページに確実に実装してください。
<head> — メタ情報とファビコン(viewport がないと、モバイルではレスポンシブのセクション全体が機能しません):
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Title — concise, sentence case</title>
<meta name="description" content="One specific sentence about the page.">
<meta name="theme-color" content="#eaf2fd">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230a84ff'/%3E%3C/svg%3E">
Open Graph(og:title/og:description/og:image)を追加するのは、ページを外部で共有/リンクする場合のみです。ローカル専用の file:// ドキュメントでは省略してください。
フォーカスリング(アクセシビリティ、必須) — すべてのカスタムコントロール(nav a、button、.code-copy、.diagram-reset、.collapse-toggle、チェックボックス)では、ブラウザー標準のアウトラインを非表示にします。キーボード操作時に見えるリングを復元してください。マウス操作時には表示せず、キーボード操作時に表示するため、:focus-visible を使います。
:focus-visible{outline:2px solid #0a84ff;outline-offset:2px;border-radius:8px}
:focus:not(:focus-visible){outline:none}
スキップリンクと <main> ランドマーク — コンテンツ(<nav> の後にあるすべての要素)を <main id="main"> で囲み、キーボードユーザーがサイドバーを飛ばせるよう、最初にフォーカス可能な要素として非表示のスキップリンクを追加してください。
<a class="skip-link" href="#main">Skip to content</a>
.skip-link{position:fixed;top:8px;left:8px;z-index:200;padding:8px 16px;border-radius:10px;
background:var(--glass-1);backdrop-filter:blur(var(--blur-1));border:1px solid var(--border);
transform:translateY(-150%);transition:transform .2s}
.skip-link:focus-visible{transform:translateY(0)}
スムーズスクロールとアンカーのオフセット — アンカーをクリックしたら滑らかに移動し、セクションが画面上端に隠れないようにしてください。
html{scroll-behavior:smooth}
section[id]{scroll-margin-top:24px}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
prefers-reduced-motion — 本文のオーブだけでなく、すべての動きを対象にすること — グローバルなガードで、動きを控える設定を求めるユーザー向けに装飾的なアニメーション(オーブ、リップル、浮遊/パルス/グロー/フローアロー、マインドマップの描画)をすべて無効にしてください。
@media(prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
transition-duration:.001ms!important;scroll-behavior:auto!important}
}
意味のある SVG 図にはテキストによる代替を用意すること — ドラッグ可能なノードグラフは情報を伝えるため、各 .diagram-box の SVG に role="img" と、図の内容を説明する <title>(最初の子要素)を追加してください。装飾のみの SVG(テキストラベルと組み合わせているアイコンなど)には aria-hidden="true" を指定してください。
データの数値には等幅数字を使うこと — 表や図の数値ラベルで列幅が揺れないようにしてください。
table, .diagram-box text, .mm .node .ct{font-variant-numeric:tabular-nums}
見出しの孤立語 — ヒーローと h2 に text-wrap:balance を、本文の段落に text-wrap:pretty を追加し、1 語だけが行に取り残されないようにしてください。
外部アーティファクトの埋め込み — archify の図を <iframe> 経由で表示
§Best Practices の「インライン SVG」ルールは、ページ内で手描きする図に適用されます。ENGINE が描画する図(/diagram → archify — 例:/propose が生成する *-seq.html の各タスク)は、自己完結型のビューアーページです。そのため、SVG を外にコピーせず、<iframe> で埋め込んでください(コピーすると archify の幾何学的な配置機能が失われます)。次の 2 点は必須です。
1. 内容に合った高さのフレームにする — 閲覧者がフレーム内をスクロールする必要がないようにします。 固定の高さ(例:height:840px)では必ず一部が切れてしまいます。2026/09/11 の計測では、archify のビューアーはメッセージ数とフレーム幅(650~1100px)に応じて高さが 905~1565px で、ツールバーと Guided views ブロックも含まれます。実行時に実際の高さを計測するレシピを使い、フレームの横には必ず単独で開くリンクを配置してください。
<iframe class="archify-embed" src="DDMMYY-feature-t1.html" title="T1 — <tên task>" loading="lazy"></iframe>
<a class="embed-open" href="DDMMYY-feature-t1.html" target="_blank" rel="noopener">Mở sơ đồ riêng ↗</a>
.archify-embed{display:block;width:100%;height:1000px;border:0;border-radius:14px;background:transparent}
.embed-open{display:inline-block;margin-top:8px;font-size:12px}
// Auto-height: đọc scrollHeight THẬT của trang archify (cùng origin) → khung vừa khít, theo dõi đổi cỡ.
document.querySelectorAll('iframe.archify-embed').forEach(f => {
const fit = () => {
let d; try { d = f.contentDocument; } catch (e) {}
if (!d || !d.documentElement) { f.dataset.fit = 'blocked'; return; } // file:// — xem giới hạn bên dưới
const h = d.documentElement.scrollHeight;
if (Math.abs(h - f.offsetHeight) > 2) f.style.height = h + 'px';
};
f.addEventListener('load', () => { fit(); try { new ResizeObserver(fit).observe(f.contentDocument.body); } catch (e) {} });
});
Playwright(archify 2.17)で計測済みです。HTTP 経由で開くと(§Auto-Host)、650px と 1100px のどちらの幅でもフレームの高さはコンテンツの scrollHeight と一致し、1 秒後もそのままで(サイズ変更ループは発生せず)、フレーム内のスクロールバーも表示されません。
file:// で開く場合の制限: Chrome と Firefox では、各 file:// ファイルが別々のオリジンとして扱われるため、親ページから contentDocument を読み取れません。フレームの高さはデフォルトの 1000px のままとなり、フレーム内でスクロールが必要になる場合があります。代替策は、§Auto-Host 経由(http://localhost:8765/...)でページを開くか、「図を単独で開く ↗」をクリックして図を全画面で表示することです。単独で開くリンクが必須なのはこのためです。
2. ページのテーマに合わせる — meta.visual_preset は空のままにします。 インストール時のデフォルトは macos(システムフォントで、liquid-glass に適しています)です。archify/examples/*.json の visual_preset をコピーしないでください(例:付属の sequence は signal-flow と等幅フォントを使用します)。別のプリセットを設定するのはユーザーが要求した場合に限り、その場合は親ページに <meta name="overstack-preset" content="<preset>"> を指定してください。この meta がない場合、ルール R20(PostToolUse hook)が、macos 以外のプリセットを使う archify アーティファクトの埋め込みをブロックします。
自己完結型 — 重大
ユーザーはこれらのファイルを直接開きます(file://、オフライン、ダブルクリック)。出力する HTML から外部へのリクエストが一切発生しないようにしてください。フォント/CSS/JS の CDN リンク、リモート画像、@import、<script src> は使用できません。すべて(CSS、JS、SVG、アイコン)は 1 つのファイル内にインラインで記述します。外部サイトへの <a href> ハイパーリンクは、リソースの読み込みではなく移動のため問題ありません。
出力先 — 重大
HTML ファイルは必ず、現在のプロジェクトルート内の llmwiki/html/ に書き込んでください。
ファイル名の先頭には、必ず今日の日付 DDMMYY- を付けてください(HTML と wiki の下書きを対にするため、出力レポートの下書きと同じ DDMMYY を使用します)。例:2026 年 6 月 4 日の場合は 040626-cell-formula-override.html。
- 単一ファイル:
llmwiki/html/DDMMYY-<slug>.html - 複数ファイル:
llmwiki/html/DDMMYY-index.html+llmwiki/html/DDMMYY-<slug>.html <slug>= 2~4 語のケバブケース、DDMMYY= 今日の日付(例:040626)。- プロジェクトルートや他のディレクトリには絶対に書き込まないでください。
llmwiki/html/が存在しない場合は、先に作成してください。
Auto-Host
HTML ファイルを作成したら、必ずプレビュー用のローカル HTTP サーバーを起動してください。
cd <project-root>
kill -9 $(lsof -ti :8765) 2>/dev/null
nohup npx serve -p 8765 > /tmp/serve.log 2>&1 &
ユーザーに次の URL を開くよう案内してください:http://localhost:8765/llmwiki/html/DDMMYY-<file>.html
ポート 8765 がすでに使用中の場合は、起動を省略してください(サーバーはすでに実行中です)。
Playwright 監査(必須 — 引き渡し前の最終ステップ。ユーザーに準備完了と伝える前に実行)
⚠️ 200826 の実例から得た教訓であり、仮説ではありません: コードを自分で読み、仕様どおり(ダーク/ライト切り替えボタン、サイドバーの折りたたみ)に作るよう指示されたページが、実際には誤って簡略化されていました。テーマボタンは画面右上に独立したフローティングチップとして配置されていました。これは、この SKILL.md で禁止している代表的なアンチパターンです(§Theme Toggle、「角に散らばるアイコンチップではない」)。また、サイドバーには .nav-toggle/.nav-close がまったくありませんでした。この不具合はページ生成時には検出されず、ユーザーが自分でページを開いて報告して初めて判明しました。コード(または SKILL.md)を読むだけでは、ページの見た目や操作が正しいかどうかは分かりません。200826 セッションのすべての PoC に適用したのと同じ規律で、実際にブラウザーで開いて測定する必要があります。
Auto-Host の実行後、ページの完成をユーザーに報告する前に、必ず Playwright を実際に実行して検証してください。 プロジェクトに @playwright/test がインストールされていない場合は、/playwright-verify(専用スキル。フレームワークがインストール済みであれば、利用できます)に従ってインストールしてください。node で直接実行する .mjs スクリプトを作成します(npx playwright test は使いません)。少なくとも以下を確認してください。
- ページ読み込み時に console/pageerror が 0 件であること。
- ページに
<nav>サイドバーがある場合:.nav-toggleと.nav-closeが DOM に存在すること(CSS から推測するのではなく、実際に DOM をクエリして確認します)。.nav-closeをクリックすると、document.bodyにnav-collapsedクラスが付き、.nav-toggleが表示されること(opacityが 0 以外)。もう一度.nav-toggleをクリックすると、nav-collapsedが消えること。これは、200826 の教訓で確認できていなかった一連の動作です。 - ページにテーマ切り替えがある場合:
<nav>内にある.theme-row/.theme-switchであること(nav.contains(themeRowElement)で確認します)。画面の隅に独立して配置されたposition:fixedのコントロールであってはいけません。切り替えをクリックするとdocument.documentElementのdata-themeが変わり、両方のテーマのスクリーンショットを撮影します。 - 最低 3 つの状態のスクリーンショットを撮影すること: ライトテーマ、ダークテーマ、そして(サイドバーがある場合)サイドバーを閉じた状態。スクラッチパッドに保存し、監査に合格した後は残す必要はありません。
import { chromium } from "@playwright/test";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 900 } });
let errors = [];
page.on("pageerror", e => errors.push(e.message));
page.on("console", m => { if (m.type()==='error') errors.push(m.text()); });
await page.goto("file://<đường-dẫn-tuyệt-đối-tới-file-vừa-sinh>.html", { waitUntil: "load" });
await page.waitForTimeout(500);
const hasNav = await page.evaluate(() => !!document.querySelector('nav'));
if (hasNav) {
const dom = await page.evaluate(() => ({
hasToggle: !!document.querySelector('.nav-toggle'),
hasClose: !!document.querySelector('.nav-close'),
}));
if (!dom.hasToggle || !dom.hasClose) errors.push('THIẾU .nav-toggle/.nav-close — sidebar không đóng/mở được');
if (dom.hasClose) {
await page.click('.nav-close'); await page.waitForTimeout(300);
const collapsed = await page.evaluate(() => document.body.classList.contains('nav-collapsed'));
if (!collapsed) errors.push('Click .nav-close không collapse được sidebar');
await page.screenshot({ path: '/tmp/audit-collapsed.png' });
await page.click('.nav-toggle'); await page.waitForTimeout(300);
}
}
await page.screenshot({ path: '/tmp/audit-light.png' });
// Dò control đổi theme bằng HÀNH VI thật (bấm thử), KHÔNG đoán theo tên
// class/id — bug 200826 dùng class="toggle" (không chứa chữ "theme"), nên một
// bộ dò theo tên class sẽ bỏ sót đúng case đã xảy ra thật. Bấm lần lượt mọi
// control có thể bấm được tới khi data-theme đổi, rồi kiểm nó có nằm trong
// .theme-row hay không — bất kể tên class/id của nó là gì.
const themeBefore = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
// loại .nav-toggle/.nav-close khỏi ứng viên — chúng CŨNG là <button>, và bấm
// nhầm .nav-close giữa vòng dò sẽ sập sidebar, khiến .theme-switch biến mất
// khỏi tầm bấm cho các lần thử sau (bug thật gặp khi tự viết script này)
const clickables = await page.$$('button:not(.nav-toggle):not(.nav-close), [role="switch"]');
let themeCtl = null, inThemeRow = false;
for (const el of clickables) {
await el.click().catch(() => {});
await page.waitForTimeout(150);
const themeAfter = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
if (themeAfter !== themeBefore) { themeCtl = el; inThemeRow = await el.evaluate(n => !!n.closest('.theme-row')); break; }
}
if (!themeCtl) errors.push('Bấm thử mọi <button>/[role=switch] không thấy data-theme đổi — thiếu theme toggle');
else if (!inThemeRow) errors.push('Tìm thấy control đổi được data-theme nhưng KHÔNG nằm trong .theme-row — khả năng là chip nổi góc rời sidebar (bug 200826)');
await page.screenshot({ path: '/tmp/audit-dark.png' });
if (errors.length) { console.log('AUDIT FAIL:', errors); process.exit(1); }
console.log('AUDIT PASS');
await browser.close();
監査に失敗した場合:修正して再監査してください。失敗した状態でページが完成したと報告してはいけません。 これは最終品質ゲートであり、コード層の medic --ci に相当します。失敗しているなら、引き渡してはいけません。監査に合格した後は、スクリプトやスクリーンショットを残す必要はありません。これは「検証して破棄」するステップであり、コミット必須の成果物ではありません(/playwright-verify の規約「スタンドアロンのスクリプトファイルは Git に入れない — スクラッチパッドから実行する」に従います)。
複数ファイルモード
Wiki ファイルごとに個別のページを生成する場合(すべてのファイルで同じ DDMMYY- 日付プレフィックスを使用):
DDMMYY-index.htmlの概要ページを作成します(すべての N ページへのリンクをカードグリッドで表示します)- Wiki ファイルごとに
DDMMYY-{slug}.htmlを作成します(slug はファイル名から生成します) - 各ページで同じ CSS デザインシステムを共有しつつ、セクションごとのアクセントカラーを使用します
- 各ページに、他のすべてのページへリンクするナビゲーションバーを設けます(現在のページをハイライト表示します)
- 各ページに、トピックの内容に基づいた独自のアニメーション付き SVG 図を設けます
インタラクティブなプロトタイプ/編集可能なデータグリッド(任意)
ユーザーが「UI の見た目を確認したい」「インタラクティブな表を試作したい」と依頼した場合や、編集可能なグリッド/スプレッドシート機能のクリック可能なデモを求めた場合は、単体で動作するインタラクティブなプロトタイプ(同じ DDMMYY-<slug>.html、バニラ JS、ビルド不要)を作成します。⚠️ 自己完結型の原則は引き続き適用されます。Tailwind CDN、Google Fonts、リモートスクリプトは読み込まないでください。 プレーンなインライン CSS(または <style> にコピーしたインラインのユーティリティレイヤー)と、Font セクションのシステムフォントスタックを使用してください。このスキルが生成するほかのページと同様に、外部へのリクエストが一切発生せず、file:// からファイルを開ける必要があります。以下の再利用可能なパターンにより、オーバーライド/カスケードの UI を一貫性のある、わかりやすいものにできます。
- ロケールに対応した数値入力 —
parseUserNumber(raw, locale):ロケールの桁区切り文字を除去し、小数点を.に置き換えてからNumber()を使います。表示にはIntl.NumberFormat(locale)を使用します。VN ↔ US/台湾の切り替えを用意します。 - オーバーライドと影響対象、色の永続化 — 各行について、オーバーライドあり(
v)とオーバーライドなし(base)の値をそれぞれ計算します。セルは、オーバーライド(オーバーライドマップ内に存在)なら、アンバーのbg-amber-50 ring-amber-300+✦、影響対象/カスケード(base[code] !== v[code])なら、エメラルドのbg-emerald-50 ring-emerald-200+↻とします。どちらもオーバーライドが削除されるまで状態を維持します(一瞬だけ表示するのではありません)。今回のコミットで値が変わったセルには、一時的なflash(約 1.4 秒)を追加します。 - 原因の表示(どのオーバーライドがどのセルを変えたか) — 摂動方式を使います。行内の各オーバーライドについて、そのオーバーライドだけを取り除いて再計算します。値が変わるセルはそのオーバーライドが原因です。
causes: affected→[ov]とaffects: ov→[affected]を構築します。次の方法で表示します。(a)セルにカーソルを合わせると、同じ行の関連セルにハイライトクラスを追加する(data-affects/data-srcs)。(b)名前を列挙するtitleツールチップ。(c)保存後に、再計算されたセルを一覧にしたトースト。 - 手軽な編集 — 編集可能な列ならどれでもダブルクリックすると、すぐにインライン入力欄を表示します。編集をブロックする
confirm()を挟まないでください。「要注意」の列は、ヘッダー内の種別タグ、ツールチップ、フラグなどで控えめに示し、すべての編集を妨げるモーダルは使用しないでください。 confirm()ではなく適切なモーダル — スタイル付きモーダルは、「すべてのオーバーライドをクリア」のような破壊的な操作に限って使用します。タイトル、okText、危険度をパラメーターで指定できるようにします。- 非同期 UI の後にフォーカスを設定 — モーダル/ダイアログを閉じた直後に編集を開始する場合は、ティック後に入力欄へフォーカスを移します(
setTimeout(...,0))。そうしないとフォーカスが設定されません。
ヘッダーの信号機風デザイン、システムフォントスタック(var(--font-text))、ドキュメントサイトと統一感のあるリキッドガラスの青/白の配色を維持してください。データグリッドのパターンで使うアンバー/エメラルドのオーバーライド状態色は、テーマではなく編集状態を示すため、唯一許可される例外です。
ベストプラクティス
- コンパクトな文字サイズ — 13″ノートPC画面に最適化(ユーザーフィードバック 2026-07-06、一度方針を変更済み — サイズを大きくしない): 大きくするのではなく、小さくします。本文の
pは13~13.5px、.leadは14px、ナビリンクは12px(縦方向のパディングは約5px)、リスト/表は12.5px、ラベル/キャプションは10~10.5px、h2は約21px、ヒーローはclamp(26px,4vw,40px)にします。「読みやすくする」ために文字サイズを大きくするのは、13″画面では間違いです。画面に表示できる内容が減り、改行が詰まり、見た目が悪くなります。読みやすくしたい場合は、サイズではなく行の高さやコントラストを調整します。数値バッジは10px未満でもかまいません。 - サイドバー:圧縮せず、スクロールさせます(ユーザーフィードバック 2026-07-07): nav の flex-column は、スペースが足りないと各項目を flex-shrink で縮めます。これを防ぐ必要があります。
nav>*{flex-shrink:0}とoverflow-y:autoを設定し、スクロールバーは完全に非表示にします。nav{scrollbar-width:none;-ms-overflow-style:none} nav::-webkit-scrollbar{width:0;height:0;display:none}。項目は常に自然な高さを保ち、スペースが足りない場合はスクロールさせます。 - 図を自分で描く場合は、必ず SVG を HTML に直接インラインで記述します(外部ファイルにしません)。唯一の例外は、archify エンジンで描画した図で、§外部アーティファクトの埋め込みに従って
<iframe>で埋め込みます。 - ヒーロー見出しのサイズには、必ず
clamp()を使います:font-size: clamp(32px,5vw,56px) - チェックリストに Unicode の
☐を使ってはいけません。必ず実際の<input type="checkbox">と<label for="...">を使い、項目をクリックできるようにします。各チェックリストには、次の CSS を追加します。
.checklist { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.checklist li { display: flex; align-items: flex-start; gap: 12px; font-size: 13px; color: var(--text-2); cursor: pointer; }
.checklist li::before { display: none; }
.checklist input[type="checkbox"] {
width: 16px; height: 16px; border-radius: 4px; border: 1.5px solid #cbd5e1;
appearance: none; -webkit-appearance: none; cursor: pointer; flex-shrink: 0;
background: rgba(255,255,255,.8); margin-top: 2px; transition: all .15s;
}
.checklist input[type="checkbox"]:checked {
background: #0a84ff; border-color: #0a84ff;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13 4L6.5 11 3 7.5' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E");
background-size: contain;
}
.checklist label { cursor: pointer; line-height: 1.5; }
.checklist input[type="checkbox"]:checked + label { text-decoration: line-through; color: #94a3b8; }
<ul class="checklist">
<li><input type="checkbox" id="c1"><label for="c1">Item text</label></li>
<li><input type="checkbox" id="c2"><label for="c2">Item text</label></li>
</ul>
- 各
<section>は、インデックスに応じたアクセントカラーを使って、必ず単独で完結するようにします(#sec-{i}の CSS ルールを使用)。 - 見た目の統一感を保つため、図全体で SVG の viewBox の幅を900に揃えます。
- 関連する Markdown ファイルを指す「詳細」フッターには、相対パスの
../file.mdリンクを使います。 - ヒーロー見出しのグラデーションには3つの色を使い、青から明るい青へと変化させます(濃いネイビーから始めないでください):
linear-gradient(135deg, #0a84ff, #5aa2e8, #cfe3fb) - ナビのロゴのグラデーション:
linear-gradient(135deg, #0a84ff, #64b5f7) - セクション数は可変です。剰余(
i % 6)を使って6色のブルーパレットを循環させます。 - HTML ファイルを書き込んだ後は、必ず Auto-Host サーバーを起動します(上記の Auto-Host セクションを参照)。
出力レポート
スキルの主要タスクがすべて完了したら、wiki に提案ドラフトを書き込みます。
手順
1. ファイル名を作成します:
- 形式:
DDMMYY-<ten>.md DDMMYY= 今日の日付(例:2026年6月2日なら020626)<ten>= 実施内容を要約する2~4語の kebab-case(例:landing-page-coteccons、brand-kit-fintech、ingest-auth-spec)
2. llmwiki/wiki/sources/draft/DDMMYY-<ten>.md に書き込みます:
---
type: draft
title: "DDMMYY-<ten>"
status: proposed
tags: [<skill-name>, output-report]
timestamp: YYYY-MM-DD
---
# DDMMYY-<ten>
**Type:** draft
**Status:** proposed
**Tags:** <skill-name>, output-report
**Proposed:** YYYY-MM-DD
## What
<One sentence — what this skill invocation produced or decided>
## Output
<Key artefacts, files created/modified, or decisions made>
## Files
| File | Action |
|------|--------|
| `path/to/file` | created / modified |
## Notes
- Invoked via: `/<skill-name>` skill
## Origin
- **Draft:** `wiki/sources/draft/DDMMYY-<ten>.md`
- **Commit:** _(filled by verify-before-commit)_
- **Date promoted:** _(filled by verify-before-commit)_
3. wiki のインデックスとログを更新します:
llmwiki/wiki/index.md— 1行追加します:| [DDMMYY-<ten>](sources/draft/DDMMYY-<ten>.md) | draft | YYYY-MM-DD |llmwiki/wiki/log.md— 追記します:## YYYY-MM-DD — <skill-name> — <ten>
スキルによって成果物も決定事項も生じなかった場合(例:
/caveman-statsのような表示のみのモード)に限り、スキップします。