# Getting started with LiteShip
From `pnpm add` in your Astro project to a boundary changing state as you drag the window edge, in about five minutes. Two concepts get you there: `Boundary.make` and `satelliteAttrs`. Everything else (tokens, styles, casting to CSS) is layered behind links.
LiteShip / CZAP / `@czap/*` naming: [GLOSSARY.md](./GLOSSARY.md). For Cloudflare Workers hosting, see [HOSTING.md](./HOSTING.md#cloudflare-workers) and [examples/cloudflare-astro/](./examples/cloudflare-astro/). Contributing to LiteShip itself (cloning the monorepo, building, running the gauntlet) is a different path: [CONTRIBUTING.md](./CONTRIBUTING.md).
## Prerequisites
- Node.js 22.13+
- pnpm 10+
- An Astro 7 project (`pnpm create astro@latest` if you don't have one)
## 1. Install
In your Astro project:
```bash
pnpm add @czap/core @czap/astro effect@beta
```
`effect` is `@czap/core`'s one peer dependency, and it must be the Effect **4 beta** (`effect@beta`) — a bare `pnpm add effect` installs the 3.x `latest` tag and fails the peer check. The [support matrix](./README.md#support-matrix) covers the pin and the stabilization plan.
## 2. Your first boundary
A boundary is a continuous-to-discrete signal mapping: here, viewport width → `{mobile, tablet, desktop}`. Put it in a module the rest of your project can import:
```ts
// src/boundaries.ts
import { Boundary } from '@czap/core';
export const viewport = Boundary.make({
input: 'viewport.width',
at: [
[0, 'mobile'],
[768, 'tablet'],
[1280, 'desktop'],
],
hysteresis: 20, // optional — default 0 (no dead-zone); see Troubleshooting
});
```
Thresholds are inclusive lower bounds sorted lowest-first, each with a unique state name. The returned definition is content-addressed: change the definition and its `id` changes with it.
## 3. Put it on the page and resize
Register the integration (it injects the client boot scanner that activates boundaries):
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { integration } from '@czap/astro';
export default defineConfig({
integrations: [integration()],
});
```
Then spread `satelliteAttrs` onto any element in a `.astro` page:
```astro
---
import { satelliteAttrs } from '@czap/astro';
import { viewport } from '../boundaries.js';
---
Resize the window to see the boundary state change.
```
Run `pnpm dev`, open the page, and drag the window edge: the element's `data-czap-state` attribute flips `mobile` → `tablet` → `desktop`. Your CSS can key off it directly:
```css
.card[data-czap-state='mobile'] {
padding: 0.5rem;
}
.card[data-czap-state='desktop'] {
padding: 2rem;
}
```
`satelliteAttrs` serializes the boundary plus a `data-czap-directive="satellite"` marker; the integration's injected boot scanner activates the boundary evaluator on the client (only the evaluator — not a whole framework tree). The `Satellite` component (`import Satellite from '@czap/astro/Satellite'`) wraps the same attributes around a div for you. Always go through `satelliteAttrs` or `Satellite` — the `data-czap-*` attributes are an internal serialization contract, not a hand-authoring surface; writing them by hand drifts the moment that contract changes.
That's the whole layer-1 loop: define states, attach them to an element, let CSS respond.
## Generated UI with a component catalog
For `client:llm` streaming, LiteShip can render **structured UI trees** instead of model-emitted HTML. You define which components exist; the model references them by name. LiteShip validates props and renders through a trusted catalog — interactions surface as DOM events for your app to handle.
```bash
pnpm add @czap/genui
```
Register a catalog (component names, prop schemas, allowed children):
```ts
// src/genui-catalog.ts
import { defineComponentCatalog } from '@czap/genui';
export const appCatalog = defineComponentCatalog({
version: 'app-1',
components: {
Card: {
tag: 'section',
props: { title: { type: 'string', required: true } },
children: 'optional',
allowedChildNames: ['Text'],
},
Text: {
tag: 'p',
props: { text: { type: 'string', required: true } },
children: 'none',
},
},
});
```
Wire the catalog into an LLM session (or add `data-czap-genui` on the directive root to use the built-in demo catalog). Stream chunks use the discriminator `{ "_genui": true, "name": "...", "props": { ... } }` — legacy token/text paths stay unchanged when the marker is absent.
```ts
import { createLLMSession } from '@czap/astro/runtime';
import { appCatalog } from './genui-catalog.js';
const session = createLLMSession({
element,
target,
mode: 'replace',
getDeviceTier: () => 'animations',
genuiCatalog: appCatalog,
});
```
Rendered output carries `data-czap-genui-render-hash` for cache/replay; click handlers emit `genui:interaction` on the directive root — your app decides what they mean (navigation, tool call, or nothing). LiteShip owns render **safety**; it does not own render **authority**.
## Dev inspector (astro dev only)
While running `pnpm dev`, open the czap boundary inspector from the Astro dev-toolbar (click the czap toolbar icon) — a panel that lists every `[data-czap-boundary]` element, live signal values, draggable threshold notches, and a **Copy Boundary.make** button for paste-back into source. DOM edits are session-only (source files are untouched). Opt out with `integration({ inspector: false })` in `astro.config.mjs`.
## 4. Cast to CSS (the compiler path)
Hand-written `[data-czap-state]` selectors work, but the same boundary can also emit its CSS. Add the compiler:
```bash
pnpm add @czap/compiler
```
`compile()` takes the boundary, a per-state property map, and an optional selector:
```ts
import { CSSCompiler } from '@czap/compiler';
import { viewport } from './boundaries.js';
const result = CSSCompiler.compile(
viewport,
{
mobile: { 'font-size': '14px', padding: '0.5rem' },
tablet: { 'font-size': '16px', padding: '1rem' },
desktop: { 'font-size': '18px', padding: '2rem' },
},
'.card',
);
// `.raw` is the serialized CSS string; `.containerRules` is the
// structured form (rule per state) you'd feed into a build pipeline.
console.log(result.raw);
// @container viewport-width (...) { .card { font-size: 14px; padding: 0.5rem } }
// @container viewport-width (...) { .card { font-size: 16px; padding: 1rem } }
// @container viewport-width (...) { .card { font-size: 18px; padding: 2rem } }
// You can also call CSSCompiler.serialize(result) to produce the same
// string from the structured form. Handy when you want to inspect
// individual rules first.
```
Give `result.raw` a home in the page — paste it into a `
```
The compile step and the page must share one definition (that's why step 2 put the boundary in `src/boundaries.ts`): the boundary's content address changes whenever the definition does, and CSS emitted against a stale definition stops matching.
## 5. Where to go from here
- [AUTHORING-MODEL.md](./AUTHORING-MODEL.md): tokens, styles, and themes — the layer above boundaries (axis-varying values, per-state property sets, multi-variant theming), opening with a one-paragraph "what it feels like to author"
- [ASTRO-STATIC-MENTAL-MODEL.md](./ASTRO-STATIC-MENTAL-MODEL.md): signals → boundaries → named states → outputs, the theory-first frame
- [ASTRO-RUNTIME-MODEL.md](./ASTRO-RUNTIME-MODEL.md): how Astro hosts the runtime, directives, and the escalation path
- [HOSTING.md](./HOSTING.md): host-application first-hour checklist (CSP, Trusted Types, common failure modes)
- [docs/api/](./api): generated API reference for every package (e.g. `Boundary.evaluate` for evaluating a boundary against sample values outside the DOM)
- [DOCS.md](./DOCS.md): full documentation map
## Working on LiteShip itself
The contributor path (cloning the monorepo, workspace install, Playwright browsers, `pnpm run build` with composite project references, the test loop and the full gauntlet) lives in [CONTRIBUTING.md](./CONTRIBUTING.md). The short version:
```bash
git clone https://github.com/freebatteryfactory/LiteShip.git
cd LiteShip
pnpm install
pnpm shakedown # first-run aggregate: doctor → build → test
```
`pnpm scripts` prints the categorized index of all dev scripts; `pnpm run doctor` is the on-demand preflight rig-check.
## Troubleshooting
### First-boundary authoring
**The same value evaluates to different states each call.** You probably reused a state name across the threshold list. `Boundary.make` requires unique state names; passing `[[0, 'small'], [768, 'small']]` throws at construction with a `CzapValidationError`. If the error fires at runtime in a hot path, the boundary was constructed lazily inside a render function — hoist it out.
**The CSS doesn't update when the window resizes.** Two usual suspects: the element never got a directive marker (the boot scanner activates `data-czap-directive="satellite"` — emitted automatically by `Satellite` / `satelliteAttrs()` when a boundary is present; Astro's own `client:visible` / `client:idle` won't wire the boundary evaluator), or the CSS was generated against a stale boundary id (rebuild after editing the boundary; content addresses change with the definition, so old emitted CSS keys won't match the new id).
**A GPU shader (or other directive) never starts on an element that also carries `satelliteAttrs`.** Two czap directives on one element collide — each takes over the node, so `satelliteAttrs()` (which stamps `data-czap-directive="satellite"`) and a `client:gpu` on the same canvas silently fight, and one loses (usually the shader). The console warns once (`directive-collision:…`) naming both. Put each directive on its own element.
**The boundary state flickers when dragging the window edge near a threshold.** Add or increase `hysteresis`. The field is optional and the default is zero (no dead-zone). A value of 16–24 px is enough to absorb display jitter on most setups; the algorithm is a half-width dead-zone, so `hysteresis: 20` requires the signal to move 10px past the threshold before committing the transition.
**`Boundary.evaluate` returns the wrong state for a value at exactly a threshold.** That's by design: thresholds are inclusive lower bounds. A boundary with `[[0, 'mobile'], [768, 'tablet']]` returns `'tablet'` for `768`, not `'mobile'`. If you need exclusive bounds, offset the threshold by 1.
### Repo development
**PowerShell shows `Γåô` / `Γ£ô` mojibake in logs.** Your terminal is decoding the repo tooling's UTF-8 output as cp437. Use `Out-File -Encoding utf8` or run `chcp 65001` first.
**Tests hang in browser mode.** Make sure Playwright browsers are installed: `pnpm exec playwright install`.
Found a different issue? Open one at [github.com/freebatteryfactory/LiteShip/issues](https://github.com/freebatteryfactory/LiteShip/issues).