{
  "docs": {
    "gettingStarted": {
      "hero": {
        "title": "Documentation",
        "subtitle": "Everything you need to build reactive web apps with No.JS"
      },
      "introduction": {
        "title": "Introduction",
        "text": "No.JS is an HTML-first reactive framework. Build dynamic, data-driven web applications using nothing but HTML attributes — no build step, no virtual DOM, no JSX.",
        "callout": "Zero dependencies · Works in all modern browsers · No build step required"
      },
      "installation": {
        "title": "Installation",
        "cdnSubtitle": "CDN (recommended)",
        "selfHostedSubtitle": "Self-hosted",
        "selfHostedText": "Download dist/iife/no.js and include it with a <script> tag. It's a single file."
      },
      "quickStart": {
        "title": "Quick Start",
        "text": "Create an index.html file: include the script, add some attributes, and you're done. No app.mount(), no createApp(), no NgModule. It just works."
      },
      "howItWorks": {
        "title": "How It Works",
        "text": "On DOMContentLoaded, No.JS walks the DOM looking for elements with known attributes. Each attribute maps to a directive that is executed by priority.",
        "card1Title": "1. Parse",
        "card1Desc": "Walks the DOM looking for elements with known attributes.",
        "card2Title": "2. Resolve",
        "card2Desc": "Each attribute maps to a directive executed by priority (data fetching first, then conditionals, then rendering).",
        "card3Title": "3. React",
        "card3Desc": "All data lives in reactive contexts (Proxy-backed). When data changes, every bound element updates automatically.",
        "card4Title": "4. Scope",
        "card4Desc": "Contexts inherit from parent elements, like lexical scoping. A bind inside an each loop can access both the loop item and ancestor data."
      },
      "coreConcepts": {
        "title": "Core Concepts",
        "reactiveContextSubtitle": "Reactive Context",
        "reactiveContextText": "Every element can have a context — a reactive data object. Contexts are created by state, get, store, etc. Child elements inherit their parent's context automatically.",
        "directivePrioritySubtitle": "Directive Priority",
        "tableCol1": "Priority",
        "tableCol2": "Directives",
        "tableCol3": "Description",
        "tableRow1": "Initialize local/global state",
        "tableRow4": "Structural (add/remove DOM)",
        "tableRow5": "Rendering (update existing DOM)",
        "tableRow7": "Side effects",
        "tableRow1b": "Fetch data, error boundaries, i18n namespace",
        "tableRow2b": "Derived values and side-effect watchers",
        "tableRow5b": "Element references",
        "tableRow15": "Drag and drop setup (Now in NoJS Elements)",
        "expressionSubtitle": "Expression Syntax",
        "expressionText": "Most directive values accept JavaScript expressions evaluated against the current context:"
      }
    },
    "cheatsheet": {
      "data": {
        "title": "Data",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "base": "Set API base URL for descendants",
        "get": "Fetch data (GET)",
        "post": "Submit data (POST)",
        "put": "Update data (PUT)",
        "patch": "Partial update (PATCH)",
        "delete": "Delete data (DELETE)",
        "query": "Read with a body (QUERY, RFC 10008)",
        "as": "Name for fetched data in context",
        "body": "Request body",
        "headers": "Request headers",
        "params": "Query parameters",
        "cached": "Cache responses (memory/local/session)",
        "into": "Write response to a named global store",
        "debounce": "Debounce reactive URL refetches (ms)",
        "retry": "Per-element retry count override",
        "refresh": "Polling interval in ms",
        "success": "Template ID to show on success",
        "then": "Expression to execute on success",
        "redirect": "Path to navigate after success",
        "confirm": "Confirmation message before request",
        "skeleton": "Show/hide a placeholder element during loading",
        "retryDelay": "Delay between retries in ms",
        "sse": "Open an SSE stream (EventSource)",
        "sseEvent": "Named event to listen for",
        "sseInsert": "How messages update the variable (replace, append, prepend)",
        "sseLimit": "Maximum array length in append/prepend mode",
        "sseCredentials": "Send cookies to cross-origin endpoints"
      },
      "state": {
        "title": "State",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "state": "Create local reactive state",
        "store": "Define/access global store",
        "computed": "Derived reactive value",
        "watch": "React to value changes",
        "persist": "Attribute of state directive — persists state to storage",
        "persistKey": "Storage key for persistence",
        "persistFields": "Comma-separated fields to persist",
        "persistSchema": "Validate restored keys against initial state",
        "model": "Two-way binding for inputs"
      },
      "rendering": {
        "title": "Rendering",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "bind": "Set text content",
        "bindHtml": "Set innerHTML (sanitized)",
        "bindStar": "Bind any attribute",
        "if": "Conditional render",
        "elseIf": "Chained conditional",
        "then": "Template for truthy",
        "else": "Template for falsy",
        "show": "Toggle visibility (CSS)",
        "hide": "Inverse of show",
        "switch": "Switch/case render",
        "case": "Case match",
        "default": "Default case"
      },
      "loops": {
        "title": "Loops",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "foreach": "Primary loop directive",
        "each": "Alias of foreach",
        "for": "Alias of foreach",
        "from": "Source array (DEPRECATED — use \"item in array\" syntax instead)",
        "template": "Template to clone",
        "index": "Index variable name",
        "key": "Unique key for diffing",
        "filter": "Filter expression",
        "sort": "Sort property (name only, not prefixed with item variable)",
        "limit": "Max items",
        "offset": "Skip items",
        "else": "Template rendered when the array is empty or null/undefined"
      },
      "events": {
        "title": "Events",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "onClick": "Click handler",
        "onSubmit": "Submit handler",
        "onInput": "Input handler",
        "onKeydown": "Key handler",
        "onInit": "Fires immediately during initialization",
        "onMounted": "Lifecycle: mounted",
        "onUnmounted": "Lifecycle: unmounted",
        "throttle": "Throttle handler execution (ms)",
        "self": "Only fires if event target is the element itself",
        "backspace": "Key modifier for Backspace key",
        "onUpdated": "Lifecycle: DOM mutation observed",
        "onError": "Lifecycle: error in subtree"
      },
      "styling": {
        "title": "Styling",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "classStar": "Toggle CSS class",
        "classMap": "Class from object",
        "styleStar": "Set inline style",
        "styleMap": "Style from object"
      },
      "forms": {
        "title": "Forms",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "validate": "Enable form/field validation (Now in NoJS Elements)",
        "error": "Error template for field",
        "success": "Success template",
        "loading": "Loading template",
        "confirm": "Confirmation dialog",
        "redirect": "Redirect on success"
      },
      "routing": {
        "title": "Routing",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "route": "Define route or link",
        "routeView": "Route outlet",
        "routeViewNamed": "Named route outlet",
        "outlet": "Target a named outlet",
        "routeActive": "Active link class",
        "guard": "Route guard condition",
        "routeActiveExact": "Exact match active class for route links",
        "redirect": "Redirect path when guard fails",
        "lazyPriority": "Load remote template before all others (Phase 0)",
        "lazyOnDemand": "Fetch route template only when first visited (route templates only)",
        "routerForward": "Navigate forward in history",
        "routerOn": "Subscribe to route changes",
        "routeWildcard": "Catch-all 404 wildcard route",
        "routerCurrent": "Current route object",
        "routeMatched": "<code>true</code> if an explicit route matched, <code>false</code> for wildcard/fallback",
        "i18nNs": "Auto-derive i18n namespace from route filename",
        "routeViewSrc": "File-based routing outlet",
        "routeIndex": "Filename for root <code>/</code> (default <code>\"index\"</code>)",
        "routeExt": "File extension (default <code>\".tpl\"</code>, fallback <code>\".html\"</code>)",
        "transitionVT": "View Transition API preset on route-view (slide, fade, scale, none)"
      },
      "animation": {
        "title": "Animation",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "animate": "Enter animation",
        "animateEnter": "Enter animation",
        "animateLeave": "Leave animation",
        "animateDuration": "Duration in ms",
        "animateStagger": "Stagger delay",
        "transition": "CSS transition (class-based, for regular elements)",
        "transitionVT": "View Transition API preset on route-view (slide, fade, scale, none)"
      },
      "dnd": {
        "title": "Drag and Drop (Now in NoJS Elements)",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "drag": "Make element draggable",
        "dragType": "Data type identifier",
        "dragEffect": "Allowed effect (move/copy/link/all)",
        "dragHandle": "Restrict drag to handle selector",
        "dragDisabled": "Disable drag conditionally",
        "dragClass": "Class added while dragging",
        "dragGroup": "Scope drag to a named group",
        "drop": "Make element a drop zone",
        "dropAccept": "Accepted drag type(s)",
        "dropEffect": "Visual feedback effect",
        "dropClass": "Class added on drag-over",
        "dropDisabled": "Disable drop conditionally",
        "dropMax": "Maximum items in drop zone",
        "dropSort": "Enable positional sorting",
        "dropPlaceholder": "Placeholder template during drag-over",
        "dropSettleClass": "Custom CSS class for settle animation",
        "dropEmptyClass": "Custom CSS class for empty state on drag-list",
        "dragList": "Sortable list bound to state array",
        "dragListKey": "Unique key for each item",
        "dragListItem": "Item template selector",
        "dragListCopy": "Copy instead of move on transfer",
        "dragListRemove": "Remove items from source on transfer",
        "dragMultiple": "Lasso / multi-select on children",
        "dragMultipleClass": "Class added to selected items",
        "dropRejectClass": "Class added on rejected drag-over"
      },
      "i18n": {
        "title": "i18n",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "t": "Translate key",
        "tStar": "Translation param",
        "tHtml": "Render translation as sanitized HTML"
      },
      "headManagement": {
        "title": "Head Management",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "pageTitle": "Set document.title reactively",
        "pageDescription": "Set <meta name=\"description\">",
        "pageCanonical": "Set <link rel=\"canonical\">",
        "pageJsonld": "Set <script type=\"application/ld+json\">"
      },
      "misc": {
        "title": "Misc",
        "col1": "Directive",
        "col2": "Example",
        "col3": "Description",
        "ref": "Named element ref",
        "call": "Trigger API call",
        "trigger": "Emit custom event",
        "use": "Instantiate template",
        "src": "Remote template (see also: lazy)",
        "loading": "Placeholder shown while remote template loads; removed on arrival",
        "include": "Synchronously clone an inline template into the current position",
        "errorBoundary": "Error boundary",
        "var": "Template variable name"
      }
    },
    "stateManagement": {
      "state": {
        "title": "state — Local State",
        "text": "Creates a reactive context scoped to the element and its children.",
        "preview": "Preview",
        "helloLabel": "Hello,",
        "countLabel": "Count:",
        "resetBtn": "Reset"
      },
      "store": {
        "title": "store — Global Store",
        "text": "A global reactive store accessible from anywhere. Ideal for auth state, theme, shared data."
      },
      "configStores": {
        "title": "Pre-initializing Stores via config()",
        "text": "You can pre-create stores with initial data inside NoJS.config(). Each key in the stores object becomes a named global store, accessible immediately via $store.name.",
        "callout": "If a store already exists (e.g. created via a store attribute), config() will not overwrite it — the existing store is preserved."
      },
      "into": {
        "title": "into — Write Fetch Results to a Store",
        "text": "The into attribute on any HTTP directive writes the response directly into a named global store.",
        "callout": "The store doesn't need to be pre-defined — into will create it if it doesn't exist."
      },
      "computed": {
        "title": "computed — Derived State",
        "text": "Values that are automatically recalculated when dependencies change.",
        "preview": "Preview",
        "priceLabel": "Price:",
        "qtyLabel": "Qty:",
        "subtotalLabel": "Subtotal:",
        "totalLabel": "Total:"
      },
      "watch": {
        "title": "watch — Side Effects",
        "text": "Execute an action whenever a value changes."
      },
      "persistence": {
        "title": "State Persistence",
        "text": "Persist state across page reloads using localStorage or sessionStorage."
      },
      "persistFields": {
        "title": "persist-fields — Selective Persistence",
        "text": "Use persist-fields to control exactly which fields are saved and restored. Fields not listed are never written to storage, which is useful for keeping sensitive values (tokens, passwords) out of localStorage/sessionStorage.",
        "text2": "persist-fields accepts a comma-separated list of field names. Whitespace around each name is trimmed, so \"theme, sidebar\" and \"theme,sidebar\" are equivalent.",
        "callout": "Omit persist-fields to persist all fields. When present, only the listed fields are saved — all other fields use their initial values on restore."
      },
      "persistSchema": {
        "title": "persist-schema — Schema Validation",
        "text": "When restoring persisted state, persist-schema validates that restored keys match the initial state shape. Keys not present in the initial state are discarded. This prevents stale or corrupted data from breaking your app after schema changes."
      },
      "scoping": {
        "title": "Context Scoping & Shadowing",
        "text": "Contexts inherit from parent elements like lexical scoping. A child state can shadow a parent property — the child sees its own value, while siblings see the parent's.",
        "callout": "Shadowing is intentional and follows JavaScript's scoping rules. If you need shared state across siblings, use a store instead."
      },
      "notify": {
        "title": "NoJS.notify() — Flush Store Updates",
        "text": "When external JavaScript mutates a store via NoJS.store, call NoJS.notify() to flush all pending DOM updates.",
        "callout": "Only needed when mutating NoJS.store from plain JavaScript — outside of HTML expressions. Store mutations inside on:click or bind expressions are handled automatically."
      }
    },
    "dataBinding": {
      "bind": {
        "title": "bind — Text Content",
        "text": "Replaces the element's textContent with the evaluated expression.",
        "filtersNote": "Expressions inside <code>bind</code> support <a href=\"/docs/filters\">pipe filters</a> to transform values for display:"
      },
      "bindHtml": {
        "title": "bind-html — Inner HTML",
        "text": "Renders evaluated expression as HTML. Sanitized by default.",
        "callout": "⚠️ Uses built-in DOMParser-based structural sanitization: strips <script> tags, blocks on* event handlers, and removes javascript: URIs.",
        "sanitizerTip": "<strong>Tip:</strong> You can provide a custom sanitizer via <code>NoJS.config({ sanitizeHtml: fn })</code>. See <a href=\"/docs/configuration#security\">Configuration &rarr; Security</a> for details."
      },
      "bindAttr": {
        "title": "bind-* — Attribute Binding",
        "text": "Bind any HTML attribute dynamically."
      },
      "model": {
        "title": "model — Two-Way Binding",
        "text": "For form inputs, model creates automatic two-way data binding.",
        "preview": "Preview",
        "nameLabel": "Name",
        "placeholder": "Type your name...",
        "checkbox": "I agree",
        "agreedLabel": "Agreed:",
        "helloPrefix": "Hello,"
      },
      "radioSelect": {
        "title": "Radio Buttons & Multi-Select",
        "text": "model works with all input types. For radio buttons, bind the same model name to a group — the value updates to the selected radio's value.",
        "multiSelectWarning": "⚠️ Multi-select model binding is not yet implemented. The current model directive uses el.value for <select> elements and does not read selectedOptions or handle the multiple attribute. An array-backed model will write back a single string value, not an array of selected values. This feature is planned for a future release. The example below is shown for reference only."
      },
      "commonMistakes": {
        "title": "Common Mistakes",
        "text": "Avoid these common data binding pitfalls:",
        "mistake1": "Using bind-html without understanding sanitization — all content is sanitized by default, but be aware of what gets stripped.",
        "mistake2": "Forgetting that bind replaces textContent — any child elements inside a bind target are removed.",
        "mistake3": "Using model on non-form elements — model only works on input, textarea, and select elements."
      }
    },
    "events": {
      "handlers": {
        "title": "on:* — Event Handlers",
        "text": "Bind any DOM event directly in HTML. Access state and context variables directly in the handler expression.",
        "preview": "Preview",
        "youTyped": "You typed:",
        "countLabel": "Count:"
      },
      "modifiers": {
        "title": "Event Modifiers",
        "text": "Modifiers let you control event behavior directly in the attribute:",
        "col1": "Modifier",
        "col2": "Description",
        "prevent": "Calls preventDefault() on the event",
        "stop": "Calls stopPropagation() on the event",
        "once": "Handler fires only once, then is automatically removed",
        "self": "Only fires if event.target is the element itself",
        "debounce": "Debounce the handler execution (append delay in ms, e.g. .debounce.300)",
        "throttle": "Throttle the handler execution (append interval in ms, e.g. .throttle.200)"
      },
      "eventAndEl": {
        "title": "$event & $el",
        "text": "$event is the native DOM event. $el refers to the current element."
      },
      "lifecycle": {
        "title": "Lifecycle Hooks",
        "col1": "Hook",
        "col2": "When",
        "onInit": "Directive first processed",
        "onMounted": "Element inserted into visible DOM",
        "onUpdated": "DOM mutation observed (via MutationObserver)",
        "onUnmounted": "Element removed from DOM",
        "onError": "Error in this element's subtree"
      },
      "keyModifiers": {
        "title": "Key Modifiers",
        "text": "Use key modifiers on keyboard events to filter by specific keys. Combine with Ctrl, Alt, Shift, or Meta for shortcuts.",
        "col1": "Modifier",
        "col2": "Key",
        "enter": "Enter / Return",
        "escape": "Escape",
        "space": "Space",
        "tab": "Tab",
        "backspace": "Backspace",
        "arrowUp": "Arrow Up",
        "arrowDown": "Arrow Down",
        "arrowLeft": "Arrow Left",
        "arrowRight": "Arrow Right",
        "ctrl": "Ctrl (Control)",
        "alt": "Alt (Option on Mac)",
        "shift": "Shift",
        "meta": "Meta (Cmd on Mac, Win on Windows)",
        "delete": "Delete"
      }
    },
    "conditionals": {
      "ifThenElse": {
        "title": "if / then / else",
        "text": "Conditionally render elements or templates based on expressions.",
        "preview": "Preview",
        "checkbox": "Logged in",
        "welcome": "✅ Welcome back!",
        "login": "Please log in."
      },
      "elseIf": {
        "title": "else-if — Chained Conditionals"
      },
      "showHide": {
        "title": "show / hide",
        "text": "Toggles display: none without adding/removing DOM elements. Better for frequently toggled elements.",
        "comparisonTitle": "if vs show",
        "colIf": "if",
        "colShow": "show",
        "mechanism": "Mechanism",
        "mechanismIf": "Adds/removes DOM elements",
        "mechanismShow": "Toggles CSS display",
        "bestFor": "Best for",
        "bestForIf": "Rarely toggled content",
        "bestForShow": "Frequently toggled content",
        "preservesState": "Preserves state",
        "preservesIf": "No (re-creates)",
        "preservesShow": "Yes"
      },
      "switchCase": {
        "title": "switch / case",
        "text": "Render one of many templates based on a value.",
        "inlineSubtitle": "Inline Content (no templates)",
        "multiValueSubtitle": "Multi-Value Case"
      },
      "elseOnLoops": {
        "title": "else on Loop Elements",
        "text": "Loop directives (foreach, each, for) support else=\"templateId\" to reference a <template> for empty-state fallback. The template renders when the list is empty ([]) or null/undefined/non-array — e.g. API state before the first fetch.",
        "callout": "The sibling else pattern for loops was removed in v1.15. Use else=\"templateId\" on the loop element itself. See the Loops page for details."
      }
    },
    "loops": {
      "foreach": {
        "title": "foreach — Iterate Over Arrays",
        "text": "The primary iteration directive. Place foreach=\"item in array\" on the repeating element itself (self-repeating pattern). The element and its children are cloned for each item. each and for are aliases with identical behavior.",
        "preview": "Preview",
        "col1": "Attribute",
        "col2": "Description",
        "foreach": "Expression: item in array",
        "from": "Source array (DEPRECATED — use item in array syntax)",
        "index": "Variable name for the index (default: $index)",
        "key": "Unique key expression for element identification and tracking",
        "filter": "Expression to filter items",
        "sort": "Property path to sort by (prefix - for descending)",
        "limit": "Maximum number of items to render",
        "offset": "Number of items to skip",
        "template": "External template ID (optional — children become template if omitted)",
        "elseTpl": "Template ID rendered when the array is empty or null/undefined (e.g. else=\"noItemsTpl\")"
      },
      "fullExample": {
        "title": "Full Example with All Attributes",
        "text": "A complete example showing foreach with filtering, sorting, pagination, and an else template for the empty-state fallback."
      },
      "aliases": {
        "title": "Aliases: each and for",
        "text": "each and for are aliases of foreach — they share the same handler and support all the same attributes."
      },
      "inline": {
        "title": "Self-Repeating Pattern",
        "text": "Place the loop directive on the repeating element itself. The element is cloned for each item in the array. Use else=\"templateId\" to reference a template for the empty state."
      },
      "deprecated": {
        "title": "Deprecated: from Attribute",
        "text": "The from attribute still works but is deprecated. Use the \"item in array\" syntax instead. Using from will emit a console warning."
      },
      "contextVars": {
        "title": "Loop Context Variables",
        "col1": "Variable",
        "col2": "Description",
        "index": "Current index (0-based)",
        "count": "Total number of items",
        "first": "true if first item",
        "last": "true if last item",
        "even": "true if index is even",
        "odd": "true if index is odd"
      },
      "nested": {
        "title": "Nested Loops",
        "text": "Child loops (foreach, each, or for) can access parent scope variables."
      },
      "reactivity": {
        "title": "Reactivity",
        "text": "Loop directives are fully reactive. When the source array changes (items added, removed, or reordered), the DOM updates automatically. Use the key attribute for efficient diffing — without it, the entire list re-renders on every change."
      },
      "objectIteration": {
        "title": "Object Iteration",
        "text": "To iterate over an object's entries, use the keys or values filter to convert it to an array first.",
        "callout": "Direct object iteration is not supported — always convert to an array with keys, values, or Object.entries() in a computed expression."
      },
      "elseTpl": {
        "title": "Else with Template Reference",
        "text": "Place else=\"templateId\" directly on the loop element. When the list is empty ([]) or null/undefined, the referenced template content replaces the loop output.",
        "callout": "The else attribute on the loop element activates for empty arrays ([]) and also for null, undefined, or non-array sources — e.g. API state before the first fetch resolves.",
        "breaking": "Breaking change (v1.15): the sibling else pattern for loops has been removed. Use else=\"templateId\" on the loop element instead."
      },
      "keyReconciliation": {
        "title": "Key-Based Reconciliation",
        "text": "By default, when the source array changes, the loop directive performs a full rebuild — all child nodes are disposed and recreated from scratch. When you supply a key attribute, the directive switches to key-based reconciliation: existing DOM nodes for items whose key is still in the list are reused and their context is updated in place. Only items that genuinely appeared or disappeared trigger DOM mutations.",
        "callout": "The key value must be unique and stable across renders — typically a database ID or UUID. Using a non-unique key (e.g. $index) defeats reconciliation since items will always appear to match.",
        "whenTitle": "When to use key",
        "colUseCase": "Use case",
        "colRecommendation": "Recommendation",
        "staticLists": "Static lists, rendered once",
        "smallLists": "Lists with < ~10 items, infrequent updates",
        "noKeyNeeded": "No key needed",
        "largeLists": "Large lists (50+ items) with frequent updates",
        "useKey": "Use key",
        "mutatingArrays": "push / splice / sort on reactive arrays",
        "useKeyPreserve": "Use key to preserve focus, scroll, and input state",
        "embeddedInputs": "Items with embedded inputs, video, canvas",
        "useKeyReset": "Use key — full rebuild resets state",
        "positionalTitle": "Positional metadata after reorder",
        "positionalText": "After a reorder (e.g. sort), the reconciler calls $notify() on the context of each retained clone. This propagates the updated $index, $first, $last, $even, $odd, and $count values to all child watchers, including nested bindings and class expressions that depend on position."
      },
      "externalTemplate": {
        "title": "External Template",
        "text": "Use the template attribute to reference a <template> element by ID. The loop element is still the repeating unit — clones of the referenced template are inserted as siblings."
      },
      "animationAttrs": {
        "title": "Loop Animation Attributes",
        "text": "All loop directives (foreach, each, for) support enter/leave animations and stagger via these attributes:",
        "col1": "Attribute",
        "col2": "Description",
        "enter": "CSS class added to each new item on insert",
        "leave": "CSS class added to items before removal",
        "stagger": "Delay (ms) between each item's enter animation",
        "duration": "Max duration (ms) before leave animation is force-completed"
      }
    },
    "templates": {
      "basic": {
        "title": "Basic Template",
        "text": "Templates are reusable HTML fragments that are never rendered directly. They are cloned when referenced by directives like then, else, template, loading, error, etc."
      },
      "var": {
        "title": "Template Variables (var)",
        "text": "Templates can declare which variable they expect from the calling context."
      },
      "slots": {
        "title": "Template Slots",
        "text": "Allow templates to accept projected content."
      },
      "remote": {
        "title": "Remote Templates (src)",
        "text": "Load templates from external HTML files."
      },
      "recursive": {
        "subtitle": "Recursive Loading",
        "text": "Remote templates are loaded recursively — if a remote template itself contains <template src=\"...\"> elements, those are automatically resolved too:"
      },
      "remoteRoutes": {
        "subtitle": "Remote Templates in Routes",
        "text": "Remote templates inside route content are also automatically resolved before the route renders. See Routing for details."
      },
      "lazy": {
        "title": "Lazy Loading (lazy)",
        "text": "Control when remote templates are fetched using the lazy attribute on <template src=\"...\"> elements. NoJS loads templates in phases to optimise time-to-first-render.",
        "col1": "Value",
        "col2": "Behaviour",
        "absent": "(absent)",
        "absentDesc": "Default auto-prioritisation: content-include templates and the current route template load before first render; other route templates are preloaded in the background after first render.",
        "priorityDesc": "Force load before everything else — even before regular content includes. Useful for critical shared layout templates.",
        "ondemandDesc": "Only valid on route templates. Never preloaded — fetched lazily the first time the user navigates to that route. Ideal for heavy or rarely-visited pages."
      },
      "phases": {
        "subtitle": "Loading Phases",
        "text": "Templates are resolved in four ordered phases: Phase 0 fetches lazy=\"priority\" templates first; Phase 1 fetches all other non-route templates plus the active route template (blocking before first render); Phase 2 preloads remaining route templates in the background after first render; and on-demand fetches lazy=\"ondemand\" route templates only when the user first navigates to them."
      },
      "loading": {
        "title": "Loading Placeholder (loading)",
        "text1": "Show a placeholder template while a remote template is being fetched. The placeholder is inserted synchronously — before any network request — and removed automatically once the real content arrives. Works for both static content-includes and nested templates inside route pages.",
        "text2": "Both plain IDs and #id syntax are accepted. The placeholder template is cloned each time, so it can be reused across multiple remote templates:"
      },
      "include": {
        "title": "Inline Template Include (include)",
        "text1": "Clone an inline template into the current position synchronously, before any fetches. Useful for injecting reusable markup (e.g. icon sets, common fragments) without making a network request.",
        "text2": "include and loading serve different purposes: include clones inline content permanently; loading inserts a temporary placeholder that disappears once a remote template finishes loading."
      }
    },
    "dataFetching": {
      "baseUrl": {
        "title": "Base URL",
        "text1": "Set once on any ancestor element. All descendant get, post, etc. resolve relative URLs against it.",
        "text2": "Override for specific sections:",
        "text3": "Absolute URLs skip base resolution:"
      },
      "config": {
        "title": "Programmatic Configuration"
      },
      "headers": {
        "title": "Per-Request Headers"
      },
      "get": {
        "title": "get — Fetch and Render Data",
        "attributesTitle": "Attributes",
        "col1": "Attribute",
        "col2": "Type",
        "col3": "Description",
        "get": "URL to fetch (GET request)",
        "as": "Name to assign the response in the context. Default: \"data\"",
        "loading": "Template ID to show while loading (e.g. \"#skeleton\")",
        "error": "Template ID to show on fetch error",
        "empty": "Template ID to show when response is empty array/null",
        "refresh": "Auto-refresh interval in ms (polling)",
        "cached": "Cache responses. cached = memory, cached=\"local\" = localStorage, cached=\"session\" = sessionStorage",
        "into": "Write response to a named global store",
        "debounce": "Debounce in ms (useful with reactive URLs)",
        "headers": "JSON string of additional headers",
        "params": "Expression that resolves to query params object"
      },
      "fullExample": {
        "title": "Full Example"
      },
      "reactiveUrls": {
        "title": "Reactive URLs",
        "text": "URLs that reference state variables re-fetch automatically when those values change."
      },
      "mutations": {
        "title": "post, put, patch, delete — Mutating Requests",
        "text": "Used on forms or triggered via call.",
        "formSubmissionTitle": "Form Submission",
        "putPatchDeleteTitle": "PUT / PATCH / DELETE"
      },
      "mutationAttrs": {
        "title": "Mutation Attributes",
        "col1": "Attribute",
        "col2": "Description",
        "method": "URL for the request",
        "body": "Request body (JSON string with interpolation). For forms, auto-serializes fields",
        "success": "Template ID to render on success. Receives response as var",
        "error": "Template ID to render on error. Receives error as var",
        "loading": "Template ID to show during request",
        "confirm": "Show browser confirm() dialog before sending",
        "redirect": "URL to navigate to on success (SPA route)",
        "then": "Expression to execute on success (e.g. \"users.push(result)\")",
        "into": "Write response to a named global store",
        "cached": "Cache responses (memory/local/session). Note: caching only applies to GET requests."
      },
      "lifecycle": {
        "title": "Request Lifecycle"
      },
      "query": {
        "title": "query — Read Requests with a Body (RFC 10008)",
        "text": "The <code>query</code> directive issues an HTTP QUERY request — a safe, idempotent, cacheable read that carries a request body. It renders, caches, and auto-fires like <code>get</code>, but sends a body like <code>post</code>. Use it when a search or filter is too complex for a URL query string but is still a read, not a mutation. No CSRF token is injected, since QUERY is a safe method.",
        "formTitle": "Form-Driven Query",
        "cacheNote": "When cached, the cache key includes the request body — so two QUERY requests to the same URL with different bodies are cached separately. Pagination (get-cursor / get-page / get-insert) remains GET-only."
      },
      "liveDemo": {
        "title": "Live Demo — API Fetch",
        "label": "Result"
      },
      "urlEncoding": {
        "title": "URL Interpolation and Encoding",
        "text": "Since v1.10, all <code>{placeholder}</code> values in URL attributes are automatically passed through <code>encodeURIComponent()</code>. This is safe for query parameters and single-level path segments, but will break multi-segment paths that contain slashes — because <code>/</code> becomes <code>%2F</code>. If your value contains path separators, concatenate outside the braces instead."
      },
      "skeleton": {
        "title": "Skeleton Placeholders",
        "text": "The <code>skeleton</code> attribute references a DOM element (by its <code>id</code>) that acts as a placeholder while the request is in flight. Unlike <code>loading=</code>, the skeleton element lives in the page markup from the start — making it SSG-friendly and preventing Cumulative Layout Shift (CLS).",
        "hiddenWhen": "The skeleton is automatically hidden (via <code>display: none</code>) when the response arrives, when a non-<code>GET</code> method is used, or when the response is served from cache.",
        "combinedTitle": "Skeleton vs. Loading",
        "combinedText": "You can use both <code>skeleton=</code> and <code>loading=</code> on the same element. The skeleton shows first (pre-rendered), then hides when the request starts and the loading template takes over until the response arrives.",
        "colContent": "Content",
        "skeletonContent": "Pre-rendered HTML element in the page",
        "loadingContent": "Template injected by No.JS at runtime",
        "colVisibility": "Visibility",
        "skeletonVisibility": "Visible on page load, hidden after response",
        "loadingVisibility": "Shown during request, hidden after response",
        "colCLS": "CLS prevention",
        "skeletonCLS": "Yes — occupies space before JS runs",
        "loadingCLS": "No — injected after JS initializes",
        "colSSG": "SSG-friendly",
        "skeletonSSG": "Yes",
        "loadingSSG": "No",
        "visibilityTitle": "Visibility Rules",
        "visibilityText": "Do not hide the skeleton with CSS (<code>display: none</code> or a <code>.hidden</code> class). No.JS manages visibility by toggling the inline <code>display</code> style — if your CSS hides the element first, the show/hide logic will conflict."
      },
      "pagination": {
        "title": "Pagination & Infinite Scroll",
        "text": "No.JS supports declarative pagination and infinite scroll through a set of <code>get-*</code> attributes. Add <code>get-trigger=\"scroll\"</code> for infinite scroll or <code>get-trigger=\"click\"</code> for a load-more button, combined with <code>get-insert=\"append\"</code> to accumulate results.",
        "seeAlso": "For the complete pagination reference — including cursor-based pagination, custom trigger labels, threshold configuration, and the <code>fetch:end</code> event — see the <a href=\"#pagination\">Pagination & Infinite Scroll</a> dedicated page."
      },
      "interceptors": {
        "title": "Interceptors",
        "text": "Interceptors let you hook into every HTTP request and response made by No.JS. Use them for authentication headers, global error handling, caching, logging, or request cancellation.",
        "basicTitle": "Basic Usage",
        "asyncTitle": "Async Interceptors",
        "asyncText": "Interceptors can be <code>async</code>. No.JS awaits each interceptor with a 5-second timeout — if the interceptor doesn't resolve in time, it is skipped and a warning is logged.",
        "cancelTitle": "Cancel Requests (NoJS.CANCEL)",
        "cancelText": "Return an object with <code>[NoJS.CANCEL]: true</code> from a request interceptor to abort the request entirely. The fetch directive treats this as an <code>AbortError</code>.",
        "respondTitle": "Short-Circuit Responses (NoJS.RESPOND)",
        "respondText": "Return an object with <code>[NoJS.RESPOND]: data</code> from a request interceptor to skip the HTTP call and use the provided data directly as the response.",
        "replaceTitle": "Replace Response Body (NoJS.REPLACE)",
        "replaceText": "Return an object with <code>[NoJS.REPLACE]: data</code> from a response interceptor to replace the parsed body with your own data.",
        "redactionTitle": "Header Redaction",
        "redactionText": "For security, No.JS automatically redacts sensitive headers (<code>Authorization</code>, <code>X-API-Key</code>, <code>Cookie</code>, etc.) in debug logs and when passing request data to untrusted interceptors. Only interceptors registered with <code>{ trusted: true }</code> receive the original headers.",
        "sentinelTitle": "Sentinel Summary",
        "colSentinel": "Sentinel",
        "colType": "Interceptor Type",
        "colEffect": "Effect",
        "typeRequest": "Request",
        "typeResponse": "Response",
        "cancelEffect": "Aborts the request (throws AbortError)",
        "respondEffect": "Skips HTTP call, returns provided data as response",
        "replaceEffect": "Replaces the parsed response body with provided data"
      }
    },
    "routing": {
      "definition": {
        "title": "Route Definition"
      },
      "params": {
        "title": "Route Parameters & Query"
      },
      "context": {
        "title": "$route — Route Context",
        "col1": "Property",
        "col2": "Description",
        "path": "Current path (e.g. \"/users/42\")",
        "params": "Route parameters (e.g. { id: \"42\" })",
        "query": "Query string params (e.g. { q: \"hello\" })",
        "hash": "URL hash (e.g. \"#section\")",
        "matched": "Whether an explicit route matched (true) or a wildcard/fallback is rendering (false)"
      },
      "activeStyle": {
        "title": "Active Route Styling"
      },
      "guards": {
        "title": "Route Guards"
      },
      "programmatic": {
        "title": "Programmatic Navigation",
        "callout": "$router.push() and $router.replace() return Promises — navigation (including remote template loading) is fully async. In on:click handlers the return value is ignored, but in scripts you can await them:"
      },
      "nested": {
        "title": "Nested Routes"
      },
      "remoteTemplates": {
        "title": "Remote Templates in Routes",
        "text1": "Route templates can include <template src=\"...\"> to load content from external files. They are automatically resolved before the route renders:",
        "text2": "Nested remote templates (a remote template that itself contains more <template src>) are recursively loaded."
      },
      "fileBased": {
        "title": "File-Based Routing",
        "text": "Instead of declaring each route template manually, point your <code>route-view</code> outlet at a folder. No.JS will automatically resolve route paths to template files inside that folder.",
        "howItWorks": "How it works",
        "list1": "Add <code>route-view</code> to your outlet element — file-based routing is enabled by default (config <code>router.templates: \"pages\"</code>). Override per-outlet with <code>src=\"folder/\"</code>.",
        "list2": "When a user navigates to <code>/analytics</code>, No.JS resolves it to <code>pages/analytics.tpl</code>",
        "list3": "The template is fetched, cached, and rendered — automatically",
        "attributesTitle": "Attributes",
        "colAttr": "Attribute",
        "colDefault": "Default",
        "colDesc": "Description",
        "srcDesc": "Base folder for template resolution (per-outlet override; config: <code>router.templates</code>)",
        "routeIndexDesc": "Filename for the root route <code>/</code>",
        "extDesc": "File extension appended to route segments (fallback: <code>\".html\"</code>)",
        "i18nNsDesc": "When present, auto-derives i18n namespace from filename",
        "callout": "<strong>Config default:</strong> The default <code>router.templates</code> is <code>\"pages\"</code>, so file-based routing works out of the box — just add <code>route-view</code> to your outlet. Override with <code>NoJS.config({ router: { templates: 'views' } })</code> or per-outlet via <code>src=\"./custom/\"</code>.",
        "exampleTitle": "Example — SaaS Dashboard",
        "exampleText": "That's it — <strong>two lines</strong> for a full SPA with six routes.",
        "mixingTitle": "Mixing Explicit & File-Based Routes",
        "mixingText": "Explicit <code>&lt;template route=\"...\"&gt;</code> declarations <strong>always take priority</strong>. This lets you combine both approaches — use file-based routing for simple pages and explicit templates for routes that need guards, params, or named outlets:",
        "autoI18nTitle": "Auto i18n Namespace",
        "autoI18nText": "When the <code>route-view</code> element has an <code>i18n-ns</code> attribute (even without a value), No.JS automatically loads the i18n namespace matching the filename:",
        "autoI18nText2": "This replaces the need to add <code>i18n-ns=\"...\"</code> on each route template individually."
      },
      "lazyLoading": {
        "title": "Lazy Template Loading",
        "text": "The lazy attribute on <template src=\"...\"> controls when a remote template is fetched relative to the first render. Use it to prioritise critical templates and defer heavy or rarely-visited pages.",
        "col1": "Value",
        "col2": "Phase",
        "col3": "Behaviour",
        "absent": "(absent)",
        "absentPhase": "1 or 2",
        "absentDesc": "Auto: non-route templates and the active route template load before first render (Phase 1); other route templates preload in the background after first render (Phase 2).",
        "priorityPhase": "0",
        "priorityDesc": "Load before everything else — even before regular content includes. Use for critical shared layout templates.",
        "ondemandPhase": "on demand",
        "ondemandDesc": "Only valid on route templates. Never preloaded — fetched the first time the user navigates to that route. Ideal for heavy or rarely-visited pages."
      },
      "anchor": {
        "title": "Anchor Links",
        "text1": "When using useHash: true, the URL hash (#) is used for routing (e.g. #/docs). This normally conflicts with standard anchor links like <a href=\"#section\"> — but No.JS handles it automatically in both hash and history modes.",
        "text2": "Anchor links that point to an element id on the page are intercepted by the router: the target element is scrolled into view smoothly, and the clicked link receives an active class. The route itself is not affected.",
        "howItWorks": "How it works:",
        "list1": "Clicking <a href=\"#introduction\"> scrolls to <div id=\"introduction\"> with smooth behavior",
        "list2": "The .active class is toggled on the clicked link (and removed from siblings)",
        "list3": "The current route path is preserved — no navigation occurs",
        "list4": "Links with a route attribute are always treated as route navigation, not anchors",
        "tip": "Tip: Style the active anchor link with .active in your CSS — the router manages the class for you."
      },
      "namedOutlets": {
        "title": "Named Outlets (route-view)",
        "text": "Multiple route-view outlets can coexist in the same page. Give each outlet a name via the attribute value, and point route templates at specific outlets using the outlet attribute.",
        "callout": "Outlets with no matching template for the current route are always cleared on navigation."
      },
      "catchAll": {
        "title": "404 / Catch-All Routes",
        "text": "Use <code>route=\"*\"</code> to define a <strong>wildcard catch-all</strong> template that renders when no explicit route matches the current path. The wildcard is always evaluated last, regardless of DOM order.",
        "text2": "Explicit routes <strong>always take priority</strong> — the wildcard only fires when <code>matchRoute()</code> returns no match.",
        "fallbackTitle": "Automatic 404 Fallback",
        "fallbackText": "If you don't define a <code>route=\"*\"</code> template, No.JS automatically shows a minimal built-in 404 page when no route matches. This ensures users always see something meaningful instead of a blank outlet.",
        "fallbackTip": "The built-in fallback is intentionally minimal and unstyled. Define your own <code>route=\"*\"</code> template for production apps.",
        "namedTitle": "Named Outlet Wildcards",
        "namedText": "Each named outlet can have its own wildcard fallback. When no route matches for an outlet, the framework resolves fallbacks in this order:",
        "namedList1": "<strong>Local wildcard</strong> — <code>&lt;template route=\"*\" outlet=\"{name}\"&gt;</code> for that specific outlet",
        "namedList2": "<strong>Global wildcard</strong> — <code>&lt;template route=\"*\"&gt;</code> (the default outlet's wildcard), used only for non-default outlets",
        "namedList3": "<strong>Built-in 404</strong> — the framework's minimal fallback page",
        "namedText2": "If the sidebar has no local wildcard, it falls back to the global <code>route=\"*\"</code>. If neither exists, the built-in 404 is used.",
        "matchedTitle": "$route.matched",
        "matchedText": "The <code>$route.matched</code> boolean tells you whether the current path hit an explicit route (<code>true</code>) or a wildcard/fallback (<code>false</code>). Use it for conditional rendering inside your templates:",
        "matchedText2": "<code>$route.matched</code> is set <strong>before</strong> the template renders, so it's always available during processing.",
        "remoteTitle": "Remote 404 Template",
        "remoteText": "Wildcard routes support all the same attributes as regular route templates, including <code>src</code> for remote loading:",
        "remoteText2": "The remote template is fetched, cached, and rendered just like any other route template — and it has full access to <code>$route.path</code>, <code>$route.matched</code>, and all other framework features.",
        "fileBasedTitle": "File-Based Routing 404",
        "fileBasedText": "When using file-based routing, navigating to a path whose <code>.tpl</code> file doesn't exist on the server (HTTP 404 or other error) automatically triggers the wildcard fallback chain.",
        "fileBasedText2": "The failed HTTP response is <strong>not</strong> cached — subsequent navigations to other paths are unaffected."
      },
      "headAttributes": {
        "title": "Route Head Attributes",
        "text": "Route templates can declare <code>page-title</code> and <code>page-description</code> directly on the <code>&lt;template&gt;</code> tag. When the route activates, the corresponding <code>&lt;head&gt;</code> tags are updated automatically.",
        "text2": "Both static strings and dynamic expressions are supported:",
        "colAttr": "Attribute",
        "colDesc": "Description",
        "pageTitleDesc": "Sets <code>document.title</code> when the route is active",
        "pageDescriptionDesc": "Sets the <code>&lt;meta name=\"description\"&gt;</code> content when the route is active",
        "pageCanonicalDesc": "Sets the <code>&lt;link rel=\"canonical\"&gt;</code> href when the route is active",
        "pageJsonldDesc": "Injects a <code>&lt;script type=\"application/ld+json\"&gt;</code> block into the document head",
        "callout": "For full head management (canonical URLs, JSON-LD, Open Graph), see the <a href=\"/docs/head-management\">Head Management</a> guide."
      },
      "deployment": {
        "title": "Deployment",
        "text": "No.JS uses the HTML5 History API by default (<code>useHash: false</code>), which produces clean URLs like <code>/about</code> and <code>/products/42</code>. These URLs require your server to serve the same <code>index.html</code> for <strong>every route</strong> — otherwise a direct visit to a client-side route returns a 404 from the server.",
        "nginxTitle": "nginx",
        "apacheTitle": "Apache",
        "apacheText": "Create a <code>.htaccess</code> file in your app's root:",
        "netlifyTitle": "Netlify",
        "netlifyText": "Create a <code>_redirects</code> file in your publish directory:",
        "netlifyToml": "Or use <code>netlify.toml</code>:",
        "vercelTitle": "Vercel",
        "vercelText": "Create a <code>vercel.json</code> in your project root:",
        "cloudflareTitle": "Cloudflare Pages",
        "cloudflareText": "Create a <code>_redirects</code> file in your project's output directory:",
        "firebaseTitle": "Firebase Hosting",
        "firebaseText": "In <code>firebase.json</code>:",
        "hashTitle": "Hash Mode",
        "hashText": "If you cannot configure your server (e.g. static file hosting without rewrite rules), use hash mode. Hash mode produces URLs like <code>https://your-site.com/#/about</code> — these work on any server without configuration, but <strong>search engines do not index hash fragments as separate pages</strong>.",
        "hashCallout": "No.JS emits a console warning when <code>useHash: true</code> is detected. To suppress it, add <code>suppressHashWarning: true</code> to your router config."
      },
      "focusBehavior": {
        "title": "Accessibility — Focus Management",
        "text": "After each navigation, No.JS moves focus to a predictable element inside the new route content. This ensures screen readers announce the page change and keyboard users land in a useful spot.",
        "text2": "When focusBehavior is 'auto' (the default), the framework walks a priority list:",
        "priority1": "<code>[autofocus]</code> — an element with the <code>autofocus</code> attribute inside the route template",
        "priority2": "<code>h1</code> — the first <code>&lt;h1&gt;</code> inside the route content",
        "priority3": "<code>[tabindex=\"-1\"]</code> — a manually focusable container",
        "priority4": "The route-view element itself (last resort)",
        "defaultTitle": "Default Behavior",
        "defaultText": "Focus management is <strong>enabled by default</strong> — you don't need to configure anything. The <code>'auto'</code> strategy works out of the box.",
        "timingTitle": "Timing",
        "timingText": "Focus is moved <strong>after</strong> the route template is fully rendered and all remote templates inside it are resolved. This guarantees the target element exists in the DOM.",
        "sideEffectsTitle": "Side Effects",
        "sideEffectsText": "The focused element receives a <code>tabindex=\"-1\"</code> if it doesn't already have a tabindex, and the page scrolls to bring it into view via <code>scrollIntoView({ block: 'nearest' })</code>.",
        "futureTitle": "Future Values",
        "futureText": "Currently only <code>'auto'</code> is supported. Future versions may add <code>'none'</code> (opt out) and <code>'target'</code> (focus a specific selector).",
        "ariaLiveTitle": "ARIA Live Region",
        "ariaLiveText": "For screen readers that don't respond to focus changes, add <code>aria-live=\"polite\"</code> to your <code>route-view</code>. The browser will announce new content as it appears:"
      },
      "viewTransitions": {
        "title": "View Transitions",
        "text": "The <code>transition</code> attribute on <code>route-view</code> now uses the <strong>View Transition API</strong> by default. Just pick a preset name and route changes are animated natively by the browser — no manual CSS required.",
        "presetsTitle": "Built-in Presets",
        "presetsText": "No.JS ships with four built-in transition presets:",
        "colPreset": "Preset",
        "colEffect": "Effect",
        "presetSlide": "Directional slide — content slides left/right based on navigation direction (forward/backward)",
        "presetFade": "Crossfade — old content fades out while new content fades in",
        "presetScale": "Scale zoom — old content scales down while new content scales up",
        "presetNone": "Instant swap — no animation, content replaces immediately",
        "configTitle": "Configuration",
        "configText": "The View Transition API is <strong>enabled by default</strong> via <code>router.viewTransition: true</code>. Set it to <code>false</code> to fall back to the legacy class-based transition system.",
        "customCssTitle": "Custom CSS",
        "customCssText": "For custom animations, target the <code>::view-transition-old(route-content)</code> and <code>::view-transition-new(route-content)</code> pseudo-elements. The <code>view-transition-name: route-content</code> is set automatically on any outlet with a <code>transition</code> attribute.",
        "howItWorksTitle": "How it Works",
        "howItWorksText": "When a route change occurs with a <code>transition</code> preset:",
        "howStep1": "The router detects navigation direction (<strong>forward</strong> or <strong>backward</strong>) from the history stack",
        "howStep2": "<code>document.startViewTransition()</code> is called, capturing the current outlet state",
        "howStep3": "The new route content is rendered inside the outlet",
        "howStep4": "The browser animates between old and new snapshots using the preset CSS rules",
        "deprecationTitle": "Migration from Class-Based Transitions",
        "deprecationText": "The old class-based transition system (<code>*-enter</code>, <code>*-enter-active</code>, <code>*-leave</code>, etc.) on <code>route-view</code> is <strong>deprecated</strong>. It still works when <code>router.viewTransition</code> is set to <code>false</code>, but the View Transition API is the recommended approach going forward.",
        "deprecationCallout": "<strong>Migration is mostly automatic.</strong> If you already use <code>transition=\"fade\"</code> on a <code>route-view</code>, it now uses the View Transition API by default — no code changes needed. Your custom <code>.fade-enter</code> / <code>.fade-leave</code> CSS is simply ignored (unless you set <code>viewTransition: false</code>)."
      }
    },
    "formsValidation": {
      "submission": {
        "title": "Declarative Form Submission"
      },
      "rules": {
        "title": "Validation Rules"
      },
      "perRuleErrors": {
        "title": "Per-Rule Error Messages",
        "text": "Use error-{rule} attributes to set a custom message for a specific rule, or error for a generic fallback."
      },
      "errorTemplates": {
        "title": "Error Templates",
        "text": "Point an error attribute to a <template> using a # prefix to render rich error UI. Inside the template, $error contains the message and $rule the failing rule name."
      },
      "errorClass": {
        "title": "Error CSS Class",
        "text": "Use error-class on the form or on individual fields to toggle a CSS class when a field is invalid and touched."
      },
      "formContext": {
        "title": "$form — Form Context",
        "text": "Inside any <form> with the validate attribute, $form provides:",
        "col1": "Property",
        "col2": "Type",
        "col3": "Description",
        "valid": "true if all fields pass validation",
        "dirty": "true if any field has been modified",
        "touched": "true if any field has been focused and blurred",
        "submitting": "true while the request is in flight",
        "pending": "true while async validators are running",
        "errors": "Map of field names → error messages",
        "values": "Current form values",
        "firstError": "Error message of the first invalid field (DOM order)",
        "errorCount": "Number of fields currently failing validation",
        "fields": "Per-field state object (valid, error, dirty, touched)",
        "reset": "Reset form to initial values, clear errors and classes"
      },
      "formFields": {
        "title": "$form.fields — Per-Field State",
        "text": "$form.fields exposes individual field state keyed by field name.",
        "asTitle": "Field Aliases with as",
        "asText": "Use the as attribute to expose a field's state under a custom name in the context."
      },
      "validateOn": {
        "title": "Validation Triggers (validate-on)",
        "text": "By default, validation runs on input and focusout. Use validate-on to change when visual feedback appears.",
        "note": "Internally, $form data is always kept up-to-date regardless of validate-on. The trigger only controls when visual feedback is shown."
      },
      "validateIf": {
        "title": "Conditional Validation (validate-if)",
        "text": "Skip validation for a field based on a condition. When validate-if evaluates to false, the field is treated as valid."
      },
      "autoDisable": {
        "title": "Auto-Disable Submit Buttons",
        "text": "Submit buttons are automatically disabled when the form is invalid. Buttons with type=\"button\" are not affected."
      },
      "customValidators": {
        "title": "Custom Validators"
      },
      "liveDemo": {
        "title": "Live Demo — Registration Form",
        "label": "Result",
        "usernameLabel": "Username",
        "emailLabel": "Email",
        "ageLabel": "Age",
        "roleLabel": "Role",
        "roleDefault": "Select a role",
        "roleDev": "Developer",
        "roleDesign": "Designer",
        "roleMgr": "Manager",
        "termsLabel": "I agree to the terms",
        "registerButton": "Register",
        "successMessage": "Registration complete! Welcome aboard."
      }
    },
    "styling": {
      "classToggle": {
        "title": "class-* — Toggle Classes",
        "text": "Add or remove CSS classes reactively based on expressions. Use class-{name} for individual toggles or class-map for multiple classes at once.",
        "multiObject": "Multiple Classes from Object",
        "multiObjectText": "Use class-map with an object expression to toggle multiple classes. Keys are class names, values are boolean expressions.",
        "fromArray": "From Array"
      },
      "classStatic": {
        "title": "Static class Interaction",
        "text": "class-* directives work alongside static class attributes. No.JS only toggles the directive-managed classes — static classes are never removed.",
        "callout": "You can safely combine class=\"card\" with class-active=\"isActive\". The card class is always present; active is toggled."
      },
      "inlineStyles": {
        "title": "style-* — Inline Styles",
        "text": "Set inline CSS properties reactively using style-{property} attributes. Property names use kebab-case (e.g. style-font-size, style-background-color).",
        "fromObject": "From Object",
        "fromObjectText": "Use style-map with an object expression to set multiple inline styles at once."
      },
      "cssCustomProperties": {
        "title": "CSS Custom Properties",
        "text": "Use style-* with CSS custom properties (variables) for theme-aware reactive styling. Property names follow the same kebab-case convention.",
        "callout": "CSS custom properties set via style-* are scoped to the element and inherited by its children — just like regular CSS custom properties."
      },
      "liveDemo": {
        "title": "Live Demo — Dynamic Styling",
        "label": "Result",
        "toggleButton": "Toggle Active"
      }
    },
    "animations": {
      "enterLeave": {
        "title": "animate — Enter/Leave Animations",
        "attrsTitle": "Animation Attributes",
        "col1": "Attribute",
        "col2": "Description",
        "row1": "CSS animation class added when the element enters",
        "row2": "CSS animation class added when the element leaves",
        "row3": "Duration in milliseconds forwarded to animationDuration and used as the fallback timeout. When omitted the fallback fires on the next event-loop tick (0 ms)",
        "row4": "Delay in milliseconds between each item in a loop (each / foreach)",
        "fallbackCallout": "Fallback timeout — No.JS listens for animationend / transitionend to clean up classes and trigger re-renders after leave animations. If the event never fires (e.g. CSS is absent, element is detached) a setTimeout safety net ensures the pipeline is never permanently stalled. When animate-duration is omitted the timeout is 0 ms — it fires on the next event-loop tick with no artificial wait. Passing an explicit animate-duration=\"300\" sets both animation-duration on the target element and the safety-net timeout to 300 ms."
      },
      "transition": {
        "title": "transition — CSS Transition Classes",
        "viewTransitionNote": "<strong>Note:</strong> Route transitions on <code>route-view</code> now use the <a href=\"/docs/routing#view-transitions\">View Transition API</a> by default. The class-based <code>transition</code> attribute described below still applies to <strong>regular elements</strong> (e.g. <code>if</code>, <code>show</code>).",
        "text1": "Follows a convention similar to Vue's transition system.",
        "text2": "No.JS adds/removes classes during the transition:",
        "col1": "Class",
        "col2": "When",
        "row1": "Start state of enter",
        "row2": "Active state of enter",
        "row3": "End state of enter",
        "row4": "Start state of leave",
        "row5": "Active state of leave",
        "row6": "End state of leave"
      },
      "loopAnimations": {
        "title": "Loop Animations",
        "text": "All loop directives (foreach, each, for) support enter/leave animations and stagger."
      },
      "builtIn": {
        "title": "Built-in Animation Names",
        "text": "No.JS ships with these CSS animations:"
      },
      "a11y": {
        "title": "Accessibility — Reduced Motion",
        "text": "No.JS automatically respects the prefers-reduced-motion media query. When the user prefers reduced motion, all CSS animations are disabled via a built-in CSS rule that sets animation-duration and transition-duration to 0.01ms.",
        "callout": "This applies globally — no per-element configuration needed. Users who prefer reduced motion see instant state changes with no animation."
      },
      "viewTransitions": {
        "title": "View Transitions (Route Navigation)",
        "text": "No.JS uses the View Transition API for smooth route transitions. Add a transition attribute to your route-view outlet to enable browser-native animated navigation between routes.",
        "configTitle": "Configuration",
        "presetsTitle": "Built-in Presets",
        "colPreset": "Preset",
        "colEffect": "Effect",
        "presetFade": "Crossfade between old and new content",
        "presetSlideLeft": "Slide content to the left",
        "presetSlideRight": "Slide content to the right",
        "presetSlideUp": "Slide content upward",
        "routingCallout": "View Transitions offer more options including custom CSS, accessibility, and configuration details. See the <a href=\"/docs/routing#view-transitions\">Routing — View Transitions</a> section for the full reference."
      },
      "liveDemo": {
        "title": "Live Demo — Toggle Animation",
        "label": "Result",
        "toggleButton": "Toggle",
        "demoText": "Hello, Animated World! ✨"
      }
    },
    "dnd": {
      "drag": {
        "title": "drag — Make an Element Draggable",
        "preview": "Preview",
        "demoText": "Drag fruits into the basket:",
        "fruitsLabel": "Fruits",
        "basketLabel": "Basket",
        "col1": "Attribute",
        "col2": "Type",
        "col3": "Default",
        "col4": "Description",
        "dragDesc": "The value being dragged",
        "dragTypeDesc": "Named type — only matching <code>drop-accept</code> zones respond",
        "dragEffectDesc": "Maps to <code>dataTransfer.effectAllowed</code>",
        "dragHandleDesc": "Restricts grab area to a child element",
        "dragImageDesc": "Custom drag ghost element",
        "dragImageOffsetDesc": "Pixel offset for custom drag image",
        "dragDisabledDesc": "When truthy, disables dragging",
        "dragClassDesc": "Class added while dragging",
        "dragGhostClassDesc": "Class added to the drag image element",
        "dragGroupDesc": "Group name for multi-select"
      },
      "drop": {
        "title": "drop — Define a Drop Zone",
        "preview": "Preview",
        "demoText": "Drag items between zones:",
        "zoneALabel": "Zone A",
        "zoneBLabel": "Zone B",
        "col1": "Attribute",
        "col2": "Type",
        "col3": "Default",
        "col4": "Description",
        "dropDesc": "Expression executed on drop",
        "dropAcceptDesc": "Accepted <code>drag-type</code>(s). Use <code>\"*\"</code> for any",
        "dropEffectDesc": "Maps to <code>dataTransfer.dropEffect</code>",
        "dropClassDesc": "Class added when valid item hovers",
        "dropRejectClassDesc": "Class added when item is rejected (wrong type or max exceeded)",
        "dropDisabledDesc": "When truthy, disables dropping",
        "dropMaxDesc": "Max items the zone accepts",
        "dropSortDesc": "Enables sortable reorder by position",
        "dropPlaceholderDesc": "Shows placeholder at insertion point",
        "dropPlaceholderClassDesc": "Class for the placeholder",
        "dropSettleClassDesc": "Custom CSS class for the settle animation",
        "dropEmptyClassDesc": "Custom CSS class for empty state on drop zone"
      },
      "dragList": {
        "title": "drag-list — Sortable List",
        "text": "A higher-level directive that combines <code>drag</code> and <code>drop</code> into a sortable list bound to a state array.",
        "preview": "Preview",
        "todoLabel": "To Do",
        "doneLabel": "Done",
        "col1": "Attribute",
        "col2": "Type",
        "col3": "Default",
        "col4": "Description",
        "dragListDesc": "Path to array in state",
        "templateDesc": "Template for each item",
        "dragListKeyDesc": "Unique key per item for stable identity",
        "dragListItemDesc": "Loop variable name in template",
        "dropSortDesc": "Layout direction",
        "dropAcceptDesc": "Types accepted (defaults to same list)",
        "dragListCopyDesc": "Copy items instead of moving",
        "dragListRemoveDesc": "Remove items when dragged out",
        "dragDisabledDesc": "Disables dragging from this list",
        "dropDisabledDesc": "Disables dropping into this list",
        "dropMaxDesc": "Max items allowed",
        "dropSettleClassDesc": "Custom CSS class for the settle animation",
        "dropEmptyClassDesc": "Custom CSS class for empty state on drag-list",
        "dropPlaceholderDesc": "Shows a placeholder where the item will be dropped"
      },
      "dragListEvents": {
        "title": "Drag-List Events",
        "col1": "Event",
        "col3": "Description",
        "reorderDesc": "Item reordered within same list",
        "receiveDesc": "Item received from another list",
        "removeDesc": "Item removed (dragged out)",
        "preview": "Preview",
        "inboxLabel": "Inbox",
        "archiveLabel": "Archive",
        "eventLogLabel": "Event log:"
      },
      "dragMultiple": {
        "title": "drag-multiple — Multi-Select",
        "text": "Enable click-to-select on child elements, then drag all selected items at once.",
        "preview": "Preview",
        "demoText": "Click items to select, <kbd>Ctrl</kbd>+click for multi, then drag to the drop zone:",
        "availableLabel": "Available",
        "collectedLabel": "Collected",
        "col1": "Attribute",
        "col2": "Default",
        "col3": "Description",
        "dragMultipleDesc": "Enables click-to-select",
        "dragMultipleClassDesc": "Class added to selected items",
        "dragGroupDesc": "Group name — all selected items move together",
        "selectionTitle": "Selection behavior:",
        "selCol1": "Action",
        "selCol2": "Result",
        "selClick": "Click",
        "selClickResult": "Selects single item (replaces previous)",
        "selCtrlClick": "Ctrl/Cmd + Click",
        "selCtrlClickResult": "Adds to selection",
        "selEscape": "Escape",
        "selEscapeResult": "Clears all selections",
        "selDrag": "Drag a selected item",
        "selDragResult": "<code>$drag</code> becomes an array of all selected items"
      },
      "implicitVars": {
        "title": "Implicit Variables",
        "text": "These variables are available inside <code>drop</code> expressions and <code>on:drop</code> handlers:",
        "col1": "Variable",
        "col2": "Type",
        "col3": "Description",
        "dragDesc": "The dragged value. Array if multi-select",
        "dragTypeDesc": "The <code>drag-type</code> of the item",
        "dragEffectDesc": "The <code>drag-effect</code>",
        "dropIndexDesc": "Insertion index within the drop zone",
        "sourceDesc": "<code>{ list, index, el }</code> — source info",
        "targetDesc": "<code>{ list, index, el }</code> — target info",
        "preview": "Preview",
        "demoText": "Each item has a different <code>drag-type</code>:",
        "dropHere": "Drop here to inspect"
      },
      "cssClasses": {
        "title": "CSS Classes",
        "text": "Automatically injected by No.JS:",
        "col1": "Class",
        "col2": "When applied",
        "draggingDesc": "On the source element while dragging",
        "dragOverDesc": "On the drop zone while a valid item hovers",
        "dropRejectDesc": "On the drop zone when the item is rejected (wrong type or max exceeded)",
        "dropPlaceholderDesc": "On the insertion placeholder",
        "selectedDesc": "On multi-selected items",
        "dropSettleDesc": "Brief settle animation on drop",
        "dragListEmptyDesc": "On a <code>drag-list</code> when it has no items",
        "preview": "Preview",
        "demoText": "Drag an item and watch the classes apply:",
        "dropZoneHint": "Drop zone (max 2) — watch <code>.nojs-drag-over</code> / <code>.nojs-drop-reject</code>"
      },
      "a11y": {
        "title": "Accessibility",
        "text": "No.JS automatically adds ARIA attributes and keyboard support:",
        "col1": "Feature",
        "col2": "Details",
        "draggableDesc": "Set on drag sources",
        "ariaGrabbedDesc": "Reflects drag state (<code>true</code>/<code>false</code>)",
        "ariaDropeffectDesc": "Set on drop zones",
        "roleListboxDesc": "On <code>drag-list</code> containers",
        "roleOptionDesc": "On <code>drag-list</code> items",
        "tabindexDesc": "For keyboard access",
        "keyboardTitle": "Keyboard shortcuts:",
        "keyCol1": "Key",
        "keyCol2": "Action",
        "spaceDesc": "Grab the focused item",
        "escapeDesc": "Cancel the drag",
        "arrowDesc": "Navigate between items while dragging",
        "enterDesc": "Drop at current position",
        "preview": "Preview",
        "demoText": "Use <kbd>Tab</kbd> to focus, <kbd>Space</kbd> to grab, <kbd>↑↓</kbd> to move, <kbd>Enter</kbd> to drop:"
      }
    },
    "filters": {
      "text": {
        "title": "Text Filters"
      },
      "number": {
        "title": "Number Filters"
      },
      "array": {
        "title": "Array Filters"
      },
      "date": {
        "title": "Date Filters"
      },
      "utility": {
        "title": "Utility Filters"
      },
      "object": {
        "title": "Object Filters"
      },
      "referenceTable": {
        "title": "Complete Filter Reference",
        "text": "All 32 built-in filters at a glance:",
        "col1": "Filter",
        "col2": "Category",
        "col3": "Description",
        "uppercase": "Converts text to UPPERCASE",
        "lowercase": "Converts text to lowercase",
        "capitalize": "Capitalizes first letter of each word",
        "truncate": "Truncates text to N characters with ellipsis",
        "slugify": "Converts to URL-friendly slug",
        "trim": "Removes leading/trailing whitespace",
        "encodeUri": "Encodes a URI component",
        "currency": "Formats number as currency",
        "number": "Formats number with locale grouping",
        "percent": "Formats number as percentage",
        "ordinal": "Adds ordinal suffix (1st, 2nd, 3rd)",
        "filesize": "Formats bytes to human-readable size",
        "reverse": "Reverses array or string",
        "unique": "Removes duplicate values",
        "pluck": "Extracts a property from each item",
        "where": "Filters array by property value",
        "sortBy": "Sorts array by property",
        "first": "Returns the first element of an array",
        "last": "Returns the last element of an array",
        "date": "Formats Date object",
        "datetime": "Formats Date with time",
        "relative": "Time elapsed since date (e.g. '5 minutes ago')",
        "fromNow": "Time until future date (e.g. 'in 3 hours')",
        "json": "Serializes value as JSON string",
        "default": "Returns fallback if value is null/undefined",
        "keys": "Returns object keys as array",
        "values": "Returns object values as array",
        "count": "Returns the length of an array",
        "join": "Joins array elements with a separator",
        "stripHtml": "Removes HTML tags from a string",
        "nl2br": "Converts newlines to <br> tags",
        "debug": "Outputs value to console and returns it"
      },
      "chaining": {
        "title": "Chaining Filters"
      },
      "custom": {
        "title": "Custom Filters"
      },
      "liveDemo": {
        "title": "Live Demo — Filters",
        "label": "Result",
        "uppercaseLabel": "Uppercase:",
        "slugifyLabel": "Slugify:"
      }
    },
    "i18n": {
      "setup": {
        "title": "Setup"
      },
      "externalFiles": {
        "title": "External Locale Files",
        "text": "Instead of inlining all translations in JavaScript, you can load them from external JSON files. This is ideal for large apps with many locales or when translations are managed by a separate tool.",
        "flatSubtitle": "Flat Mode (one file per locale)",
        "flatText": "Structure:",
        "nsSubtitle": "Namespace Mode (split by feature)",
        "nsText": "Split translations by feature for code-splitting and on-demand loading:",
        "nsRouteSubtitle": "Namespace per Route",
        "nsRouteText": "Use i18n-ns on a route template to load a namespace on-demand when the route is navigated to:",
        "nsElementSubtitle": "Namespace on Any Element",
        "nsElementText": "Use i18n-ns on any element to load a namespace before its children are processed:",
        "cachingSubtitle": "Caching",
        "cachingText": "Fetched JSON files are cached in memory by default. Set cache: false during development:"
      },
      "usage": {
        "title": "Usage"
      },
      "tHtml": {
        "title": "HTML Translations (t-html)",
        "text": "Add the t-html attribute to render a translation value as sanitized HTML instead of plain text. This is useful when translations contain markup such as links, bold text, or other inline HTML.",
        "callout": "The HTML output is passed through _sanitizeHtml() to prevent XSS. Dangerous tags and attributes are stripped automatically."
      },
      "formatting": {
        "title": "Number & Date Formatting"
      },
      "fallback": {
        "title": "Fallback Behavior",
        "text": "When a translation key is missing in the current locale, No.JS falls back to the fallbackLocale (default: same as defaultLocale). If the key is missing in both, the raw key path is displayed.",
        "callout": "Set fallbackLocale explicitly to ensure users always see meaningful text, even for partially translated locales."
      },
      "detection": {
        "title": "Browser Locale Detection",
        "text": "Enable automatic locale detection from the browser's navigator.language setting with detectBrowser: true. The detected locale is matched against your available locales.",
        "supportedText": "When loading locales lazily with loadPath, list the codes you ship in supportedLocales so detection can match the browser language before any file is fetched. First-visit priority: a persisted nojs-locale, then the detected browser language, then defaultLocale.",
        "callout": "When combined with persist: true, the detected locale is saved so subsequent visits use the same language without re-detecting."
      },
      "liveDemo": {
        "title": "Live Demo — Locale Switcher",
        "label": "Result",
        "localeLabel": "Locale:"
      }
    },
    "actionsRefs": {
      "call": {
        "title": "call — Trigger API Requests from Any Element"
      },
      "callAttrs": {
        "title": "Attributes",
        "text": "call supports the same attributes as the HTTP directives (get, post, etc.), including loading templates, redirect, custom headers, and more.",
        "col1": "Attribute",
        "col2": "Type",
        "col3": "Description",
        "call": "URL for the request (supports {variable} interpolation)",
        "method": "HTTP method. Default: \"get\"",
        "body": "Request body (JSON string with {variable} interpolation)",
        "headers": "JSON string of additional request headers",
        "as": "Name to assign the response in the context. Default: \"data\"",
        "into": "Write response to a named global store",
        "loading": "Template ID to show during the request (e.g. \"#spinner\")",
        "error": "Template ID to render on error. Receives error via var",
        "success": "Template ID to render on success. Receives response via var",
        "then": "Expression to execute on success (e.g. \"items.push(result)\")",
        "redirect": "SPA route to navigate to on success",
        "confirm": "Show browser confirm() dialog before sending"
      },
      "loadingTpl": {
        "title": "Loading Template",
        "text": "Show a loading indicator while the request is in flight. The element is disabled during loading and its content is restored afterwards."
      },
      "customHeaders": {
        "title": "Custom Headers",
        "text": "Pass per-request headers as a JSON string:"
      },
      "redirectAfter": {
        "title": "Redirect After Success",
        "text": "Navigate to an SPA route after a successful request:"
      },
      "abort": {
        "title": "Abort / SwitchMap",
        "text": "Rapid clicks automatically abort the previous in-flight request before starting a new one, preventing race conditions. Only the result of the last click is applied."
      },
      "events": {
        "title": "Events",
        "text": "call emits events on the NoJS event bus (listen with NoJS.on(...)):",
        "col1": "Event",
        "col2": "Description",
        "fetchSuccess": "{ url, data } on successful response",
        "fetchError": "{ url, error } on failure",
        "fetchEnd": "{ url } when the request completes (success or failure)"
      },
      "lifecycle": {
        "title": "Request Lifecycle"
      },
      "trigger": {
        "title": "trigger — Emit Custom Events"
      },
      "triggerAttrs": {
        "title": "Trigger Attributes",
        "text": "Customize trigger behavior with additional attributes:",
        "col1": "Attribute",
        "col2": "Description",
        "triggerData": "Data payload attached to the CustomEvent's detail property"
      },
      "ref": {
        "title": "ref — Named References",
        "text": "Access DOM elements without querySelector:"
      },
      "refsMap": {
        "title": "$refs — Ref Map",
        "text": "All elements with ref are accessible via $refs in the current scope:"
      }
    },
    "customDirectives": {
      "directive": {
        "title": "NoJS.directive()"
      },
      "usage": {
        "title": "Usage"
      },
      "priority": {
        "title": "Priority Levels",
        "text": "Priority determines when your directive runs relative to built-in directives. Lower numbers run first.",
        "col1": "Range",
        "col2": "When",
        "range0": "Before everything (state initialization)",
        "range1": "After state, with data fetching",
        "range10": "With structural directives (if, each)",
        "range20": "With rendering directives (bind, on:*)",
        "range30": "After everything (validation, side effects)"
      },
      "disposal": {
        "title": "Disposal & Cleanup",
        "text": "Custom directives must clean up after themselves. Use the _onDispose callback to remove event listeners, clear timers, and unsubscribe watchers.",
        "callout": "Directives registered after init (via plugins) are not frozen — but they cannot override built-in directives."
      },
      "webComponents": {
        "title": "Web Components Compatibility",
        "text": "No.JS directives work on custom elements:"
      },
      "componentPatterns": {
        "title": "Component-like Patterns with Templates"
      }
    },
    "errorHandling": {
      "perElement": {
        "title": "Per-Element Error Handling"
      },
      "retry": {
        "title": "Retry Behavior",
        "text": "Failed HTTP requests (5xx errors and network failures) are automatically retried. Configure the number of retries per element with the retry attribute, and the delay between retries with retry-delay.",
        "callout": "Retries only apply to server errors (5xx) and network failures. Client errors (4xx) are not retried."
      },
      "globalHandler": {
        "title": "Global Error Handler"
      },
      "errorBoundary": {
        "title": "error-boundary — Catch Errors in Subtree",
        "text": "Wrap a section of your page with error-boundary to catch errors that occur within its subtree. The boundary intercepts two kinds of errors:",
        "type1": "Expression evaluation errors — dispatched as nojs:error CustomEvents that bubble up from handler expressions (e.g. a bind or on:click expression throws).",
        "type2": "Window-level errors — uncaught JS errors and resource load failures (e.g. an <img> 404) that originate from elements inside the boundary.",
        "httpCaveat": "error-boundary does NOT catch failed HTTP requests made by get/post/put/patch/delete directives. Use the error attribute on the fetch element for per-request error handling, or NoJS.on('fetch:error', ...) for global HTTP error handling."
      },
      "boundaryEvents": {
        "title": "Error Boundary Events",
        "text": "When an error boundary catches an error, it dispatches a nojs:error CustomEvent on the boundary element. Listen with on:error to log errors or show notifications.",
        "callout": "The $event.detail object contains: message (string), source (element), and error (original Error object)."
      },
      "expressionErrors": {
        "title": "Expression Errors",
        "text": "When an expression fails to evaluate (e.g. accessing a property of undefined), No.JS catches the error, logs a warning via _warn(), and returns undefined. One broken expression never crashes the entire page.",
        "callout": "Enable NoJS.config({ debug: true }) to see detailed expression evaluation warnings in the console."
      },
      "commonMistakes": {
        "title": "Common Mistakes"
      }
    },
    "configuration": {
      "globalSettings": {
        "title": "Global Settings"
      },
      "configStores": {
        "title": "Pre-initializing Stores",
        "text": "Use the stores key in NoJS.config() to pre-create named global stores with initial data. Each entry becomes a reactive store accessible via $store.name anywhere in your markup.",
        "callout": "Stores created via config() will not overwrite stores that already exist — the first definition wins."
      },
      "configOptions": {
        "title": "Config Option Details",
        "sanitizeTitle": "sanitize",
        "sanitizeType": "Type: boolean | Default: true",
        "sanitizeText": "Controls whether HTML content rendered via bind-html is sanitized through a DOMParser-based structural sanitizer. When enabled, all potentially dangerous tags and attributes (e.g., <script>, onerror) are stripped before insertion into the DOM.",
        "devtoolsTitle": "devtools",
        "devtoolsType": "Type: boolean | Default: false",
        "devtoolsText": "Enables the No.JS devtools panel, accessible via window.__NOJS_DEVTOOLS__. When active, it exposes reactive state, registered directives, active routes, and component trees for inspection in the browser console.",
        "templatesCacheTitle": "templates.cache",
        "templatesCacheType": "Type: boolean | Default: true",
        "templatesCacheText1": "Controls whether the HTML content of remotely-fetched .tpl files is stored in an in-memory Map after the first request. On repeated navigations to the same route, the cached HTML is used directly and no HTTP request is made. The cache lives for the duration of the page session (no TTL — template assets are static).",
        "templatesCacheText2": "Set to false during local development if you want changes to .tpl files to be reflected without a hard page reload.",
        "loadPathTitle": "i18n.loadPath",
        "loadPathType": "Type: string | null | Default: null",
        "loadPathText": "URL template for loading locale JSON files via fetch. Use {locale} and optionally {ns} as placeholders. When null, translations must be provided inline via NoJS.i18n({ locales }).",
        "nsTitle": "i18n.ns",
        "nsType": "Type: string[] | Default: []",
        "nsText": "Array of namespace identifiers to preload at init(). Each namespace corresponds to a separate JSON file per locale. Additional namespaces can be loaded on-demand via the i18n-ns directive or route attribute.",
        "cacheTitle": "i18n.cache",
        "cacheType": "Type: boolean | Default: true",
        "cacheText": "Controls whether fetched locale JSON files are stored in an in-memory Map after the first request. Set to false during development for hot-reload of translation files.",
        "i18nPersistTitle": "i18n.persist",
        "i18nPersistType": "Type: boolean | Default: false",
        "i18nPersistText": "When true, the currently selected locale is persisted to localStorage. On the next page load, the persisted locale is restored automatically — useful for remembering a user's language preference across sessions.",
        "exprCacheSizeTitle": "exprCacheSize",
        "exprCacheSizeType": "Type: number | Default: 500",
        "exprCacheSizeText": "Maximum number of entries in each of the two internal LRU caches used by the expression evaluator: one for parsed expression ASTs and one for parsed statement ASTs. When the limit is reached the least-recently-used entry is evicted. Increase it if your app evaluates a large number of distinct template expressions; decrease it to reduce memory pressure in constrained environments.",
        "maxEventListenersTitle": "maxEventListeners",
        "maxEventListenersType": "Type: number | Default: 100",
        "maxEventListenersText": "Maximum number of listener functions allowed per event name on the NoJS event bus (NoJS.on()). When the limit is reached, a MaxListenersExceeded warning is logged and the new listener is still registered. Increase this if your application legitimately uses many listeners for the same event.",
        "appIdTitle": "appId",
        "appIdType": "Type: string | Default: \"\"",
        "appIdText": "An optional application identifier. Exposed via the devtools panel for distinguishing between multiple NoJS instances or environments.",
        "sanitizeHtmlTitle": "sanitizeHtml",
        "sanitizeHtmlType": "Type: function | null | Default: null",
        "sanitizeHtmlText": "Custom sanitizer hook for bind-html content. When set to a function, the built-in sanitizer is bypassed entirely and the provided function is used for all bind-html content. Useful for integrating external sanitizers like DOMPurify.",
        "dangerouslyDisableSanitizeTitle": "dangerouslyDisableSanitize",
        "dangerouslyDisableSanitizeType": "Type: boolean | Default: false",
        "dangerouslyDisableSanitizeText": "When true, ALL sanitization of bind-html content is bypassed. This is not recommended — it exposes your application to XSS attacks. Use sanitizeHtml to provide a custom sanitizer instead.",
        "sanitizeLockCallout": "The sanitize, dangerouslyDisableSanitize, and sanitizeHtml config keys are locked after init() completes. Calling NoJS.config() after initialization will not change these values — this prevents plugins or late-running scripts from weakening the security posture.",
        "suppressHashWarningTitle": "router.suppressHashWarning",
        "suppressHashWarningType": "Type: boolean | Default: false",
        "suppressHashWarningText": "When using history mode (useHash: false), NoJS logs a console warning if it detects a hash fragment in the URL that looks like a route path. Set this to true to suppress that warning.",
        "focusBehaviorTitle": "router.focusBehavior",
        "focusBehaviorType": "Type: string | Default: \"none\"",
        "focusBehaviorText": "Controls focus management after route navigation. When set to 'auto', NoJS moves focus to a predictable element inside the new route content (autofocus, h1, tabindex, or the outlet itself). Set to 'none' to disable.",
        "viewTransitionTitle": "router.viewTransition",
        "viewTransitionType": "Type: boolean | Default: true",
        "viewTransitionText": "Controls whether route transitions use the View Transition API. When true (default), route changes are animated natively by the browser using built-in presets (slide, fade, scale, none). Set to false to fall back to the legacy class-based transition system."
      },
      "apiProperties": {
        "title": "API Properties",
        "baseApiUrlTitle": "NoJS.baseApiUrl",
        "baseApiUrlText": "Getter/setter for the base API URL used by all fetch directives and NoJS.http calls. Can be read or reassigned at runtime.",
        "versionTitle": "NoJS.version",
        "versionText": "Read-only property that returns the current No.JS framework version string."
      },
      "interceptors": {
        "title": "Request Interceptors"
      },
      "security": {
        "title": "Security",
        "xssTitle": "XSS Protection",
        "xssList1": "bind always sets textContent, never innerHTML — safe by default.",
        "xssList2": "bind-html sanitizes content using a built-in DOMParser-based structural sanitizer (strips <script> tags, blocks on* event handlers, removes javascript: URIs).",
        "xssList3": "Template expressions are evaluated by a custom sandboxed parser — no eval() or Function() is used, and dangerous properties like __proto__ and constructor are blocked.",
        "csrfTitle": "CSRF Protection",
        "cspSecTitle": "Content Security Policy",
        "cspSecText1": "No.JS uses a custom expression parser that is fully CSP-compliant — no eval() or Function() constructor is used. No unsafe-eval directive is required in your Content Security Policy."
      }
    },
    "plugins": {
      "use": {
        "title": "NoJS.use()",
        "text": "Register a plugin before or after NoJS.init(). If the app is already initialized, the plugin's init hook runs immediately.",
        "objectFormTitle": "Object Form",
        "objectFormText": "The standard way to define a plugin. Provide a name, an install function, and optional lifecycle hooks.",
        "functionTitle": "Function Shorthand",
        "functionText": "For simple plugins, pass a named function. The function name becomes the plugin name.",
        "functionCallout": "Anonymous functions and arrow functions are rejected — the plugin must have a name.",
        "optionsTitle": "Options",
        "optionsText": "The second argument to NoJS.use() is passed to the plugin's install function.",
        "optionsTrustedNote": "The trusted option is special — see Trusted Interceptors below."
      },
      "interface": {
        "title": "Plugin Interface",
        "thProperty": "Property",
        "thType": "Type",
        "thRequired": "Required",
        "thDescription": "Description",
        "yes": "Yes",
        "no": "No",
        "nameDesc": "Unique identifier. Duplicate names are rejected.",
        "versionDesc": "Semver string for debugging.",
        "capabilitiesDesc": "Declared capabilities (logged in debug mode).",
        "installDesc": "Called synchronously by NoJS.use().",
        "initDesc": "Called after NoJS.init() completes (DOM is ready).",
        "disposeDesc": "Called during NoJS.dispose() for cleanup.",
        "lifecycleTitle": "Lifecycle",
        "lifecycleInstall": "install runs immediately and synchronously. Use it to register interceptors, globals, directives, event listeners, and stores.",
        "lifecycleInit": "init runs after the DOM is processed and the router is active. Use it for work that depends on rendered elements or route state.",
        "lifecycleDispose": "dispose runs during app teardown. Use it to close WebSocket connections, clear intervals, flush pending analytics, etc.",
        "duplicateTitle": "Duplicate Detection",
        "duplicateText": "A plugin name can only be registered once. If NoJS.use() is called again with the same name but a different object, a warning is logged and the call is ignored.",
        "freezingTitle": "Directive Registry Freezing",
        "freezingText": "Core directives (state, bind, get, on:*, etc.) are frozen after the framework loads. Plugins can register new directives but cannot override built-in ones."
      },
      "globals": {
        "title": "NoJS.global()",
        "text": "Inject a reactive variable accessible as $name in any template expression.",
        "namingTitle": "Naming Conventions",
        "namingPrefix": "Global names are accessed with a $ prefix in templates: NoJS.global('foo', ...) becomes $foo.",
        "namingNamespace": "Use your plugin name as a namespace to avoid collisions: $analytics, $auth, $featureFlags.",
        "reservedTitle": "Reserved Names",
        "reservedText": "The following names cannot be used with NoJS.global():",
        "reservedProto": "Prototype pollution vectors (__proto__, constructor, prototype) are also blocked.",
        "reactivityTitle": "Reactivity",
        "reactivityText": "Object values passed to NoJS.global() are automatically wrapped in a reactive context. Mutations trigger DOM updates just like store or state changes.",
        "ownershipTitle": "Ownership Tracking",
        "ownershipText": "When a plugin registers a global during its install phase, it becomes the owner. If a different plugin overwrites that global, a warning is logged."
      },
      "sentinels": {
        "title": "Interceptor Sentinels",
        "text": "Request interceptors can return special objects keyed by sentinel Symbols to control the fetch pipeline.",
        "cancelTitle": "NoJS.CANCEL — Abort the Request",
        "cancelText": "Return an object with [NoJS.CANCEL]: true to prevent the request from being sent. The fetch throws an AbortError.",
        "respondTitle": "NoJS.RESPOND — Serve a Cached Response",
        "respondText": "Return an object with [NoJS.RESPOND]: data to short-circuit the request and return data directly as the response. No HTTP request is made.",
        "replaceTitle": "NoJS.REPLACE — Replace Response Data",
        "replaceText": "Return an object with [NoJS.REPLACE]: data from a response interceptor to replace the parsed response body with custom data.",
        "summaryTitle": "Summary",
        "thSentinel": "Sentinel",
        "thUsedIn": "Used In",
        "thEffect": "Effect",
        "cancelUsedIn": "Request interceptor",
        "respondUsedIn": "Request interceptor",
        "replaceUsedIn": "Response interceptor",
        "cancelEffect": "Aborts the request (throws AbortError)",
        "respondEffect": "Returns data directly, skips HTTP call",
        "replaceEffect": "Replaces the parsed response body"
      },
      "trusted": {
        "title": "Trusted Interceptors",
        "text": "By default, interceptors receive redacted copies of requests and responses — sensitive headers and URL parameters are stripped or replaced with [REDACTED].",
        "fullAccess": "Auth plugins that need access to the real headers can be installed with { trusted: true }.",
        "callout": "A console warning is logged when a plugin is installed with trusted access. Only grant trusted to plugins you control or have audited.",
        "redactedTitle": "Redacted Headers",
        "requestLabel": "Request headers stripped before passing to untrusted interceptors:",
        "responseLabel": "Response headers stripped from the response proxy:"
      },
      "dispose": {
        "title": "NoJS.dispose()",
        "text": "Tears down the entire application: disposes plugins, clears globals, and removes interceptors.",
        "orderTitle": "Disposal Order",
        "orderStep1": "Plugins are disposed in reverse installation order (last installed, first disposed).",
        "orderStep2": "Each plugin's dispose function is given a 3-second timeout. If it exceeds the timeout, an error is logged and disposal continues with the next plugin.",
        "orderStep3": "After all plugins are disposed, globals and interceptors are cleared.",
        "asyncTitle": "Async Dispose",
        "asyncText": "Plugin dispose functions can be async. The framework awaits each one (subject to the 3-second timeout).",
        "callout": "Plugins cannot be installed during disposal. Calls to NoJS.use() while dispose() is running are ignored with a warning."
      },
      "security": {
        "title": "Security Guidelines",
        "text": "When authoring plugins, follow these practices to keep your application secure.",
        "namespaceTitle": "Namespace Everything",
        "namespaceText": "Prefix globals, stores, and event names with your plugin name to avoid collisions.",
        "evalTitle": "Never Use eval or Function",
        "evalText": "Values passed to NoJS.global() are checked for dangerous references. eval and Function are blocked.",
        "cleanupTitle": "Clean Up in dispose",
        "cleanupText": "Always provide a dispose hook that clears intervals, closes connections, and removes event listeners.",
        "overwriteTitle": "Avoid Overwriting Others' Globals",
        "overwriteText": "If your plugin detects that a global is already owned by another plugin, consider using a different name rather than silently overwriting.",
        "validateTitle": "Validate Options",
        "validateText": "Check required options in install and warn early."
      },
      "example": {
        "title": "Complete Example",
        "text": "A full analytics plugin demonstrating the plugin lifecycle, globals, interceptors, and disposal."
      }
    },
    "headManagement": {
      "intro": {
        "text": "No.JS lets you control <head> metadata declaratively from anywhere in your markup. Use hidden elements with page-title, page-description, page-canonical, and page-jsonld attributes — they are evaluated reactively and update the document head automatically.",
        "routingTip": "For route-level head management, you can also use <code>page-title</code> and <code>page-description</code> directly on <code>&lt;template route=\"...\"&gt;</code> tags. See <a href=\"/docs/routing#route-head-attributes\">Route Head Attributes</a>."
      },
      "placement": {
        "title": "Placement",
        "text": "Place head-management elements inside any state scope. They must have the hidden attribute — they are never rendered visually.",
        "reactive": "All values are reactive — when the underlying state changes, the document head is updated immediately."
      },
      "pageTitle": {
        "title": "page-title",
        "text": "Sets document.title to the evaluated expression.",
        "expression": "The value is a standard No.JS expression evaluated against the element's context."
      },
      "pageDescription": {
        "title": "page-description",
        "text": "Sets the content attribute of the <meta name=\"description\"> tag. If the tag doesn't exist, it is created.",
        "duplicate": "Only one page-description should be active at a time. If multiple are present, the last one processed wins."
      },
      "pageCanonical": {
        "title": "page-canonical",
        "text": "Sets the href attribute of the <link rel=\"canonical\"> tag. If the tag doesn't exist, it is created.",
        "existing": "If a canonical link already exists in your HTML, the attribute value is updated in place."
      },
      "pageJsonld": {
        "title": "page-jsonld",
        "text": "Injects a <script type=\"application/ld+json\"> block into the document head. The inner text of the element is used as the JSON-LD payload.",
        "scriptNote": "The content is inserted as-is — make sure it is valid JSON. Use <code>{variable}</code> interpolation to inject dynamic values.",
        "marker": "The generated script tag is marked with <code>data-nojs</code> so it can be identified and cleaned up on unmount.",
        "fullExampleTitle": "Full Product Page Example"
      },
      "notes": {
        "title": "Notes & Edge Cases",
        "competingTitle": "Multiple Directives Competing",
        "competingText": "If multiple elements set the same head attribute (e.g. two page-title elements), the last one processed wins. When an element is unmounted, its head contribution is reverted to the previous value.",
        "cleanupTitle": "Cleanup on Unmount",
        "cleanupText": "When an element with a head directive is removed from the DOM (e.g. inside an if block or a route change), its head contribution is automatically cleaned up.",
        "captureTitle": "JSON-LD Template Capture",
        "captureText": "The page-jsonld element's innerHTML is captured at processing time. If you need reactive JSON-LD, place the element inside a state scope and use {expression} interpolation."
      }
    },
    "pagination": {
      "quickStart": {
        "title": "Quick Start",
        "text": "Add get-trigger and get-insert to any get directive to enable pagination. Here is a minimal infinite scroll example:",
        "note": "The get directive fetches page 1 on load. When the user scrolls near the bottom, it automatically fetches the next page and appends the new items."
      },
      "attributes": {
        "title": "Attributes Reference",
        "col1": "Attribute",
        "col2": "Values",
        "col3": "Default",
        "col4": "Description",
        "getInsert": "Controls where new content is placed relative to existing content. Without this attribute, fetched content replaces the current content entirely.",
        "getTrigger": "Controls when the fetch fires. Without this attribute, the fetch fires immediately on mount.",
        "getPage": "Initial page number for offset-based pagination. The page context variable increments automatically after each successful fetch.",
        "getCursor": "Enable cursor-based pagination. The cursor is extracted from the response header (X-Cursor) or JSON body field.",
        "getCursorField": "Custom JSON field path for cursor extraction using dot notation (e.g. pagination.nextToken). Without this, No.JS probes cursor, next_cursor, nextCursor, next.",
        "getThreshold": "IntersectionObserver rootMargin. Controls how early the fetch triggers before the element enters the viewport.",
        "getTriggerLabel": "Custom label text for the auto-generated load-more button when using get-trigger=\"button\"."
      },
      "examples": {
        "title": "Live Examples",
        "infiniteScroll": {
          "title": "Infinite Scroll",
          "desc": "Automatically loads the next page when the user scrolls near the bottom of the list.",
          "label": "Live Result"
        },
        "loadMore": {
          "title": "Load More Button",
          "desc": "Displays a button that the user clicks to load the next page of results.",
          "label": "Live Result"
        },
        "lazyLoad": {
          "title": "Lazy Load on Visible",
          "desc": "Content is fetched only when the element scrolls into the viewport. Ideal for below-the-fold sections.",
          "label": "Live Result"
        },
        "hoverPrefetch": {
          "title": "Hover Prefetch",
          "desc": "Content is fetched on the first mouseenter event. Useful for tooltips and preview cards.",
          "label": "Live Result",
          "hoverMe": "Hover over this card to load preview content"
        },
        "programmatic": {
          "title": "Programmatic Fetch",
          "desc": "The fetch does not fire automatically. Use $refs to trigger it manually via a button or other event.",
          "label": "Live Result",
          "clickToLoad": "Click the button to trigger the fetch manually."
        },
        "cursorPagination": {
          "title": "Cursor Pagination",
          "desc": "Uses cursor-based pagination instead of page numbers. The cursor is extracted from the JSON response body.",
          "label": "Live Result"
        },
        "liveFeed": {
          "title": "Live Feed (Prepend)",
          "desc": "New content is inserted at the top of the list, simulating a live feed or news ticker.",
          "label": "Live Result"
        }
      },
      "matrix": {
        "title": "Composition Matrix",
        "text": "The get-trigger and get-insert attributes combine to form a 5x3 matrix of behaviors. Here is how each combination works:",
        "col1": "Trigger",
        "col2": "No insert (replace)",
        "col3": "append",
        "col4": "prepend",
        "immediateReplace": "Default behavior — fetch replaces content on mount.",
        "immediateAppend": "Fetch on mount, append result below existing content.",
        "immediatePrepend": "Fetch on mount, prepend result above existing content.",
        "scrollReplace": "Falls back to visible behavior (scroll requires insert mode).",
        "scrollAppend": "Infinite scroll — appends new pages as user scrolls down.",
        "scrollPrepend": "Infinite scroll — prepends new pages as user scrolls up.",
        "buttonReplace": "Falls back to immediate behavior (button requires insert mode).",
        "buttonAppend": "Load-more button — appends next page on click.",
        "buttonPrepend": "Load-more button — prepends next page on click.",
        "visibleReplace": "Lazy load — fetches and replaces content when element enters viewport.",
        "visibleAppend": "Lazy load — fetches and appends content when element enters viewport.",
        "visiblePrepend": "Lazy load — fetches and prepends content when element enters viewport.",
        "hoverReplace": "Prefetch — fetches and replaces content on first mouseenter.",
        "hoverAppend": "Prefetch — fetches and appends content on first mouseenter.",
        "hoverPrepend": "Prefetch — fetches and prepends content on first mouseenter.",
        "noneReplace": "Manual — no auto-fetch, use $refs.refresh() to trigger.",
        "noneAppend": "Manual — no auto-fetch, $refs.refresh() appends.",
        "nonePrepend": "Manual — no auto-fetch, $refs.refresh() prepends."
      },
      "endOfData": {
        "title": "End-of-Data Detection",
        "text": "No.JS detects the end of paginated data in three ways, depending on the response format:",
        "emptyBody": "Empty response body",
        "emptyBodyText": "When the server returns an empty response body (Content-Length: 0 or empty string), No.JS stops pagination and hides the load-more button.",
        "lastPageHeader": "X-NoJS-Last-Page header",
        "lastPageHeaderText": "The server can send an X-NoJS-Last-Page: true response header to explicitly signal there are no more pages.",
        "nullCursor": "Null cursor",
        "nullCursorText": "For cursor-based pagination, when the cursor field in the JSON response is null or absent, No.JS stops pagination."
      },
      "templates": {
        "title": "Templates with Pagination",
        "text": "Loading, error, and empty templates work seamlessly with pagination. The loading template is shown during each page fetch, and the error template is shown if any page fails.",
        "loadingNote": "During pagination (not the first fetch), a small inline loading indicator is shown at the insertion edge rather than replacing the entire content.",
        "emptyNote": "The empty template is only shown when the first page returns no data. Subsequent empty pages simply stop pagination."
      },
      "scrollPosition": {
        "title": "Scroll Position Preservation",
        "text": "When using get-insert=\"prepend\", No.JS automatically preserves the user's scroll position. New content is inserted above the current viewport, and the scroll offset is adjusted so the user's view does not jump."
      }
    },
    "directiveCompatibility": {
      "overview": {
        "title": "Directive Compatibility",
        "text": "No.JS processes directives by priority order (state 0 > HTTP 1 > computed/watch 2 > ref 5 > structural 10 > rendering/events 20 > validation 30). Most directives compose freely, but certain same-element combinations require awareness of processing order and scope mechanics. This page documents every known interaction.",
        "callout": "Combinations marked \"Defined semantics\" work correctly but follow specific rules you should understand. Combinations marked \"Use workaround\" have a known limitation with a documented alternative pattern."
      },
      "matrix": {
        "title": "Compatibility Matrix",
        "structuralTitle": "Structural + Structural (same element)",
        "conditionalTitle": "Conditional + Other Directives (same element)",
        "loopTitle": "Loop + Other Directives (same element)",
        "bindingTitle": "Binding + Binding (same element)"
      },
      "semantics": {
        "title": "Defined Semantics (v1.18.0)",
        "ifGates": {
          "title": "if gates the entire element",
          "text": "As of v1.18.0, if gates the entire element, not just its children. This means all directives on the same element respect the if condition:",
          "http": "HTTP directives (get, post, etc.) on the same element only fire when the condition is true. Zero network requests while false.",
          "computed": "Computed values and watch side effects on the same element only run when the condition is true. No phantom computations.",
          "head": "Head directives (page-title, page-description, page-canonical, page-jsonld) only apply when the condition is true.",
          "binding": "Binding, model, and event directives on the same element are gated in the same way."
        },
        "perClone": {
          "title": "Per-clone computed/watch in loops",
          "text": "When computed or watch appears on a loop element, each clone gets its own independent instance. A computed creates a per-item derived value scoped to that clone's context. A watch fires independently per clone when the watched property changes."
        },
        "usePriority": {
          "title": "use priority 9",
          "text": "The use directive runs at priority 9 (just below structural directives at 10), ensuring templates are instantiated before if/each processing."
        },
        "httpPlacement": {
          "title": "HTTP verb placement",
          "text": "HTTP directives (get, post, put, patch, delete) should be placed on a parent element, not on the loop element itself. The loop element is used as a clone template — placing an HTTP directive on it would fire one request per clone."
        }
      },
      "limitations": {
        "title": "Known Limitations",
        "switchLoop": {
          "title": "switch + loop on case children",
          "text": "Placing a loop directive (each/foreach/for) on a case child of a switch renders both branches; the switch logic goes inert. Workaround: wrap the loop in a container element inside the case."
        },
        "ifLoop": {
          "title": "if + loop on the same element",
          "text": "if and a loop directive on the same element cannot coordinate — the condition cannot remove items, and empty shells may render. Workaround: wrap the loop in a container element with the if."
        },
        "elseIfLoop": {
          "title": "else-if + loop on the same element",
          "text": "else-if combined with a loop on the same element breaks the conditional chain. Items render regardless of the preceding if condition. Additionally, a sibling else element is orphaned. Workaround: wrap the loop in a container element with the else-if."
        },
        "duplicateRef": {
          "title": "Duplicate ref in loop",
          "text": "Using ref on a loop element means each clone overwrites the same ref name. Only the last clone's element is accessible via $refs. Workaround: use bind-ref with a dynamic expression to create unique ref names per clone."
        },
        "bindValueModel": {
          "title": "bind-value + model on the same input",
          "text": "Both bind-value and model set up two-way binding pipelines with input listeners. The two handlers can fight over the value during typing, especially for number inputs. Workaround: use only model for two-way binding."
        },
        "watchOnChange": {
          "title": "watch + on:change on the same element",
          "text": "Both watch and on:change claim the change event, leading to duplicate handler execution. Workaround: use only one mechanism — watch for reactive side effects or on:change for DOM event handling."
        },
        "tBind": {
          "title": "t + bind double writer",
          "text": "Both t (i18n translation) and bind write to the element's textContent. The attribute processed last wins. Workaround: use t with interpolation parameters (t-name, t-count) or use bind alone."
        },
        "httpOnLoop": {
          "title": "HTTP verb on loop element",
          "text": "Placing an HTTP directive (get, post, etc.) directly on the loop element causes one network request per clone, since clones retain the HTTP attribute. Workaround: place the HTTP directive on a parent element."
        }
      },
      "migration": {
        "title": "Migration Notes (v1.18.0)",
        "ifGates": {
          "title": "if now gates the entire element",
          "before": "Before v1.18.0, if only controlled children visibility. Directives with higher priority than if (such as HTTP at priority 1, computed/watch at priority 2, and head directives at priority 1) would still execute on the element even when the condition was false.",
          "after": "After v1.18.0, if gates the entire element. All directives on the same element respect the if condition.",
          "check": "If you relied on a falsy if branch to still compute values or fire HTTP requests, move those directives to a separate element without an if condition."
        },
        "usePriority": {
          "title": "use priority changed to 9",
          "text": "The use directive now has an explicit priority of 9, placing it just below structural directives (10). This ensures templates are instantiated before if/each processing. This change is backward-compatible for standard usage."
        }
      }
    },
    "sse": {
      "basicTitle": "Basic Usage",
      "basicText": "The sse directive opens an EventSource connection to a server endpoint and exposes incoming data as a reactive context variable via the as attribute. JSON payloads are parsed automatically; non-JSON data is stored as a plain string.",
      "text": "When the server sends non-JSON data (plain text, CSV lines, etc.), the as variable receives the raw string.",
      "attributesTitle": "Attributes",
      "col1": "Attribute",
      "col2": "Type",
      "col3": "Description",
      "sse": "URL of the SSE endpoint. Supports interpolated expressions (e.g. /api/{channel}).",
      "as": "Name of the reactive context variable that receives each message's data.",
      "sseEvent": "Named SSE event type to listen for. Without this, only unnamed (default) messages are captured.",
      "sseInsert": "How new messages update the variable: replace (default), append (oldest-first array), or prepend (newest-first array).",
      "sseLimit": "Maximum array length when using append or prepend mode. Oldest entries are dropped when the limit is exceeded. Has no effect in replace mode (triggers a console warning).",
      "sseCredentials": "Boolean attribute. When present, the EventSource is opened with withCredentials: true, sending cookies on cross-origin requests.",
      "into": "Store name. When set, each message is also written to the named global store under the as key.",
      "error": "ID of a <template> element to render when the connection permanently fails (readyState === CLOSED).",
      "then": "Expression executed after each message is processed. Receives $event (the parsed data).",
      "insertModesTitle": "Insert Modes",
      "insertModesText": "The sse-insert attribute controls how successive messages update the context variable. Replace is the default and suits single-value streams like stock tickers. Append and prepend collect messages into an array, capped by sse-limit. Using append or prepend without sse-limit triggers a console warning about potential unbounded memory growth. Setting sse-limit in replace mode has no effect and also triggers a warning.",
      "namedEventsTitle": "Named Events",
      "namedEventsText": "By default, the directive listens for unnamed SSE messages (the message event). Set sse-event to listen for a specific named event type instead. Multiple elements can subscribe to different event types on the same endpoint — each opens its own EventSource connection.",
      "connectionStateTitle": "Connection State ($sse)",
      "connectionStateText": "Every sse element exposes a reactive $sse object on its context with three boolean properties reflecting the EventSource readyState.",
      "stateCol1": "Property",
      "stateCol2": "Type",
      "stateCol3": "Description",
      "connecting": "true while the EventSource is in CONNECTING state (initial connect or auto-reconnect).",
      "open": "true when the connection is open and receiving messages.",
      "errorProp": "true only when the connection has permanently failed (readyState === CLOSED). During auto-reconnect, error remains false.",
      "statesCallout": "During auto-reconnect the browser's EventSource transitions to CONNECTING. NoJS mirrors this: $sse.connecting becomes true, $sse.open becomes false, and $sse.error stays false. The error flag only fires when the connection cannot be recovered (CLOSED).",
      "liveFeedTitle": "Live Feed Example",
      "liveFeedText": "Combine prepend insert mode, a limit, and connection-state indicators for a complete real-time activity feed with automatic reconnection status.",
      "reactiveUrlTitle": "Reactive URLs",
      "reactiveUrlText": "When the sse URL contains interpolated expressions, the directive watches for changes. If the resolved URL changes, the current EventSource is closed and a new one is opened to the updated endpoint. In append/prepend mode, accumulated data is reset to an empty array. In replace mode (the default), the previous value persists until the first message arrives on the new connection.",
      "storeTitle": "Store Integration",
      "storeText": "Add the into attribute to write each message into a global store alongside the local context variable. Other elements anywhere in the DOM can read the data via $store.storeName.",
      "errorHandlingTitle": "Error Handling",
      "errorHandlingText": "Point the error attribute to a <template> to render fallback content when the connection permanently closes. The template receives a synthetic object with a single message field (\"SSE connection closed\") — not the browser error object. The error template only renders when readyState reaches CLOSED (terminal close); it is never shown during auto-reconnect.",
      "thenTitle": "Per-Message Callback",
      "thenText": "The then attribute accepts an expression that runs after each message is processed and the context variable is updated. The parsed message data is available as $event. Use it for side effects like incrementing counters or triggering actions.",
      "noLoadingTitle": "No Loading Template",
      "noLoadingText": "Unlike HTTP directives (get, post), the sse directive does not support a loading template attribute. SSE connections are long-lived — there is no single request/response cycle to bracket. Use $sse.connecting to show a status indicator while the connection is being established.",
      "authTitle": "Authentication",
      "authText": "The EventSource API does not support custom headers. Two authentication strategies are available: query-string tokens and cookie-based credentials.",
      "authCallout": "The <code>sse-credentials</code> attribute sets <code>withCredentials: true</code> on the EventSource, which sends cookies on cross-origin requests. The server must respond with the appropriate CORS headers (<code>Access-Control-Allow-Credentials: true</code>, explicit <code>Access-Control-Allow-Origin</code>).",
      "interceptorsNote": "NoJS fetch interceptors do not apply to SSE connections. EventSource is a separate browser API — header injection, caching, and retry logic from the fetch pipeline are not available.",
      "connectionLimitTitle": "Connection Limits",
      "connectionLimitText": "Browsers enforce a per-origin limit of roughly 6 concurrent HTTP/1.1 connections. Each sse element opens its own EventSource connection — connections are never shared or coalesced, even when multiple elements point to the same URL. NoJS logs a warning when a 6th connection to the same origin is opened. To stay within limits, use HTTP/2 (which multiplexes streams over a single TCP connection), reduce the number of sse elements on the page, or close unused connections via conditional rendering (e.g. an if directive)."
    }
  }
}
