kanban-run

star 164

Run the AI team pipeline for kanban tasks — orchestration loop with 6 agents (Planner, Critic, Builder, Shield, Inspector, Ranger), single-step execution, and code review. Use /kanban-run to execute tasks through the 7-column pipeline. AUTO-TRIGGER when: user says "implement task NNN" or any task ID + implement/build/do combination; or user confirms with "yes/ok/go/do it" after Claude proposes implementing a specific kanban task.

cyanluna-git By cyanluna-git schedule Updated 6/2/2026

name: kanban-run description: Run the AI team pipeline for kanban tasks — orchestration loop with 6 agents (Planner, Critic, Builder, Shield, Inspector, Ranger), single-step execution, and code review. Use /kanban-run to execute tasks through the 7-column pipeline. AUTO-TRIGGER when: user says "implement task NNN" or any task ID + implement/build/do combination; or user confirms with "yes/ok/go/do it" after Claude proposes implementing a specific kanban task. license: MIT

Auto-Trigger Rules

ALWAYS invoke this skill (without waiting for /kanban-run) when:

  1. User mentions a kanban task ID and requests implementation:

    • "implement task #NNN" / "build task NNN" / "do NNN" / "run NNN"
    • Korean equivalents: "태스크 NNN 구현해줘" / "NNN 해줘" / "NNN 번 작업해줘"
    • Any message pairing a task number with implement / build / work on / do
  2. Claude has proposed implementing a specific kanban task and the user confirms:

    • Pattern: Claude says "Shall I implement task #NNN [title]?" → User replies "yes", "ok", "go", "do it", "응", "해줘", "그래", "ㅇㅇ"
    • This confirmation must trigger /kanban-run <ID> automatically — do not implement manually
  3. User says "next task" / "continue" / "다음 태스크 해줘" when a task is in progress:

    • Fetch board context first, identify next todo task, then run it

When auto-triggered: extract task ID and call /kanban-run <ID> — never implement code manually and patch kanban state afterward.

Shared context: read ../kanban/shared.md for DB path, pipeline levels, status transitions, DB operations, error handling, and agent context flow. Safety principles: read ../kanban/principles.mdmandatory, not optional. Schema: read ../kanban/schema.md for full DB schema, column descriptions, and JSON field formats.

Commands

In Codex environments, this skill may be invoked directly as a slash command text such as $kanban-run <ID> or $kanban-run <ID> --auto.

/kanban-run step <ID> — Single Step

Execute only the next pipeline step then exit. Same logic as /kanban-run but no loop.

/kanban-run <ID> [--auto] — Run Full Pipeline

Default: pause for user confirmation at Plan Review and Impl Review approvals. --auto: fully automatic (circuit breaker still fires).

Orchestration Loop (Level-Aware)

L1 Quick:
  todo → Worker(builder) implements → commit → done

L2 Standard:
  todo → Plan Agent(planner) → impl (skip plan_review)
  impl → Worker(builder) + TDD Tester(shield) → impl_review
  impl_review → Code Review → [user confirm] → commit → done / reject → impl

L3 Full:
  todo → Plan Agent(planner) → plan_review
  plan_review → Review Agent(critic) → [user confirm: y/c/n] → impl / ceo-review / reject→plan
    └─ [c] CEO Review: product angle check → update plan → back to plan_review
  impl → Worker(builder) + TDD Tester(shield) → impl_review
  impl_review → Code Review(inspector) → [user confirm] → test / reject → impl
  test → Test Runner(ranger) → pass → commit → done / fail → impl

Circuit breaker: plan_review_count > 3 OR impl_review_count > 3 → stop, ask user

CEO Review (L3 plan_review only)

When the user selects [c] at the plan_review confirmation prompt, run a CEO-perspective analysis inline before proceeding to impl. Not a separate agent — run as a structured prompt to the current model:

Adopt the perspective of a skeptical product founder reviewing this plan.
Ask:
  (1) Is this feature actually necessary, or can the need be met more simply?
  (2) Does this align with the project's stated purpose? [load from project brief]
  (3) Is there a 10x simpler implementation that solves 80% of the problem?
  (4) What might we regret about this decision in 6 months?
Output: bullet list of concerns, or "No concerns — looks right-sized."

After CEO review output, present: [y] proceed to impl / [r] revise plan / [n] reject

Read the task's level field first to determine which steps to execute.

Model Routing (Provider-Aware)

Resolve real model names from ../kanban/models.json using provider:

  • KANBAN_MODEL_PROVIDER env var if set (claude or codex)
  • else codex when CODEX_* env is present
  • else claude when CLAUDE_* env is present
  • else claude when .claude/ exists
  • else codex when .codex/ exists
  • else default_provider from models.json

For Codex, the router should prefer the higher-capability entries in models.json for the full kanban-run pipeline.

MODEL_PROVIDER=${KANBAN_MODEL_PROVIDER:-}
if [ -z "$MODEL_PROVIDER" ] && [ -n "${CODEX_THREAD_ID:-}${CODEX_CI:-}" ]; then MODEL_PROVIDER=codex; fi
if [ -z "$MODEL_PROVIDER" ] && [ -n "${CLAUDE_PROJECT_DIR:-}${CLAUDECODE:-}" ]; then MODEL_PROVIDER=claude; fi
if [ -z "$MODEL_PROVIDER" ] && [ -d .claude ]; then MODEL_PROVIDER=claude; fi
if [ -z "$MODEL_PROVIDER" ] && [ -d .codex ]; then MODEL_PROVIDER=codex; fi

read_model() {
  local key="$1"
  python3 - "$MODEL_PROVIDER" "$key" <<'PY'
import json, pathlib, sys
p = pathlib.Path("../kanban/models.json")
d = json.loads(p.read_text())
provider = sys.argv[1] or d["default_provider"]
key = sys.argv[2]
print(d["providers"][provider][key])
PY
}

read_effort() {
  local key="$1"
  python3 - "$MODEL_PROVIDER" "$key" <<'PY'
import json, pathlib, sys
p = pathlib.Path("../kanban/models.json")
d = json.loads(p.read_text())
provider = sys.argv[1] or d["default_provider"]
key = sys.argv[2]
print(d.get("reasoning_effort", {}).get(provider, {}).get(key, ""))
PY
}

MODEL_PLANNER=$(read_model planner)
MODEL_CRITIC=$(read_model critic)
MODEL_BUILDER=$(read_model builder)
MODEL_SHIELD=$(read_model shield)
MODEL_INSPECTOR=$(read_model inspector)
MODEL_RANGER=$(read_model ranger)
EFFORT_PLANNER=$(read_effort planner)
EFFORT_CRITIC=$(read_effort critic)
EFFORT_BUILDER=$(read_effort builder)
EFFORT_SHIELD=$(read_effort shield)
EFFORT_INSPECTOR=$(read_effort inspector)
EFFORT_RANGER=$(read_effort ranger)

Implementation

# 1. Read current task state (status + level only)
TASK=$(curl -s "${AUTH_HEADER[@]}" "$BASE_URL/api/task/$ID?project=$PROJECT&fields=status,level")
STATUS=$(echo "$TASK" | jq -r '.status')

# 2. Dispatch agent (see Agent Dispatch below)
# 3. After agent: append to agent_log (see schema.md for format)
# 4. Re-read state, loop until done or circuit breaker

Agent Nicknames & Identity

Each agent has a fixed nickname used consistently across all records. The task card becomes a work log — every field and every log entry is signed.

Nickname Role Model Key Reasoning Effort (codex) Status trigger
Planner Plan Agent planner high todo
Critic Plan Review Agent critic medium plan_review
Builder Worker Agent builder high impl (step 1)
Shield TDD Tester shield medium impl (step 2)
Inspector Code Review Agent inspector medium impl_review
Ranger Test Runner ranger medium test

See ../kanban/schema.md for JSON formats and the Signature Header Rule.

Agent Dispatch

Template files are at ../kanban/templates/.

Status Template Nickname Model Key
todo templates/plan-agent.md Planner planner
plan_review templates/review-agent.md Critic critic
impl step 1 templates/worker-agent.md Builder builder
impl step 2 templates/tdd-tester.md Shield shield
impl_review templates/code-review-agent.md Inspector inspector
test templates/test-runner.md Ranger ranger

Agent minimum fields (fetch only what each agent needs):

Nickname Required Fields
Planner title,description,plan_review_comments
Critic title,description,plan,decision_log,done_when
Builder title,description,plan,done_when,plan_review_comments,review_comments
Shield title,description,implementation_notes
Inspector title,description,plan,done_when,implementation_notes
Ranger title,implementation_notes

Dispatch procedure — execute in this order for every agent:

⓪ Fetch project brief (once per pipeline run, cache for all agents)
   PROJECT_DATA = curl GET /api/projects/$PROJECT
   PROJECT_BRIEF = extract .brief field (empty string if null or project not found)
   This is injected into every agent template via <project_brief> placeholder.

⓪ʙ Resolve dependencies & review feedback (once per pipeline run, cache for all agents)

   **Parse dependencies from description:**
   ```bash
   # Extract dependency IDs from description (case-insensitive)
   DESCRIPTION=$(curl -s "${AUTH_HEADER[@]}" "$BASE_URL/api/task/$ID?project=$PROJECT&fields=description" | jq -r '.description // ""')
   DEP_IDS=$(echo "$DESCRIPTION" | grep -ioP 'Depends on:\s*\K#\d+(?:,\s*#\d+)*' | grep -oP '\d+' || true)

Circular dependency check: If $ID (current task) appears in any dependency's own Depends on: line, emit error and abort:

for DEP_ID in $DEP_IDS; do
  DEP_TASK=$(curl -s "${AUTH_HEADER[@]}" "$BASE_URL/api/task/$DEP_ID?project=$PROJECT&fields=title,status,description,decision_log,implementation_notes")
  HTTP_CODE=$(echo "$DEP_TASK" | jq -r '.id // empty')
  if [ -z "$HTTP_CODE" ]; then
    echo "WARNING: dependency #$DEP_ID not found (404), skipping"
    continue
  fi
  DEP_DESC=$(echo "$DEP_TASK" | jq -r '.description // ""')
  if echo "$DEP_DESC" | grep -iqP "Depends on:.*#$ID\\b"; then
    echo "ERROR: circular dependency detected — #$ID ↔ #$DEP_ID. Aborting."
    exit 1
  fi
  # Cache: DEPS[$DEP_ID] = { title, status, decision_log, implementation_notes }
done

Build per-agent dependency context string: For each cached dependency, assemble context based on the current agent:

  • Planner: decision_log (500 chars) + implementation_notes (500 chars)
  • Builder: implementation_notes (500 chars)
  • Inspector: decision_log (300 chars)

Truncation: if field length > limit, take first N chars + ...[truncated]. If dep status != done: prepend [IN PROGRESS] warning to that dep's block. If no dependencies: DEPS_CONTEXT="" (empty string — placeholder removed cleanly).

Format per dependency:

### #<DEP_ID>: <title> [<status>]
[IN PROGRESS]

**Decision Log:**
<truncated decision_log>

**Implementation Notes:**
<truncated implementation_notes>

Extract review feedback for re-runs:

# Critic feedback (for Planner re-run)
CRITIC_FEEDBACK=""
PLAN_REVIEW_COMMENTS=$(echo "$TASK" | jq -r '.plan_review_comments // ""')
if [ -n "$PLAN_REVIEW_COMMENTS" ] && [ "$PLAN_REVIEW_COMMENTS" != "null" ]; then
  CRITIC_FEEDBACK=$(echo "$PLAN_REVIEW_COMMENTS" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) > 0:
  print(data[-1].get('comment', ''))
")
fi

# Inspector feedback (for Builder re-run)
INSPECTOR_FEEDBACK=""
REVIEW_COMMENTS=$(echo "$TASK" | jq -r '.review_comments // ""')
if [ -n "$REVIEW_COMMENTS" ] && [ "$REVIEW_COMMENTS" != "null" ]; then
  INSPECTOR_FEEDBACK=$(echo "$REVIEW_COMMENTS" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) > 0:
  print(data[-1].get('comment', ''))
")
fi

① Read task fields (use per-agent fields to minimize token usage)

Planner

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,description,plan_review_comments

Critic

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,description,plan,decision_log,done_when

Builder

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,description,plan,done_when,plan_review_comments,review_comments

Shield

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,description,implementation_notes

Inspector

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,description,plan,done_when,implementation_notes

Ranger

TASK = curl GET /api/task/$ID?project=$PROJECT&fields=title,implementation_notes Extract only the fields listed above for each agent

② Mark agent as active curl PATCH /api/task/$ID → { "current_agent": "" }

③ Read template file Read tool: ../kanban/templates/.md

④ Fill placeholders in template Replace every occurrence of: → actual task ID → actual project name → project brief from step ⓪ (empty string if not set) → task title <description> → task description (requirements) <plan> → plan field value <decision_log> → decision_log field value <done_when> → done_when field value <implementation_notes> → implementation_notes field value <plan_review_comments> → plan_review_comments field value <dependencies_context> → per-agent dep context from step ⓪ʙ (empty string if none) <critic_feedback> → latest plan_review_comments comment (empty if first run) <inspector_feedback> → latest review_comments comment (empty if first run) <TIMESTAMP> → current UTC time (ISO 8601) <MODEL_PLANNER> → $MODEL_PLANNER <MODEL_CRITIC> → $MODEL_CRITIC <MODEL_BUILDER> → $MODEL_BUILDER <MODEL_SHIELD> → $MODEL_SHIELD <MODEL_INSPECTOR> → $MODEL_INSPECTOR <MODEL_RANGER> → $MODEL_RANGER <EFFORT_PLANNER> → $EFFORT_PLANNER <EFFORT_CRITIC> → $EFFORT_CRITIC <EFFORT_BUILDER> → $EFFORT_BUILDER <EFFORT_SHIELD> → $EFFORT_SHIELD <EFFORT_INSPECTOR> → $EFFORT_INSPECTOR <EFFORT_RANGER> → $EFFORT_RANGER</p> <p> Recommended helper script:</p> <pre><code class="language-bash">PROMPT=$(python3 ../kanban/scripts/render_agent_prompt.py \ --template ../kanban/templates/<agent>.md \ --models ../kanban/models.json \ --provider "$MODEL_PROVIDER" \ --set ID="$ID" \ --set PROJECT="$PROJECT" \ --set project_brief="$PROJECT_BRIEF" \ --set title="$TITLE" \ --set description="$DESCRIPTION" \ --set plan="$PLAN" \ --set decision_log="$DECISION_LOG" \ --set done_when="$DONE_WHEN" \ --set implementation_notes="$IMPLEMENTATION_NOTES" \ --set plan_review_comments="$PLAN_REVIEW_COMMENTS" \ --set dependencies_context="$DEPS_CONTEXT" \ --set critic_feedback="$CRITIC_FEEDBACK" \ --set inspector_feedback="$INSPECTOR_FEEDBACK" \ --set TIMESTAMP="$TIMESTAMP") </code></pre> <p> If a field is missing, pass empty string (<code>--set key=""</code>). Use <code>--strict</code> only when every unresolved <code><...></code> token should be treated as an error.</p> <p>⑤ Launch Task tool with filled prompt If MODEL_PROVIDER is <code>codex</code>: Task( subagent_type = "general-purpose", model = "<resolved model from models.json>", model_reasoning_effort= "<resolved effort from models.json>", prompt = <filled template content> )</p> <p> Otherwise (<code>claude</code>): Task( subagent_type = "general-purpose", model = "<resolved model from models.json>", prompt = <filled template content> )</p> <p>⑥ After Task completes — append signed entry to agent_log (use schema.md › "Appending to agent_log" snippet, set agent=<Nickname>, model=<model>, message=<summary>)</p> <pre><code> After Builder + Shield both complete, move to `impl_review`: ```bash curl -s "${AUTH_HEADER[@]}" -X PATCH "$BASE_URL/api/task/$ID?project=$PROJECT" \ -H 'Content-Type: application/json' \ -d '{"status": "impl_review", "current_agent": null}' </code></pre> <p><strong>Default mode</strong>: after <code>plan_review</code> and <code>impl_review</code> agents complete, ask user with AskUserQuestion to accept/reject before advancing. <strong>Auto mode (<code>--auto</code>)</strong>: auto-accept the agent's decision.</p> <h4>→ Done Transition (all levels)</h4> <pre><code class="language-bash"># 1. Commit pending changes if [ -n "$(git status --porcelain 2>/dev/null)" ]; then git add -A git commit -m "feat: <TITLE> [kanban #<ID>]" fi COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "no-git") # 2. Move to done curl -s "${AUTH_HEADER[@]}" -X PATCH "$BASE_URL/api/task/$ID?project=$PROJECT" \ -H 'Content-Type: application/json' \ -d '{"status": "done"}' # 3. Record commit hash in notes curl -s "${AUTH_HEADER[@]}" -X POST "$BASE_URL/api/task/$ID/note?project=$PROJECT" \ -H 'Content-Type: application/json' \ -d "{\"content\": \"Commit: $COMMIT_HASH\"}" </code></pre> <p>If no commits yet, skip note or record <code>"Commit: (none)"</code>.</p> <h3><code>/kanban-run review <ID></code> — Code Review</h3> <p>Trigger Code Review agent for a task in <code>impl_review</code> status (same as impl_review step).</p> </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/cyanluna-git/cyanluna.skills --skill kanban-run </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>164</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>41</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/51350627?v=4" alt="cyanluna-git" 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>cyanluna-git</span> <a href="/?creator=cyanluna-git" 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>