DraftThis article is currently in draft mode

Entity Component UI

Raw
Hi
Anchors
TODO: translate the raw thoughts into initial anchors.
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.

- This makes complex UIs testable, predictable, and easier to co-develop (including with LLMs).

Hero visual ideas:

- Running UI, View Model interface, and test cases in three panes
- Could be a simple text input with autocomplete/hints at the bottom

Inspired by 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. Analogies: Entity ID = UID; System = WorldState.Plugin Primary goals: (unit) testability, composability, observability. Characteristics:

  • Plugins can be tested in complete isolation of each other (e.g. SpatialNav).
  • Source code clearly shows what components each plugin contributes and depends on.
  • Central World Store is highly inspectable by being a simple lookup from UIDs to component values.
  • Reactivity is managed entirely through jotai atoms

Context

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.

Key Concepts

Entities

  • What: Unique identifiers representing distinct objects in the UI or application (e.g., modules, variables, workspace info, user accounts, etc.).
  • Why: Entities let us compose data and behavior in a flexible, modular way.

Components

  • What: Reusable pieces of functionality (with their own states/atoms) that can be attached to entities.
  • Why: Components let you attach specialized behavior (e.g., focusable, draggable, collapsable) to any entity without duplicating logic.

Uniques

  • What: Singleton objects that exist independently of entities (e.g., global settings, user session state).
  • Why: Uniques allow for truly global or application-wide states that any part of the system can reference.

Entity Map / WorldState

  • What: A central registry that manages entities, components, and uniques.
  • Why: The WorldState orchestrates creation, updates, and disposal, ensuring a single source of truth.

Plugins

  • What: Extension modules that respond to entity creation, provide derived components, add global uniques, or enforce constraints.
  • Why: Plugins allow building feature layers (like a tree, a grid, or specialized editing behaviors) without entangling the core system logic.

Benefits

Composability

  • You can mix and match components to form new behaviors without duplicating code.
  • Example: Attaching CLabel and CFocusable to an entity to make it both labeled and keyboard-focusable.

Type Safety

  • The system's use of TypeScript generics ensures correct composition of entities and components.
  • You get compile-time verification that the right components exist on an entity.

UI Framework Agnostic

  • The "ECS"-like logic (entities, components, plugins) is decoupled from rendering (e.g. React).

Centralized State Management with Jotai

  • Components store data in Jotai atoms, which are known for their straightforward and granular reactivity.
  • This fosters predictable state updates and fine-grained performance optimizations.

Easy Testing

  • Components can be tested in isolation (like microservices).
  • You can directly test reactivity, plugin interactions, and more, all without hooking up any UI.

Example Usage

Defining a Module Entity

// Components
export class CEditability extends World.Component("editability")<
  CEditability,
  { canBeEditedAtom: Atom<boolean>; disabledReasonAtom: Atom<null | string> }
>() {}

export class CLabel extends World.Component("label")<CLabel, { textEditor: MyTextEditorType }>() {}

// Use tags (zero-value components) to help plugins match on entities of a specific type
export class CModuleTag extends World.Component("moduleTag")<CLabel, {}>() {}

// Entity
export class ModuleEntity extends World.Entity("module", {
  // Required initial components
  components: [CModuleTag, CEditability, CLabel],
  // Components expected to be provided by plugins
  componentsProvidedByPlugins: [],
})<ModuleEntity>() {}

Creating and Using the Entity in the WorldState

// 1. Create a world with or without plugins
const world = new WorldStateImpl({ store, pool, plugins: [] });

// 2. Add a new module entity
const moduleUID = world.addEntity(World.uid(null, null, "module-1"), ModuleEntity, {
  moduleTag: CModuleTag.of({}),
  editability: CEditability.of({
    canBeEditedAtom: atom(true),
    disabledReasonAtom: atom(null),
  }),
  label: CLabel.of({
    textEditor: {
      /* ...some editor instance... */
    },
  }),
});

// 3. Retrieve and update
const labelAtom = world.getComponentAtom(moduleUID, CLabel);
store.set(labelAtom, {
  textEditor: {
    /* updated editor data */
  },
});

Derived Components via a Plugin

// For any entity with CEditability, automatically add a CDisableable
class CDisableable extends World.Component("disableable")<CDisableable, { isDisabledAtom: Atom<boolean> }>() {}

const disablePlugin = {
  name: "disable-plugin",
  setup(build: World.WorldStateBuild) {
    build.onEntityCreated(
      {
        // The plugin can match _any_ entity with CEditability, as opposed to only ModuleEntities
        requires: [CEditability],
        provides: [CDisableable],
      },
      (uid, { editability }) => ({
        disableable: CDisableable.of({
          isDisabledAtom: atom((get) => !get(editability.canBeEditedAtom)),
        }),
      }),
    );
  },
};

Best Practices

Keep Components Small and Focused

Each component should do one job. For example, CFocusable for keyboard focus logic, CTreeMovable for drag/move logic.

Prefer Composition Over Inheritance

ECS fosters horizontal composition. Attach more components to get new behavior, rather than a deep inheritance hierarchy.

Use Jotai's <AtomValue /> or Minimal Hooks

If you use React, <AtomValue /> or minimal hooks reduce extraneous re-renders. For non-React frameworks, the concept is similar: subscribe only to the atoms you need.

const labelAtom = world.getComponentAtom(moduleUID, CLabel);
// if the atom is a primitive, you can render it directly as text
return <AtomValue atom={labelAtom} />;
// or use it with a render function
return <AtomValue atom={labelAtom}>{(label) => <span $>{label?.text}</span>}</AtomValue>;
// depends on the component...
return <AtomValue atom={labelAtom}>{(label) => <div $ ref={(elt) => label.mount(elt)} />}</AtomValue>;

Test in Isolation

Since WorldState logic is framework-agnostic, you can test the business logic of each component or plugin thoroughly without rendering UI.

Leverage the ECS for Concurrency

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.

Conclusion

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.

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": "/entity-component-ui/",
  "current_url": "/entity-component-ui/",
  "lang": "en",
  "page": {
    "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": {
      "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": null,
      "higher": null,
      "translations": [],
      "backlinks": []
    },
    "higher": {
      "relative_path": "view-model-interfaces.md",
      "colocated_path": null,
      "content": "<h1 id=\"view-model-interfaces\">View Model Interfaces <div data-loc=\"content/view-model-interfaces.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;view-model-interfaces.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\">View Model Interfaces are a pattern for designing the boundary between view and business logic. I have leveraged, to my benefit, this set of rules to follow in several projects.<br><br>- Where either the performance needed to be very high<br>- By performance I mean reactivity like needs to be very controlled<br>- The complexity that comes from raw React hooks was difficult to follow<br>- When I've had to manage complex UI state that might involve CSS animations or user inputs and the like where I really need to understand and know exactly what's going to change in the DOM to not interrupt the user who might be selecting text or inserting inputs and so forth.<br>  I accomplished a lot of. There's also a fourth thing which is also equally important is being able when a certain part of my application becomes sufficiently complicated enough I have found that adhering to these rules for that boundary leads to much easier testability, much easier prototyping, and a simpler UI.<br><br>The controversial thesis here is that you should take the time to split out. You should take the time to adopt a reactivity toolset that is not dependent on the UI or rendering contexts. You should use fine-grained reactivity tooling (whether it's signals or observables or the like) so that when you are defining the interface, it's extremely clear from a type system point of view what's going to change and where to draw your boundaries.<br><br>The audience here is somewhat familiar with different patterns of encapsulating UI. They probably have a lot of experience with React, Svelte, or Vue. I'm going to be focusing on React.<br><br>The too-long-didn't-read, it's that... Your view model interface should be entirely interactive outside of the view's lifecycle, however your view lifecycle works (whether that's React hook context) you shouldn't have to rely on that when designing your view model interface and following these patterns will lead to more easily testable code, more easily iterated on code, and better LLM performance for complex code, complex UIs.<br><br>The reason that the LLMs perform better is because you can define a clear boundary between \"here's what the state looks like and how it is reactive in a type-driven way\" and have an AI implement the UI above that. You'll know that the AI's implementation of that UI is type-checked and following your plan.<br><br>These view model interfaces also provide a central point of documentation for what's expected out of the view and what the different sections of a UI are, so it gives you an opportunity to inform an AI how the view is supposed to be laid out before it gets started and the AI doesn't get distracted by the actual implementation.<br><br>My personal hook here has been, I just tend to struggle with managing to hold in my head all of events management plus styling plus render performance plus contexts plus testing concerns. Like early on in my career, I realized I just struggled a lot as the normal React applications got more complex and I found myself in a lot of conversations about hooks and how to manage hooks and how to manage the reactivity of a certain part of an application. I always could just step back and say, \"What the fuck is actually going on?\" Like I think I know and I can kinda look at a few things at the same time to get an idea, but at the end of the day it was really unclear. Once you had a couple of use effects involved and some use states and maybe a context, I really lost the feeling that I could understand what was happening.<br><br>Hook: As my software projects and jobs became targeting more complicated UIs and authorable UIs, I found a video about Flutter BLOC pattern from Paulo Soares in 2018 that described this kind of thinking pattern to apply. From then on, I have been leveraging this kind of pattern. Originally, I actually called it MVVM or the block pattern, but the more that I've implemented it and tried to explain it to people, it became more obvious that it's the important part of the pattern is actually just a view model interface and having a definition of what a view model interface is. So that's what we're going to talk about in this article.<br><br>So yeah, the context here is that you're trying to reason about hooks and how they're interacting together and you're trying to tie things together and you're also trying to communicate to other engineers how something works and you're managing. contexts and I mean you don't have that kind of memory to remember like spatially how all these contexts like work together. You probably really enjoyed the container and display components patterns and react early on and you were wondering what the next iteration of those ideas might look like.<br><br>The turn here is we define everything that the UI should look like, the actual even structure of the UI, we define that in a view model, like a type and an interface. That interface is going to expose as little as closely to how the UI gets mapped as possible, so that when we go to render the UI, you shouldn't have to reach for anything beyond a dot map, and you shouldn't have to call any functions with anything beyond, like, input text. If there's an event that changes the, your goal is to basically make all of the inner workings of your business logic, fetch state, everything opaque to the UI code.<br><br>By following this rule, we can actually forego testing the majority of our UI side and skip a lot of complexity by not needing to set up end-to-end testing from React all the way to our business logic. We can actually test our entire application interacting directly with a view model which is much simpler than interacting with the UI directly or trying to only interact with the state machine itself.<br><br>Proof: In fact, for most of my projects now, I only test when the only testing I do up front is on the view model itself, and usually that's only if the view has sufficiently complicated logic. I found that by breaking apart the view from the actual business logic, there's a lot fewer things that you need to test or that can go wrong because it's a lot easier to reason about.</div>\n</details>\n\n\n<!-- https://www.getzola.org/documentation/content/shortcodes/ -->\n<details class=\"article-spoiler\" data-loc=\"content&#x2F;view-model-interfaces.md:47\">\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\">Goal: Persuade React engineers to define a typed, reactive boundary (VM) outside the view lifecycle.<br>Audience: Intermediate+ React devs; familiar with hooks; open to signals/observables.<br>Thesis: Separate view from business logic via a fine-grained, type-first VM to gain predictability and testability.<br>Objections to address (implicitly via examples): extra layer? overkill for simple UIs? integration with React?<br>Proof strategy: small interface sketch + VM-only tests + React projection.</div>\n</details>\n\n<h2 id=\"thesis-why-this-matters\">Thesis (why this matters) <div data-loc=\"content/view-model-interfaces.md:55\" class=\"heading-src\">⋅</div></h2>\n<p>If your UI complexity is rising (effects, selection, async, animations), defining a view model interface outside the view lifecycle makes behavior predictable and testable, and keeps React code a pure projection.</p>\n<h2 id=\"definition-what-it-is\">Definition (what it is) <div data-loc=\"content/view-model-interfaces.md:59\" class=\"heading-src\">⋅</div></h2>\n<p>A View Model Interface is a small, typed surface that:</p>\n<ul>\n<li>Exposes reactive values (signals/observables) as read-only \"$\" streams</li>\n<li>Provides imperative methods for intent (no DOM details)</li>\n<li>Is framework-agnostic (lives outside React lifecycle)</li>\n<li>Maps 1:1 to rendered structure (dot-map friendly)</li>\n</ul>\n<h2 id=\"principles-how-to-shape-it\">Principles (how to shape it) <div data-loc=\"content/view-model-interfaces.md:68\" class=\"heading-src\">⋅</div></h2>\n<ul>\n<li>Name the UI regions explicitly in the interface</li>\n<li>Prefer fine-grained reactive primitives (no global invalidation)</li>\n<li>Accept domain inputs; return UI-ready outputs</li>\n<li>Keep event handlers side-effectful but opaque to the view</li>\n</ul>\n<h2 id=\"when-it-shines-when-not-to-use\">When it shines / When not to use <div data-loc=\"content/view-model-interfaces.md:75\" class=\"heading-src\">⋅</div></h2>\n<ul>\n<li>Shines: rich inputs, keyboard nav, selections, async suggestions, controlled animations</li>\n<li>Shines: teams splitting view/business logic; AI-assisted UI implementation</li>\n<li>Shines: complex animations that need precise triggering from business logic</li>\n<li>Not for: trivial forms, static pages, or where a single <code>useState</code> suffices</li>\n</ul>\n<h2 id=\"testing-workflow-evidence-over-rhetoric\">Testing Workflow (evidence over rhetoric) <div data-loc=\"content/view-model-interfaces.md:82\" class=\"heading-src\">⋅</div></h2>\n<ul>\n<li>Drive behavior by calling VM methods and asserting reactive outputs</li>\n<li>UI tests become thin: render is a projection over VM</li>\n<li>Animation triggers are testable: increment atom, assert on UI state changes</li>\n</ul>\n<h2 id=\"introduction\">Introduction <div data-loc=\"content/view-model-interfaces.md:88\" class=\"heading-src\">⋅</div></h2>\n<p>View Model Interfaces are a pattern for designing the boundary between view and business logic.</p>\n<p>Let's take a look at a simple example of <em>what</em> a view model interface for the following autocomplete input might look like.</p>\n\n<div data-loc=\"content&#x2F;view-model-interfaces.md:94\" 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;view-model-interfaces.md:95\"\n  class=\"flex flex-col\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4 px-6 pt-5\"\n  >\n    Interface\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch  overflow-auto max-h-[600px] [&>.codeblock]:!m-0 [&>.codeblock]:!rounded-none\"\n  >\n    <div class=\"codeblock\" data-loc=\"content&#x2F;view-model-interfaces.md:96\">\n  \n  <div class=\"codeblock-content\" data-id=\"autocomplete-interface\">\n    <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"content/_sources/autocomplete-demo/types.ts:3\"><code><span class=\"line\"><span style=\"color:#908CAA;font-style:italic\">//</span><span style=\"color:#6E6A86;font-style:italic\"> From the interface alone, we can picture</span></span>\n<span class=\"line\"><span style=\"color:#908CAA;font-style:italic\">//</span><span style=\"color:#6E6A86;font-style:italic\"> what this is and how it behaves.</span></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> interface</span><span style=\"color:#9CCFD8\"> AutocompleteVM</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">  inputText$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">string</span><span style=\"color:#908CAA\">>;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">  updateInput</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">text</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> pos</span><span style=\"color:#31748F\">:</span><span style=\"color:#F6C177\"> \"end\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#9CCFD8\"> number</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> void</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\"> Creating a selection should dismiss </span><span style=\"color:#908CAA;font-style:italic\">*/</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">  updateSelection</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">pos</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> number</span><span style=\"color:#908CAA\">,</span><span style=\"color:#C4A7E7;font-style:italic\"> anchor</span><span style=\"color:#31748F\">?:</span><span style=\"color:#9CCFD8\"> number</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">  pressKey</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">key</span><span style=\"color:#31748F\">:</span><span style=\"color:#F6C177\"> \"enter\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"up\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"down\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"escape\"</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">  options$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span></span>\n<span class=\"line\"><span style=\"color:#9CCFD8\">    Array</span><span style=\"color:#908CAA\">&#x3C;{</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;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:#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\">      selected$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</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:#EBBCBA\">      click</span><span style=\"color:#908CAA\">()</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">    }></span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  >;</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n  </div>\n   \n</div>\n  </div>\n</div>\n\n\n\n<div\n  data-loc=\"content&#x2F;view-model-interfaces.md:122\"\n  class=\"flex flex-col\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4 px-6 pt-5\"\n  >\n    Test\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch  overflow-auto max-h-[600px] [&>.codeblock]:!m-0 [&>.codeblock]:!rounded-none\"\n  >\n    <link rel=\"stylesheet\" href=\"/js/autocomplete-demo.entrypoint.css\">\n<div data-loc=\"content/view-model-interfaces.md:125\" id=\"autocomplete-demo\"></div>\n<script src=\"/js/autocomplete-demo.entrypoint.js\" type=\"module\"></script>\n\n<div class=\"codeblock\" data-loc=\"content&#x2F;view-model-interfaces.md:128\">\n  \n  <div class=\"codeblock-content\" data-id=\"autocomplete-test\">\n    <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"content/view-model-interfaces/autocomplete-sample.ts:9\"><code><span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">autocomplete</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">updateInput</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"Update @ind\"</span><span style=\"color:#908CAA\">,</span><span style=\"color:#F6C177\"> \"end\"</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\"> options</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">autocomplete</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">options$</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">expect</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">options</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">toMatchObject</span><span style=\"color:#E0DEF4\">([</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"src/index.html\"</span><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"src/index.ts\"</span><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"src/utils/index.ts\"</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:#EBBCBA\">expect</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">options</span><span style=\"color:#E0DEF4\">[</span><span style=\"color:#EBBCBA\">0</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">selected$</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">toBe</span><span style=\"color:#E0DEF4\">(</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:#EBBCBA\">expect</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">options</span><span style=\"color:#E0DEF4\">[</span><span style=\"color:#EBBCBA\">1</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">selected$</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">toBe</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">false</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">autocomplete</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">pressKey</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"down\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">expect</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">options</span><span style=\"color:#E0DEF4\">[</span><span style=\"color:#EBBCBA\">0</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">selected$</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">toBe</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">false</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">expect</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">options</span><span style=\"color:#E0DEF4\">[</span><span style=\"color:#EBBCBA\">1</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">selected$</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">toBe</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">true</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">;</span></span></code></pre>\n  </div>\n   \n</div>\n  </div>\n</div></div>\n  </div>\n</div>\n<h2 id=\"benefits\">Benefits <div data-loc=\"content/view-model-interfaces.md:151\" class=\"heading-src\">⋅</div></h2>\n<ul>\n<li>Unit-testable without DOM: assert on VM streams and actions</li>\n<li>Predictable updates: fine-grained reactivity, no accidental re-renders</li>\n<li>Simpler React: render from data, dot-map, minimal effects</li>\n</ul>\n<h1 id=\"example\">Example <div data-loc=\"content/view-model-interfaces.md:157\" class=\"heading-src\">⋅</div></h1>\n\n<div data-loc=\"content&#x2F;view-model-interfaces.md:159\" 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\"><!-- Left pane -->\n\n\n<div\n  data-loc=\"content&#x2F;view-model-interfaces.md:163\"\n  class=\"flex flex-col\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4 px-6 pt-5\"\n  >\n    Interface\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch  [&>.codeblock]:!m-0 [&>.codeblock]:!rounded-none\"\n  >\n    <!-- <div id=\"todo-vm-demo\" class=\"transform scale-75 lg:scale-90 origin-top-right lg:absolute lg:right-4 lg:top-4 z-10 min-w-lg\"></div> -->\n\n<div class=\"codeblock\" data-loc=\"content&#x2F;view-model-interfaces.md:167\">\n  \n  <div class=\"codeblock-content\" data-id=\"todo-vm-livestore\">\n    <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"content/_sources/view-model-interfaces/todo-vm/scope.ts:9\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> type</span><span style=\"color:#9CCFD8\"> TodoItemVM</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;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:#EBBCBA;font-style:italic\">  text$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">string</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!todo.text\"></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">  completed$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">boolean</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!todo.completed\"></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">  toggleCompleted</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">  remove</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</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\"> type</span><span style=\"color:#9CCFD8\"> TodoListVM</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">  header</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">    newTodoText$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">string</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!newTodoText\"></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    updateNewTodoText</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">text</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    addTodo</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</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\">  itemList</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">    items$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">TodoItemVM</span><span style=\"color:#E0DEF4\">[]</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!visibleTodos\"></span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  };</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">  footer</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">    incompleteCount$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#9CCFD8\">number</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!incompleteCount\"></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA;font-style:italic\">    currentFilter$</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Queryable</span><span style=\"color:#908CAA\">&#x3C;</span><span style=\"color:#F6C177\">\"all\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"active\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"completed\"</span><span style=\"color:#908CAA\">>;</span><span style=\"color:#E0DEF4\"> </span><span data-debug=\"!currentFilter\"></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    showAll</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    showActive</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    showCompleted</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    clearCompleted</span><span style=\"color:#31748F\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#9CCFD8\"> void</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  </div>\n</div>\n\n\n<!-- Right pane -->\n\n\n<div\n  data-loc=\"content&#x2F;view-model-interfaces.md:203\"\n  class=\"flex flex-col\"\n>\n  \n  <h3\n    class=\"text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4 px-6 pt-5\"\n  >\n    React Code\n  </h3>\n  \n  <div\n    class=\"flex-1 flex flex-col justify-stretch  [&>.codeblock]:!m-0 [&>.codeblock]:!rounded-none\"\n  >\n    <div data-loc=\"content/view-model-interfaces.md:205\" id=\"todo-vm-demo\" class=\"mb-6\"></div> \n<script src=\"/js/todo-vm.entrypoint.js\" type=\"module\"></script>\n<link rel=\"stylesheet\" href=\"/js/todo-vm.entrypoint.css\"/>\n\n<div class=\"codeblock\" data-loc=\"content&#x2F;view-model-interfaces.md:209\">\n  \n  <div class=\"codeblock-content\" data-id=\"todo-vm-ui-livestore\">\n    <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"content/_sources/view-model-interfaces/todo-vm/components/MainSection.tsx:8\"><code><span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> const</span><span style=\"color:#EBBCBA;font-style:italic\"> MainSection</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> React</span><span style=\"color:#31748F\">.</span><span style=\"color:#9CCFD8\">FC</span><span style=\"color:#31748F\"> =</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\"> vm</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> useTodoVM</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:#E0DEF4\"> (</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">    &#x3C;</span><span style=\"color:#9CCFD8\">section</span><span style=\"color:#C4A7E7;font-style:italic\"> className</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"todos\"</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">      &#x3C;</span><span style=\"color:#9CCFD8\">ul</span><span style=\"color:#C4A7E7;font-style:italic\"> className</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"todo-list\"</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">        &#x3C;</span><span style=\"color:#9CCFD8\">LiveValue</span><span style=\"color:#C4A7E7;font-style:italic\"> query</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">vm</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">itemList</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">items$</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">          {(</span><span style=\"color:#C4A7E7;font-style:italic\">items</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">            items</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">map</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">item</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4\"> (</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">              &#x3C;</span><span style=\"color:#9CCFD8\">li</span><span style=\"color:#C4A7E7;font-style:italic\"> className</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"state\"</span><span style=\"color:#C4A7E7;font-style:italic\"> key</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">item</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">key</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                &#x3C;</span><span style=\"color:#9CCFD8\">LiveValue</span><span style=\"color:#C4A7E7;font-style:italic\"> query</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">item</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">completed$</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">                  {(</span><span style=\"color:#C4A7E7;font-style:italic\">completed</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4\"> (</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                    &#x3C;</span><span style=\"color:#9CCFD8\">input</span><span style=\"color:#C4A7E7;font-style:italic\"> className</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"toggle\"</span><span style=\"color:#C4A7E7;font-style:italic\"> type</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"checkbox\"</span><span style=\"color:#C4A7E7;font-style:italic\"> checked</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">completed</span><span style=\"color:#908CAA\">}</span><span style=\"color:#C4A7E7;font-style:italic\"> onChange</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">item</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">toggleCompleted</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">                  )</span><span style=\"color:#908CAA\">}</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                &#x3C;/</span><span style=\"color:#9CCFD8\">LiveValue</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                &#x3C;</span><span style=\"color:#9CCFD8\">label</span><span style=\"color:#6E6A86\"> ></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                  &#x3C;</span><span style=\"color:#9CCFD8\">LiveValue</span><span style=\"color:#C4A7E7;font-style:italic\"> query</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">item</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">text$</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\"> /></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                &#x3C;/</span><span style=\"color:#9CCFD8\">label</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">                &#x3C;</span><span style=\"color:#9CCFD8\">button</span><span style=\"color:#C4A7E7;font-style:italic\"> type</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"button\"</span><span style=\"color:#C4A7E7;font-style:italic\"> className</span><span style=\"color:#31748F\">=</span><span style=\"color:#F6C177\">\"destroy\"</span><span style=\"color:#C4A7E7;font-style:italic\"> onClick</span><span style=\"color:#31748F\">=</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\">item</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">remove</span><span style=\"color:#908CAA\">}</span><span style=\"color:#6E6A86\"> /></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">              &#x3C;/</span><span style=\"color:#9CCFD8\">li</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">            ))</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">          }</span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">        &#x3C;/</span><span style=\"color:#9CCFD8\">LiveValue</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">      &#x3C;/</span><span style=\"color:#9CCFD8\">ul</span><span style=\"color:#6E6A86\">></span></span>\n<span class=\"line\"><span style=\"color:#6E6A86\">    &#x3C;/</span><span style=\"color:#9CCFD8\">section</span><span style=\"color:#6E6A86\">></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\">};</span></span></code></pre>\n  </div>\n   \n</div>\n  </div>\n</div></div>\n  </div>\n</div>\n<h2 id=\"animation-triggers\">Animation Triggers <div data-loc=\"content/view-model-interfaces.md:247\" class=\"heading-src\">⋅</div></h2>\n<p>View Models can expose animation triggers using <code>PrimitiveAtom&lt;number&gt;</code> for precise control over visual feedback:</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;\">interface </span><span>ProposalVM {\n</span><span>  </span><span style=\"color:#65737e;\">// ... other properties\n</span><span>\n</span><span>  </span><span style=\"color:#65737e;\">// Animation triggers - writable atoms that increment to trigger effects\n</span><span>  </span><span style=\"color:#bf616a;\">animationTriggers</span><span>: {\n</span><span>    </span><span style=\"color:#bf616a;\">commitAddedAtom</span><span>: PrimitiveAtom&lt;number&gt;; </span><span style=\"color:#65737e;\">// Flash when commit added\n</span><span>    </span><span style=\"color:#bf616a;\">mergedAtom</span><span>: PrimitiveAtom&lt;number&gt;; </span><span style=\"color:#65737e;\">// Pulse when merged\n</span><span>  };\n</span><span>}\n</span></code></pre>\n<p><strong>Why this pattern?</strong></p>\n<ul>\n<li><strong>Declarative</strong>: Reading the interface reveals animation capabilities</li>\n<li><strong>Testable</strong>: Writable atoms allow resetting and manual triggering in tests</li>\n<li><strong>Framework-agnostic</strong>: Works with any reactive renderer</li>\n<li><strong>Self-describing</strong>: Clear semantic meaning in the interface</li>\n</ul>\n<p><strong>Component consumption:</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:#8fa1b3;\">ProposalComponent </span><span>= ({ </span><span style=\"color:#bf616a;\">proposal </span><span>}: { </span><span style=\"color:#bf616a;\">proposal</span><span>: ProposalVM }) </span><span style=\"color:#b48ead;\">=&gt; </span><span>{\n</span><span>  </span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">trigger </span><span>= </span><span style=\"color:#8fa1b3;\">useAtomValue</span><span>(</span><span style=\"color:#bf616a;\">proposal</span><span>.</span><span style=\"color:#bf616a;\">animationTriggers</span><span>.</span><span style=\"color:#bf616a;\">commitAddedAtom</span><span>);\n</span><span>  </span><span style=\"color:#b48ead;\">const </span><span>[</span><span style=\"color:#bf616a;\">isAnimating</span><span>, </span><span style=\"color:#bf616a;\">setIsAnimating</span><span>] = </span><span style=\"color:#8fa1b3;\">useState</span><span>(</span><span style=\"color:#d08770;\">false</span><span>);\n</span><span>\n</span><span>  </span><span style=\"color:#8fa1b3;\">useEffect</span><span>(() </span><span style=\"color:#b48ead;\">=&gt; </span><span>{\n</span><span>    </span><span style=\"color:#b48ead;\">if </span><span>(</span><span style=\"color:#bf616a;\">trigger </span><span>&gt; </span><span style=\"color:#d08770;\">0</span><span>) {\n</span><span>      </span><span style=\"color:#8fa1b3;\">setIsAnimating</span><span>(</span><span style=\"color:#d08770;\">true</span><span>);\n</span><span>      </span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">timer </span><span>= </span><span style=\"color:#96b5b4;\">setTimeout</span><span>(() </span><span style=\"color:#b48ead;\">=&gt; </span><span style=\"color:#8fa1b3;\">setIsAnimating</span><span>(</span><span style=\"color:#d08770;\">false</span><span>), </span><span style=\"color:#d08770;\">500</span><span>);\n</span><span>      </span><span style=\"color:#b48ead;\">return </span><span>() </span><span style=\"color:#b48ead;\">=&gt; </span><span style=\"color:#96b5b4;\">clearTimeout</span><span>(</span><span style=\"color:#bf616a;\">timer</span><span>);\n</span><span>    }\n</span><span>  }, [</span><span style=\"color:#bf616a;\">trigger</span><span>]);\n</span><span>\n</span><span>  </span><span style=\"color:#65737e;\">// Use OKLCH for dynamic colors\n</span><span>  </span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">color </span><span>= </span><span style=\"color:#bf616a;\">proposal</span><span>.backgroundColor;\n</span><span>  </span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">animStyle </span><span>= </span><span style=\"color:#bf616a;\">isAnimating </span><span>? {\n</span><span>    boxShadow: `</span><span style=\"color:#a3be8c;\">0 0 0 2px oklch(0.75 ${</span><span style=\"color:#bf616a;\">color</span><span style=\"color:#a3be8c;\">.</span><span style=\"color:#bf616a;\">chroma</span><span style=\"color:#a3be8c;\">} ${</span><span style=\"color:#bf616a;\">color</span><span style=\"color:#a3be8c;\">.</span><span style=\"color:#bf616a;\">hue</span><span style=\"color:#a3be8c;\">})</span><span>`,\n</span><span>    backgroundColor: `</span><span style=\"color:#a3be8c;\">oklch(0.95 ${</span><span style=\"color:#bf616a;\">color</span><span style=\"color:#a3be8c;\">.</span><span style=\"color:#bf616a;\">chroma </span><span>* </span><span style=\"color:#d08770;\">0.3</span><span style=\"color:#a3be8c;\">} ${</span><span style=\"color:#bf616a;\">color</span><span style=\"color:#a3be8c;\">.</span><span style=\"color:#bf616a;\">hue</span><span style=\"color:#a3be8c;\">})</span><span>`,\n</span><span>  } : {};\n</span><span>\n</span><span>  </span><span style=\"color:#b48ead;\">return </span><span>&lt;div style={</span><span style=\"color:#bf616a;\">animStyle</span><span>} className=&quot;</span><span style=\"color:#a3be8c;\">transition-all duration-500</span><span>&quot;&gt;\n</span><span>    {</span><span style=\"color:#65737e;\">/* content */</span><span>}\n</span><span>  &lt;/</span><span style=\"color:#bf616a;\">div</span><span>&gt;;\n</span><span>};\n</span></code></pre>\n<p><strong>Business logic side:</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:#8fa1b3;\">dispatchCommit </span><span>= (</span><span style=\"color:#bf616a;\">proposalId</span><span>: ProposalID) </span><span style=\"color:#b48ead;\">=&gt; </span><span>{\n</span><span>  </span><span style=\"color:#65737e;\">// ... commit logic\n</span><span>\n</span><span>  </span><span style=\"color:#65737e;\">// Trigger animation\n</span><span>  </span><span style=\"color:#b48ead;\">const </span><span style=\"color:#bf616a;\">atoms </span><span>= </span><span style=\"color:#8fa1b3;\">memoProposalAnimationAtoms</span><span>(</span><span style=\"color:#bf616a;\">proposalId</span><span>);\n</span><span>  </span><span style=\"color:#bf616a;\">jotaiStore</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">atoms</span><span>.</span><span style=\"color:#bf616a;\">commitAddedAtom</span><span>, (</span><span style=\"color:#bf616a;\">prev</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span style=\"color:#bf616a;\">prev </span><span>+ </span><span style=\"color:#d08770;\">1</span><span>);\n</span><span>};\n</span></code></pre>\n<p><strong>Testing:</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;\">// Reset animation state\n</span><span style=\"color:#bf616a;\">jotaiStore</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">proposal</span><span>.</span><span style=\"color:#bf616a;\">animationTriggers</span><span>.</span><span style=\"color:#bf616a;\">commitAddedAtom</span><span>, </span><span style=\"color:#d08770;\">0</span><span>);\n</span><span>\n</span><span style=\"color:#65737e;\">// Manually trigger for testing\n</span><span style=\"color:#bf616a;\">jotaiStore</span><span>.</span><span style=\"color:#96b5b4;\">set</span><span>(</span><span style=\"color:#bf616a;\">proposal</span><span>.</span><span style=\"color:#bf616a;\">animationTriggers</span><span>.</span><span style=\"color:#bf616a;\">commitAddedAtom</span><span>, (</span><span style=\"color:#bf616a;\">n</span><span>) </span><span style=\"color:#b48ead;\">=&gt; </span><span style=\"color:#bf616a;\">n </span><span>+ </span><span style=\"color:#d08770;\">1</span><span>);\n</span></code></pre>\n<p>This pattern ensures animations are triggered from business logic events while keeping the UI a pure projection of state.</p>\n<h2 id=\"defining-a-scope\">Defining a Scope <div data-loc=\"content/view-model-interfaces.md:322\" class=\"heading-src\">⋅</div></h2>\n<div class=\"codeblock\" data-loc=\"content&#x2F;view-model-interfaces.md:324\">\n  \n  <div class=\"codeblock-content\" data-id=\"todo-scope-livestore\">\n    <pre class=\"shiki rose-pine\" style=\"background-color:#191724;color:#e0def4\" tabindex=\"0\" data-loc=\"content/_sources/view-model-interfaces/todo-vm/scope.ts:38\"><code><span class=\"line muted\"><span style=\"color:#31748F\">const</span><span style=\"color:#EBBCBA;font-style:italic\"> createID</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">name</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#F6C177\"> `</span><span style=\"color:#908CAA\">${</span><span style=\"color:#E0DEF4;font-style:italic\">name</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">_</span><span style=\"color:#908CAA\">${</span><span style=\"color:#EBBCBA\">nanoid</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">12</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">}</span><span style=\"color:#F6C177\">`</span><span style=\"color:#908CAA\">;</span><span style=\"color:#E0DEF4\"> </span></span>\n<span class=\"line\"><span style=\"color:#31748F\">export</span><span style=\"color:#31748F\"> function</span><span style=\"color:#EBBCBA\"> createTodoListScope</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">store</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> Store</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> TodoListVM</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#31748F\">  const</span><span style=\"color:#E0DEF4;font-style:italic\"> currentFilter$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> computed</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\">uiState$</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">filter</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"filter\"</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\">  const</span><span style=\"color:#E0DEF4;font-style:italic\"> newTodoText$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> computed</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\">uiState$</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">newTodoText</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"newTodoText\"</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\">  const</span><span style=\"color:#E0DEF4;font-style:italic\"> createTodoItemVM</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> memoFn</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">id</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> TodoItemVM</span><span style=\"color:#31748F\"> =></span><span style=\"color:#908CAA\"> {</span></span>\n<details class=\"code-fold\" data-fold-kind=\"indent\"><summary class=\"code-fold-summary\"><span class=\"line\"><span style=\"color:#31748F\">    const</span><span style=\"color:#E0DEF4;font-style:italic\"> completed$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> queryDb</span><span style=\"color:#E0DEF4\">(</span></span><span class=\"code-fold-ellipsis\" aria-hidden=\"true\">[…]</span></summary><div class=\"code-fold-content\"><span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      tables</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">todos</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">select</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"completed\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">where</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">first</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> behaviour</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"error\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">      {</span><span style=\"color:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"todoItem.completed\"</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> deps</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">id</span><span style=\"color:#E0DEF4\">] </span><span style=\"color:#908CAA\">},</span></span></div></details><span class=\"line\"><span style=\"color:#E0DEF4\">    )</span><span style=\"color:#908CAA\">;</span></span>\n<details class=\"code-fold\" data-fold-kind=\"indent\"><summary class=\"code-fold-summary\"><span class=\"line\"><span style=\"color:#31748F\">    const</span><span style=\"color:#E0DEF4;font-style:italic\"> text$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> queryDb</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">tables</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">todos</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">select</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"text\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">where</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">first</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> behaviour</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"error\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA\"> {</span></span><span class=\"code-fold-ellipsis\" aria-hidden=\"true\">[…]</span></summary><div class=\"code-fold-content\"><span class=\"line\"><span style=\"color:#E0DEF4\">      label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"todoItem.text\"</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">      deps</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">id</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#908CAA\">,</span></span></div></details><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\">    return</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">      key</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      completed$</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      text$</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      toggleCompleted</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">        store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">query</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">completed$</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">?</span><span style=\"color:#E0DEF4;font-style:italic\"> events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">todoUncompleted</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#31748F\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">todoCompleted</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      remove</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">todoDeleted</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4;font-style:italic\"> id</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> deletedAt</span><span style=\"color:#908CAA\">:</span><span style=\"color:#31748F\"> new</span><span style=\"color:#EBBCBA\"> Date</span><span style=\"color:#E0DEF4\">() </span><span style=\"color:#908CAA\">}</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">    };</span></span>\n<span class=\"line\"><span style=\"color:#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:#EBBCBA;font-style:italic\"> visibleTodosQuery</span><span style=\"color:#31748F\"> =</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">filter</span><span style=\"color:#31748F\">:</span><span style=\"color:#F6C177\"> \"all\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"active\"</span><span style=\"color:#31748F\"> |</span><span style=\"color:#F6C177\"> \"completed\"</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">    queryDb</span><span style=\"color:#E0DEF4\">(</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">      ()</span><span style=\"color:#31748F\"> =></span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">        tables</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">todos</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">where</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">          completed</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> filter</span><span style=\"color:#31748F\"> ===</span><span style=\"color:#F6C177\"> \"all\"</span><span style=\"color:#31748F\"> ?</span><span style=\"color:#EBBCBA\"> undefined</span><span style=\"color:#31748F\"> :</span><span style=\"color:#E0DEF4;font-style:italic\"> filter</span><span style=\"color:#31748F\"> ===</span><span style=\"color:#F6C177\"> \"completed\"</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">          deletedAt</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> {</span><span style=\"color:#E0DEF4\"> op</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"=\"</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> value</span><span style=\"color:#908CAA\">:</span><span style=\"color:#EBBCBA\"> null</span><span style=\"color:#908CAA\"> },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">        }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">      {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">        label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"visibleTodos\"</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">        map</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">rows</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> rows</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">map</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">(</span><span style=\"color:#C4A7E7;font-style:italic\">row</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#EBBCBA\"> createTodoItemVM</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">row</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">id</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">        deps</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4\"> [</span><span style=\"color:#E0DEF4;font-style:italic\">filter</span><span style=\"color:#E0DEF4\">]</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">      },</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">    )</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"></span>\n<details class=\"code-fold\" data-fold-kind=\"indent\"><summary class=\"code-fold-summary\"><span class=\"line\"><span style=\"color:#31748F\">  const</span><span style=\"color:#E0DEF4;font-style:italic\"> visibleTodos$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> computed</span><span style=\"color:#E0DEF4\">(</span></span><span class=\"code-fold-ellipsis\" aria-hidden=\"true\">[…]</span></summary><div class=\"code-fold-content\"><span class=\"line\"><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\">      const</span><span style=\"color:#E0DEF4;font-style:italic\"> filter</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">currentFilter$</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:#EBBCBA\"> get</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#EBBCBA\">visibleTodosQuery</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">filter</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:#E0DEF4\"> label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"visibleTodos\"</span><span style=\"color:#908CAA\"> },</span></span></div></details><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\"> incompleteCount$</span><span style=\"color:#31748F\"> =</span><span style=\"color:#EBBCBA\"> queryDb</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">tables</span><span style=\"color:#31748F\">.</span><span style=\"color:#E0DEF4;font-style:italic\">todos</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">count</span><span style=\"color:#E0DEF4\">()</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">where</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> completed</span><span style=\"color:#908CAA\">:</span><span style=\"color:#EBBCBA\"> false</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> deletedAt</span><span style=\"color:#908CAA\">:</span><span style=\"color:#EBBCBA\"> null</span><span style=\"color:#908CAA\"> }</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\">    label</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"incompleteCount\"</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>\n<span class=\"line\"><span style=\"color:#31748F\">  return</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">    header</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      newTodoText$</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      updateNewTodoText</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> (</span><span style=\"color:#C4A7E7;font-style:italic\">text</span><span style=\"color:#31748F\">:</span><span style=\"color:#9CCFD8\"> string</span><span style=\"color:#908CAA\">)</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">uiStateSet</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> newTodoText</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> text</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      addTodo</span><span style=\"color:#908CAA\">:</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\"> newTodoText</span><span style=\"color:#31748F\"> =</span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">query</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">newTodoText$</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">trim</span><span style=\"color:#E0DEF4\">()</span><span style=\"color:#908CAA\">;</span></span>\n<span class=\"line\"><span style=\"color:#31748F\">        if</span><span style=\"color:#E0DEF4\"> (</span><span style=\"color:#E0DEF4;font-style:italic\">newTodoText</span><span style=\"color:#E0DEF4\">) </span><span style=\"color:#908CAA\">{</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">          store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">            events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">todoCreated</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> id</span><span style=\"color:#908CAA\">:</span><span style=\"color:#EBBCBA\"> createID</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#F6C177\">\"todo\"</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span><span style=\"color:#E0DEF4\"> text</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> newTodoText</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\">            events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">uiStateSet</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> newTodoText</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">)</span><span style=\"color:#908CAA\">,</span><span style=\"color:#908CAA;font-style:italic\"> //</span><span style=\"color:#6E6A86;font-style:italic\"> update text</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\">        }</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:#E0DEF4\">    itemList</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4\">      items$</span><span style=\"color:#908CAA\">:</span><span style=\"color:#E0DEF4;font-style:italic\"> visibleTodos$</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\">    footer</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      incompleteCount$</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#E0DEF4;font-style:italic\">      currentFilter$</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      showAll</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">uiStateSet</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> filter</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"all\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      showActive</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">uiStateSet</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> filter</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"active\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      showCompleted</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">uiStateSet</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> filter</span><span style=\"color:#908CAA\">:</span><span style=\"color:#F6C177\"> \"completed\"</span><span style=\"color:#908CAA\"> }</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#EBBCBA\">      clearCompleted</span><span style=\"color:#908CAA\">:</span><span style=\"color:#908CAA\"> ()</span><span style=\"color:#31748F\"> =></span><span style=\"color:#E0DEF4;font-style:italic\"> store</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">commit</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#E0DEF4;font-style:italic\">events</span><span style=\"color:#31748F\">.</span><span style=\"color:#EBBCBA\">todoClearedCompleted</span><span style=\"color:#E0DEF4\">(</span><span style=\"color:#908CAA\">{</span><span style=\"color:#E0DEF4\"> deletedAt</span><span style=\"color:#908CAA\">:</span><span style=\"color:#31748F\"> new</span><span style=\"color:#EBBCBA\"> Date</span><span style=\"color:#E0DEF4\">() </span><span style=\"color:#908CAA\">}</span><span style=\"color:#E0DEF4\">))</span><span style=\"color:#908CAA\">,</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">    },</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">  };</span></span>\n<span class=\"line\"><span style=\"color:#908CAA\">}</span></span></code></pre>\n  </div>\n  \n</div>\n",
      "permalink": "/view-model-interfaces/",
      "slug": "view-model-interfaces",
      "ancestors": [
        "_index.md"
      ],
      "title": "Please write view model interfaces",
      "description": "Designing the boundary between view and business logic.",
      "updated": null,
      "date": "2025-10-17",
      "year": 2025,
      "month": 10,
      "day": 17,
      "taxonomies": {},
      "authors": [],
      "extra": {
        "category": "engineering",
        "nav_section": "Managing Complexity",
        "nav_order": 1
      },
      "path": "/view-model-interfaces/",
      "components": [
        "view-model-interfaces"
      ],
      "summary": null,
      "toc": [
        {
          "level": 1,
          "id": "view-model-interfaces",
          "permalink": "/view-model-interfaces/#view-model-interfaces",
          "title": "View Model Interfaces ⋅",
          "children": [
            {
              "level": 2,
              "id": "thesis-why-this-matters",
              "permalink": "/view-model-interfaces/#thesis-why-this-matters",
              "title": "Thesis (why this matters) ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "definition-what-it-is",
              "permalink": "/view-model-interfaces/#definition-what-it-is",
              "title": "Definition (what it is) ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "principles-how-to-shape-it",
              "permalink": "/view-model-interfaces/#principles-how-to-shape-it",
              "title": "Principles (how to shape it) ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "when-it-shines-when-not-to-use",
              "permalink": "/view-model-interfaces/#when-it-shines-when-not-to-use",
              "title": "When it shines / When not to use ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "testing-workflow-evidence-over-rhetoric",
              "permalink": "/view-model-interfaces/#testing-workflow-evidence-over-rhetoric",
              "title": "Testing Workflow (evidence over rhetoric) ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "introduction",
              "permalink": "/view-model-interfaces/#introduction",
              "title": "Introduction ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "benefits",
              "permalink": "/view-model-interfaces/#benefits",
              "title": "Benefits ⋅",
              "children": []
            }
          ]
        },
        {
          "level": 1,
          "id": "example",
          "permalink": "/view-model-interfaces/#example",
          "title": "Example ⋅",
          "children": [
            {
              "level": 2,
              "id": "animation-triggers",
              "permalink": "/view-model-interfaces/#animation-triggers",
              "title": "Animation Triggers ⋅",
              "children": []
            },
            {
              "level": 2,
              "id": "defining-a-scope",
              "permalink": "/view-model-interfaces/#defining-a-scope",
              "title": "Defining a Scope ⋅",
              "children": []
            }
          ]
        }
      ],
      "word_count": 1900,
      "reading_time": 10,
      "assets": [],
      "draft": true,
      "lang": "en",
      "lower": null,
      "higher": null,
      "translations": [],
      "backlinks": []
    },
    "translations": [],
    "backlinks": []
  },
  "zola_version": "0.21.0"
}