# KitJS

**Reactive HTML from one browser script.**

KitJS adds local state, bindings, events, conditional rendering, lists, and
small components to ordinary HTML through `data-kit-*` attributes. It is a
standalone classic script: no frontend build, virtual DOM, package loader, Go
runtime, or Kitwork server is required.

> **Stable release build:** this checkout and its generated browser artifacts
> identify as `1.0.0`. Public npm/CDN availability and npm `latest` resolution
> are recorded only after independent retrieval; see
> [release evidence](RELEASE_READINESS.md). Pin the exact version and SRI shown
> below for production.

`@kitwork/kitjs` is the standalone browser package. Kitwork JITJS—
`router.jitjs(...)`, `/jit/...` delivery, managed component/service graphs, and
`data-kit-action`—is server-side Kitwork functionality and is not a KitJS
package API.

## Start with one CDN script

For a quick experiment, the bare jsDelivr entry follows npm `latest` and may
be automatically minified by jsDelivr:

```html
<script defer src="https://cdn.jsdelivr.net/npm/@kitwork/kitjs"></script>
```

That URL upgrades automatically and its bytes may change. Do not attach the
`dist/kit.js` SRI to it. Production pages should pin the exact readable file
and its matching SRI as shown next.

Use the Kit profile when links and forms should use normal browser navigation:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>KitJS counter</title>
  <script
    defer
    src="https://cdn.jsdelivr.net/npm/@kitwork/kitjs@1.0.0/dist/kit.js"
    integrity="sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI="
    crossorigin="anonymous"></script>
</head>
<body>
  <section data-kit-scope="count: 0">
    <button type="button" data-kit-click="count--">−</button>
    <output data-kit-text="count">0</output>
    <button type="button" data-kit-click="count++">+</button>
  </section>
</body>
</html>
```

That is enough for local reactive UI. `data-kit-scope` creates a shallow state
boundary, descendant directives use the nearest boundary, and synchronous
writes are batched into one render. No component registration is needed.

## Choose one profile

KitJS publishes two complete, mutually exclusive profiles:

| Profile | Exact file | Navigation |
|---|---|---|
| **Kit** | `dist/kit.js` | Normal browser document navigation |
| **Hydrate** | `dist/hydrate.kit.js` | Optional compatibility-checked Drive/Morph, with native fallback |

Use Hydrate only when you want eligible same-origin links and GET forms to
continue without replacing the live document:

```html
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@kitwork/kitjs@1.0.0/dist/hydrate.kit.js"
  integrity="sha256-AbI+PkXOVgSxZDNiMjQCxehrcASWQuW+mJHrSmfS57k="
  crossorigin="anonymous"></script>
```

Hydrate already contains the complete Kit runtime. Never load both profiles on
one page. When a Hydrate destination is incompatible, the browser performs a
normal navigation before KitJS mutates the current document.

## Scopes and directives

A scope contains data, not arbitrary JavaScript behavior:

```html
<section data-kit-scope="count: 3; open: true; profile: { name: 'Ada' }">
  <button type="button" data-kit-click="open = !open">Toggle</button>
  <p data-kit-show="open">
    <strong data-kit-text="profile.name">Ada</strong>
    clicked <output data-kit-text="count">3</output> times.
  </p>
</section>
```

The object form is also supported:

```html
<section data-kit-scope="{ count: 3, open: true }"></section>
```

Top-level state names are ASCII identifiers. Values may contain `null`,
booleans, finite numbers, strings, arrays, and plain objects. Scope declarations
do not accept calls, property reads, lambdas, or executable initialization
programs.

The main authored directives are:

| Purpose | Example |
|---|---|
| Local state | `data-kit-scope="count: 0; open: true"` |
| Text | `data-kit-text="user?.name ?? 'Guest'"` |
| Visibility | `data-kit-show="open"` |
| Safe attributes | `data-kit-bind="aria-expanded: open; disabled: busy"` |
| Classes | `data-kit-class="open ? 'block opacity-100' : 'hidden opacity-0'"` |
| Continuous styles | `data-kit-style="width: progress + '%'; opacity: visible ? 1 : 0"` |
| Form state | `data-kit-model="name"` |
| Events | `data-kit-click:prevent="save()"` |
| Conditional element | `<p data-kit-if="ready">Ready</p>` |
| Conditional fragment | `<template data-kit-if="ready">...</template>` |
| Keyed list | `<template data-kit-for="item, i of items" data-kit-key="item.id">...</template>` |
| Ownership opt-out | `data-kit-ignore` |

Unknown directives and invalid expressions fail closed.

### `if`, `show`, and `for`

Use `data-kit-if` directly on an ordinary element for a single branch:

```html
<section data-kit-scope="ready: false">
  <button type="button" data-kit-click="ready = !ready">Toggle</button>
  <p data-kit-if="ready">Ready.</p>
</section>
```

Use a template when the branch needs multiple top-level nodes, no wrapper, or
content that must remain inert until materialized:

```html
<template data-kit-if="ready">
  <h2>Ready</h2>
  <p>The complete fragment mounts together.</p>
</template>
```

`data-kit-for` and `data-kit-key` remain template-only. `data-kit-show` keeps a
node mounted and changes visibility; `data-kit-if` disposes the branch when it
becomes false. An initially true direct branch keeps the exact authored host.
After it unmounts, a later true value creates fresh node and component identity.

An ordinary element with `data-kit-if` is normal browser DOM before KitJS
prepares it, so it may paint or start resources first. Client-side conditions
are not authorization, secrecy, inertness, or no-flash boundaries. Scripts are
forbidden in structural branches; keep secrets and authorization decisions out
of client-authored HTML.

## Events and expressions

Event attributes use the native event name:

```html
<form data-kit-submit:prevent="save()">
  <input data-kit-keydown:escape="close()">
  <button data-kit-click:once="count++">Run once</button>
  <button data-kit-click:debounce(250)="search()">Search</button>
</form>
```

Supported events are `click`, `dblclick`, `submit`, `input`, `change`,
`keydown`, `keyup`, `pointerdown`, `pointerup`, `focusin`, and `focusout`.
Modifiers are `self`, `prevent`, `stop`, `once`, `outside`, `enter`, `escape`,
and `debounce(ms)`.

Bindings are read-only. Actions may assign or apply `++`/`--` to an existing
writable top-level field. Multiple writes commit together only when the whole
synchronous action succeeds.

The expression language supports literals, arrays and objects, arithmetic,
comparisons, logical and nullish operators, conditionals, safe method calls,
expression lambdas, and continuous optional chains. It is deliberately not
JavaScript: declarations, loops, constructors, template literals, member
assignment, browser globals, and prototype escape names are unavailable.

## Standalone components

Use a component only when a boundary needs trusted methods or lifecycle. CDN
package components are registered directly with an unversioned name; the
general host form is `data-kit-component="name"`:

```html
<section data-kit-component="counter">
  <button type="button" data-kit-click="increment()">Increment</button>
  <output data-kit-text="count">0</output>
</section>

<script
  defer
  src="https://cdn.jsdelivr.net/npm/@kitwork/kitjs@1.0.0/dist/kit.js"
  integrity="sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI="
  crossorigin="anonymous"></script>
<script defer src="/assets/components.js"></script>
```

```js
// /assets/components.js
kit.component("counter", {
  count: 0,
  increment() {
    this.count += 1;
  }
});
```

Each host receives an isolated shallow instance. A `data-kit-scope` on the same
host may seed existing writable data fields, but it cannot add fields or replace
methods. `data-kit-as="$name"` gives the component an action-only alias.

If a host names a component that has not been registered, KitJS reports the
missing definition once and leaves the authored fallback DOM in place. It does
not fetch component code and it does not reload the page. Register definitions
through the ordered script shown above; a later explicit registration can still
mount matching connected hosts.

The optional trusted `init(context)` hook receives a frozen context:

| Member | Purpose |
|---|---|
| `host` | Current component host, then `null` after disposal |
| `owned(selector)` | Fresh matches owned by this boundary |
| `listen(target, type, fn, options)` | Native listener with automatic cleanup |
| `cleanup(fn)` | Register an idempotent disposer |
| `afterRender(fn)` | Run once after this boundary's next render |

Simple components do not need `init`. Trusted component JavaScript has normal
page authority and must never be built from untrusted source.

The complete public JavaScript API is:

```text
kit.version
kit.component(name, plainObject)
```

`globalThis.kit` is frozen. There is no public compiler, renderer, manual
mount/destroy control, plugin loader, navigation object, or service registry.
`data-kit-version` is unsupported, and there is no `data-kit-local` marker.

## Hydrate and Drive

Drive enhances compatible same-origin links and GET forms. It does not require
a special router and it never executes scripts discovered in fetched HTML.

For a route group to remain compatible:

- the Hydrate script must be a classic external `defer` script and a direct
  child of `head`;
- every page must preserve its resolved URL, position, order, and complete
  attributes;
- every other executable script that must persist must follow the same external
  direct-head topology;
- cross-origin scripts, including jsDelivr, must carry valid SRI and must not
  use `data-kit-drive="stable"`.

For a self-hosted same-origin external script, `data-kit-drive="stable"` may be
used as an author promise that its URL, bytes, position, order, and attributes
will remain unchanged:

```html
<script defer src="/assets/hydrate.kit.js" data-kit-drive="stable"></script>
```

The marker is not a content check. Prefer a versioned or content-hashed URL with
SRI for production. Inline scripts, body scripts, modules, import maps,
speculation rules, `async`, `nomodule`, or any executable topology change make
the destination incompatible. The result is supported native navigation, not a
partially applied Morph.

An inline `kit.component(...)` registration is therefore fine with the Kit
profile, but it makes Hydrate leave navigation to the browser. To preserve
Drive eligibility, put component registrations in one external file whose
exact tag is shared by every compatible route.

The same rule applies to any executable inline script, including a simple
`<script>console.log("hello world")</script>`. Hydrate does not reload because
of the script itself; it declines Drive, and the next navigation becomes a
normal full document load where the browser executes that script.

Use `data-kit-drive="false"` on a link, GET form, submitter, or ancestor when a
route must always navigate normally.

Hydrate validates the initial script topology before intercepting navigation.
An invalid initial page leaves links and forms native and emits one diagnostic.
An incompatible fetched destination falls back before mutating title, head,
history, or body. Responses over 8 MiB, parsed documents over 100,000 nodes, or
documents deeper than 256 also fall back natively.

Same-document fragment links remain browser-native. Compatible cross-route
visits preserve requested fragments, focus, scroll, forms, Back/Forward state,
and component cleanup according to the browser contract.

### Ownership during Morph

- `data-kit-ignore` leaves a host and its subtree inert to KitJS and opaque to
  Morph. It is an ownership marker, not a sanitizer.
- `data-kit-retain="key"` preserves a registered standalone component host and
  its live state across compatible Hydrate navigation. Keys must be unique and
  stable; retained components cannot live inside structural branches.

## Security model

- Authored expressions never execute through `eval()` or `Function`.
- Expressions cannot access `window`, `document`, the native event object, or
  the trusted `kit` object.
- `data-kit-bind` blocks event handlers, raw style, `srcdoc`, HTML replacement
  sinks, unsafe URL schemes, and `data-kit-*` targets.
- `data-kit-style` validates the complete property map before writing.
- Hydrate never executes scripts from fetched HTML; incompatible pages navigate
  through the browser document loader.
- Component definitions are trusted application JavaScript and retain normal
  browser authority.

KitJS narrows the authority of authored expressions; it is not a general HTML
sanitizer and cannot make an unsafe application safe by itself.

## Release artifact identity

The deterministic `1.0.0` build in this checkout produces these exact readable
profile files:

| Profile | Bytes | SHA-256 | SRI |
|---|---:|---|---|
| Kit | 206,607 | `2d7b750cae101b8decbac50dc334d0a7b1f4e3a1b5fe038d74e84101c5d52192` | `sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI=` |
| Hydrate | 314,424 | `01b23e3e45ce5604b1643362323402c5e86b70049642e5be9891eb4a67d2e7b9` | `sha256-AbI+PkXOVgSxZDNiMjQCxehrcASWQuW+mJHrSmfS57k=` |

These local identities do not by themselves claim public availability. The
immutable `1.0.0-rc.2` publication evidence remains in
[`RELEASE_READINESS.md`](RELEASE_READINESS.md) until the stable tag, package,
CDN files, signatures, and provenance are independently recorded.

`1.0.0` includes direct ordinary-element `data-kit-if`, first published in
`1.0.0-rc.2`. The immutable `1.0.0-rc.1` artifact still requires
`<template data-kit-if>`.

## More documentation

- [Browser contract](KITJS_SPEC.md) — exact standalone syntax and behavior
- [Vietnamese guide](DOCS_VI.md) — concise package guide in Vietnamese
- [Static deployment](STATIC_DEPLOYMENT.md) — CDN, self-hosting, SRI, CSP, and caching
- [Static Hydrate example](examples/static-hydrate/README.md) — advanced Drive topology
- [Support policy](SUPPORT.md) — release and browser evidence
- [Security policy](SECURITY.md) — private vulnerability reporting
- [Building and releasing](BUILDING.md) — maintainer-only source and release workflow
- [Release readiness](RELEASE_READINESS.md) — hashes, CI, provenance, and publication evidence
- [Roadmap](ROADMAP.md) — compatibility and tooling direction

## License

[MIT](LICENSE) © 2026 Huỳnh Nhân Quốc.
