premortem

star 31

Лёгкий советник «раздвигающий шторки» — premortem-сессия которая находит дыры в плане с разных углов, предлагает варианты решений по каждой и помогает юзеру быстро принять осознанные решения. История запусков сохраняется в ./docs/premortem/. Use when user types «премортем», «premortem», «найди дыры в плане», «посмотри со стороны на план», «what could kill this plan». Do NOT use for: vague ideas without a concrete plan; one-right-answer questions; creative editing of a draft; already-irreversible decisions.

AndyShaman By AndyShaman schedule Updated 5/9/2026

name: premortem description: > Лёгкий советник «раздвигающий шторки» — premortem-сессия которая находит дыры в плане с разных углов, предлагает варианты решений по каждой и помогает юзеру быстро принять осознанные решения. История запусков сохраняется в ./docs/premortem/. Use when user types «премортем», «premortem», «найди дыры в плане», «посмотри со стороны на план», «what could kill this plan». Do NOT use for: vague ideas without a concrete plan; one-right-answer questions; creative editing of a draft; already-irreversible decisions.

Premortem skill

Overview

Скилл = советник, не риск-аудитор. Показывает что юзер не видит → предлагает варианты → юзер выбирает → агент потом исполняет. Четыре фазы: pre-flight (синхронизация контекста), silent scan (3 параллельных помощника находят 5–8 дыр), live диалог (краткие решения по каждой дыре, расширение топа), запись (<plan-name>.md + история в history/).

Цель — лёгкая 10–15-минутная сессия, не тяжёлый аудит.

Главный принцип: скрипты делают всю детерминированную механику (I/O, schema, ID, sort, dedup, classifier-switch). LLM — только семантика.

Триггеры

Включаем:

  • «премортем», «premortem», «premortem this», «run premortem»
  • «найди дыры в плане»
  • «посмотри со стороны на план»
  • «что я упускаю в <конкретный план>»
  • «future-proof this <plan/launch/decision>»
  • «what could kill this <plan/launch>»

НЕ включаем (слишком широкие):

  • «should I», «what could go wrong», «stress test», «what am I missing», «devil's advocate this» — конфликтуют с базовым поведением модели.

Когда применимо

  • Конкретный план / запуск / решение с обратимым выбором.
  • Юзер готов потратить 10–15 минут.
  • Есть минимум контекста (предмет / аудитория / успех).

Когда НЕ применимо

  • Идея без формы — сначала помочь оформить план.
  • Вопрос с одним правильным ответом — просто ответить.
  • Уже принятое необратимое решение — премортем не вернёт назад.
  • Творческая правка черновика — это редактирование, не премортем.

Phase 1: Pre-flight — синхронизация контекста

Цель: установить 5 минимумов: предмет / аудитория / критерий успеха / горизонт оценки / reference class.

Действия:

  1. Прочитать контекст. Просканировать текущий разговор и доступные файлы (Read / Glob). Извлечь то что уже сказано — не спрашивать заново.

  2. Найти пробелы. Сравнить с пятью минимумами. Записать что есть, что нет.

  3. Задать максимум 2 вопроса за раз. Если не хватает 4 элементов — спросить 2, получить ответ, спросить ещё 2. Не делать «анкету» из 5 вопросов сразу.

  4. Reference class — отдельный вопрос. «На что это похоже из уже сделанных проектов? Назови 1–2 примера — твоих или известных тебе». Если юзер говорит «не знаю» — двигаться дальше с пустым reference_class, помощники получат сигнал.

  5. Горизонт оценки — выбор из меню.

    Через сколько оценим что план провалился?
    А) через 30 дней
    Б) через 3 месяца
    В) через 12 месяцев
    Г) после первого пилота
    Д) после запуска
    Е) после привлечения первых N клиентов
    Ж) иное (напиши своё)
    
  6. Неясности → assumption. Если после 2 раундов вопросов что-то остаётся туманным — пометить как assumption и передать в Phase 2 в составе угла «Допущения». Не блокировать сессию.

  7. Resolve plan-name. Спросить или вывести из контекста. Прогнать через CLI:

    uv run scripts/cli.py slugify "<plan name from user>"
    

    Запомнить slug — он будет именем файла.

  8. Существует ли файл? Запустить:

    uv run scripts/cli.py find docs/premortem --name "<plan_name>"
    

    Если файл уже есть — это повторный запуск, перейти к секции «Rerun logic» ниже. Если нет — продолжать новый сценарий.

Phase 2: Silent scan — взгляд с разных сторон

Цель: 3 параллельных помощника находят 5–8 уникальных дыр.

Действия:

  1. Классифицировать тип плана и выбрать углы. Запустить:

    uv run scripts/cli.py classify "<plan_text>"
    

    Получить JSON: {profile, angles, confidence, matched_keywords}.

    Если confidence < 0.4 — fallback: задать одному LLM-помощнику вопрос «какие 6 углов лучше всего видят дыры этого плана» (одной строкой, без объяснений). Использовать его ответ.

    Юзеру показать только список выбранных углов одной строкой:

    Смотрю с углов: Клиент, Исполнитель, Противник, Допущения, Конкурент, Будущий поддерживающий.
    
  2. Установить premortem-фрейм.

    «Представим что к [горизонту] этот план провалился. Не "может провалиться" — провалился, факт. Что произошло?»

    (См. references/frame-language.md.)

  3. Запустить 3 параллельных помощника одним сообщением.

    Каждый получает свой промпт из references/helper-prompt-template.md плюс назначенные углы (по 2 угла на помощника). Распределение: 6 углов на 3 помощника = (2,2,2). Один помощник получает «Будущий поддерживающий» с обязательным WYSIATI-микрошагом.

    Модель: Opus 4.7 по умолчанию. Если юзер запустил скилл с флагом --model sonnet — Sonnet. Если флага нет, скилл может предложить выбор перед stage 2 одной строкой:

    «Запускаю на Opus ($1–2). Sonnet будет дешевле ($0.50–1) — переключить?»

    Параллельно: ВСЕ 3 в ОДНОМ сообщении с тремя Task tool calls.

  4. Получить 6 сырых ответов (3 × до 2). Прогнать каждый через валидатор:

    uv run scripts/cli.py validate-helper < helper_response.json
    

    Retry policy. Максимум 3 attempts на помощника. Классификация ошибок:

    Тип ошибки Retryable? Сообщение для retry
    Невалидный JSON / extra prose да «Верни ТОЛЬКО JSON, без преамбулы»
    Schema fail (отсутствует поле) да «Не хватает поля X в дыре N»
    maxItems >2 да «Максимум 2 дыры; верни лучшие 2»
    Generic risks без привязки к плану да «Дыры слишком общие; привяжи к деталям плана»
    Empty holes ({"holes": []}) НЕТ принять как валидный пустой ответ
    Low confidence на всех дырах НЕТ принять как есть, пометить assumption

    Если после 3 attempts всё ещё ошибка — отбросить ответ помощника, продолжить с тем что есть. Не чинить вручную.

  5. Hash-дедуп. Прогнать все валидные дыры через:

    uv run scripts/cli.py dedup < all_holes.json
    

    Убирает явные дубли по нормализованному описанию (sha256). Оставшиеся дубли (одна дыра, разные слова) обработает шаг 6.

  6. Семантический дедуп (LLM clustering). Если после hash-дедупа осталось >8 дыр — попросить LLM сгруппировать близкие в кластеры. Промпт:

    Вот N сырых дыр от трёх помощников. Какие из них на самом деле ОДНА И ТА ЖЕ дыра,
    видимая с разных углов? Верни JSON:
    [
      {
        "merged": [индексы дыр объединённые в одну],
        "title": "новое объединённое название",
        "merged_from_angles": ["Клиент", "Допущения"],  // углы-источники
        "merged_descriptions": ["исходное описание 1", "исходное описание 2"]
      },
      ...
    ]
    
    ПРАВИЛА:
    - Не теряй нюансы: если две дыры близки но имеют разный угол атаки — оставь обе.
    - Если все 8+ — действительно разные дыры, верни пустой массив (ничего не объединять).
    - Объединение допустимо только если ≥80% совпадение по сути.
    

    Применить кластеры: для каждой объединённой дыры в Hole.merged_from записать ID исходных дыр (после назначения ID на шаге 8), в Hole.углы — все исходные углы, в описание — выбрать самое богатое исходное описание.

  7. Лимит на 8. Если после семантического дедупа всё ещё >8 — ранжировать по важность × уверенность × novelty (где novelty = 1 для новых, 0.5 для похожих на ранее найденные при rerun) и взять топ-8. Не отбрасывать assumption-дыры приоритетно — они часто и есть самое ценное.

  8. Назначить ID. Каждой уникальной дыре дать ID через:

    uv run scripts/cli.py next-id --existing "H-001,H-002"
    

    На первом запуске стартуем с H-001. Сохранить порядок: первая дыра → H-001, вторая → H-002, и т.д.

  9. Сохранить промежуточный snapshot in-memory (объект Plan). На диск пока не пишем — это произойдёт в Phase 4.

Phase 3: Live диалог — выбор решений

Цель: юзер быстро принимает решения по 5–8 дырам в кратком режиме. Топ 1–3 + high-impact дыры расширяются до полного режима постфактум.

Per-hole loop (краткий режим по умолчанию):

  1. Презентация дыры.

    ### H-NNN: <title>
    <описание (1 абзац)>
    Почему важно: <одна фраза>
    Угол: <угол>
    Уверенность: <маркер>
    
  2. Варианты решения. Сгенерировать 2–3 стратегических варианта (LLM, не скрипт). Каждый = направление с +/–, не задача.

    А: <направление 1> (+быстро, –не масштабируется)
    Б: <направление 2> (+глубоко, –долго)
    В: <направление 3> (+качество, –теряем момент)
    
  3. Юзер. Выбирает / просит ещё / откладывает / отвергает / просит «расширь H-NNN» (тогда сразу полный режим для этой дыры).

  4. Если выбрал — фиксация в краткий режим:

    • что выбрали (одна строка)
    • почему (одна фраза)
    • первый шаг (одна задача)
    • исполнитель: агент / человек / оба
  5. Переход к следующей дыре.

После всех дыр — финальная приоритизация:

  1. Проверка: есть ли принятые дыры?

    Если 0 принятых дыр (все ОТЛОЖЕНО / ОТВЕРГНУТО) — спросить юзера явно:

    «Ты не принял ни одного решения. Возможны варианты: А) сохранить как обзор без действий (юзер увидел дыры, решил пока ничего не делать) Б) вернуться к 1–2 дырам и принять решения В) пересмотреть — может я плохо предложил варианты

    Что делаем?»

    Если А — переходим к шагу 5 (session_recommendation), пропускаем топ и bias. Если Б — возвращаемся к Phase 3 per-hole loop для выбранных юзером дыр. Если В — перегенерируем варианты решений для названных дыр.

  2. Топ 1–3. Скилл сам отбирает из ПРИНЯТЫХ по важность × обратимость × уверенность. Если ПРИНЯТЫХ < 3 — берём все.

  3. Авто-расширение до полного режима. Для каждой дыры из топ 1–3 ИЛИ для любой ПРИНЯТОЙ дыры с (важность=высокая И уверенность ≠ предположение) — задать недостающие поля одним проходом:

    • 2–3 первых шага (prevent)
    • сигнал успеха (detect)
    • стоп-условие
    • что делать если уже случилось (limit damage)
    • открытые вопросы

    Пометить эти дыры режим: полный.

  4. Bias-проверка топа (1 вопрос юзеру).

    Скилл сам прогоняет топ через 6 биасов в системном промпте (см. references/bias-checklist.md), выбирает наиболее вероятный риск искажения и показывает юзеру:

    «Какой биас мог сильнее всего исказить твой топ? Я думаю — <биас>. Коррекция: <конкретное действие>. Согласен или другой?»

    Юзер подтверждает или меняет. Записывается одна строка в bias_проверка.

  5. Session recommendation — явный шаг.

    Скилл предлагает рекомендацию по сессии (один из 4 enum-значений) на основе паттерна принятых решений:

    • continue — большинство решений идут вперёд, нет дыр со статусом ОТЛОЖЕНО среди топа
    • reduce_stake — топ содержит решения вида «пилот / меньше / медленнее»
    • delay — есть ОТЛОЖЕНО в топе или явно сказано «доделать позиционирование / подождать»
    • abort — все или большинство критических дыр ОТВЕРГНУТЫ, или решения = «отказаться»

    Скилл показывает свою предварительную рекомендацию + причину, юзер подтверждает или меняет:

    «Моя рекомендация по сессии: delay — есть отложенные дыры в топе. Ты подтверждаешь эту общую рекомендацию или предпочитаешь: continue / reduce_stake / abort?»

    Записывается в Plan.session_recommendation.

  6. Reverse премортем (условный). Запускается только если session_recommendation in {reduce_stake, delay, abort}. Если continue — пропускается.

    «Прошёл [горизонт]. Не сделали (или сделали меньше). Оказалось зря — почему?»

    LLM генерирует 3 сильные причины + одну строку «что перевешивает». Записывается в reverse_премортем.

Phase 4: Запись с итерациями

Цель: один main-файл + история снимков. Любая последовательность запусков оставляет файл валидным.

Действия:

  1. Собрать Plan dataclass. В памяти на основе данных из Phase 3.

  2. Атомарная запись.

    uv run scripts/cli.py write --plan-json <plan.json> --path docs/premortem/<slug>.md
    

    CLI: валидирует → пишет во временный файл → fsync → os.rename. Lock через fcntl.

  3. Снимок в history/.

    uv run scripts/cli.py snapshot docs/premortem/<slug>.md
    

    Создаёт history/<slug>-<YYYY-MM-DD-HHMM>.md.

  4. Финальное сообщение юзеру.

    Премортем сохранён: docs/premortem/<slug>.md (запуск N).
    Топ 1–3:
    1. H-XXX — <title>. Первый шаг: ...
    2. H-YYY — ...
    3. H-ZZZ — ...
    
    История: docs/premortem/history/<slug>-<ts>.md
    

    Не пересказывать всё содержимое файла — юзер сам прочитает.

Rerun logic (повторный запуск)

Триггерится в Phase 1 шаг 8: файл уже существует.

Действия:

  1. Прочитать существующий файл. Скрипт plan_io.py read.

  2. Показать сводку. Запустить:

    uv run scripts/cli.py stats docs/premortem/<slug>.md
    

    Юзеру:

    Нашёл прошлый премортем по «<plan_name>» от <дата>.
    <всего> дыр: <принято> ПРИНЯТО, <в_работе> В РАБОТЕ, <отложено> ОТЛОЖЕНО, <закрыто> ЗАКРЫТА.
    Что хочешь сделать?
    А) обновить статусы существующих дыр
    Б) найти новые дыры
    В) и то и другое
    Г) начать заново под новый этап проекта
    
  3. А или В: обновить статусы. Пройти по всем активным дырам (не ОТВЕРГНУТО / не ЗАКРЫТА), спросить «Что с H-NNN: ? Не изменилось / в работе / закрыто (опиши как) / отвергнуто (причина)». Обновить <code>статус</code> и <code>история_статуса</code>.</p> </li> <li><p><strong>Б или В: новые дыры.</strong> Запустить Phase 2 заново, передать помощникам список существующих дыр в формате:</p> <pre><code>Уже найденные дыры (не повторять): H-001: <title> H-002: <title> ... </code></pre> <p>Помощники возвращают находки + статус каждой: <code>same</code> / <code>worse</code> / <code>resolved</code> / <code>new</code> / <code>new-aspect</code>.</p> </li> <li><p><strong>Г: начать заново.</strong> Юзер уточняет — это новый этап того же плана (тогда продолжаем в тот же файл, увеличиваем <code>запусков</code>) или новый план (тогда спрашиваем новое имя, создаём новый файл, оставляем старый нетронутым).</p> </li> <li><p><strong>Обновить frontmatter.</strong> <code>обновлён</code> = сегодня, <code>запусков</code> += 1.</p> </li> <li><p><strong>Снимок и запись.</strong> Как в Phase 4.</p> </li> </ol> <h2>Что скилл НЕ делает</h2> <ul> <li>Не предлагает численные вероятности — LLM не калиброван.</li> <li>Не использует 12-вопросный bias-чеклист как фазу — только 6 биасов в системном промпте.</li> <li>Не генерит HTML / Slack / Notion — только md файл.</li> <li>Не пытается заменить decision journal или OKR.</li> <li>Не запускает reverse premortem если рекомендация = продолжать.</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/AndyShaman/premortem --skill premortem </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>31</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>4</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/166172682?u=9066dd3b372a20e445ca68940cbab9a105d40c79&v=4" alt="AndyShaman" 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>AndyShaman</span> <a href="/?creator=AndyShaman" 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>