[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-permissions-api-query-before-prompt":44,"search-suggestions":58},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","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",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"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","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"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","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"permissions-api-query-before-prompt","Permissions API — check without asking",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🔍 Read a permission without asking for it\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Pick a permission name and click \u003Cstrong>Check state\u003C\u002Fstrong>. It calls\n      \u003Ccode>navigator.permissions.query()\u003C\u002Fcode> — a real call to your real browser.\n      Nothing pops up, no matter what it finds. Then compare that to what the\n      actual feature does when called directly.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Pick a permission to read\u003C\u002Fdiv>\n      \u003Cdiv class=\"name-grid\" id=\"nameGrid\">\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">Support for each name varies by browser — that's the point of trying more than one.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Check state (read-only)\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn primary\" id=\"checkBtn\">Check state — \u003Ccode id=\"selectedName\">geolocation\u003C\u002Fcode>\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"clearLog\">Clear log\u003C\u002Fbutton>\n        \u003Clabel class=\"toggle\" style=\"margin-left:6px\">\n          \u003Cinput type=\"checkbox\" id=\"watchToggle\" \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>watch for live changes\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"status\" style=\"margin-top:14px\">\n        \u003Cdiv class=\"state-row\">\n          \u003Cspan>Current state:\u003C\u002Fspan>\n          \u003Cspan class=\"state-pill error\" id=\"statePill\">not checked\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"stateHint\">Click \"Check state\" — this never triggers a system dialog, whatever the answer is.\u003C\u002Fp>\n\n      \u003Cdiv class=\"counter-row\">\n        \u003Cspan class=\"counter\">Checks made: \u003Cb id=\"checkCount\">0\u003C\u002Fb>\u003C\u002Fspan>\n        \u003Cspan class=\"counter\">System prompts triggered by checking: \u003Cb id=\"promptCount\">0\u003C\u002Fb>\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"log\" id=\"log\" style=\"margin-top:12px\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> Now compare: call the real feature\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">\n        This button doesn't check anything first — it just asks for the browser's\n        current position directly, the way the \"obvious fix\" in the post does.\n        If the state above is \u003Ccode>prompt\u003C\u002Fcode>, watch what happens.\n      \u003C\u002Fp>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cbutton class=\"btn bad\" id=\"realCallBtn\">Call getCurrentPosition() directly\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"realCallResult\">\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.name-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));\n    gap: 8px;\n    margin-top: 12px;\n  }\n  .name-btn {\n    text-align: left;\n  }\n  .name-btn[aria-pressed=\"true\"] {\n    outline: 2px solid var(--accent, #6ee7ff);\n    outline-offset: 2px;\n  }\n  .state-row {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    flex-wrap: wrap;\n  }\n  .state-pill {\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.04em;\n    font-size: 0.78rem;\n    padding: 4px 10px;\n    border-radius: 999px;\n    border: 1px solid var(--line);\n  }\n  .state-pill.granted { background: rgba(60, 200, 130, 0.16); border-color: rgba(60, 200, 130, 0.5); }\n  .state-pill.denied  { background: rgba(230, 90, 90, 0.16);  border-color: rgba(230, 90, 90, 0.5); }\n  .state-pill.prompt  { background: rgba(230, 190, 60, 0.16); border-color: rgba(230, 190, 60, 0.5); }\n  .state-pill.error   { background: rgba(150, 150, 160, 0.16); border-color: var(--line); }\n  .counter-row {\n    display: flex;\n    gap: 18px;\n    flex-wrap: wrap;\n    margin-top: 10px;\n  }\n  .counter {\n    font-size: 0.85rem;\n  }\n  .counter b { font-size: 1.1rem; }","\"use strict\";\nconst $ = (id) => document.getElementById(id);\n\nconst NAMES = [\"geolocation\", \"notifications\", \"camera\", \"microphone\", \"persistent-storage\", \"clipboard-read\"];\nlet selected = \"geolocation\";\nlet checkCount = 0;\nlet promptCount = 0;\nlet watchedStatus = null;\n\nfunction buildGrid() {\n  const grid = $(\"nameGrid\");\n  NAMES.forEach((name) => {\n    const btn = document.createElement(\"button\");\n    btn.className = \"btn sm name-btn\";\n    btn.textContent = name;\n    btn.dataset.name = name;\n    btn.setAttribute(\"aria-pressed\", name === selected ? \"true\" : \"false\");\n    btn.addEventListener(\"click\", () => {\n      selected = name;\n      $(\"selectedName\").textContent = name;\n      document.querySelectorAll(\".name-btn\").forEach((b) =>\n        b.setAttribute(\"aria-pressed\", b.dataset.name === selected ? \"true\" : \"false\")\n      );\n      resetWatch();\n    });\n    grid.appendChild(btn);\n  });\n}\n\nfunction log(text, kind) {\n  const line = document.createElement(\"div\");\n  const time = new Date().toLocaleTimeString();\n  line.textContent = \"[\" + time + \"] \" + text;\n  if (kind) line.style.color = \"var(--\" + kind + \", inherit)\";\n  $(\"log\").prepend(line);\n}\n\nfunction setPill(state) {\n  const pill = $(\"statePill\");\n  pill.className = \"state-pill \" + state;\n  pill.textContent = state;\n}\n\nfunction resetWatch() {\n  watchedStatus = null;\n  $(\"watchToggle\").checked = false;\n  setPill(\"error\");\n  $(\"statePill\").textContent = \"not checked\";\n  $(\"stateHint\").textContent = 'Click \"Check state\" — this never triggers a system dialog, whatever the answer is.';\n}\n\nasync function checkState() {\n  checkCount++;\n  $(\"checkCount\").textContent = checkCount;\n  if (!navigator.permissions || !navigator.permissions.query) {\n    setPill(\"error\");\n    log(\"This browser has no navigator.permissions API at all.\", \"warn\");\n    return;\n  }\n  try {\n    const status = await navigator.permissions.query({ name: selected });\n    setPill(status.state);\n    $(\"stateHint\").textContent =\n      \"Read successfully — no dialog appeared. This is exactly the peek getCurrentPosition() can't give you.\";\n    log('query({name:\"' + selected + '\"}) → \"' + status.state + '\"', \"good\");\n\n    if ($(\"watchToggle\").checked) {\n      watchedStatus = status;\n      status.onchange = () => {\n        setPill(status.state);\n        log('changed live → \"' + status.state + '\" (you flipped it in browser settings)', \"good\");\n      };\n      log(\"Now watching this permission for live changes.\", \"muted\");\n    }\n  } catch (err) {\n    setPill(\"error\");\n    $(\"stateHint\").textContent =\n      '\"' + selected + '\" isn\\'t a name this browser recognizes for query() — it rejected instead of resolving.';\n    log('query({name:\"' + selected + '\"}) rejected: ' + err.message, \"bad\");\n  }\n}\n\nfunction callRealFeature() {\n  if (selected !== \"geolocation\") {\n    $(\"realCallResult\").textContent =\n      'This button always calls the Geolocation API directly — switch the picker above to \"geolocation\" to compare it against your last check.';\n    return;\n  }\n  const before = document.visibilityState;\n  const startedAt = performance.now();\n  let dialogLikely = false;\n  const timer = setTimeout(() => { dialogLikely = true; }, 60);\n\n  navigator.geolocation.getCurrentPosition(\n    () => {\n      clearTimeout(timer);\n      report(startedAt, dialogLikely, \"granted a position\");\n    },\n    (err) => {\n      clearTimeout(timer);\n      const reason = err.code === err.PERMISSION_DENIED ? \"denied (no dialog — already decided)\" : \"failed (\" + err.message + \")\";\n      report(startedAt, dialogLikely, reason);\n    },\n    { timeout: 8000 }\n  );\n\n  function report(startedAt, dialogLikely, outcome) {\n    const took = Math.round(performance.now() - startedAt);\n    if (dialogLikely) {\n      promptCount++;\n      $(\"promptCount\").textContent = promptCount;\n    }\n    $(\"realCallResult\").textContent =\n      \"Direct call resolved after \" + took + \"ms → \" + outcome +\n      (dialogLikely ? \". It was still deciding, so this call is what put the system dialog on screen.\" : \". No dialog this time — the browser already had an answer on file.\");\n    log(\"getCurrentPosition() → \" + outcome + (dialogLikely ? \" (this call triggered the dialog)\" : \"\"), dialogLikely ? \"bad\" : \"muted\");\n  }\n}\n\nbuildGrid();\n$(\"checkBtn\").addEventListener(\"click\", checkState);\n$(\"realCallBtn\").addEventListener(\"click\", callRealFeature);\n$(\"clearLog\").addEventListener(\"click\", () => { $(\"log\").innerHTML = \"\"; });\n$(\"watchToggle\").addEventListener(\"change\", (e) => {\n  if (!e.target.checked) watchedStatus = null;\n});",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"getCurrentPosition() Doesn't Just Check — It Prompts","You want to peek at whether location access is already granted before showing your own 'why we need this' banner. Calling getCurrentPosition() to find out fires the real system prompt. navigator.permissions.query() reads the state without ever asking.","\u002Fmedia\u002Fcovers\u002Fpermissions-api-query-before-prompt.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,98,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",111,{"slug":64,"name":65,"articles":66},"javascript","Javascript",93,{"slug":68,"name":69,"articles":70},"frontend","Frontend",73,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",39,{"slug":76,"name":77,"articles":78},"css","Css",36,{"slug":80,"name":81,"articles":82},"typescript","Typescript",16,{"slug":84,"name":85,"articles":86},"performance","Performance",14,{"slug":88,"name":89,"articles":90},"react","React",13,{"slug":92,"name":93,"articles":94},"browser","Browser",10,{"slug":96,"name":97,"articles":94},"node","Node",{"slug":99,"name":100,"articles":101},"html","Html",8,{"slug":103,"name":104,"articles":105},"accessibility","Accessibility",7]