{
  "version": 3,
  "sources": ["season_sheet.js"],
  "sourcesContent": ["/*\n * static/js/season_sheet.js \u2014 the bulletin page's season-heatmap sheet\n * (SNOW-117, extracted from bulletin.html by SNOW-621).\n *\n * `apps/public/templates/public/bulletin.html` carried 232 lines of inline\n * JavaScript across four blocks, the bulk of it this one. Inline script\n * cannot be reached by the Vitest harness, so the two pieces here with\n * exact numeric behaviour \u2014 the touch threshold and the first-open latch \u2014\n * had no unit coverage and could not get any while they lived in the\n * template. Finding M14.\n *\n * Only this block moved. The other three are short enough that extracting\n * them would trade one problem for another.\n *\n * What it owns\n * ------------\n * The sheet is a fixed-position shell whose open/closed state lives in\n * `data-season-sheet` on the shell itself; the trigger mirrors it in\n * `aria-expanded`. The grid inside is deferred \u2014 a `#season-grid`\n * placeholder fires one HTMX GET on first open via the custom\n * `snowdesk:load` event (SNOW-170) \u2014 so opening does two different things\n * depending on whether that fetch has happened yet. Once it lands, the\n * page's own day is ringed. Every rated day is a plain link, so one tap\n * opens that day's bulletin (SNOW-1163, which retired SNOW-1158's\n * select-then-read readout).\n *\n * Loaded with `defer` from `bulletin.html`, inside the same\n * `{% if season_calendar %}` guard that renders the markup, so the\n * elements exist by the time it runs. It also returns early when they are\n * absent, which is what makes it safe to unit-test against a partial\n * fixture.\n */\n\n(function () {\n  'use strict';\n\n  // Squared pixel distance a touch may travel and still count as a tap\n  // rather than a scroll \u2014 8px, squared to avoid a square root per\n  // touchend.\n  //\n  // The backdrop dismisses the sheet, and the grid inside it scrolls\n  // horizontally. Without a threshold, a scroll gesture that starts on the\n  // backdrop lifts into a synthetic click and closes the sheet the user was\n  // trying to read. 8px is the usual slop allowance for a finger that meant\n  // to stay still.\n  var TAP_SLOP_SQUARED = 64;\n\n  // A ringed day of the calendar \u2014 never the key's ringed swatch.\n  var DAY_RING = '.season-day-ring:not(.season-key-swatch)';\n\n  var sheet = document.querySelector('[data-season-sheet]');\n  var trigger = document.querySelector('[data-season-trigger]');\n  if (!sheet || !trigger) return;\n\n  /**\n   * Centre the scroller on the selected day, or on today.\n   *\n   * @returns {void}\n   */\n  function scrollToSelected() {\n    var scroller = sheet.querySelector('[data-season-scroll]');\n    var target = sheet.querySelector(DAY_RING);\n    if (scroller && target) {\n      scroller.scrollLeft = Math.max(\n        0,\n        target.offsetLeft - scroller.clientWidth / 2 + target.clientWidth / 2,\n      );\n    }\n  }\n\n  /**\n   * Open or close the sheet.\n   *\n   * The first open and every later one differ: the grid is fetched lazily,\n   * so the first open can only start the request and must leave the scroll\n   * to the `htmx:afterSwap` handler once the grid is actually in the DOM.\n   * A later open has the grid already and scrolls immediately. The\n   * `data-loaded` latch is what tells them apart, and it is set BEFORE the\n   * trigger fires so a double-tap cannot start two fetches.\n   *\n   * @param {'open'|'closed'} state\n   * @returns {void}\n   */\n  function setState(state) {\n    sheet.dataset.seasonSheet = state;\n    trigger.setAttribute('aria-expanded', state === 'open' ? 'true' : 'false');\n    if (state !== 'open') return;\n\n    var grid = document.getElementById('season-grid');\n    if (grid && grid.dataset.loaded !== 'true') {\n      grid.dataset.loaded = 'true';\n      if (window.htmx) {\n        window.htmx.trigger(grid, 'snowdesk:load');\n      }\n    } else {\n      scrollToSelected();\n    }\n  }\n\n  /**\n   * Ring the page's own day.\n   *\n   * The ring is applied here from `#season-grid`'s `data-selected-date`\n   * rather than server-rendered, so the cached grid partial is the same\n   * bytes for every day of the season. `aria-current=\"date\"` tells a\n   * screen reader which day's bulletin the page is showing.\n   *\n   * @param {Element} grid  the #season-grid container\n   * @returns {void}\n   */\n  function ringPageDay(grid) {\n    var selectedDate = /** @type {HTMLElement} */ (grid).dataset.selectedDate;\n    if (!selectedDate) return;\n    var cell = grid.querySelector('.season-day[data-date=\"' + selectedDate + '\"]');\n    if (!cell) return;\n    cell.classList.add('season-day-ring');\n    cell.setAttribute('aria-current', 'date');\n  }\n\n  // After the HTMX swap completes: ring the page's own day, then scroll.\n  document.body.addEventListener('htmx:afterSwap', function (e) {\n    if (!e.detail || !e.detail.target || e.detail.target.id !== 'season-grid') return;\n    ringPageDay(e.detail.target);\n    scrollToSelected();\n  });\n\n  trigger.addEventListener('click', function () {\n    setState(sheet.dataset.seasonSheet === 'open' ? 'closed' : 'open');\n  });\n\n  sheet.querySelectorAll('[data-season-dismiss]').forEach(function (el) {\n    if (el.tagName === 'BUTTON') {\n      // The \u2715 close button \u2014 a plain click, with no scroll to conflict with.\n      el.addEventListener(\n        'click',\n        function () {\n          setState('closed');\n        },\n        false,\n      );\n      return;\n    }\n\n    // The backdrop. Touch-aware: dismiss only when the finger did not\n    // travel far enough to have been a scroll.\n    var startX = 0;\n    var startY = 0;\n    el.addEventListener(\n      'touchstart',\n      function (e) {\n        startX = e.touches[0].clientX;\n        startY = e.touches[0].clientY;\n        el.classList.add('is-pressing');\n      },\n      { passive: true },\n    );\n    el.addEventListener('touchend', function (e) {\n      el.classList.remove('is-pressing');\n      // Always suppress the synthetic click \u2014 the click listener below is a\n      // desktop fallback only. Without this a scroll gesture leaks a ghost\n      // click and the sheet closes as the user lifts their finger.\n      e.preventDefault();\n      var dx = e.changedTouches[0].clientX - startX;\n      var dy = e.changedTouches[0].clientY - startY;\n      if (dx * dx + dy * dy < TAP_SLOP_SQUARED) {\n        setState('closed');\n      }\n    });\n    el.addEventListener('touchcancel', function () {\n      el.classList.remove('is-pressing');\n    });\n    el.addEventListener('click', function () {\n      setState('closed');\n    });\n  });\n\n  document.addEventListener('keydown', function (e) {\n    if (e.key === 'Escape' && sheet.dataset.seasonSheet === 'open') {\n      setState('closed');\n    }\n  });\n}());\n"],
  "mappings": "CAiCC,UAAY,CACX,aAWA,IAAIA,EAAmB,GAGnBC,EAAW,2CAEXC,EAAQ,SAAS,cAAc,qBAAqB,EACpDC,EAAU,SAAS,cAAc,uBAAuB,EAC5D,GAAI,CAACD,GAAS,CAACC,EAAS,OAOxB,SAASC,GAAmB,CAC1B,IAAIC,EAAWH,EAAM,cAAc,sBAAsB,EACrDI,EAASJ,EAAM,cAAcD,CAAQ,EACrCI,GAAYC,IACdD,EAAS,WAAa,KAAK,IACzB,EACAC,EAAO,WAAaD,EAAS,YAAc,EAAIC,EAAO,YAAc,CACtE,EAEJ,CAeA,SAASC,EAASC,EAAO,CAGvB,GAFAN,EAAM,QAAQ,YAAcM,EAC5BL,EAAQ,aAAa,gBAAiBK,IAAU,OAAS,OAAS,OAAO,EACrEA,IAAU,OAEd,KAAIC,EAAO,SAAS,eAAe,aAAa,EAC5CA,GAAQA,EAAK,QAAQ,SAAW,QAClCA,EAAK,QAAQ,OAAS,OAClB,OAAO,MACT,OAAO,KAAK,QAAQA,EAAM,eAAe,GAG3CL,EAAiB,EAErB,CAaA,SAASM,EAAYD,EAAM,CACzB,IAAIE,EAA2CF,EAAM,QAAQ,aAC7D,GAAKE,EACL,KAAIC,EAAOH,EAAK,cAAc,0BAA4BE,EAAe,IAAI,EACxEC,IACLA,EAAK,UAAU,IAAI,iBAAiB,EACpCA,EAAK,aAAa,eAAgB,MAAM,GAC1C,CAGA,SAAS,KAAK,iBAAiB,iBAAkB,SAAU,EAAG,CACxD,CAAC,EAAE,QAAU,CAAC,EAAE,OAAO,QAAU,EAAE,OAAO,OAAO,KAAO,gBAC5DF,EAAY,EAAE,OAAO,MAAM,EAC3BN,EAAiB,EACnB,CAAC,EAEDD,EAAQ,iBAAiB,QAAS,UAAY,CAC5CI,EAASL,EAAM,QAAQ,cAAgB,OAAS,SAAW,MAAM,CACnE,CAAC,EAEDA,EAAM,iBAAiB,uBAAuB,EAAE,QAAQ,SAAUW,EAAI,CACpE,GAAIA,EAAG,UAAY,SAAU,CAE3BA,EAAG,iBACD,QACA,UAAY,CACVN,EAAS,QAAQ,CACnB,EACA,EACF,EACA,MACF,CAIA,IAAIO,EAAS,EACTC,EAAS,EACbF,EAAG,iBACD,aACA,SAAUG,EAAG,CACXF,EAASE,EAAE,QAAQ,CAAC,EAAE,QACtBD,EAASC,EAAE,QAAQ,CAAC,EAAE,QACtBH,EAAG,UAAU,IAAI,aAAa,CAChC,EACA,CAAE,QAAS,EAAK,CAClB,EACAA,EAAG,iBAAiB,WAAY,SAAUG,EAAG,CAC3CH,EAAG,UAAU,OAAO,aAAa,EAIjCG,EAAE,eAAe,EACjB,IAAIC,EAAKD,EAAE,eAAe,CAAC,EAAE,QAAUF,EACnCI,EAAKF,EAAE,eAAe,CAAC,EAAE,QAAUD,EACnCE,EAAKA,EAAKC,EAAKA,EAAKlB,GACtBO,EAAS,QAAQ,CAErB,CAAC,EACDM,EAAG,iBAAiB,cAAe,UAAY,CAC7CA,EAAG,UAAU,OAAO,aAAa,CACnC,CAAC,EACDA,EAAG,iBAAiB,QAAS,UAAY,CACvCN,EAAS,QAAQ,CACnB,CAAC,CACH,CAAC,EAED,SAAS,iBAAiB,UAAW,SAAU,EAAG,CAC5C,EAAE,MAAQ,UAAYL,EAAM,QAAQ,cAAgB,QACtDK,EAAS,QAAQ,CAErB,CAAC,CACH,GAAE",
  "names": ["TAP_SLOP_SQUARED", "DAY_RING", "sheet", "trigger", "scrollToSelected", "scroller", "target", "setState", "state", "grid", "ringPageDay", "selectedDate", "cell", "el", "startX", "startY", "e", "dx", "dy"]
}
