Tera Documentation
{
"config": {
"base_url": "/",
"mode": "build",
"title": "Phosphor",
"description": "Things we learned while building Phosphor",
"languages": {},
"default_language": "en",
"generate_feed": true,
"generate_feeds": true,
"feed_filenames": [
"atom.xml"
],
"taxonomies": [],
"author": null,
"build_search_index": true,
"extra": {
"logo_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"108\" height=\"26\" fill=\"none\" viewBox=\"0 0 174 42\">\n<g fill=\"currentColor\">\n<path d=\"M.4 32.6c1.9 0 3-.9 3.8-3.9l4.6-22c.7-3-.7-4-2.5-4v-.5H19c9.2 0 11 5.5 10 10.1-1 4.8-4.8 9.5-16.5 9l.1-.5c7.3.4 10.5-3.9 11.3-8.4.9-4.3-.3-9.4-6.2-9.4h-3L9.2 28.7c-.7 3 1.2 3.9 3.7 3.9v.6H.5v-.6Z\"/><path d=\"M34.5 30.5c0-1.3.9-2.7 2.5-2.7 1.5 0 2.5 1.2 2.3 3-.2 1.2.3 1.6 1 1.5 1.7-.2 4.6-4.2 4.7-10.7 0-6.4-2-8.6-3.6-8.6-2.9 0-7.8 8-9.8 17.9l-.5 2.3h-4.7l5.7-27c.6-2.6 0-3.4-2-3.4v-.6c2.6-.2 5-.5 7.7-2.2l.3.3-.8 4-4 18.1C36 16 40 10 43.9 10c3.4 0 5.4 3.5 5.1 8.4-.3 6.2-4.8 15.2-10.8 15.2-2.4 0-3.6-1.6-3.6-3Zm15.9-9.2c1.6-7 7.4-11 12.6-11 5.2 0 9 5 7.3 12-1.6 7.2-7.4 11.3-12.5 11.3-5.5 0-9-5.2-7.4-12.3Zm7.9 11.5c2.7 0 5.6-4.1 7-11.1 1.4-7 .3-10.8-2.7-10.7-3 0-6 4.3-7.3 11.1-1.4 7 0 10.8 3 10.8Zm11.4-3.3c-.4-1.6.2-3 1.4-3.4a2.2 2.2 0 0 1 3 1.3c.5 1.4-.7 2.4-.6 3.8.2 1 1.2 1.9 3 1.8 1.8 0 3.5-1 4-3 .4-2-1-3.8-3.7-6.5-3-3-4.4-5.2-3.9-8.1.7-3 3.5-5.1 7.3-5.2 3-.1 5.9 1.2 6.5 3.6.5 1.5 0 3-1.4 3.4a2.1 2.1 0 0 1-2.8-1.3c-.5-1.6.8-2.5.6-3.5-.2-.9-1.1-1.7-2.6-1.6a3.8 3.8 0 0 0-3.7 3.2c-.2 1.7.5 3 3.1 5.5 2.7 2.6 5.2 5 4.5 8.5-.7 3.8-4 5.6-7.7 5.6-2.8 0-6.2-1.3-7-4.2Zm17.4 12c1.7 0 3-1 3-4v-22c0-2-.8-2.8-2.9-2.8V12c2.4-.2 5-.7 7.3-1.7l.4.2v4.6a7.7 7.7 0 0 1 7.1-4.8c5.3 0 9 5 9 11.4s-4.5 11.8-10.3 11.8A7.4 7.4 0 0 1 95 31v6.5c0 3 1.6 4 3.7 4v.5H87.1v-.6Zm13.3-8.7c3.3 0 5.7-3.7 5.7-9.4 0-6.9-3-10.5-6.5-10.5-2.4 0-4 1.6-4.7 3.7v7.6c0 4.8 2 8.6 5.5 8.6Z\"/><path d=\"M125 32.6c2.2 0 3.8-.9 3.4-3.9l-1.4-10c-.5-3.6-1.9-5.7-4.6-5.7-2.3 0-4 2.2-4.3 4.8v10.9c0 3 1.3 3.9 3 3.9v.6h-10.9v-.6c1.8 0 3.1-.8 3.1-3.9v-23c0-2.2-.9-3-2.9-3v-.5c2.3-.2 5-.6 7.3-1.7l.4.2V16c.8-3 3.3-5.6 6.7-5.6 3.7 0 6.2 2.5 7 8.2l1.5 10.2c.4 3 1.7 3.9 3.5 3.9v.5h-11.7v-.5ZM134.4 21.8a11 11 0 0 1 11.1-11.5c6 0 11.2 4.9 11.2 11.6 0 6.8-5 11.7-11 11.7-6.2 0-11.3-5.1-11.3-11.8Zm12.6 11c3.7-.6 5.3-5.7 4.5-12-.8-6-3.8-10.1-7.4-9.6-3.7.5-5.3 5.6-4.5 11.6.8 6.2 3.7 10.4 7.4 10Z\"/><path d=\"M155.9 32.6c1.8 0 3-.9 3-4v-13c0-2.2-.8-2.9-2.8-3v-.5a21.2 21.2 0 0 0 7.3-1.7l.4.2v4.8c.7-2.5 2.6-5 5.4-5 2.1 0 3.5 1.5 3.5 3.1 0 1.4-1 2.7-2.6 2.7s-2-1.4-2.4-2.3c-.3-.7-.5-1-1-1-1.4 0-2.7 2-3 4.2v11.6c0 3 1.8 3.9 4 3.9v.6H156v-.6Z\"/>\n</g>\n</svg>\n",
"drafts": true,
"sections": [
"Getting Started",
"Managing Complexity",
"Moving Quickly",
"Understanding Quickly",
"Architecture Patterns",
"Interactive Demos",
"Developer Tools",
"Advanced Topics"
],
"author": "Phosphor"
},
"markdown": {
"highlight_code": true,
"error_on_missing_highlight": false,
"highlight_theme": "base16-ocean-dark",
"highlight_themes_css": [],
"render_emoji": false,
"external_links_class": null,
"external_links_target_blank": false,
"external_links_no_follow": false,
"external_links_no_referrer": false,
"smart_punctuation": false,
"definition_list": false,
"bottom_footnotes": false,
"extra_syntaxes_and_themes": [],
"lazy_async_image": false,
"insert_anchor_links": "none",
"github_alerts": false
},
"search": {
"index_format": "elasticlunr_javascript"
},
"generate_sitemap": true,
"generate_robots_txt": true,
"exclude_paginated_pages_in_sitemap": "none"
},
"current_path": "/incantations/",
"current_url": "/incantations/",
"lang": "en",
"page": {
"relative_path": "incantations.md",
"colocated_path": null,
"content": "<h1 id=\"writing-process-phosphor-engineering\">Writing Process - Phosphor Engineering <div data-loc=\"content/incantations.md:10\" class=\"heading-src\">⋅</div></h1>\n<h2 id=\"macro-expansion-pass-1\">Macro Expansion (Pass 1) <div data-loc=\"content/incantations.md:12\" class=\"heading-src\">⋅</div></h2>\n<p><strong>Context</strong>: You are expanding section bullets from bones.md into full prose for draft.md.</p>\n<p><strong>Inputs</strong>: anchors.md (locked context) + bones.md section + current section ID</p>\n<p><strong>Output</strong>: Expanded prose for the specified [S#] section with natural transitions</p>\n<p><strong>Requirements</strong>:</p>\n<ul>\n<li>Follow anchors.md constraints exactly</li>\n<li>Maintain technical depth specified in anchors</li>\n<li>Add smooth transitions between bullet points</li>\n<li>Include concrete examples where bones mention them</li>\n<li>Keep section IDs [S#] in draft.md headers</li>\n<li>No inline tags yet (that's Pass 2)</li>\n</ul>\n<p><strong>Style</strong>: Technical advisor voice per be-a-technical-advisor.md</p>\n<hr />\n<h2 id=\"micro-edit-pass-3\">Micro Edit (Pass 3) <div data-loc=\"content/incantations.md:33\" class=\"heading-src\">⋅</div></h2>\n<p><strong>Context</strong>: You are resolving inline annotation tags from draft.md via unified diff</p>\n<p><strong>Inputs</strong>: anchors.md + draft.md section with inline tags + specific tags to resolve</p>\n<p><strong>Output</strong>: Unified diff showing exact changes to resolve tags</p>\n<p><strong>Requirements</strong>:</p>\n<ul>\n<li>Address each tagged issue precisely</li>\n<li>Follow tag modifiers ([!] = must-fix, [~] = nice-to-have, [->suggestion])</li>\n<li>Maintain section structure and flow</li>\n<li>Don't rewrite untagged content</li>\n<li>Verify claims with concrete evidence</li>\n<li>Add examples where [ex] tags appear</li>\n</ul>\n<hr />\n<h2 id=\"quality-check-pass-4\">Quality Check (Pass 4) <div data-loc=\"content/incantations.md:52\" class=\"heading-src\">⋅</div></h2>\n<p><strong>Context</strong>: Final audit before publish</p>\n<p><strong>Checklist</strong>:</p>\n<ul>\n<li><input disabled=\"\" type=\"checkbox\"/>\nAll [!] tags resolved</li>\n<li><input disabled=\"\" type=\"checkbox\"/>\nEvidence ratio met (per anchors.md)</li>\n<li><input disabled=\"\" type=\"checkbox\"/>\nInteractive demos working</li>\n<li><input disabled=\"\" type=\"checkbox\"/>\nForbidden phrases removed</li>\n<li><input disabled=\"\" type=\"checkbox\"/>\nSuccess criteria achieved</li>\n<li><input disabled=\"\" type=\"checkbox\"/>\nHook visceral, conclusion actionable</li>\n</ul>\n",
"permalink": "/incantations/",
"slug": "incantations",
"ancestors": [
"_index.md"
],
"title": "Incantations",
"description": null,
"updated": null,
"date": "2025-10-20",
"year": 2025,
"month": 10,
"day": 20,
"taxonomies": {},
"authors": [],
"extra": {
"nav_section": "Advanced Topics",
"nav_order": 1
},
"path": "/incantations/",
"components": [
"incantations"
],
"summary": null,
"toc": [
{
"level": 1,
"id": "writing-process-phosphor-engineering",
"permalink": "/incantations/#writing-process-phosphor-engineering",
"title": "Writing Process - Phosphor Engineering ⋅",
"children": [
{
"level": 2,
"id": "macro-expansion-pass-1",
"permalink": "/incantations/#macro-expansion-pass-1",
"title": "Macro Expansion (Pass 1) ⋅",
"children": []
},
{
"level": 2,
"id": "micro-edit-pass-3",
"permalink": "/incantations/#micro-edit-pass-3",
"title": "Micro Edit (Pass 3) ⋅",
"children": []
},
{
"level": 2,
"id": "quality-check-pass-4",
"permalink": "/incantations/#quality-check-pass-4",
"title": "Quality Check (Pass 4) ⋅",
"children": []
}
]
}
],
"word_count": 230,
"reading_time": 2,
"assets": [],
"draft": true,
"lang": "en",
"lower": {
"relative_path": "click-to-source.md",
"colocated_path": null,
"content": "<h1 id=\"click-to-source-embedding-runtime-locations\">Click to Source: Embedding Runtime Locations <div data-loc=\"content/click-to-source.md:10\" class=\"heading-src\">⋅</div></h1>\n\n<p>Build-time transforms inject source locations into runtime code. Alt+Click any element to jump to its source file.</p>\n<h2 id=\"try-it\">Try It <div data-loc=\"content/click-to-source.md:19\" class=\"heading-src\">⋅</div></h2>\n<div data-loc=\"content/click-to-source.md:21\" id=\"click-to-source-demo\">\nHold Alt and hover over any button, then click to view its source. The mock console shows what file would open, and the viewer displays the source code.\n</div>\n<script src=\"/js/click-to-source-demo.entrypoint.js\" type=\"module\"></script>\n<h2 id=\"react-component-locations\">React Component Locations <div data-loc=\"content/click-to-source.md:26\" class=\"heading-src\">⋅</div></h2>\n<p>Transform <code>$</code> prop shorthand into <code>data-loc</code> attributes:</p>\n<div class=\"codeblock\" data-loc=\"content/click-to-source.md:30\">\n \n <div class=\"codeblock-content\" data-id=\"react-loc-transform\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"tools/bun-dev-and-react-$-className-loc.mts:35\"><code><span class=\"line\"><span style=\"color:#31748F\">if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">enableReact$Loc</span><span style=\"color:#31748F\"> &&</span><span style=\"color:#E0DEF4;font-style:italic\"> path</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">endsWith</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\".tsx\"</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> code</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">matchAll</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">CLASSNAME_$_RE</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\">!</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">_</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> pre</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> post</span><span style=\"color:#908CAA\">]</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4;font-style:italic\"> line</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> column</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> indexToLineAndColumn</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">code</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> pre</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> link</span><span style=\"color:#31748F\"> =</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">path</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">slice</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">inCodebase</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> IN_CODEBASE_SLICE</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">line</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">column</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> let</span><span style=\"color:#E0DEF4;font-style:italic\"> dataAttrs</span><span style=\"color:#31748F\"> =</span><span style=\"color:#F6C177\"> `data-loc=</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">JSON</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">stringify</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">link</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">pre</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">startsWith</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"<\"</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> dataAttrs</span><span style=\"color:#31748F\"> =</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">dataAttrs</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\"> data-name=</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">JSON</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">stringify</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">pre</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">split</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\" \"</span><span style=\"color:#E0DEF4\">)[</span><span style=\"color:#EBBCBA\">0</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">slice</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">1</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">trim</span><span style=\"color:#E0DEF4\">())</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">post</span><span style=\"color:#31748F\"> ===</span><span style=\"color:#F6C177\"> \"=\"</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> string</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">overwrite</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> pre</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> _</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#31748F\"> -</span><span style=\"color:#E0DEF4;font-style:italic\"> post</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#908CAA\">,</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">dataAttrs</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\"> className`</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> else</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> string</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">overwrite</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> pre</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> _</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#31748F\"> -</span><span style=\"color:#E0DEF4;font-style:italic\"> post</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> dataAttrs</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n </div>\n \n</div>\n<pre data-lang=\"tsx\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-tsx \"><code class=\"language-tsx\" data-lang=\"tsx\"><span> </span><span style=\"color:#65737e;\">// Input:\n</span><span> <</span><span style=\"color:#bf616a;\">div </span><span style=\"color:#d08770;\">$</span><span>="</span><span style=\"color:#a3be8c;\">card</span><span>">Content</</span><span style=\"color:#bf616a;\">div</span><span>>\n</span><span>\n</span><span> </span><span style=\"color:#65737e;\">// Output (at build):\n</span><span> <</span><span style=\"color:#bf616a;\">div </span><span style=\"color:#d08770;\">data-loc</span><span>="</span><span style=\"color:#a3be8c;\">components/Card.tsx:26:7</span><span>" </span><span style=\"color:#d08770;\">className</span><span>="</span><span style=\"color:#a3be8c;\">card</span><span>">Content</</span><span style=\"color:#bf616a;\">div</span><span>>\n</span></code></pre>\n<p>Runtime handler walks React fiber tree and opens the file:</p>\n<div class=\"codeblock\" data-loc=\"content/click-to-source.md:42\">\n \n <div class=\"codeblock-content\" data-id=\"click-to-source-handler\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/lib/dev/click-to-source.client.ts:156\"><code><span class=\"line\"><span style=\"color:#31748F\">const</span><span style=\"color:#EBBCBA;font-style:italic\"> getPath</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">fiber</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Fiber</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> element</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> HTMLElement</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> undefined</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> undefined</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> First check for data-loc attribute if element is provided</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">element</span><span style=\"color:#31748F\">?.</span><span style=\"color:#E0DEF4;font-style:italic\">dataset</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">loc</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> element</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">dataset</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">loc</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> source</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> fiber</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">_debugSource</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#E0DEF4;font-style:italic\"> fiber</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">_debugInfo</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#E0DEF4;font-style:italic\"> fiber</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">_source</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">source</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#EBBCBA\"> undefined</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4;font-style:italic\"> fileName</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> lineNumber</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> 1</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> columnNumber</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> 1</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> source</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">fileName</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">lineNumber</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">columnNumber</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">};</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">const</span><span style=\"color:#EBBCBA;font-style:italic\"> getLayersForElement</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">element</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> HTMLElement</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> root</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ComponentLayer</span><span style=\"color:#E0DEF4\">[] </span><span style=\"color:#31748F\">=></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> let</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> getReactInstanceForElement</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">element</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> layers</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ComponentLayer</span><span style=\"color:#E0DEF4\">[] </span><span style=\"color:#31748F\">=</span><span style=\"color:#E0DEF4\"> []</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> while</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">instance</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> Try to find the DOM element for this fiber to check for data-loc</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> fiberElement</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">stateNode</span><span style=\"color:#31748F\"> instanceof</span><span style=\"color:#9CCFD8\"> HTMLElement</span><span style=\"color:#31748F\"> ?</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">stateNode</span><span style=\"color:#31748F\"> :</span><span style=\"color:#EBBCBA\"> undefined</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> path</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> getPath</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">instance</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> fiberElement</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">path</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> name</span><span style=\"color:#31748F\"> =</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> typeof</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">type</span><span style=\"color:#31748F\"> ===</span><span style=\"color:#F6C177\"> \"string\"</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> ?</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">type</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> :</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">type</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">displayName</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">type</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">name</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">type</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">render</span><span style=\"color:#31748F\">?.</span><span style=\"color:#E0DEF4;font-style:italic\">name</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#F6C177\"> \"undefined\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> layers</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">push</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> name</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> path</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> path</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">replace</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">root</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">/`</span><span style=\"color:#908CAA\">,</span><span style=\"color:#F6C177\"> \"\"</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">}</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> instance</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">_debugOwner</span><span style=\"color:#31748F\"> ??</span><span style=\"color:#EBBCBA\"> undefined</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> layers</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">};</span></span></code></pre>\n </div>\n \n</div>\n<h2 id=\"devstring-locations\">DevString Locations <div data-loc=\"content/click-to-source.md:44\" class=\"heading-src\">⋅</div></h2>\n<p>Tagged templates automatically capture call-site locations:</p>\n<div class=\"codeblock\" data-loc=\"content/click-to-source.md:48\">\n \n <div class=\"codeblock-content\" data-id=\"devstring-transform\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"tools/bun-dev-and-react-$-className-loc.mts:23\"><code><span class=\"line\"><span style=\"color:#31748F\">if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">enableDevLoc</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> code</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">matchAll</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">DEV_RE</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\">!</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4;font-style:italic\"> line</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> column</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> indexToLineAndColumn</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">code</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">_</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> fn</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> message</span><span style=\"color:#908CAA\">]</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> match</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> link</span><span style=\"color:#31748F\"> =</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">path</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">slice</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">inCodebase</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> IN_CODEBASE_SLICE</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">line</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">:</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">column</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> string</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">overwrite</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">index</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> index</span><span style=\"color:#31748F\"> +</span><span style=\"color:#E0DEF4;font-style:italic\"> _</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">length</span><span style=\"color:#908CAA\">,</span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">fn</span><span style=\"color:#908CAA\">}</span><span style=\"color:#31748F\">\\`</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">message</span><span style=\"color:#908CAA\">}</span><span style=\"color:#31748F\">\\`</span><span style=\"color:#F6C177\">.ctx({ loc: </span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">JSON</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">stringify</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">link</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\"> })`</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n </div>\n \n</div>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#65737e;\">// Input:\n</span><span style=\"color:#8fa1b3;\">handler</span><span>(</span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">User pressed X</span><span>`);\n</span><span>\n</span><span style=\"color:#65737e;\">// Output (at build):\n</span><span style=\"color:#8fa1b3;\">handler</span><span>(</span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">User pressed X</span><span>`.</span><span style=\"color:#8fa1b3;\">ctx</span><span>({ loc: "</span><span style=\"color:#a3be8c;\">Card.tsx:83:12</span><span>" }));\n</span></code></pre>\n<p><strong>Usage:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">function </span><span style=\"color:#8fa1b3;\">deleteCard</span><span>(</span><span style=\"color:#bf616a;\">reason</span><span>: DevString) {\n</span><span> </span><span style=\"color:#ebcb8b;\">console</span><span>.</span><span style=\"color:#96b5b4;\">log</span><span>("</span><span style=\"color:#a3be8c;\">Called from:</span><span>", </span><span style=\"color:#bf616a;\">reason</span><span>.</span><span style=\"color:#8fa1b3;\">toJSON</span><span>().</span><span style=\"color:#bf616a;\">context</span><span>.</span><span style=\"color:#bf616a;\">loc</span><span>);\n</span><span>}\n</span><span>\n</span><span style=\"color:#8fa1b3;\">deleteCard</span><span>(</span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">User clicked delete</span><span>`);\n</span><span style=\"color:#65737e;\">// Automatically knows source location\n</span></code></pre>\n<p>DevStrings compose with <code>.because()</code> to build audit trails:</p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">rootEvent </span><span>= </span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">keydown from root</span><span>`;\n</span><span style=\"color:#8fa1b3;\">handler</span><span>(</span><span style=\"color:#bf616a;\">rootEvent</span><span>.</span><span style=\"color:#8fa1b3;\">because</span><span>(</span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">Looking for action handler</span><span>`));\n</span><span style=\"color:#65737e;\">// Creates chain: "keydown from root → Looking for action handler"\n</span></code></pre>\n<h2 id=\"editor-integration\">Editor Integration <div data-loc=\"content/click-to-source.md:77\" class=\"heading-src\">⋅</div></h2>\n<p>Local HTTP endpoint opens files:</p>\n<div class=\"codeblock\" data-loc=\"content/click-to-source.md:81\">\n \n <div class=\"codeblock-content\" data-id=\"open-in-editor\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/lib/dev/openInDevEditor.ts:1\"><code><span class=\"line\"><span style=\"color:#31748F\">let</span><span style=\"color:#E0DEF4;font-style:italic\"> lastOpenedFile</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> null</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> null</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\">/**</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86;font-style:italic\"> * uses our launch-editor endpoint to open the file in the dev's editor</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86;font-style:italic\"> * this has been set up as a vite plugin.</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> */</span></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> const</span><span style=\"color:#EBBCBA;font-style:italic\"> openInDevEditor</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">loc</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">lastOpenedFile</span><span style=\"color:#31748F\"> ===</span><span style=\"color:#E0DEF4;font-style:italic\"> loc</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> lastOpenedFile</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> loc</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> setTimeout</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">lastOpenedFile</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> null</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span><span style=\"color:#EBBCBA\"> 500</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> void</span><span style=\"color:#EBBCBA\"> fetch</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">`http://localhost:5090/__open-in-editor?file=</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">loc</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">catch</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">error</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> console</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">error</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"Failed to open in editor\"</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> error</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">};</span></span></code></pre>\n </div>\n \n</div>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#8fa1b3;\">openInDevEditor</span><span>("</span><span style=\"color:#a3be8c;\">Card.tsx:26:7</span><span>");\n</span><span> ↓\n</span><span style=\"color:#8fa1b3;\">fetch</span><span>("</span><span style=\"color:#a3be8c;\">http://localhost:5090/__open-in-editor?file=Card.tsx:26:7</span><span>");\n</span><span> ↓\n</span><span style=\"color:#65737e;\">// Editor opens at that line\n</span></code></pre>\n<h2 id=\"setup\">Setup <div data-loc=\"content/click-to-source.md:91\" class=\"heading-src\">⋅</div></h2>\n<p><strong>Build plugin:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">import </span><span>{ </span><span style=\"color:#bf616a;\">bunDevStringAndReact$ClassNameLoc </span><span>} </span><span style=\"color:#b48ead;\">from </span><span>"</span><span style=\"color:#a3be8c;\">./bun-dev-and-react-$-className-loc.mts</span><span>";\n</span><span>\n</span><span style=\"color:#b48ead;\">await </span><span style=\"color:#bf616a;\">Bun</span><span>.</span><span style=\"color:#8fa1b3;\">build</span><span>({\n</span><span> plugins: [\n</span><span> </span><span style=\"color:#8fa1b3;\">bunDevStringAndReact$ClassNameLoc</span><span>({\n</span><span> enableReact$Loc: </span><span style=\"color:#d08770;\">true</span><span>,\n</span><span> enableDevLoc: </span><span style=\"color:#d08770;\">true</span><span>,\n</span><span> }),\n</span><span> ],\n</span><span>});\n</span></code></pre>\n<p><strong>Runtime:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">import </span><span>{ </span><span style=\"color:#bf616a;\">initClickToSource </span><span>} </span><span style=\"color:#b48ead;\">from </span><span>"</span><span style=\"color:#a3be8c;\">./click-to-source.client.ts</span><span>";\n</span><span>\n</span><span style=\"color:#b48ead;\">if </span><span>(</span><span style=\"color:#b48ead;\">import</span><span>.meta.</span><span style=\"color:#bf616a;\">env</span><span>.</span><span style=\"color:#bf616a;\">DEV</span><span>) {\n</span><span> </span><span style=\"color:#8fa1b3;\">initClickToSource</span><span>();\n</span><span>}\n</span></code></pre>\n",
"permalink": "/click-to-source/",
"slug": "click-to-source",
"ancestors": [
"_index.md"
],
"title": "Click to Source: Embedding Runtime Locations",
"description": null,
"updated": null,
"date": "2025-10-20",
"year": 2025,
"month": 10,
"day": 20,
"taxonomies": {},
"authors": [],
"extra": {
"nav_section": "Moving Quickly",
"nav_order": 1
},
"path": "/click-to-source/",
"components": [
"click-to-source"
],
"summary": null,
"toc": [
{
"level": 1,
"id": "click-to-source-embedding-runtime-locations",
"permalink": "/click-to-source/#click-to-source-embedding-runtime-locations",
"title": "Click to Source: Embedding Runtime Locations ⋅",
"children": [
{
"level": 2,
"id": "try-it",
"permalink": "/click-to-source/#try-it",
"title": "Try It ⋅",
"children": []
},
{
"level": 2,
"id": "react-component-locations",
"permalink": "/click-to-source/#react-component-locations",
"title": "React Component Locations ⋅",
"children": []
},
{
"level": 2,
"id": "devstring-locations",
"permalink": "/click-to-source/#devstring-locations",
"title": "DevString Locations ⋅",
"children": []
},
{
"level": 2,
"id": "editor-integration",
"permalink": "/click-to-source/#editor-integration",
"title": "Editor Integration ⋅",
"children": []
},
{
"level": 2,
"id": "setup",
"permalink": "/click-to-source/#setup",
"title": "Setup ⋅",
"children": []
}
]
}
],
"word_count": 255,
"reading_time": 2,
"assets": [],
"draft": false,
"lang": "en",
"lower": null,
"higher": null,
"translations": [],
"backlinks": []
},
"higher": {
"relative_path": "keyboard-navigation-demo.md",
"colocated_path": null,
"content": "<h1 id=\"let-s-build-composable-keyboard-navigation-together\">Let's Build Composable Keyboard Navigation Together <div data-loc=\"content/keyboard-navigation-demo.md:10\" class=\"heading-src\">⋅</div></h1>\n<blockquote>\n<p><strong>Prerequisites</strong>: We'll assume you're comfortable with React and TypeScript. We'll introduce Entity-Component-System (ECS) concepts as we go - no prior game dev experience needed!</p>\n<p><strong>Time to read</strong>: ~15 minutes<br />\n<strong>What we'll learn</strong>: How to build keyboard navigation using composable plugins that don't know about each other</p>\n</blockquote>\n<h2 id=\"the-problem-we-re-solving\">The Problem We're Solving <div data-loc=\"content/keyboard-navigation-demo.md:17\" class=\"heading-src\">⋅</div></h2>\n<p>We're building a complex UI, and we need keyboard shortcuts everywhere. Our text editor needs Cmd+B for bold, our cards need arrow keys for navigation, our buttons need Enter to activate.</p>\n<p>We could write one giant keyboard handler that knows about every component. But we've been down that road before - it becomes a tangled mess the moment we need context-sensitive shortcuts or want to test things in isolation. Every time we add a component, we're editing that massive switch statement. Every time a shortcut conflicts, we're debugging spaghetti code.</p>\n<p>Let's try something different. We'll build a system where components <strong>declare</strong> what they need, and a <strong>plugin</strong> wires everything together automatically. No tight coupling, no spaghetti code, and every piece testable in isolation.</p>\n<h2 id=\"what-we-re-building\">What We're Building <div data-loc=\"content/keyboard-navigation-demo.md:25\" class=\"heading-src\">⋅</div></h2>\n<p>We'll create three interactive cards, each with different keyboard shortcuts. When we focus a card, its shortcuts become active. Press ↑/↓ to navigate between cards, then try each card's unique actions.</p>\n<p>By the end, we'll understand how four small building blocks compose into a working keyboard system - without any of them knowing about the others.</p>\n<h2 id=\"our-approach-entities-components-and-plugins\">Our Approach: Entities, Components, and Plugins <div data-loc=\"content/keyboard-navigation-demo.md:31\" class=\"heading-src\">⋅</div></h2>\n<p>We're borrowing a pattern from game development called <strong>Entity-Component-System</strong> (ECS):</p>\n<ul>\n<li><strong>Entity</strong> = A unique identifier for a thing in the system, not a class or instance</li>\n<li><strong>Component</strong> = Data attached to an entity via a component type key</li>\n<li><strong>Plugin</strong> = Behavior that queries entities with specific component combinations and reacts to changes</li>\n</ul>\n<p><strong>The mapping to React:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#bf616a;\">React</span><span> → </span><span style=\"color:#bf616a;\">ECS\n</span><span>─────────────────────────────────────\n</span><span style=\"color:#bf616a;\">Component instance</span><span> → </span><span style=\"color:#8fa1b3;\">Entity </span><span>(</span><span style=\"color:#bf616a;\">just a UID</span><span>)\n</span><span style=\"color:#bf616a;\">Props</span><span>/</span><span style=\"color:#bf616a;\">state shape</span><span> → </span><span style=\"color:#8fa1b3;\">Component </span><span>(</span><span style=\"color:#bf616a;\">data attached to UID</span><span>)\n</span><span style=\"color:#bf616a;\">Context </span><span>+ </span><span style=\"color:#bf616a;\">useEffect</span><span> → </span><span style=\"color:#ebcb8b;\">Plugin </span><span>(</span><span style=\"color:#bf616a;\">reactive behavior</span><span>)\n</span><span style=\"color:#bf616a;\">useState</span><span> → </span><span style=\"color:#8fa1b3;\">Atom </span><span>(</span><span style=\"color:#bf616a;\">Jotai reactive state</span><span>)\n</span></code></pre>\n<p><strong>The key insight</strong>: Components are just data. Plugins add behavior by querying for that data. Nothing is tightly coupled.</p>\n<p>Let's see this in practice.</p>\n<h2 id=\"try-it-out-first\">Try It Out First <div data-loc=\"content/keyboard-navigation-demo.md:54\" class=\"heading-src\">⋅</div></h2>\n<p>Before diving into theory, let's play with what we're building:</p>\n<link rel=\"stylesheet\" href=\"/js/keyboard-demo.entrypoint.css\">\n<div data-loc=\"content/keyboard-navigation-demo.md:59\" id=\"keyboard-demo\"></div>\n<script src=\"/js/keyboard-demo.entrypoint.js\" type=\"module\"></script>\n<p>Watch how the demo responds. Notice that only the focused card's shortcuts work - the others are \"dormant\" until focused.</p>\n<p>Now let's understand how we built this.</p>\n<h2 id=\"our-four-building-blocks\">Our Four Building Blocks <div data-loc=\"content/keyboard-navigation-demo.md:66\" class=\"heading-src\">⋅</div></h2>\n<p>Let's break down our keyboard system into four composable pieces.</p>\n<h3 id=\"block-1-making-things-focusable\">Block 1: Making Things Focusable <div data-loc=\"content/keyboard-navigation-demo.md:70\" class=\"heading-src\">⋅</div></h3>\n<p>First, we need to mark which entities can receive focus. We'll create a <code>CFocusable</code> component:</p>\n<div class=\"codeblock\" data-loc=\"content/keyboard-navigation-demo.md:74\">\n \n <div class=\"codeblock-content\" data-id=\"cfocusable-component\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/keyboard-demo/plugins/CFocusable.ts:8\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> class</span><span style=\"color:#9CCFD8\"> CFocusable</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#E0DEF4;font-style:italic\"> World</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">Component</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"focusable\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\"><</span></span>\n<span class=\"line\"><span style=\"color:#9CCFD8\"> CFocusable</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> handler</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Handler</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">ExitDirection</span><span style=\"color:#908CAA\">>;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> hasDirectFocusAtom</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Atom</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">boolean</span><span style=\"color:#908CAA\">>;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">() </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n </div>\n \n</div>\n<p><strong>What this gives us:</strong></p>\n<ul>\n<li><code>hasDirectFocusAtom</code>: A reactive boolean that's <code>true</code> when <strong>this specific entity</strong> has focus</li>\n<li><code>handler</code>: Called when focus enters from a direction (we'll use this later for spatial nav)</li>\n</ul>\n<p><strong>Let's use it:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">buttonUID </span><span>= </span><span style=\"color:#8fa1b3;\">uid</span><span>(</span><span style=\"color:#d08770;\">null</span><span>, </span><span style=\"color:#d08770;\">null</span><span>, "</span><span style=\"color:#a3be8c;\">my-button</span><span>");\n</span><span>\n</span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">addEntity</span><span>(</span><span style=\"color:#bf616a;\">buttonUID</span><span>, </span><span style=\"color:#bf616a;\">ButtonEntity</span><span>, {\n</span><span> focusable: </span><span style=\"color:#bf616a;\">CFocusable</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({\n</span><span> hasDirectFocusAtom: </span><span style=\"color:#8fa1b3;\">atom</span><span>(</span><span style=\"color:#d08770;\">false</span><span>),\n</span><span> </span><span style=\"color:#8fa1b3;\">handler</span><span>: () </span><span style=\"color:#b48ead;\">=> </span><span style=\"color:#8fa1b3;\">handled</span><span>`</span><span style=\"color:#a3be8c;\">button focused</span><span>`,\n</span><span> }),\n</span><span>});\n</span></code></pre>\n<p>Notice: Our component doesn't know <strong>HOW</strong> focus works, just that it <strong>CAN</strong> be focused. It's pure data.</p>\n<h3 id=\"block-2-tracking-which-entity-has-focus\">Block 2: Tracking Which Entity Has Focus <div data-loc=\"content/keyboard-navigation-demo.md:96\" class=\"heading-src\">⋅</div></h3>\n<p>We have focusable entities, but we need to track <strong>which one</strong> currently has focus. That's a singleton concern - only one entity can have focus at a time.</p>\n<p>We'll create a \"Unique\" (a singleton component):</p>\n<div class=\"codeblock\" data-loc=\"content/keyboard-navigation-demo.md:102\">\n \n <div class=\"codeblock-content\" data-id=\"ucurrent-focus-unique\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/keyboard-demo/plugins/CFocusable.ts:48\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> class</span><span style=\"color:#9CCFD8\"> UCurrentFocus</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#E0DEF4;font-style:italic\"> World</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">Unique</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"currentFocus\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\"><</span></span>\n<span class=\"line\"><span style=\"color:#9CCFD8\"> UCurrentFocus</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> {</span><span style=\"color:#EBBCBA;font-style:italic\"> activeFocusAtom</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> PrimitiveAtom</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">UID</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> null</span><span style=\"color:#908CAA\">></span><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">() </span><span style=\"color:#908CAA\">{}</span></span></code></pre>\n </div>\n \n</div>\n<p><strong>What this gives us:</strong></p>\n<ul>\n<li><code>activeFocusAtom</code>: Holds the UID of whichever entity currently has focus (or <code>null</code>)</li>\n</ul>\n<p><strong>How it connects:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#65737e;\">// When we focus a card:\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">focusUnique </span><span>= </span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">getUniqueOrThrow</span><span>(</span><span style=\"color:#bf616a;\">UCurrentFocus</span><span>);\n</span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#bf616a;\">store</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">focusUnique</span><span>.</span><span style=\"color:#bf616a;\">activeFocusAtom</span><span>, </span><span style=\"color:#bf616a;\">cardUID</span><span>);\n</span><span>\n</span><span style=\"color:#65737e;\">// Anywhere else in our app:\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">focusedEntityUID </span><span>= </span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#bf616a;\">store</span><span>.</span><span style=\"color:#96b5b4;\">get</span><span>(</span><span style=\"color:#bf616a;\">focusUnique</span><span>.</span><span style=\"color:#bf616a;\">activeFocusAtom</span><span>);\n</span></code></pre>\n<p>Notice: <code>CFocusable</code> and <code>UCurrentFocus</code> don't import each other. They communicate through atoms. The <strong>CFocusable Plugin</strong> (which we'll see soon) is what wires them together.</p>\n<h3 id=\"block-3-declaring-actions\">Block 3: Declaring Actions <div data-loc=\"content/keyboard-navigation-demo.md:121\" class=\"heading-src\">⋅</div></h3>\n<p>Now we need entities to declare what keyboard actions they support:</p>\n<div class=\"codeblock\" data-loc=\"content/keyboard-navigation-demo.md:125\">\n \n <div class=\"codeblock-content\" data-id=\"cactions-component\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/keyboard-demo/plugins/CActions.ts:6\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> AnyAction</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> label</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> defaultKeybinding</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> DefaultKeyCombo</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> description</span><span style=\"color:#31748F\">?:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> icon</span><span style=\"color:#31748F\">?:</span><span style=\"color:#9CCFD8\"> any</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> self</span><span style=\"color:#31748F\">?:</span><span style=\"color:#9CCFD8\"> boolean</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> hideFromLastPressed</span><span style=\"color:#31748F\">?:</span><span style=\"color:#9CCFD8\"> boolean</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">};</span></span>\n<span class=\"line\"><span style=\"color:#31748F\">type</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#31748F\"> =</span><span style=\"color:#9CCFD8\"> Record</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">string</span><span style=\"color:#908CAA\">,</span><span style=\"color:#9CCFD8\"> AnyAction</span><span style=\"color:#908CAA\">>;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> ActionEvent</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#EBBCBA;font-style:italic\"> target</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> UID</span><span style=\"color:#908CAA\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> namespace</span><span style=\"color:#9CCFD8\"> CActions</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> export</span><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> Bindings</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#908CAA\">></span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#9CCFD8\">P</span><span style=\"color:#31748F\"> in</span><span style=\"color:#31748F\"> keyof</span><span style=\"color:#9CCFD8\"> T</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Handler</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">ActionEvent</span><span style=\"color:#908CAA\">></span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> Falsey</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> };</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#31748F\"> =</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#908CAA\">></span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> bindingSource</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> DevString</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> registryKey</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ActionRegistryKey</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#908CAA\">>;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> bindingsAtom</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Atom</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">CActions</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">Bindings</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#908CAA\">>>;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">};</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> class</span><span style=\"color:#9CCFD8\"> ActionRegistryKey</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#31748F\"> =</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#908CAA\">></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> constructor</span><span style=\"color:#908CAA\">(</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> public</span><span style=\"color:#31748F\"> readonly</span><span style=\"color:#C4A7E7;font-style:italic\"> key</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> public</span><span style=\"color:#31748F\"> readonly</span><span style=\"color:#C4A7E7;font-style:italic\"> meta</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#EBBCBA;font-style:italic\"> source</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> DevString</span><span style=\"color:#908CAA\">;</span><span style=\"color:#EBBCBA;font-style:italic\"> sectionName</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> public</span><span style=\"color:#31748F\"> readonly</span><span style=\"color:#C4A7E7;font-style:italic\"> bindables</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> T</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> )</span><span style=\"color:#908CAA\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> class</span><span style=\"color:#9CCFD8\"> CActions</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#E0DEF4;font-style:italic\"> World</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">Component</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"actions\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">CActions</span><span style=\"color:#908CAA\">,</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#E0DEF4\">[]</span><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">() </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> static</span><span style=\"color:#EBBCBA\"> bind</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#908CAA\">>(</span><span style=\"color:#C4A7E7;font-style:italic\">key</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#908CAA\">>)</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> CActions</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">of</span><span style=\"color:#E0DEF4\">([</span><span style=\"color:#E0DEF4;font-style:italic\">key</span><span style=\"color:#31748F\"> as</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#E0DEF4\">])</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> static</span><span style=\"color:#EBBCBA\"> merge</span><span style=\"color:#908CAA\">(</span><span style=\"color:#31748F\">...</span><span style=\"color:#C4A7E7;font-style:italic\">bindings</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Array</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">ActionBindings</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#E0DEF4\">[]</span><span style=\"color:#908CAA\">>)</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> out</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ActionBindings</span><span style=\"color:#E0DEF4\">[] </span><span style=\"color:#31748F\">=</span><span style=\"color:#E0DEF4\"> []</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> b</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> bindings</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">Array</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">isArray</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">b</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> out</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">push</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#31748F\">...</span><span style=\"color:#E0DEF4;font-style:italic\">b</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> else</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> out</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">push</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">b</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> CActions</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">of</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">out</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> static</span><span style=\"color:#EBBCBA\"> defineActions</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#31748F\"> extends</span><span style=\"color:#9CCFD8\"> AnyBindables</span><span style=\"color:#908CAA\">>(</span></span>\n<span class=\"line\"><span style=\"color:#C4A7E7;font-style:italic\"> key</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#C4A7E7;font-style:italic\"> meta</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#EBBCBA;font-style:italic\"> source</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> DevString</span><span style=\"color:#908CAA\">;</span><span style=\"color:#EBBCBA;font-style:italic\"> sectionName</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#C4A7E7;font-style:italic\"> actions</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> T</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> )</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ActionRegistryKey</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">T</span><span style=\"color:#908CAA\">></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#31748F\"> new</span><span style=\"color:#EBBCBA\"> ActionRegistryKey</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">key</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> meta</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> actions</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n </div>\n \n</div>\n<p><strong>What this gives us:</strong></p>\n<ul>\n<li>A way to <strong>define</strong> available actions (<code>defineActions</code>)</li>\n<li>A way to <strong>bind</strong> handlers to those actions per entity</li>\n<li>Actions are just metadata: label, key binding, description</li>\n</ul>\n<p><strong>Let's define some actions for our cards:</strong></p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">CardActions </span><span>= </span><span style=\"color:#bf616a;\">CActions</span><span>.</span><span style=\"color:#8fa1b3;\">defineActions</span><span>(\n</span><span> "</span><span style=\"color:#a3be8c;\">card-actions</span><span>",\n</span><span> { source: </span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">Card actions</span><span>`, sectionName: "</span><span style=\"color:#a3be8c;\">Card Actions</span><span>" },\n</span><span> {\n</span><span> delete: {\n</span><span> label: "</span><span style=\"color:#a3be8c;\">Delete Card</span><span>",\n</span><span> defaultKeybinding: "</span><span style=\"color:#a3be8c;\">X</span><span>" </span><span style=\"color:#b48ead;\">as const</span><span>,\n</span><span> description: "</span><span style=\"color:#a3be8c;\">Remove this card</span><span>",\n</span><span> },\n</span><span> edit: {\n</span><span> label: "</span><span style=\"color:#a3be8c;\">Edit Card</span><span>",\n</span><span> defaultKeybinding: "</span><span style=\"color:#a3be8c;\">E</span><span>" </span><span style=\"color:#b48ead;\">as const</span><span>,\n</span><span> description: "</span><span style=\"color:#a3be8c;\">Edit card content</span><span>",\n</span><span> },\n</span><span> },\n</span><span>);\n</span></code></pre>\n<p>Now <strong>attach handlers</strong> to a specific card entity:</p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">addEntity</span><span>(</span><span style=\"color:#bf616a;\">cardUID</span><span>, </span><span style=\"color:#bf616a;\">CardEntity</span><span>, {\n</span><span> actions: </span><span style=\"color:#bf616a;\">CActions</span><span>.</span><span style=\"color:#8fa1b3;\">bind</span><span>({\n</span><span> bindingSource: </span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">Card 1 actions</span><span>`,\n</span><span> registryKey: </span><span style=\"color:#bf616a;\">CardActions</span><span>,\n</span><span> bindingsAtom: </span><span style=\"color:#8fa1b3;\">atom</span><span>({\n</span><span> </span><span style=\"color:#8fa1b3;\">delete</span><span>: () </span><span style=\"color:#b48ead;\">=> </span><span>{\n</span><span> </span><span style=\"color:#8fa1b3;\">alert</span><span>("</span><span style=\"color:#a3be8c;\">Deleted!</span><span>");\n</span><span> </span><span style=\"color:#b48ead;\">return </span><span style=\"color:#8fa1b3;\">handled</span><span>`</span><span style=\"color:#a3be8c;\">delete</span><span>`;\n</span><span> },\n</span><span> </span><span style=\"color:#8fa1b3;\">edit</span><span>: () </span><span style=\"color:#b48ead;\">=> </span><span>{\n</span><span> </span><span style=\"color:#8fa1b3;\">alert</span><span>("</span><span style=\"color:#a3be8c;\">Editing!</span><span>");\n</span><span> </span><span style=\"color:#b48ead;\">return </span><span style=\"color:#8fa1b3;\">handled</span><span>`</span><span style=\"color:#a3be8c;\">edit</span><span>`;\n</span><span> },\n</span><span> }),\n</span><span> }),\n</span><span>});\n</span></code></pre>\n<p>Notice: We <strong>defined</strong> the action schema once, then <strong>bound</strong> different handlers per entity. One card might delete, another might archive. Same action definition, different behavior.</p>\n<h3 id=\"block-4-wiring-it-all-together-actionsplugin\">Block 4: Wiring It All Together - ActionsPlugin <div data-loc=\"content/keyboard-navigation-demo.md:177\" class=\"heading-src\">⋅</div></h3>\n<p>Here's where the magic happens. We need something that:</p>\n<ol>\n<li>Listens for keyboard events</li>\n<li>Finds the currently focused entity</li>\n<li>Matches keys to actions</li>\n<li>Executes the handler</li>\n</ol>\n<p>That's what our <code>ActionsPlugin</code> does:</p>\n<div class=\"codeblock\" data-loc=\"content/keyboard-navigation-demo.md:188\">\n \n <div class=\"codeblock-content\" data-id=\"actions-plugin-core\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/keyboard-demo/plugins/ActionsPlugin.ts:20\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> ActionsPlugin</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> World</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">definePlugin</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> name</span><span style=\"color:#908CAA\">:</span><span style=\"color:#EBBCBA\"> dev</span><span style=\"color:#F6C177\">`ActionsPlugin`</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> setup</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">build</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> build</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> Track the currently focused entity</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> currentFocusAtom</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> atom</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">get</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> pipeNonNull</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">build</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">getUniqueAtom</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">UCurrentFocus</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">a</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">a</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">activeFocusAtom</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> )</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> rootUIDAtom</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> atom</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">UID</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> null</span><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">null</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> currentDispatchSpotAtom</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> atom</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">get</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">currentFocusAtom</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">??</span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">rootUIDAtom</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> handleOnce</span><span style=\"color:#31748F\"> =</span><span style=\"color:#31748F\"> new</span><span style=\"color:#EBBCBA\"> WeakSet</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">KeyboardEvent</span><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">()</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> build</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">addUnique</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">UKeydownRootHandler</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> handler</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">reason</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> keyboardEvent</span><span style=\"color:#908CAA\">)</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">handleOnce</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">has</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">keyboardEvent</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#31748F\">return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> handleOnce</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">add</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">keyboardEvent</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">keyboardEvent</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">defaultPrevented</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> world</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">build</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">worldAtom</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">world</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> dispatchFromUID</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">currentDispatchSpotAtom</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">dispatchFromUID</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> Walk up the parent chain looking for keydown handlers</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> result</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> CParent</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">dispatch</span><span style=\"color:#E0DEF4\">(</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> dev</span><span style=\"color:#F6C177\">`keydown from root`</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">because</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">reason</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> world</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> dispatchFromUID</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> CKeydownHandler</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> reason</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> keyboardEvent</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> )</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> Prevent default browser behavior when we handle the key</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">result</span><span style=\"color:#31748F\"> !==</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> keyboardEvent</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">preventDefault</span><span style=\"color:#E0DEF4\">()</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> result</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> rootUIDAtom</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span></code></pre>\n </div>\n \n</div>\n<p><strong>Here's what happens when we press a key:</strong></p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>User presses "X"\n</span><span> ↓\n</span><span>ActionsPlugin.UKeydownRootHandler receives event\n</span><span> ↓\n</span><span>Query: Which entity has focus? (from UCurrentFocus)\n</span><span> ↓\n</span><span>Walk up parent chain: Does this entity have CKeydownHandler?\n</span><span> ↓\n</span><span>Match key "X" to action "delete"\n</span><span> ↓\n</span><span>Call the handler we bound earlier\n</span><span> ↓\n</span><span>preventDefault() so browser doesn't scroll\n</span></code></pre>\n<p>The beautiful part? <strong>None of these components import each other.</strong> The plugin queries the world: \"Give me the focused entity. Does it have CActions? Great, wire up keyboard handling for it.\"</p>\n<div class=\"codeblock\" data-loc=\"content/keyboard-navigation-demo.md:210\">\n \n <div class=\"codeblock-content\" data-id=\"actions-plugin-binding\">\n <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"scripts/keyboard-demo/plugins/ActionsPlugin.ts:63\"><code><span class=\"line\"><span style=\"color:#908CAA;font-style:italic\">//</span><span style=\"color:#6E6A86;font-style:italic\"> Provide keydown handler for entities with CActions</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">build</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">onEntityCreated</span><span style=\"color:#E0DEF4\">(</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> requires</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">CActions</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> provides</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">CKeydownHandler</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">uid</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#C4A7E7;font-style:italic\"> actions</span><span style=\"color:#908CAA\"> })</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> combinedCombosAtom</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> atom</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">get</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> ComboData</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\"> actionKey</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> handler</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">reason</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> DevString</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> event</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ActionEvent</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> Outcome</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> &</span><span style=\"color:#9CCFD8\"> AnyAction</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#31748F\"> =</span><span style=\"color:#31748F\"> new</span><span style=\"color:#EBBCBA\"> Map</span><span style=\"color:#908CAA\"><</span><span style=\"color:#9CCFD8\">string</span><span style=\"color:#908CAA\">,</span><span style=\"color:#9CCFD8\"> ComboData</span><span style=\"color:#E0DEF4\">[]</span><span style=\"color:#908CAA\">></span><span style=\"color:#E0DEF4\">()</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> actionSet</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> actions</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> resolvedBindings</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">actionSet</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">bindingsAtom</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#908CAA\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">actionKey</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> maybeHandler</span><span style=\"color:#908CAA\">]</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> Object</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">entries</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">resolvedBindings</span><span style=\"color:#E0DEF4\">)) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">maybeHandler</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">continue</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> bindable</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> actionSet</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">registryKey</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">bindables</span><span style=\"color:#E0DEF4\">[</span><span style=\"color:#E0DEF4;font-style:italic\">actionKey</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">bindable</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">continue</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> defaultKey</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> bindable</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">defaultKeybinding</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> combo</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> normalizedKeyCombo</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">defaultKey</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> ENV_KEYBOARD_KIND</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">normalized</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> comboData</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> ComboData</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> actionKey</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> handler</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> maybeHandler</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> ...</span><span style=\"color:#E0DEF4;font-style:italic\">bindable</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> };</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> list</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">combo</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">list</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">set</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">combo</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">comboData</span><span style=\"color:#E0DEF4\">])</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#31748F\"> else</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\"> list</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">push</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">comboData</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> keydownHandler</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> CKeydownHandler</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">of</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\"> handler</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">reason</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> event</span><span style=\"color:#908CAA\">)</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> Omit shift for letter keys so \"Shift+X\" matches \"X\"</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> combos</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> addModifiersToKeyCombo</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">ENV_KEYBOARD_KIND</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4;font-style:italic\"> event</span><span style=\"color:#908CAA\">,</span><span style=\"color:#EBBCBA\"> true</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">event</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">defaultPrevented</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">combinedCombosAtom</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> combo</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> combos</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> comboDatas</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> combosMap</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">combo</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">normalized</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">!</span><span style=\"color:#E0DEF4;font-style:italic\">comboDatas</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">continue</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> for</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#31748F\">const</span><span style=\"color:#E0DEF4;font-style:italic\"> comboData</span><span style=\"color:#31748F\"> of</span><span style=\"color:#E0DEF4;font-style:italic\"> comboDatas</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> const</span><span style=\"color:#E0DEF4;font-style:italic\"> outcome</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> comboData</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">handler</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">dev</span><span style=\"color:#F6C177\">`Key combo pressed: </span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">combo</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">normalized</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">because</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">reason</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\"> target</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> uid</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">outcome</span><span style=\"color:#31748F\"> !==</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> outcome</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#E0DEF4;font-style:italic\"> Outcome</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">Passthrough</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#31748F\"> return</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4;font-style:italic\"> keydownHandler</span><span style=\"color:#908CAA\"> };</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span></code></pre>\n </div>\n \n</div>\n<p>This is the actual per-entity handler creation. When we add an entity with <code>CActions</code>, the plugin automatically:</p>\n<ul>\n<li>Reads all action definitions</li>\n<li>Normalizes key combos (so \"X\" and \"Shift-X\" both match)</li>\n<li>Creates a <code>CKeydownHandler</code> that matches keys to handlers</li>\n<li>Plugs it into the event system</li>\n</ul>\n<p>We don't call any of this ourselves. It Just Works™.</p>\n<h2 id=\"what-we-learned\">What We Learned <div data-loc=\"content/keyboard-navigation-demo.md:221\" class=\"heading-src\">⋅</div></h2>\n<p>Let's step back and appreciate what we built:</p>\n<h3 id=\"white-check-mark-we-can-test-everything-in-isolation\">✅ We Can Test Everything In Isolation <div data-loc=\"content/keyboard-navigation-demo.md:225\" class=\"heading-src\">⋅</div></h3>\n<p>Want to test if \"X\" triggers delete? No React needed:</p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">world </span><span>= </span><span style=\"color:#8fa1b3;\">createTestWorld</span><span>();\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">cardUID </span><span>= </span><span style=\"color:#8fa1b3;\">addCardEntity</span><span>(</span><span style=\"color:#bf616a;\">world</span><span>, {\n</span><span> onDelete: </span><span style=\"color:#bf616a;\">mockFn</span><span>,\n</span><span>});\n</span><span>\n</span><span style=\"color:#65737e;\">// Simulate focus\n</span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#bf616a;\">store</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">focusAtom</span><span>, </span><span style=\"color:#bf616a;\">cardUID</span><span>);\n</span><span>\n</span><span style=\"color:#65737e;\">// Simulate keypress\n</span><span style=\"color:#bf616a;\">rootHandler</span><span>.</span><span style=\"color:#8fa1b3;\">handler</span><span>(</span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">test</span><span>`, { key: "</span><span style=\"color:#a3be8c;\">x</span><span>" });\n</span><span>\n</span><span style=\"color:#8fa1b3;\">expect</span><span>(</span><span style=\"color:#bf616a;\">mockFn</span><span>).</span><span style=\"color:#8fa1b3;\">toHaveBeenCalled</span><span>();\n</span></code></pre>\n<h3 id=\"white-check-mark-components-are-composable\">✅ Components Are Composable <div data-loc=\"content/keyboard-navigation-demo.md:244\" class=\"heading-src\">⋅</div></h3>\n<p>A simple button might only have <code>CFocusable</code>. A rich text editor adds <code>CActions</code> with 50 shortcuts. A card adds both plus <code>CSelectable</code>. Mix and match:</p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#65737e;\">// Simple button\n</span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">addEntity</span><span>(</span><span style=\"color:#bf616a;\">uid</span><span>, </span><span style=\"color:#bf616a;\">SimpleButton</span><span>, {\n</span><span> focusable: </span><span style=\"color:#bf616a;\">CFocusable</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({...}),\n</span><span>});\n</span><span>\n</span><span style=\"color:#65737e;\">// Rich editor\n</span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">addEntity</span><span>(</span><span style=\"color:#bf616a;\">uid</span><span>, </span><span style=\"color:#bf616a;\">RichEditor</span><span>, {\n</span><span> focusable: </span><span style=\"color:#bf616a;\">CFocusable</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({...}),\n</span><span> actions: </span><span style=\"color:#bf616a;\">CActions</span><span>.</span><span style=\"color:#8fa1b3;\">merge</span><span>(\n</span><span> </span><span style=\"color:#bf616a;\">TextFormattingActions</span><span>, </span><span style=\"color:#65737e;\">// Bold, italic, etc.\n</span><span> </span><span style=\"color:#bf616a;\">BlockActions</span><span>, </span><span style=\"color:#65737e;\">// Lists, headings\n</span><span> </span><span style=\"color:#bf616a;\">ClipboardActions</span><span>, </span><span style=\"color:#65737e;\">// Cut, copy, paste\n</span><span> ),\n</span><span>});\n</span></code></pre>\n<h3 id=\"white-check-mark-everything-is-observable\">✅ Everything Is Observable <div data-loc=\"content/keyboard-navigation-demo.md:265\" class=\"heading-src\">⋅</div></h3>\n<p>All state lives in Jotai atoms. DevTools can show us:</p>\n<ul>\n<li>Which entity has focus right now</li>\n<li>What actions are available</li>\n<li>When each action was last pressed</li>\n</ul>\n<h3 id=\"white-check-mark-nothing-is-tightly-coupled\">✅ Nothing Is Tightly Coupled <div data-loc=\"content/keyboard-navigation-demo.md:273\" class=\"heading-src\">⋅</div></h3>\n<p>Look at what we <strong>didn't</strong> do:</p>\n<ul>\n<li>❌ Import KeyboardManager in every component</li>\n<li>❌ Call registerShortcut() imperatively</li>\n<li>❌ Have components know about each other</li>\n<li>❌ Write glue code to connect pieces</li>\n</ul>\n<p>Instead:</p>\n<ul>\n<li>✅ Components declare data (\"I can be focused\", \"I have actions\")</li>\n<li>✅ Plugins react to component presence (\"Oh, you have both? Let me wire you up\")</li>\n<li>✅ Everything communicates through atoms</li>\n<li>✅ Adding a new entity requires <strong>zero</strong> changes to existing code</li>\n</ul>\n<h2 id=\"when-should-we-use-this-approach\">When Should We Use This Approach? <div data-loc=\"content/keyboard-navigation-demo.md:289\" class=\"heading-src\">⋅</div></h2>\n<p>Let's be honest about trade-offs.</p>\n<p><strong>This pattern shines when:</strong></p>\n<ul>\n<li>✅ We're already using ECS architecture in our app (like we do in Phosphor)</li>\n<li>✅ We have complex nesting and need context-sensitive shortcuts</li>\n<li>✅ We want every piece testable in isolation</li>\n<li>✅ Our app has 10+ different shortcut contexts</li>\n<li>✅ We value composition over simplicity</li>\n</ul>\n<p><strong>Consider simpler alternatives when:</strong></p>\n<ul>\n<li>❌ We're building a small app with <20 shortcuts</li>\n<li>❌ We want minimal bundle size (this adds ~30KB with Jotai + ECS)</li>\n<li>❌ Our team isn't familiar with reactive state patterns</li>\n<li>❌ We just need basic \"hotkey → function\" mapping</li>\n</ul>\n<h3 id=\"simpler-alternatives-we-considered\">Simpler Alternatives We Considered <div data-loc=\"content/keyboard-navigation-demo.md:308\" class=\"heading-src\">⋅</div></h3>\n<table><thead><tr><th>Approach</th><th>Bundle Size</th><th>Learning Curve</th><th>Test Isolation</th><th>Context-Sensitive</th></tr></thead><tbody>\n<tr><td><strong>ECS (Ours)</strong></td><td>~30KB</td><td>High</td><td>Excellent</td><td>Excellent</td></tr>\n<tr><td><a href=\"https://github.com/jamiebuilds/tinykeys\">tinykeys</a></td><td>2KB</td><td>Low</td><td>Good</td><td>Manual</td></tr>\n<tr><td>React Context</td><td>0KB</td><td>Medium</td><td>Medium</td><td>Good</td></tr>\n<tr><td><a href=\"https://github.com/ccampbell/mousetrap\">Mousetrap</a></td><td>8KB</td><td>Low</td><td>Poor</td><td>Manual</td></tr>\n</tbody></table>\n<p><strong>For our use case</strong> (complex editor with nested contexts), the composition benefits outweigh the complexity cost. For most apps, a 2KB library like tinykeys is probably the right call.</p>\n<h2 id=\"tracing-a-keypress-together\">Tracing a Keypress Together <div data-loc=\"content/keyboard-navigation-demo.md:319\" class=\"heading-src\">⋅</div></h2>\n<p>Let's walk through exactly what happens when we press \"X\" to delete a card. This demystifies the \"magic\":</p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>📍 Step 1: DOM Event (keyboard-demo.entrypoint.tsx:29)\n</span><span> document.addEventListener('keydown', ...)\n</span><span> Event fires with event.key = "x"\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 2: Root Handler (ActionsPlugin.ts:33-42)\n</span><span> UKeydownRootHandler.handler() receives event\n</span><span> Check currentDispatchSpotAtom: Is anything focused?\n</span><span> Result: Card 2 has focus\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 3: Parent Walk (ActionsPlugin.ts:44-55)\n</span><span> CParent.dispatch() walks up the entity tree\n</span><span> Current: Card 2 → Does it have CKeydownHandler? YES ✓\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 4: Key Normalization (ActionsPlugin.ts:105-107)\n</span><span> addModifiersToKeyCombo("x", event, omitShift=true)\n</span><span> "x" → "X" (uppercase)\n</span><span> No modifiers, final combo: "X"\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 5: Action Lookup (ActionsPlugin.ts:110-115)\n</span><span> combosMap.get("X") → [{action: "delete", handler: fn}]\n</span><span> Call handler(dev`Key combo`, {target: card2UID})\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 6: Our Handler (createKeyboardDemo.ts:83)\n</span><span> onDelete() runs\n</span><span> alert("Deleted Card 2!")\n</span><span> Returns: handled`delete`\n</span><span>\n</span><span> ↓\n</span><span>\n</span><span>📍 Step 7: Prevent Default (ActionsPlugin.ts:50-52)\n</span><span> outcome !== Passthrough, so:\n</span><span> event.preventDefault() ← stops browser scroll\n</span><span> Return "handled" to stop propagation\n</span></code></pre>\n<p><strong>Key insight</strong>: Notice how information flows through <strong>atoms</strong> and <strong>component queries</strong>, never through direct imports or method calls. That's the decoupling in action.</p>\n<h2 id=\"what-s-next\">What's Next? <div data-loc=\"content/keyboard-navigation-demo.md:371\" class=\"heading-src\">⋅</div></h2>\n<p>Now that we understand composable keyboard navigation, we can:</p>\n<ul>\n<li>Add spatial navigation (arrow keys navigate a 2D grid)</li>\n<li>Build focus trapping for modals</li>\n<li>Create a command palette with searchable actions</li>\n<li>Support user-customizable keybindings</li>\n</ul>\n<p>The pattern scales because we're composing data, not coupling objects.</p>\n<h2 id=\"reflection\">Reflection <div data-loc=\"content/keyboard-navigation-demo.md:382\" class=\"heading-src\">⋅</div></h2>\n<p>We started with a problem: keyboard shortcuts without spaghetti code.</p>\n<p>We solved it by separating concerns:</p>\n<ul>\n<li><code>CFocusable</code> says \"I can receive focus\" (data)</li>\n<li><code>CActions</code> says \"I have these shortcuts\" (data)</li>\n<li><code>ActionsPlugin</code> says \"When those exist together, wire them up\" (behavior)</li>\n</ul>\n<p>No component knows about the others. Add a new shortcut? Update one entity's <code>CActions</code>. Add a new focusable element? Add <code>CFocusable</code>. The plugin handles the rest.</p>\n<p>That's the power of Entity-Component-System for UI.</p>\n",
"permalink": "/keyboard-navigation-demo/",
"slug": "keyboard-navigation-demo",
"ancestors": [
"_index.md"
],
"title": "Let's Build Composable Keyboard Navigation Together",
"description": null,
"updated": null,
"date": "2025-10-20",
"year": 2025,
"month": 10,
"day": 20,
"taxonomies": {},
"authors": [],
"extra": {
"nav_section": "Interactive Demos",
"nav_order": 1
},
"path": "/keyboard-navigation-demo/",
"components": [
"keyboard-navigation-demo"
],
"summary": null,
"toc": [
{
"level": 1,
"id": "let-s-build-composable-keyboard-navigation-together",
"permalink": "/keyboard-navigation-demo/#let-s-build-composable-keyboard-navigation-together",
"title": "Let's Build Composable Keyboard Navigation Together ⋅",
"children": [
{
"level": 2,
"id": "the-problem-we-re-solving",
"permalink": "/keyboard-navigation-demo/#the-problem-we-re-solving",
"title": "The Problem We're Solving ⋅",
"children": []
},
{
"level": 2,
"id": "what-we-re-building",
"permalink": "/keyboard-navigation-demo/#what-we-re-building",
"title": "What We're Building ⋅",
"children": []
},
{
"level": 2,
"id": "our-approach-entities-components-and-plugins",
"permalink": "/keyboard-navigation-demo/#our-approach-entities-components-and-plugins",
"title": "Our Approach: Entities, Components, and Plugins ⋅",
"children": []
},
{
"level": 2,
"id": "try-it-out-first",
"permalink": "/keyboard-navigation-demo/#try-it-out-first",
"title": "Try It Out First ⋅",
"children": []
},
{
"level": 2,
"id": "our-four-building-blocks",
"permalink": "/keyboard-navigation-demo/#our-four-building-blocks",
"title": "Our Four Building Blocks ⋅",
"children": [
{
"level": 3,
"id": "block-1-making-things-focusable",
"permalink": "/keyboard-navigation-demo/#block-1-making-things-focusable",
"title": "Block 1: Making Things Focusable ⋅",
"children": []
},
{
"level": 3,
"id": "block-2-tracking-which-entity-has-focus",
"permalink": "/keyboard-navigation-demo/#block-2-tracking-which-entity-has-focus",
"title": "Block 2: Tracking Which Entity Has Focus ⋅",
"children": []
},
{
"level": 3,
"id": "block-3-declaring-actions",
"permalink": "/keyboard-navigation-demo/#block-3-declaring-actions",
"title": "Block 3: Declaring Actions ⋅",
"children": []
},
{
"level": 3,
"id": "block-4-wiring-it-all-together-actionsplugin",
"permalink": "/keyboard-navigation-demo/#block-4-wiring-it-all-together-actionsplugin",
"title": "Block 4: Wiring It All Together - ActionsPlugin ⋅",
"children": []
}
]
},
{
"level": 2,
"id": "what-we-learned",
"permalink": "/keyboard-navigation-demo/#what-we-learned",
"title": "What We Learned ⋅",
"children": [
{
"level": 3,
"id": "white-check-mark-we-can-test-everything-in-isolation",
"permalink": "/keyboard-navigation-demo/#white-check-mark-we-can-test-everything-in-isolation",
"title": "✅ We Can Test Everything In Isolation ⋅",
"children": []
},
{
"level": 3,
"id": "white-check-mark-components-are-composable",
"permalink": "/keyboard-navigation-demo/#white-check-mark-components-are-composable",
"title": "✅ Components Are Composable ⋅",
"children": []
},
{
"level": 3,
"id": "white-check-mark-everything-is-observable",
"permalink": "/keyboard-navigation-demo/#white-check-mark-everything-is-observable",
"title": "✅ Everything Is Observable ⋅",
"children": []
},
{
"level": 3,
"id": "white-check-mark-nothing-is-tightly-coupled",
"permalink": "/keyboard-navigation-demo/#white-check-mark-nothing-is-tightly-coupled",
"title": "✅ Nothing Is Tightly Coupled ⋅",
"children": []
}
]
},
{
"level": 2,
"id": "when-should-we-use-this-approach",
"permalink": "/keyboard-navigation-demo/#when-should-we-use-this-approach",
"title": "When Should We Use This Approach? ⋅",
"children": [
{
"level": 3,
"id": "simpler-alternatives-we-considered",
"permalink": "/keyboard-navigation-demo/#simpler-alternatives-we-considered",
"title": "Simpler Alternatives We Considered ⋅",
"children": []
}
]
},
{
"level": 2,
"id": "tracing-a-keypress-together",
"permalink": "/keyboard-navigation-demo/#tracing-a-keypress-together",
"title": "Tracing a Keypress Together ⋅",
"children": []
},
{
"level": 2,
"id": "what-s-next",
"permalink": "/keyboard-navigation-demo/#what-s-next",
"title": "What's Next? ⋅",
"children": []
},
{
"level": 2,
"id": "reflection",
"permalink": "/keyboard-navigation-demo/#reflection",
"title": "Reflection ⋅",
"children": []
}
]
}
],
"word_count": 1559,
"reading_time": 8,
"assets": [],
"draft": true,
"lang": "en",
"lower": null,
"higher": null,
"translations": [],
"backlinks": []
},
"translations": [],
"backlinks": []
},
"zola_version": "0.21.0"
}