[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"playground-storage-event-cross-tab-sync":3,"verticals":19,"search-suggestions":58},{"slug":4,"title":5,"summary":6,"html":7,"css":8,"js":9,"config":10,"doc":13},"storage-event-cross-tab-sync","storage event — cross-tab playground",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Does this tab know it logged itself out?\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      This demo needs a \u003Cstrong>real second browser tab\u003C\u002Fstrong> — open one below, then log out\n      from either tab and watch which one notices on its own.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> This tab\u003C\u002Fdiv>\n      \u003Cp>\u003Cspan class=\"tab-badge\">\u003Cspan class=\"sw\">\u003C\u002Fspan>\u003Cspan id=\"tabId\">tab-????\u003C\u002Fspan>\u003C\u002Fspan> — every open copy of this page gets its own id.\u003C\u002Fp>\n      \u003Cdiv class=\"status-big\" id=\"statusBig\">\u003Cspan class=\"emoji\">⏳\u003C\u002Fspan>\u003Cspan id=\"statusText\">Reading saved state…\u003C\u002Fspan>\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row-wrap\">\n        \u003Cbutton class=\"btn bad sm\" id=\"logoutBtn\">Log out\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn good sm\" id=\"loginBtn\">Log back in\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost sm\" id=\"openTabBtn\">Open a second tab →\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Clabel class=\"toggle\" style=\"margin-top:12px\">\n        \u003Cinput type=\"checkbox\" id=\"fixToggle\" \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n        \u003Cspan>Apply the fix — call the handler locally right after writing\u003C\u002Fspan>\n      \u003C\u002Flabel>\n      \u003Cp class=\"hint\" id=\"fixHint\">\n        Off: this tab only updates its own status from a \u003Ccode>storage\u003C\u002Fcode> event — and that event\n        never fires in the tab that made the change.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> What this tab has seen\u003C\u002Fdiv>\n      \u003Cdiv class=\"output log\" id=\"log\" style=\"max-height: 220px; overflow:auto;\">\u003C\u002Fdiv>\n      \u003Cp class=\"hint late-join\">\n        Open a \u003Cem>third\u003C\u002Fem> tab anytime, even minutes after a logout happens elsewhere — it will\n        show the correct status immediately, because it reads \u003Ccode>localStorage\u003C\u002Fcode> directly on\n        load. It never needed to \"hear\" the change at all.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n.tab-badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-size: 0.8rem; font-weight: 700; letter-spacing: 0.02em; }\n  .tab-badge .sw { width: 8px; height: 8px; border-radius: 999px; background: var(--accent); }\n  .status-big { display: flex; align-items: center; gap: 10px; padding: 14px; border-radius: var(--radius-sm); border: 1px solid var(--line); background: var(--panel-2); font-size: 1.05rem; font-weight: 700; margin: 10px 0; }\n  .status-big.in { color: #4ade80; }\n  .status-big.out { color: #fbbf24; }\n  .status-big .emoji { font-size: 1.4rem; }\n  .btn-row-wrap { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n  .late-join { margin-top: 10px; }","\"use strict\";\nconst $ = (id) => document.getElementById(id);\nconst KEY = \"pg-storage-event-demo:session\";\n\nfunction randomId() {\n  try {\n    return crypto.randomUUID().slice(0, 8);\n  } catch {\n    return Math.random().toString(16).slice(2, 10);\n  }\n}\nconst TAB_ID = randomId();\n$(\"tabId\").textContent = \"tab-\" + TAB_ID;\n\nlet applyFix = false;\n\nfunction log(msg, kind) {\n  const line = document.createElement(\"div\");\n  line.textContent = msg;\n  line.style.color = kind === \"good\" ? \"var(--good, #4ade80)\" : kind === \"warn\" ? \"var(--warn, #fbbf24)\" : \"inherit\";\n  $(\"log\").prepend(line);\n}\n\nfunction renderStatus(loggedIn) {\n  const el = $(\"statusBig\");\n  el.className = \"status-big \" + (loggedIn ? \"in\" : \"out\");\n  el.querySelector(\".emoji\").textContent = loggedIn ? \"✅\" : \"🚪\";\n  $(\"statusText\").textContent = loggedIn ? \"Logged in\" : \"Logged out\";\n}\n\n\u002F\u002F The ONLY place the visible status updates from — exactly like a real app\n\u002F\u002F that trusts the storage event as its single source of truth. That choice\n\u002F\u002F is what exposes the gotcha: it's also called once on load (every tab, new\n\u002F\u002F or old, reads the persisted truth directly) but NOT from this tab's own\n\u002F\u002F write path, unless the fix toggle is on.\nfunction reactToChange(loggedIn, source) {\n  renderStatus(loggedIn);\n  log(`📨 status updated → ${loggedIn ? \"logged in\" : \"logged out\"} (source: ${source})`, \"good\");\n}\n\nfunction readInitial() {\n  let raw = null;\n  try {\n    raw = localStorage.getItem(KEY);\n  } catch {\n    log(\"⚠️ localStorage isn't available here (private mode or sandboxed iframe).\", \"warn\");\n    return;\n  }\n  if (!raw) {\n    renderStatus(true);\n    log(\"No saved state yet — defaulting to logged in.\");\n    return;\n  }\n  try {\n    const data = JSON.parse(raw);\n    reactToChange(data.loggedIn, \"read on load, tab-\" + data.by);\n  } catch {\n    renderStatus(true);\n  }\n}\n\nfunction writeState(loggedIn) {\n  const payload = JSON.stringify({ loggedIn, by: TAB_ID, at: Date.now() });\n  try {\n    localStorage.setItem(KEY, payload);\n  } catch {\n    log(\"⚠️ localStorage write failed.\", \"warn\");\n    return;\n  }\n  log(`✍️ wrote ${loggedIn ? \"logged in\" : \"logged out\"} to localStorage (this tab).`);\n  if (applyFix) {\n    reactToChange(loggedIn, \"manual call, fix applied\");\n  } else {\n    log(\"🤷 no storage event here — this tab's own status won't move until something else reads it.\", \"warn\");\n  }\n}\n\nwindow.addEventListener(\"storage\", (e) => {\n  if (e.key !== KEY || !e.newValue) return;\n  const data = JSON.parse(e.newValue);\n  reactToChange(data.loggedIn, \"storage event from tab-\" + data.by);\n});\n\n$(\"logoutBtn\").addEventListener(\"click\", () => writeState(false));\n$(\"loginBtn\").addEventListener(\"click\", () => writeState(true));\n$(\"openTabBtn\").addEventListener(\"click\", () => window.open(location.href, \"_blank\"));\n$(\"fixToggle\").addEventListener(\"change\", (e) => {\n  applyFix = e.target.checked;\n  $(\"fixHint\").textContent = applyFix\n    ? \"On: right after writing, this tab also calls its own handler — now it updates itself, same as every other tab.\"\n    : \"Off: this tab only updates its own status from a storage event — and that event never fires in the tab that made the change.\";\n});\n\nreadInitial();\nlog(`tab-${TAB_ID} ready.`);",{"height":11,"autorun":12},520,true,{"slug":4,"title":14,"excerpt":15,"coverUrl":16,"vertical":17},"I Built Cross-Tab Logout. The storage Event Skipped One Tab.","A cross-tab 'log out everywhere' feature built on the storage event worked in every tab except the one the user actually clicked in. The storage event never fires in the tab that made the change — and that's spec, not a bug.","\u002Fmedia\u002Fcovers\u002Fstorage-event-cross-tab-sync.png",{"slug":18},"dev",[20,34,46],{"id":21,"slug":18,"name":22,"tagline":23,"description":24,"accentFrom":25,"accentTo":26,"icon":27,"defaultLocale":28,"locales":29,"features":31,"position":33},"019fe637-3d33-714b-b57f-23e163ffca0c","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[28,30],"fa",{"courses":12,"paths":12,"articles":12,"exams":32,"flashcards":32,"packages":12,"community":12,"certificates":12,"teams":12,"commerce":12},false,0,{"id":35,"slug":36,"name":37,"tagline":38,"description":39,"accentFrom":40,"accentTo":25,"icon":41,"defaultLocale":28,"locales":42,"features":44,"position":45},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[28,30,43],"es",{"courses":32,"paths":32,"articles":12,"exams":32,"flashcards":12,"packages":32,"community":12,"certificates":12,"teams":32,"commerce":32},2,{"id":47,"slug":48,"name":49,"tagline":50,"description":51,"accentFrom":52,"accentTo":53,"icon":54,"defaultLocale":28,"locales":55,"features":56,"position":57},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[28],{"courses":32,"paths":32,"articles":12,"exams":32,"flashcards":32,"packages":32,"community":12,"certificates":32,"teams":32,"commerce":32},3,[59,63,67,71,75,79,83,87,91,95,99,103],{"slug":60,"name":61,"articles":62},"webdev","Webdev",126,{"slug":64,"name":65,"articles":66},"javascript","Javascript",104,{"slug":68,"name":69,"articles":70},"frontend","Frontend",80,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",49,{"slug":76,"name":77,"articles":78},"css","Css",40,{"slug":80,"name":81,"articles":82},"typescript","Typescript",18,{"slug":84,"name":85,"articles":86},"performance","Performance",16,{"slug":88,"name":89,"articles":90},"react","React",15,{"slug":92,"name":93,"articles":94},"browser","Browser",12,{"slug":96,"name":97,"articles":98},"node","Node",11,{"slug":100,"name":101,"articles":102},"html","Html",9,{"slug":104,"name":105,"articles":106},"accessibility","Accessibility",8]