DraftThis article is currently in draft mode

Tera Reference

{{ tera_examples_as_shortcode() }}

Alice is 30 years old.

String Filters

lower: hello

capitalize: Hello world

title: Hello World

upper: HELLO WORLD

replace: Hello, Bob

linebreaksbr: Line one\nLine two

trim: "spaces"

truncate: hello wo…

split + join: path > to > file.txt

slugify: hello-world

Number Filters

round: 43

round(floor): 42

round(ceil): 43

Math: 15 (addition), 7 (subtraction), 20 (multiplication), 5 (division)

List Operations

join: red, green, blue

slice: one, two

concat: 1, 2, 3, 4

sort: 1, 2, 3

reverse: three, two, one

first: one

last: three

nth(1): two

length: 3

unique: 1, 2, 3

Control Flow

✓ Adult (age >= 18)

Loop example:

  • Index: 1, Item: one (first)
  • Index: 2, Item: two
  • Index: 3, Item: three (last)

Conditional Tests

value is defined - variable exists

fake_var is undefined - variable doesn't exist

age >= 18 - comparison operators work

name == "Alice" - equality test

age >= 18 and name == "Alice" - logical AND

not (age < 18 or age > 65) - logical OR with NOT

Advanced Patterns

Styled number list (conditional classes):

1 2 3 4 5

Page description: No description available

Chained filters: HELLO, TERA!

Page Context Examples

These examples work with data passed from Zola

page.title: Tera Reference

page.date: 2025-10-20

page.word_count: 37

config.title: Phosphor

Tera Template Context (Zola data-loc)
	

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": "/tera/",
  "current_url": "/tera/",
  "lang": "en",
  "page": {
    "relative_path": "tera.md",
    "colocated_path": null,
    "content": "<h1 id=\"tera-reference\">Tera Reference <div data-loc=\"content/tera.md:10\" class=\"heading-src\">⋅</div></h1>\n\n<div data-loc=\"content&#x2F;tera.md:12\" class=\"!col-start-1 col-span-3 !max-w-[initial] -mx-6 md:mx-0\">\n  <div class=\"bg-bg-elevated border-y md:border md:rounded-lg shadow-inner overflow-hidden border-border-subtle\">\n    <div class=\"grid lg:grid-cols-2 divide-y lg:divide-y-0 lg:divide-x divide-border\">\n<div\n  data-loc=\"content&#x2F;tera.md:13\"\n  class=\"flex flex-col p-6\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4\"\n  >\n    Tera Documentation\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch \"\n  >\n    \n<pre class=\"space-y-1.5 text-sm whitespace-normal\">\n\t<h2 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Tera Documentation</h2>\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#introduction\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Introduction</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#tera-basics\" target=\"_blank\"\n\t\t\t\t\t>Tera Basics</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#raw\" target=\"_blank\"\n\t\t\t\t\t>Raw</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#whitespace-control\" target=\"_blank\"\n\t\t\t\t\t>Whitespace control</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#comments\" target=\"_blank\"\n\t\t\t\t\t>Comments</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#data-structures\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Data structures</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#literals\" target=\"_blank\"\n\t\t\t\t\t>Literals</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#variables\" target=\"_blank\"\n\t\t\t\t\t>Variables</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#expressions\" target=\"_blank\"\n\t\t\t\t\t>Expressions</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#manipulating-data\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Manipulating data</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#assignments\" target=\"_blank\"\n\t\t\t\t\t>Assignments</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#filters\" target=\"_blank\"\n\t\t\t\t\t>Filters</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#tests\" target=\"_blank\"\n\t\t\t\t\t>Tests</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#functions\" target=\"_blank\"\n\t\t\t\t\t>Functions</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#control-structures\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Control structures</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#if\" target=\"_blank\"\n\t\t\t\t\t>If</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#for\" target=\"_blank\"\n\t\t\t\t\t>For</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#include\" target=\"_blank\"\n\t\t\t\t\t>Include</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#macros\" target=\"_blank\"\n\t\t\t\t\t>Macros</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#inheritance\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Inheritance</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#base-template\" target=\"_blank\"\n\t\t\t\t\t>Base template</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#child-template\" target=\"_blank\"\n\t\t\t\t\t>Child template</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n\t\t\n\t\t\n\t\t\n\t<div class=\"mb-4\">\n\t\t<a href=\"https://keats.github.io/tera/docs/#built-ins\" target=\"_blank\"\n\t\t\t><h3 class=\"text-sm font-medium uppercase tracking-wider text-text-tertiary mb-2.5\">Built-ins</h3></a\n\t\t>\n\t\t<ul class=\"space-y-1.5 text-sm pl-6\">\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#built-in-filters\" target=\"_blank\"\n\t\t\t\t\t>Built-in filters</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#built-in-tests\" target=\"_blank\"\n\t\t\t\t\t>Built-in tests</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t\t<li>\n\t\t\t\t<a href=\"https://keats.github.io/tera/docs/#built-in-functions\" target=\"_blank\"\n\t\t\t\t\t>Built-in functions</a\n\t\t\t\t>\n\t\t\t</li>\n\t\t\t\n\t\t</ul>\n\t</div>\n\t\n</pre>\n  </div>\n</div>\n\n\n<div\n  data-loc=\"content&#x2F;tera.md:16\"\n  class=\"flex flex-col p-6\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4\"\n  >\n    {{ tera_examples_as_shortcode() }}\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch \"\n  >\n               \n<article class=\"tera-reference border-2 border-border-subtle rounded-lg p-6 space-y-8\">\n  <h1 class=\"text-3xl font-bold\">Alice is 30 years old.</h1>\n\n  \n  <section class=\"string-examples space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">String Filters</h2>\n    <p><code>lower:</code> hello</p>\n    <p><code>capitalize:</code> Hello world</p>\n    <p><code>title:</code> Hello World</p>\n    <p><code>upper:</code> HELLO WORLD</p>\n    <p><code>replace:</code> Hello, Bob</p>\n    <p><code>linebreaksbr:</code> Line one\\nLine two</p>\n    <p><code>trim:</code> \"spaces\"</p>\n    <p><code>truncate:</code> hello wo…</p>\n    <p><code>split + join:</code> path &gt; to &gt; file.txt</p>\n    <p><code>slugify:</code> hello-world</p>\n  </section>\n\n  \n  <section class=\"number-examples space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">Number Filters</h2>\n    <p><code>round:</code> 43</p>\n    <p><code>round(floor):</code> 42</p>\n    <p><code>round(ceil):</code> 43</p>\n    <p>\n      <code>Math:</code> 15 (addition), 7 (subtraction), 20 (multiplication), 5 (division)\n    </p>\n  </section>\n\n  \n  <section class=\"list-examples space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">List Operations</h2>\n\n    \n    <p><code>join:</code> red, green, blue</p>\n    <p><code>slice:</code> one, two</p>\n    <p><code>concat:</code> 1, 2, 3, 4</p>\n\n    \n    <p><code>sort:</code> 1, 2, 3</p>\n    <p><code>reverse:</code> three, two, one</p>\n    <p><code>first:</code> one</p>\n    <p><code>last:</code> three</p>\n    <p><code>nth(1):</code> two</p>\n    <p><code>length:</code> 3</p>\n    <p><code>unique:</code> 1, 2, 3</p>\n  </section>\n\n  \n  <section class=\"control-flow space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">Control Flow</h2>\n\n    \n    <div>\n      \n      <p class=\"text-green-600\">✓ Adult (age >= 18)</p>\n      \n    </div>\n\n    \n    <div>\n      <p><strong>Loop example:</strong></p>\n      <ul class=\"list-disc list-inside\">\n        \n        <li>\n          Index: 1, Item: one (first) \n        </li>\n        \n        <li>\n          Index: 2, Item: two  \n        </li>\n        \n        <li>\n          Index: 3, Item: three  (last)\n        </li>\n        \n      </ul>\n    </div>\n  </section>\n\n  \n  <section class=\"tests space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">Conditional Tests</h2>\n     \n\n    <div class=\"space-y-2\">\n       \n      <p>✓ <code>value is defined</code> - variable exists</p>\n       \n      <p>✓ <code>fake_var is undefined</code> - variable doesn't exist</p>\n        \n      <p>✓ <code>age >= 18</code> - comparison operators work</p>\n       \n      <p>✓ <code>name == \"Alice\"</code> - equality test</p>\n        \n      <p>✓ <code>age >= 18 and name == \"Alice\"</code> - logical AND</p>\n       \n      <p>✓ <code>not (age < 18 or age > 65)</code> - logical OR with NOT</p>\n      \n    </div>\n  </section>\n\n  \n  <section class=\"advanced-patterns space-y-4\">\n    <h2 class=\"text-2xl font-semibold\">Advanced Patterns</h2>\n\n    \n    <div>\n      <p><strong>Styled number list (conditional classes):</strong></p>\n      <div class=\"flex gap-2\">\n        \n        <span class=\"px-3 py-1 rounded bg-gray-100\"> 1 </span>\n        \n        <span class=\"px-3 py-1 rounded bg-gray-100\"> 2 </span>\n        \n        <span class=\"px-3 py-1 rounded bg-blue-100\"> 3 </span>\n        \n        <span class=\"px-3 py-1 rounded bg-blue-100\"> 4 </span>\n        \n        <span class=\"px-3 py-1 rounded bg-blue-100\"> 5 </span>\n        \n      </div>\n    </div>\n\n     \n    <p><strong>Page description:</strong> No description available</p>\n\n     \n    <p><strong>Chained filters:</strong> HELLO, TERA!</p>\n  </section>\n\n  \n  <section class=\"context-examples space-y-2\">\n    <h2 class=\"text-2xl font-semibold\">Page Context Examples</h2>\n    <p class=\"text-sm text-text-secondary\">These examples work with data passed from Zola</p>\n\n    <div class=\"space-y-2\">\n      <p><code>page.title:</code> Tera Reference</p>\n      <p><code>page.date:</code> 2025-10-20</p>\n      <p><code>page.word_count:</code> 37</p>\n      <p><code>config.title:</code> Phosphor</p>\n    </div>\n  </section>\n</article>\n  </div>\n</div></div>\n  </div>\n</div>\n",
    "permalink": "/tera/",
    "slug": "tera",
    "ancestors": [
      "_index.md"
    ],
    "title": "Tera Reference",
    "description": null,
    "updated": null,
    "date": "2025-10-20",
    "year": 2025,
    "month": 10,
    "day": 20,
    "taxonomies": {},
    "authors": [],
    "extra": {
      "nav_section": "_",
      "nav_order": 1
    },
    "path": "/tera/",
    "components": [
      "tera"
    ],
    "summary": null,
    "toc": [
      {
        "level": 1,
        "id": "tera-reference",
        "permalink": "/tera/#tera-reference",
        "title": "Tera Reference ⋅",
        "children": []
      }
    ],
    "word_count": 37,
    "reading_time": 1,
    "assets": [],
    "draft": true,
    "lang": "en",
    "lower": {
      "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&#x2F;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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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>, &quot;</span><span style=\"color:#a3be8c;\">my-button</span><span>&quot;);\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;\">=&gt; </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&#x2F;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\">&#x3C;</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\">&#x3C;</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&#x2F;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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</span><span style=\"color:#9CCFD8\">CActions</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">Bindings</span><span style=\"color:#908CAA\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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\">&#x3C;</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>  &quot;</span><span style=\"color:#a3be8c;\">card-actions</span><span>&quot;,\n</span><span>  { source: </span><span style=\"color:#8fa1b3;\">dev</span><span>`</span><span style=\"color:#a3be8c;\">Card actions</span><span>`, sectionName: &quot;</span><span style=\"color:#a3be8c;\">Card Actions</span><span>&quot; },\n</span><span>  {\n</span><span>    delete: {\n</span><span>      label: &quot;</span><span style=\"color:#a3be8c;\">Delete Card</span><span>&quot;,\n</span><span>      defaultKeybinding: &quot;</span><span style=\"color:#a3be8c;\">X</span><span>&quot; </span><span style=\"color:#b48ead;\">as const</span><span>,\n</span><span>      description: &quot;</span><span style=\"color:#a3be8c;\">Remove this card</span><span>&quot;,\n</span><span>    },\n</span><span>    edit: {\n</span><span>      label: &quot;</span><span style=\"color:#a3be8c;\">Edit Card</span><span>&quot;,\n</span><span>      defaultKeybinding: &quot;</span><span style=\"color:#a3be8c;\">E</span><span>&quot; </span><span style=\"color:#b48ead;\">as const</span><span>,\n</span><span>      description: &quot;</span><span style=\"color:#a3be8c;\">Edit card content</span><span>&quot;,\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;\">=&gt; </span><span>{\n</span><span>        </span><span style=\"color:#8fa1b3;\">alert</span><span>(&quot;</span><span style=\"color:#a3be8c;\">Deleted!</span><span>&quot;);\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;\">=&gt; </span><span>{\n</span><span>        </span><span style=\"color:#8fa1b3;\">alert</span><span>(&quot;</span><span style=\"color:#a3be8c;\">Editing!</span><span>&quot;);\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&#x2F;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\">&#x3C;</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\">&#x3C;</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 &quot;X&quot;\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 &quot;X&quot; to action &quot;delete&quot;\n</span><span>  ↓\n</span><span>Call the handler we bound earlier\n</span><span>  ↓\n</span><span>preventDefault() so browser doesn&#39;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&#x2F;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\"> &#x26;</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\">&#x3C;</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: &quot;</span><span style=\"color:#a3be8c;\">x</span><span>&quot; });\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 &lt;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(&#39;keydown&#39;, ...)\n</span><span>   Event fires with event.key = &quot;x&quot;\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(&quot;x&quot;, event, omitShift=true)\n</span><span>   &quot;x&quot; → &quot;X&quot; (uppercase)\n</span><span>   No modifiers, final combo: &quot;X&quot;\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(&quot;X&quot;) → [{action: &quot;delete&quot;, 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(&quot;Deleted Card 2!&quot;)\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 &quot;handled&quot; 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": []
    },
    "higher": {
      "relative_path": "entity-component-ui.md",
      "colocated_path": null,
      "content": "<h1 id=\"entity-component-ui\">Entity Component UI <div data-loc=\"content/entity-component-ui.md:12\" class=\"heading-src\">⋅</div></h1>\n\n<!-- https://www.getzola.org/documentation/content/shortcodes/ -->\n<details class=\"article-spoiler\" data-loc=\"content&#x2F;entity-component-ui.md:14\">\n  <summary>\n    <span class=\"article-spoiler-title\">Raw</span>\n  </summary>\n  <!-- https://www.getzola.org/documentation/content/shortcodes/#shortcodes-with-body -->\n  <div class=\"article-spoiler-content whitespace-pre-wrap text-xs\">Hi</div>\n</details>\n\n\n<!-- https://www.getzola.org/documentation/content/shortcodes/ -->\n<details class=\"article-spoiler\" data-loc=\"content&#x2F;entity-component-ui.md:18\">\n  <summary>\n    <span class=\"article-spoiler-title\">Anchors</span>\n  </summary>\n  <!-- https://www.getzola.org/documentation/content/shortcodes/#shortcodes-with-body -->\n  <div class=\"article-spoiler-content whitespace-pre-wrap text-xs\">TODO: translate the raw thoughts into initial anchors.<br>Controversial thesis: You should split view from business logic with a type-first, fine-grained reactive View Model Interface that lives outside the view lifecycle.<br><br>- This makes complex UIs testable, predictable, and easier to co-develop (including with LLMs).<br><br>Hero visual ideas:<br><br>- Running UI, View Model interface, and test cases in three panes<br>- Could be a simple text input with autocomplete/hints at the bottom</div>\n</details>\n\n<p><strong>Inspired by</strong> the composability of the Entity-Component-System (ECS) pattern, popularized in game development, and the testability of the Model-View-View-Model (MVVM) pattern, popularized in Swift, Flutter, and Xamarin.\n<strong>Analogies</strong>: Entity ID = UID; System = WorldState.Plugin\n<strong>Primary goals</strong>: (unit) testability, composability, observability.\n<strong>Characteristics</strong>:</p>\n<ul>\n<li>Plugins can be tested in complete isolation of each other (e.g. SpatialNav).</li>\n<li>Source code clearly shows what components each plugin contributes and depends on.</li>\n<li>Central World Store is highly inspectable by being a simple lookup from UIDs to component values.</li>\n<li>Reactivity is managed entirely through jotai atoms</li>\n</ul>\n<h2 id=\"context\">Context <div data-loc=\"content/entity-component-ui.md:41\" class=\"heading-src\">⋅</div></h2>\n<p>Our WorldState architecture is inspired by Entity Component System (ECS) patterns from game development, adapted for dense UI implementations. It decouples behavior, data, and UI rendering across multiple layers. This is especially useful in large-scale or collaborative applications that manage complex data flows–like CRDT-based documents, hierarchical data, or multiple concurrent user sessions.</p>\n<h2 id=\"key-concepts\">Key Concepts <div data-loc=\"content/entity-component-ui.md:45\" class=\"heading-src\">⋅</div></h2>\n<h3 id=\"entities\">Entities <div data-loc=\"content/entity-component-ui.md:47\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li><strong>What</strong>: Unique identifiers representing distinct objects in the UI or application (e.g., modules, variables, workspace info, user accounts, etc.).</li>\n<li><strong>Why</strong>: Entities let us compose data and behavior in a flexible, modular way.</li>\n</ul>\n<h3 id=\"components\">Components <div data-loc=\"content/entity-component-ui.md:52\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li><strong>What</strong>: Reusable pieces of functionality (with their own states/atoms) that can be attached to entities.</li>\n<li><strong>Why</strong>: Components let you attach specialized behavior (e.g., focusable, draggable, collapsable) to any entity without duplicating logic.</li>\n</ul>\n<h3 id=\"uniques\">Uniques <div data-loc=\"content/entity-component-ui.md:57\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li><strong>What</strong>: Singleton objects that exist independently of entities (e.g., global settings, user session state).</li>\n<li><strong>Why</strong>: Uniques allow for truly global or application-wide states that any part of the system can reference.</li>\n</ul>\n<h3 id=\"entity-map-worldstate\">Entity Map / WorldState <div data-loc=\"content/entity-component-ui.md:62\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li><strong>What</strong>: A central registry that manages entities, components, and uniques.</li>\n<li><strong>Why</strong>: The WorldState orchestrates creation, updates, and disposal, ensuring a single source of truth.</li>\n</ul>\n<h3 id=\"plugins\">Plugins <div data-loc=\"content/entity-component-ui.md:67\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li><strong>What</strong>: Extension modules that respond to entity creation, provide derived components, add global uniques, or enforce constraints.</li>\n<li><strong>Why</strong>: Plugins allow building feature layers (like a tree, a grid, or specialized editing behaviors) without entangling the core system logic.</li>\n</ul>\n<h2 id=\"benefits\">Benefits <div data-loc=\"content/entity-component-ui.md:72\" class=\"heading-src\">⋅</div></h2>\n<h3 id=\"composability\">Composability <div data-loc=\"content/entity-component-ui.md:74\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li>You can mix and match components to form new behaviors without duplicating code.</li>\n<li>Example: Attaching <code>CLabel</code> and <code>CFocusable</code> to an entity to make it both labeled and keyboard-focusable.</li>\n</ul>\n<h3 id=\"type-safety\">Type Safety <div data-loc=\"content/entity-component-ui.md:79\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li>The system's use of TypeScript generics ensures correct composition of entities and components.</li>\n<li>You get compile-time verification that the right components exist on an entity.</li>\n</ul>\n<h3 id=\"ui-framework-agnostic\">UI Framework Agnostic <div data-loc=\"content/entity-component-ui.md:84\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li>The \"ECS\"-like logic (entities, components, plugins) is decoupled from rendering (e.g. React).</li>\n</ul>\n<h3 id=\"centralized-state-management-with-jotai\">Centralized State Management with Jotai <div data-loc=\"content/entity-component-ui.md:88\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li>Components store data in Jotai atoms, which are known for their straightforward and granular reactivity.</li>\n<li>This fosters predictable state updates and fine-grained performance optimizations.</li>\n</ul>\n<h3 id=\"easy-testing\">Easy Testing <div data-loc=\"content/entity-component-ui.md:93\" class=\"heading-src\">⋅</div></h3>\n<ul>\n<li>Components can be tested in isolation (like microservices).</li>\n<li>You can directly test reactivity, plugin interactions, and more, all without hooking up any UI.</li>\n</ul>\n<h2 id=\"example-usage\">Example Usage <div data-loc=\"content/entity-component-ui.md:98\" class=\"heading-src\">⋅</div></h2>\n<h3 id=\"defining-a-module-entity\">Defining a Module Entity <div data-loc=\"content/entity-component-ui.md:100\" class=\"heading-src\">⋅</div></h3>\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;\">// Components\n</span><span style=\"color:#b48ead;\">export class </span><span style=\"color:#ebcb8b;\">CEditability </span><span style=\"color:#b48ead;\">extends </span><span style=\"color:#bf616a;\">World</span><span style=\"color:#eff1f5;\">.</span><span style=\"color:#8fa1b3;\">Component</span><span style=\"color:#eff1f5;\">(</span><span>&quot;</span><span style=\"color:#a3be8c;\">editability</span><span>&quot;</span><span style=\"color:#eff1f5;\">)&lt;\n</span><span style=\"color:#eff1f5;\">  CEditability,\n</span><span style=\"color:#eff1f5;\">  { </span><span style=\"color:#bf616a;\">canBeEditedAtom</span><span>: </span><span style=\"color:#eff1f5;\">Atom&lt;boolean&gt;; </span><span style=\"color:#bf616a;\">disabledReasonAtom</span><span>: </span><span style=\"color:#eff1f5;\">Atom&lt;null </span><span>| </span><span style=\"color:#eff1f5;\">string&gt; }\n</span><span style=\"color:#eff1f5;\">&gt;() {}\n</span><span>\n</span><span style=\"color:#b48ead;\">export class </span><span style=\"color:#ebcb8b;\">CLabel </span><span style=\"color:#b48ead;\">extends </span><span style=\"color:#bf616a;\">World</span><span style=\"color:#eff1f5;\">.</span><span style=\"color:#8fa1b3;\">Component</span><span style=\"color:#eff1f5;\">(</span><span>&quot;</span><span style=\"color:#a3be8c;\">label</span><span>&quot;</span><span style=\"color:#eff1f5;\">)&lt;CLabel, { </span><span style=\"color:#bf616a;\">textEditor</span><span>: </span><span style=\"color:#eff1f5;\">MyTextEditorType }&gt;() {}\n</span><span>\n</span><span style=\"color:#65737e;\">// Use tags (zero-value components) to help plugins match on entities of a specific type\n</span><span style=\"color:#b48ead;\">export class </span><span style=\"color:#ebcb8b;\">CModuleTag </span><span style=\"color:#b48ead;\">extends </span><span style=\"color:#bf616a;\">World</span><span style=\"color:#eff1f5;\">.</span><span style=\"color:#8fa1b3;\">Component</span><span style=\"color:#eff1f5;\">(</span><span>&quot;</span><span style=\"color:#a3be8c;\">moduleTag</span><span>&quot;</span><span style=\"color:#eff1f5;\">)&lt;CLabel, {}&gt;() {}\n</span><span>\n</span><span style=\"color:#65737e;\">// Entity\n</span><span style=\"color:#b48ead;\">export class </span><span style=\"color:#ebcb8b;\">ModuleEntity </span><span style=\"color:#b48ead;\">extends </span><span style=\"color:#bf616a;\">World</span><span style=\"color:#eff1f5;\">.</span><span style=\"color:#8fa1b3;\">Entity</span><span style=\"color:#eff1f5;\">(</span><span>&quot;</span><span style=\"color:#a3be8c;\">module</span><span>&quot;</span><span style=\"color:#eff1f5;\">, {\n</span><span style=\"color:#eff1f5;\">  </span><span style=\"color:#65737e;\">// Required initial components\n</span><span style=\"color:#eff1f5;\">  components: [</span><span style=\"color:#bf616a;\">CModuleTag</span><span style=\"color:#eff1f5;\">, </span><span style=\"color:#bf616a;\">CEditability</span><span style=\"color:#eff1f5;\">, </span><span style=\"color:#bf616a;\">CLabel</span><span style=\"color:#eff1f5;\">],\n</span><span style=\"color:#eff1f5;\">  </span><span style=\"color:#65737e;\">// Components expected to be provided by plugins\n</span><span style=\"color:#eff1f5;\">  componentsProvidedByPlugins: [],\n</span><span style=\"color:#eff1f5;\">})&lt;ModuleEntity&gt;() {}\n</span></code></pre>\n<h3 id=\"creating-and-using-the-entity-in-the-worldstate\">Creating and Using the Entity in the WorldState <div data-loc=\"content/entity-component-ui.md:123\" class=\"heading-src\">⋅</div></h3>\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;\">// 1. Create a world with or without plugins\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">world </span><span>= new WorldStateImpl({ </span><span style=\"color:#bf616a;\">store</span><span>, </span><span style=\"color:#bf616a;\">pool</span><span>, plugins: [] });\n</span><span>\n</span><span style=\"color:#65737e;\">// 2. Add a new module entity\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">moduleUID </span><span>= </span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">addEntity</span><span>(</span><span style=\"color:#bf616a;\">World</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>, &quot;</span><span style=\"color:#a3be8c;\">module-1</span><span>&quot;), </span><span style=\"color:#bf616a;\">ModuleEntity</span><span>, {\n</span><span>  moduleTag: </span><span style=\"color:#bf616a;\">CModuleTag</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({}),\n</span><span>  editability: </span><span style=\"color:#bf616a;\">CEditability</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({\n</span><span>    canBeEditedAtom: </span><span style=\"color:#8fa1b3;\">atom</span><span>(</span><span style=\"color:#d08770;\">true</span><span>),\n</span><span>    disabledReasonAtom: </span><span style=\"color:#8fa1b3;\">atom</span><span>(</span><span style=\"color:#d08770;\">null</span><span>),\n</span><span>  }),\n</span><span>  label: </span><span style=\"color:#bf616a;\">CLabel</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({\n</span><span>    textEditor: {\n</span><span>      </span><span style=\"color:#65737e;\">/* ...some editor instance... */\n</span><span>    },\n</span><span>  }),\n</span><span>});\n</span><span>\n</span><span style=\"color:#65737e;\">// 3. Retrieve and update\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">labelAtom </span><span>= </span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">getComponentAtom</span><span>(</span><span style=\"color:#bf616a;\">moduleUID</span><span>, </span><span style=\"color:#bf616a;\">CLabel</span><span>);\n</span><span style=\"color:#bf616a;\">store</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">labelAtom</span><span>, {\n</span><span>  textEditor: {\n</span><span>    </span><span style=\"color:#65737e;\">/* updated editor data */\n</span><span>  },\n</span><span>});\n</span></code></pre>\n<h3 id=\"derived-components-via-a-plugin\">Derived Components via a Plugin <div data-loc=\"content/entity-component-ui.md:152\" class=\"heading-src\">⋅</div></h3>\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;\">// For any entity with CEditability, automatically add a CDisableable\n</span><span style=\"color:#b48ead;\">class </span><span style=\"color:#ebcb8b;\">CDisableable </span><span style=\"color:#b48ead;\">extends </span><span style=\"color:#bf616a;\">World</span><span style=\"color:#eff1f5;\">.</span><span style=\"color:#8fa1b3;\">Component</span><span style=\"color:#eff1f5;\">(</span><span>&quot;</span><span style=\"color:#a3be8c;\">disableable</span><span>&quot;</span><span style=\"color:#eff1f5;\">)&lt;CDisableable, { </span><span style=\"color:#bf616a;\">isDisabledAtom</span><span>: </span><span style=\"color:#eff1f5;\">Atom&lt;boolean&gt; }&gt;() {}\n</span><span>\n</span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">disablePlugin </span><span>= {\n</span><span>  name: &quot;</span><span style=\"color:#a3be8c;\">disable-plugin</span><span>&quot;,\n</span><span>  </span><span style=\"color:#8fa1b3;\">setup</span><span>(</span><span style=\"color:#bf616a;\">build</span><span>: World.WorldStateBuild) {\n</span><span>    </span><span style=\"color:#bf616a;\">build</span><span>.</span><span style=\"color:#8fa1b3;\">onEntityCreated</span><span>(\n</span><span>      {\n</span><span>        </span><span style=\"color:#65737e;\">// The plugin can match _any_ entity with CEditability, as opposed to only ModuleEntities\n</span><span>        requires: [</span><span style=\"color:#bf616a;\">CEditability</span><span>],\n</span><span>        provides: [</span><span style=\"color:#bf616a;\">CDisableable</span><span>],\n</span><span>      },\n</span><span>      (</span><span style=\"color:#bf616a;\">uid</span><span>, { </span><span style=\"color:#bf616a;\">editability </span><span>}) </span><span style=\"color:#b48ead;\">=&gt; </span><span>({\n</span><span>        disableable: </span><span style=\"color:#bf616a;\">CDisableable</span><span>.</span><span style=\"color:#8fa1b3;\">of</span><span>({\n</span><span>          isDisabledAtom: </span><span style=\"color:#8fa1b3;\">atom</span><span>((</span><span style=\"color:#bf616a;\">get</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span>!</span><span style=\"color:#8fa1b3;\">get</span><span>(</span><span style=\"color:#bf616a;\">editability</span><span>.</span><span style=\"color:#bf616a;\">canBeEditedAtom</span><span>)),\n</span><span>        }),\n</span><span>      }),\n</span><span>    );\n</span><span>  },\n</span><span>};\n</span></code></pre>\n<h2 id=\"best-practices\">Best Practices <div data-loc=\"content/entity-component-ui.md:177\" class=\"heading-src\">⋅</div></h2>\n<h3 id=\"keep-components-small-and-focused\">Keep Components Small and Focused <div data-loc=\"content/entity-component-ui.md:179\" class=\"heading-src\">⋅</div></h3>\n<p>Each component should do one job. For example, <code>CFocusable</code> for keyboard focus logic, <code>CTreeMovable</code> for drag/move logic.</p>\n<h3 id=\"prefer-composition-over-inheritance\">Prefer Composition Over Inheritance <div data-loc=\"content/entity-component-ui.md:183\" class=\"heading-src\">⋅</div></h3>\n<p>ECS fosters horizontal composition. Attach more components to get new behavior, rather than a deep inheritance hierarchy.</p>\n<h3 id=\"use-jotai-s-atomvalue-or-minimal-hooks\">Use Jotai's <code>&lt;AtomValue /&gt;</code> or Minimal Hooks <div data-loc=\"content/entity-component-ui.md:187\" class=\"heading-src\">⋅</div></h3>\n<p>If you use React, <code>&lt;AtomValue /&gt;</code> or minimal hooks reduce extraneous re-renders. For non-React frameworks, the concept is similar: subscribe only to the atoms you need.</p>\n<pre data-lang=\"tsx\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-tsx \"><code class=\"language-tsx\" data-lang=\"tsx\"><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">labelAtom </span><span>= </span><span style=\"color:#bf616a;\">world</span><span>.</span><span style=\"color:#8fa1b3;\">getComponentAtom</span><span>(</span><span style=\"color:#bf616a;\">moduleUID</span><span>, </span><span style=\"color:#bf616a;\">CLabel</span><span>);\n</span><span style=\"color:#65737e;\">// if the atom is a primitive, you can render it directly as text\n</span><span style=\"color:#b48ead;\">return </span><span>&lt;</span><span style=\"color:#ebcb8b;\">AtomValue </span><span style=\"color:#d08770;\">atom</span><span>=</span><span style=\"color:#ab7967;\">{</span><span style=\"color:#bf616a;\">labelAtom</span><span style=\"color:#ab7967;\">} </span><span>/&gt;;\n</span><span style=\"color:#65737e;\">// or use it with a render function\n</span><span style=\"color:#b48ead;\">return </span><span>&lt;</span><span style=\"color:#ebcb8b;\">AtomValue </span><span style=\"color:#d08770;\">atom</span><span>=</span><span style=\"color:#ab7967;\">{</span><span style=\"color:#bf616a;\">labelAtom</span><span style=\"color:#ab7967;\">}</span><span>&gt;</span><span style=\"color:#ab7967;\">{</span><span>(</span><span style=\"color:#bf616a;\">label</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span>&lt;</span><span style=\"color:#bf616a;\">span </span><span style=\"color:#d08770;\">$</span><span>&gt;</span><span style=\"color:#ab7967;\">{</span><span style=\"color:#bf616a;\">label</span><span>?.text</span><span style=\"color:#ab7967;\">}</span><span>&lt;/</span><span style=\"color:#bf616a;\">span</span><span>&gt;</span><span style=\"color:#ab7967;\">}</span><span>&lt;/</span><span style=\"color:#ebcb8b;\">AtomValue</span><span>&gt;;\n</span><span style=\"color:#65737e;\">// depends on the component...\n</span><span style=\"color:#b48ead;\">return </span><span>&lt;</span><span style=\"color:#ebcb8b;\">AtomValue </span><span style=\"color:#d08770;\">atom</span><span>=</span><span style=\"color:#ab7967;\">{</span><span style=\"color:#bf616a;\">labelAtom</span><span style=\"color:#ab7967;\">}</span><span>&gt;</span><span style=\"color:#ab7967;\">{</span><span>(</span><span style=\"color:#bf616a;\">label</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span>&lt;</span><span style=\"color:#bf616a;\">div </span><span style=\"color:#d08770;\">$ ref</span><span>=</span><span style=\"color:#ab7967;\">{</span><span>(</span><span style=\"color:#bf616a;\">elt</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span style=\"color:#bf616a;\">label</span><span>.</span><span style=\"color:#8fa1b3;\">mount</span><span>(</span><span style=\"color:#bf616a;\">elt</span><span>)</span><span style=\"color:#ab7967;\">} </span><span>/&gt;</span><span style=\"color:#ab7967;\">}</span><span>&lt;/</span><span style=\"color:#ebcb8b;\">AtomValue</span><span>&gt;;\n</span></code></pre>\n<h3 id=\"test-in-isolation\">Test in Isolation <div data-loc=\"content/entity-component-ui.md:201\" class=\"heading-src\">⋅</div></h3>\n<p>Since WorldState logic is framework-agnostic, you can test the business logic of each component or plugin thoroughly without rendering UI.</p>\n<h3 id=\"leverage-the-ecs-for-concurrency\">Leverage the ECS for Concurrency <div data-loc=\"content/entity-component-ui.md:205\" class=\"heading-src\">⋅</div></h3>\n<p>CRDT merges, multiple user sessions, or real-time data streams can be managed more cleanly by hooking them into WorldState components or uniques, instead of scattering logic throughout the code.</p>\n<h2 id=\"conclusion\">Conclusion <div data-loc=\"content/entity-component-ui.md:209\" class=\"heading-src\">⋅</div></h2>\n<p>Our WorldState approach (powered by ECS) provides a robust, modular, and testable foundation for building complex UI and application logic. By separating data (via Jotai atoms), behavior (via components), and global singletons (via uniques), we can scale our application functionality while keeping each layer maintainable and comprehensible.</p>\n",
      "permalink": "/entity-component-ui/",
      "slug": "entity-component-ui",
      "ancestors": [
        "_index.md"
      ],
      "title": "Entity Component UI",
      "description": "For complex UIs, we break view models into separate components.",
      "updated": null,
      "date": "2025-10-17",
      "year": 2025,
      "month": 10,
      "day": 17,
      "taxonomies": {},
      "authors": [],
      "extra": {
        "category": "engineering",
        "nav_section": "Managing Complexity",
        "nav_order": 2
      },
      "path": "/entity-component-ui/",
      "components": [
        "entity-component-ui"
      ],
      "summary": null,
      "toc": [
        {
          "level": 1,
          "id": "entity-component-ui",
          "permalink": "/entity-component-ui/#entity-component-ui",
          "title": "Entity Component UI ⋅",
          "children": [
            {
              "level": 2,
              "id": "context",
              "permalink": "/entity-component-ui/#context",
              "title": "Context ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "key-concepts",
              "permalink": "/entity-component-ui/#key-concepts",
              "title": "Key Concepts ⋅",
              "children": [
                {
                  "level": 3,
                  "id": "entities",
                  "permalink": "/entity-component-ui/#entities",
                  "title": "Entities ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "components",
                  "permalink": "/entity-component-ui/#components",
                  "title": "Components ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "uniques",
                  "permalink": "/entity-component-ui/#uniques",
                  "title": "Uniques ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "entity-map-worldstate",
                  "permalink": "/entity-component-ui/#entity-map-worldstate",
                  "title": "Entity Map / WorldState ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "plugins",
                  "permalink": "/entity-component-ui/#plugins",
                  "title": "Plugins ⋅",
                  "children": []
                }
              ]
            },
            {
              "level": 2,
              "id": "benefits",
              "permalink": "/entity-component-ui/#benefits",
              "title": "Benefits ⋅",
              "children": [
                {
                  "level": 3,
                  "id": "composability",
                  "permalink": "/entity-component-ui/#composability",
                  "title": "Composability ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "type-safety",
                  "permalink": "/entity-component-ui/#type-safety",
                  "title": "Type Safety ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "ui-framework-agnostic",
                  "permalink": "/entity-component-ui/#ui-framework-agnostic",
                  "title": "UI Framework Agnostic ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "centralized-state-management-with-jotai",
                  "permalink": "/entity-component-ui/#centralized-state-management-with-jotai",
                  "title": "Centralized State Management with Jotai ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "easy-testing",
                  "permalink": "/entity-component-ui/#easy-testing",
                  "title": "Easy Testing ⋅",
                  "children": []
                }
              ]
            },
            {
              "level": 2,
              "id": "example-usage",
              "permalink": "/entity-component-ui/#example-usage",
              "title": "Example Usage ⋅",
              "children": [
                {
                  "level": 3,
                  "id": "defining-a-module-entity",
                  "permalink": "/entity-component-ui/#defining-a-module-entity",
                  "title": "Defining a Module Entity ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "creating-and-using-the-entity-in-the-worldstate",
                  "permalink": "/entity-component-ui/#creating-and-using-the-entity-in-the-worldstate",
                  "title": "Creating and Using the Entity in the WorldState ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "derived-components-via-a-plugin",
                  "permalink": "/entity-component-ui/#derived-components-via-a-plugin",
                  "title": "Derived Components via a Plugin ⋅",
                  "children": []
                }
              ]
            },
            {
              "level": 2,
              "id": "best-practices",
              "permalink": "/entity-component-ui/#best-practices",
              "title": "Best Practices ⋅",
              "children": [
                {
                  "level": 3,
                  "id": "keep-components-small-and-focused",
                  "permalink": "/entity-component-ui/#keep-components-small-and-focused",
                  "title": "Keep Components Small and Focused ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "prefer-composition-over-inheritance",
                  "permalink": "/entity-component-ui/#prefer-composition-over-inheritance",
                  "title": "Prefer Composition Over Inheritance ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "use-jotai-s-atomvalue-or-minimal-hooks",
                  "permalink": "/entity-component-ui/#use-jotai-s-atomvalue-or-minimal-hooks",
                  "title": "Use Jotai's <AtomValue /> or Minimal Hooks ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "test-in-isolation",
                  "permalink": "/entity-component-ui/#test-in-isolation",
                  "title": "Test in Isolation ⋅",
                  "children": []
                },
                {
                  "level": 3,
                  "id": "leverage-the-ecs-for-concurrency",
                  "permalink": "/entity-component-ui/#leverage-the-ecs-for-concurrency",
                  "title": "Leverage the ECS for Concurrency ⋅",
                  "children": []
                }
              ]
            },
            {
              "level": 2,
              "id": "conclusion",
              "permalink": "/entity-component-ui/#conclusion",
              "title": "Conclusion ⋅",
              "children": []
            }
          ]
        }
      ],
      "word_count": 1048,
      "reading_time": 6,
      "assets": [],
      "draft": true,
      "lang": "en",
      "lower": null,
      "higher": null,
      "translations": [],
      "backlinks": []
    },
    "translations": [],
    "backlinks": []
  },
  "zola_version": "0.21.0"
}