create-pr

star 176

「PR を作って」「pull request を出して」要求で発動。 push → 構造化 PR 本文 (pull_request_template.md 準拠) で PR 作成 → CI 監視ループ → review thread 返信+resolve まで 1 skill で完結。 skill 間 handoff を排除し工程の取りこぼしを防ぐ。 マイルストーン非付与、 auto-merge 非使用。

ayutaz By ayutaz schedule Updated 5/17/2026

name: create-pr description: 「PR を作って」「pull request を出して」要求で発動。 push → 構造化 PR 本文 (pull_request_template.md 準拠) で PR 作成 → CI 監視ループ → review thread 返信+resolve まで 1 skill で完結。 skill 間 handoff を排除し工程の取りこぼしを防ぐ。 マイルストーン非付与、 auto-merge 非使用。 argument-hint: "[base-branch] [--title ] [--no-watch]" disable-model-invocation: false allowed-tools: Bash(git *) Bash(gh *) Bash(sleep *) Bash(python *) Bash(cat *) Read Write ScheduleWakeup TaskList TaskStop</h2> <h2>PR 作成 + CI 監視 + レビュー対応 (end-to-end)</h2> <p><code>/create-pr</code> 1 コマンドで PR ライフサイクル全体を完結する skill:</p> <p><strong>push → PR 作成 → CI 監視ループ → review thread 返信 + resolve → 報告</strong></p> <p>過去 <code>/create-pr</code> → <code>/watch-pr</code> → <code>/reply-review</code> を別々の skill 呼び出しで連鎖していたが、 「次の skill を Skill ツールで呼べ」 という散文 handoff は LLM が実行を飛ばすと工程が抜ける (PR #496 / #505 で発生)。 本 skill は全工程を <strong>inline フェーズ</strong> として持ち、 skill 間 handoff をゼロにする。</p> <blockquote> <p>単体利用: CI 監視のみ → <code>/watch-pr <PR#></code>、 review 対応のみ → <code>/reply-review <PR#></code>、 backlog 集計 → <code>/check-review-backlog</code>。 これらは本 skill のフェーズ 5 / 6 と同一手順の standalone 版。</p> </blockquote> <h2>自動発動条件</h2> <ul> <li>「PR を作って / 出して」「pull request を作って」「この変更で PR にして」「branch を push して PR にして」</li> </ul> <p>明示呼び出し: <code>/create-pr</code> / <code>/create-pr <base-branch></code> / <code>/create-pr --no-watch</code> (PR 作成のみ、 監視ループに入らない)</p> <h2>引数</h2> <ul> <li><code>$ARGUMENTS</code> 空: base = <code>dev</code> (memory <code>feedback_merge_caution</code>: 通常 dev を base)</li> <li><code><base-branch></code>: 明示指定 (例: <code>main</code>)</li> <li><code>--title <title></code>: title 上書き (デフォルトは最新コミット message から抽出)</li> <li><code>--no-watch</code>: フェーズ 5/6 (CI 監視・review 対応) を skip し PR 作成で終了</li> </ul> <h2>制約 (memory 参照)</h2> <ul> <li><strong>マイルストーン非付与</strong> (<code>feedback_pr_no_milestones</code>): <code>--milestone</code> を付けない。 本文に「M1」等も書かない</li> <li><strong>auto-merge 禁止</strong> (<code>feedback_merge_caution</code>): <code>gh pr merge --auto</code> 等を使わない。 マージはユーザー判断</li> <li><strong>本文書き換えは body-file</strong> (<code>feedback_pr_body_over_comments</code>): 既存 PR 更新は <code>gh pr edit --body-file</code>。 新規コメント追記しない。 review thread への reply はこの制約の対象外</li> <li><strong>--no-verify 禁止</strong> (CLAUDE.md): hook bypass 系を使わない</li> <li><strong>review thread の自動 reply は SAFE 系のみ</strong>: stale / Copilot style noise のみ自動 reply+resolve。 人間 reviewer・logic/security 指摘は user 判断 (フェーズ 6)</li> </ul> <h2>PR 本文フォーマット (重要 — テンプレート準拠必須)</h2> <p><strong>PR 本文は <code>.github/pull_request_template.md</code> の必須セクションをすべて含むこと。</strong> <code>validate-pr-body</code> CI ゲートが以下を検査し、 欠けると PR が必ず red になる (PR #505 で発生した既知バグ — 旧フォーマットはこのゲートを通らなかった):</p> <ul> <li><code>## Test Plan</code> セクションが存在し非空 (<strong>大文字 P</strong>。 <code>## Test plan</code> は grep <code>^## Test Plan</code> に不一致で fail)</li> <li><code>## Risk Level</code> セクションでチェックボックスがちょうど <strong>1 個</strong> <code>- [x]</code></li> <li><code>## Affected Components</code> セクションでチェックボックス最低 1 個 <code>- [x]</code></li> </ul> <p>PR title は <strong>70 文字以内</strong>、 <code>type(scope):</code> prefix (例 <code>fix(g2p):</code> <code>ci:</code> <code>feat(workflow):</code>)。</p> <p>PR 本文は以下を <strong>この順</strong> で含める (1-8 は template 準拠の必須セクション、 9-10 は create-pr 独自の value-add):</p> <ol> <li><code>## Summary</code> — 解決する問題 / 動機 (1-3 文)。 時系列表現 (Phase 1/2) は使わない</li> <li><code>## Affected Components</code> — 該当を <code>- [x]</code>: Python / Rust / C# / C++ / Go / WASM-npm / Docker / CI-CD / Documentation</li> <li><code>## Type</code> — Bug fix / New feature / Refactoring / Documentation / CI/CD / Dependencies</li> <li><code>## Risk Level</code> — patch / minor / major の <strong>ちょうど 1 個</strong>を <code>- [x]</code> (patch=bugfix/内部, minor=新機能/非破壊, major=破壊的変更)</li> <li><code>## Contract Impact</code> — <code>docs/spec/*.toml</code> 影響。 無ければ <code>- [x] None</code></li> <li><code>## 変更内容</code> — 機能カテゴリ別の表 <code>機能名 / 動作 / これがないと起こること</code> の 3 列</li> <li><code>## 設計判断</code> — 意思決定の根拠 bullet (conservative/aggressive 判断・誤検出回避・bypass 経路・既存整合)</li> <li><code>## Test Plan</code> — <code>- [ ]</code> で reviewer がそのまま使える具体的手順 (抽象表現禁止)</li> <li><code>## Checklist</code> — Tests pass locally / No GPL-LGPL deps / Documentation updated</li> <li><code>## Related Issues</code> — <code>Closes #N</code> 等、 無ければ「なし」</li> </ol> <p>禁止: 時系列 (Phase/開発過程)、 マイルストーン番号、 「LLM が生成」 等の co-authored note。</p> <h2>実行手順</h2> <h3>フェーズ 1: ブランチ状態確認</h3> <pre><code class="language-bash">git status --short git log --oneline <base>..HEAD git diff --stat <base>..HEAD git rev-parse --abbrev-ref HEAD git rev-parse --abbrev-ref @{u} 2>/dev/null || echo "no-upstream" </code></pre> <p>確認: working tree、 commit が 1 つ以上 ahead か、 upstream 設定済みか。 ブランチが <code>dev</code>/<code>main</code> なら停止 (feature ブランチ必須)。</p> <h3>フェーズ 2: PR 本文 draft 作成</h3> <p><code>git log <base>..HEAD --pretty=format:"%h %s%n%b"</code> で全 commit を読み、 機能カテゴリ・規模・トレードオフを抽出。 上記 10 セクションを埋めて <code>/tmp/pr-body-<branch-slug>.md</code> に書き出す。</p> <h3>フェーズ 2.5: PR 本文 self-check (validate-pr-body 先取り)</h3> <p>push 前に必須セクションを検証する。 1 つでも欠けたら修正してからフェーズ 3 へ:</p> <pre><code class="language-bash">B=/tmp/pr-body-<branch-slug>.md for s in "## Summary" "## Affected Components" "## Type" "## Risk Level" "## Contract Impact" "## Test Plan" "## Checklist" "## Related Issues"; do grep -qF "$s" "$B" || echo "MISSING: $s" done # Risk Level は [x] ちょうど 1 個であること (出力が 1 でなければ fail) awk '/^## Risk Level/{f=1;next}/^## /{f=0}f' "$B" | grep -cE '^- \[x\] ' # Affected Components は [x] 1 個以上 awk '/^## Affected Components/{f=1;next}/^## /{f=0}f' "$B" | grep -cE '^- \[x\] ' </code></pre> <h3>フェーズ 3: push</h3> <pre><code class="language-bash">git push -u origin <branch-name> # upstream 未設定時。 設定済みなら git push </code></pre> <h3>フェーズ 4: PR 作成 / 既存 PR 更新</h3> <p><code>gh pr list --head <branch> --json number</code> で既存 PR を判定:</p> <pre><code class="language-bash"># 新規 gh pr create --base <base> --title "<title>" --body-file /tmp/pr-body-<branch-slug>.md # 既存 (本ブランチに PR があれば本文置換) gh pr edit <PR#> --body-file /tmp/pr-body-<branch-slug>.md </code></pre> <p><code>--milestone</code> は付けない。 PR URL / 番号を控える。</p> <p><code>--no-watch</code> 指定時はここで終了 (PR URL を報告)。 それ以外はフェーズ 5 へ自動継続 (確認を挟まない)。</p> <h3>フェーズ 5: CI 監視ループ (inline — skill handoff なし)</h3> <p>PR 作成後、 CI を完了まで監視する。 <strong><code>/watch-pr</code> skill は呼ばず以下を本 skill 内で実行する。</strong></p> <p><strong>5.1 ポーリング</strong> — <code>gh pr checks <PR> --json name,bucket</code> を取得し bucket (pass/fail/pending/skipping/cancel) を集計。</p> <p><strong>5.2 判定</strong>:</p> <ul> <li><strong>pending ≥ 1</strong> → 5.3 (継続監視)</li> <li><strong>fail = 0 かつ pending = 0</strong> → all green。 フェーズ 6 を実行し all-resolved ならフェーズ 7 で完了報告</li> <li><strong>fail ≥ 1</strong> → 5.4 (失敗分析) を実行後、 フェーズ 6 → フェーズ 7</li> </ul> <p><strong>5.3 継続監視 (self-pace)</strong>:</p> <ul> <li>background watcher が未 arm なら arm する (<code>Bash</code> を <code>run_in_background: true</code> で): <code>gh pr checks <PR> --json name,bucket</code> を ~120s 間隔でポーリングし、 pending=0 になったら集計行を出力して exit するループ。 完了通知でループに再入する。</li> <li>fallback として <code>ScheduleWakeup(delaySeconds=1800, prompt="/create-pr ...<元の引数>")</code> を設定。</li> <li>watcher 完了通知 か wakeup で 5.1 に再入。 各再入でフェーズ 6 (review) も実行する。</li> <li>4 時間以上 pending が続く場合はループを終了し user に報告。</li> </ul> <p><strong>5.4 失敗分析</strong> — 失敗 job のログを <code>gh run view <run-id> --log-failed</code> で fetch し分類:</p> <table> <thead> <tr> <th>分類</th> <th>signature</th> <th>推奨アクション</th> </tr> </thead> <tbody><tr> <td>format drift</td> <td><code>ruff</code> <code>cargo fmt</code> <code>gofmt</code> <code>clippy</code> 系</td> <td><code>pre-commit run --all-files</code> で修復</td> </tr> <tr> <td>test fail</td> <td><code>FAILED</code> <code>assert</code> <code>panicked</code></td> <td>テスト名を抽出して提示</td> </tr> <tr> <td>build error</td> <td><code>error[E</code> <code>cannot find</code></td> <td>ログ提示、 user 方針確認</td> </tr> <tr> <td>flake</td> <td><code>timeout</code> <code>network</code> <code>rate limit</code></td> <td><code>gh run rerun <id> --failed</code> 提案</td> </tr> <tr> <td>contract drift</td> <td><code>MISMATCH</code> <code>OUT OF SYNC</code></td> <td>該当 <code>/check-*</code> skill 案内</td> </tr> <tr> <td>validate-pr-body</td> <td><code>Missing required section</code></td> <td>フェーズ 2 に戻り本文修正 → <code>gh pr edit --body-file</code></td> </tr> </tbody></table> <p>分類結果と推奨アクションを user に提示。 format drift / validate-pr-body は本 skill で修正→push まで実施してよい。 test/build error は user 判断。</p> <h3>フェーズ 6: review thread 返信 + resolve (inline — skill handoff なし)</h3> <p>CI 監視の各 iteration および all-green 時に <strong>必ず実行する。</strong> <code>/reply-review</code> skill は呼ばず以下を実行:</p> <p><strong>6.1 未解決 thread 取得</strong>:</p> <pre><code class="language-bash">gh api graphql -f query=' query($pr: Int!) { repository(owner: "ayutaz", name: "piper-plus") { pullRequest(number: $pr) { reviewThreads(first: 50) { nodes { id isResolved comments(first: 1) { nodes { databaseId path body author { login } originalCommit { oid } } } } } } } }' -F pr=<PR> --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false)' </code></pre> <p><strong>6.2 各 thread を分類</strong>:</p> <ul> <li><strong>SAFE-stale</strong>: コメント以降に該当ファイル更新済 (<code>git log <originalCommit.oid>..HEAD -- <path></code> が非空)</li> <li><strong>SAFE-copilot-style</strong>: Copilot bot (<code>author.login</code> = <code>copilot-pull-request-reviewer</code> or <code>*[bot]</code>) + style noise (<code>^Consider (using|renaming)\b</code> <code>^Optional:</code> <code>\bnit:</code> 等)</li> <li><strong>REVIEW-human</strong>: author が人間 reviewer</li> <li><strong>REVIEW-blocker</strong>: Copilot/CodeQL の logic / security / API 指摘 (style noise でない)</li> </ul> <p><strong>6.3 SAFE 系の自動対応</strong> — 各 SAFE thread に REST API で reply → GraphQL で resolve:</p> <pre><code class="language-bash"># reply (in_reply_to は REST の comment databaseId) gh api repos/ayutaz/piper-plus/pulls/<PR>/comments --method POST \ -F in_reply_to=<databaseId> -f body="対応しました (commit <hash>)。<要約>" # resolve (threadId は GraphQL node ID) gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=<threadId> </code></pre> <p><strong>6.4 REVIEW 系</strong> — 自動 reply / resolve せず、 表形式で user に提示し判断を促す。 user が修正を指示したら: 修正 → commit → push → 該当 thread に reply (commit hash 付き) → resolve。 修正コミット後の reply+resolve を忘れないこと (PR #505 で抜けた工程)。</p> <blockquote> <p>詳細な分類 regex と API の注意点 (node ID と databaseId の取り違え等) は <code>reply-review/SKILL.md</code> を参照。 手順自体は上記で完結している。</p> </blockquote> <h3>フェーズ 7: 最終報告</h3> <pre><code class="language-text">PR #<N>: https://github.com/ayutaz/piper-plus/pull/<N> CI: <all green / red N 件 / pending N 件> Review: <resolved N 件 / 自動対応 N 件 / user 判断待ち N 件> 次のアクション: - <red なら> 失敗分類と推奨アクション - <REVIEW 残あれば> user 判断が必要な thread - <all green + resolved 全部> merge 判断は user に委ねる (auto-merge 禁止) </code></pre> <h2>guard hook 回避</h2> <p>PR body 内に <code>--no-verify</code> 等の禁止文字列があると pre-commit / GitHub hook が誤検出することがある:</p> <ul> <li>実行コマンドは抽象表記にする (「auto-merge」「hook bypass フラグ」等)</li> <li>禁止文字列を含めたい場合は <code>--body-file</code> 経由で渡す (引数で渡さない)</li> </ul> <h2>使用例</h2> <pre><code class="language-text">/create-pr # dev base に PR 作成 → CI 監視 → review 対応まで自動 /create-pr main # main base /create-pr --title "fix(g2p): ..." # title 上書き /create-pr --no-watch # PR 作成のみ (監視ループに入らない) </code></pre> <h2>関連 skill</h2> <ul> <li><code>/watch-pr <PR#></code>: 本 skill フェーズ 5 の standalone 版 (既存 PR の CI 監視のみ)</li> <li><code>/reply-review <PR#></code>: 本 skill フェーズ 6 の standalone 版 (既存 PR の review 対応のみ)</li> <li><code>/check-review-backlog</code>: 全 open PR の未解決 review 集計</li> <li><code>/loop /watch-pr <PR#></code>: CI を長時間継続監視 (本 skill フェーズ 5 を loop 化)</li> <li><code>/sync-docs</code>: PR 作成前のドキュメント整合性監査 (推奨フロー: <code>/sync-docs</code> → <code>/create-pr</code>)</li> </ul> <h2>期待効果</h2> <ul> <li><strong>skill 間 handoff ゼロ</strong> — push / PR 作成 / CI 監視 / review 対応が 1 skill の連続フェーズ。 「次の skill を呼び忘れて工程が抜ける」 (PR #496 / #505) を構造的に防止</li> <li>PR 本文が <code>pull_request_template.md</code> 準拠で <code>validate-pr-body</code> を必ず通る</li> <li>PR body の構造標準化、 フェーズ/マイルストーン表記の排除</li> <li>review thread の reply+resolve 漏れ防止</li> </ul> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/ayutaz/piper-plus --skill create-pr </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>176</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>22</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/41669061?u=fda4e7b3f55846543fc232812bd2d1617d5f5f51&v=4" alt="ayutaz" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>ayutaz</span> <a href="/?creator=ayutaz" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>