Skip to content

Commit f7371bc

Browse files
serpentbladeclaude
andcommitted
docs: trim deeply-technical implementation detail from pitch pages
The "For X teams/shops" pages are persuasive intros aimed at framework users who haven't adopted Rozie yet. Mid-pitch detours into compiler implementation (verbatim emit dumps, per-target lowering catalogs, modifier-grammar caveats) break the pacing — the reader has been told "Rozie handles this for you", then handed twenty lines of the handler. - `for-react-teams.md` — drop the `useControllableState` emit dump; the pitch is the consumer code, not the compiler's controllable-state glue. Strip "the compiler walks the auto-tracked signal reads inside $onMount, $watch, and <listeners> blocks" from the exhaustive-deps paragraph — landing the result without the internals. - `for-angular-shops.md` — drop the verbatim `rozie-out SearchInput angular` reassurance block (the contrast is already made by the upstream "what Angular shops typically write" handwritten block); link to the example page instead. Lighten the "Not in v1" caveats list (modifier valueTransform, $watch ordering, TS floor) into a one-paragraph "Documented edges" pointer to /parity + /compatibility. - `for-lit-teams.md` — replace the inline `@property/repeat/html` bridge emit example with the consumer-side fill; replace the long-form "Note the canonical Lit shape: @CustomElement decorator, SignalWatcher(LitElement) from @lit-labs/preact-signals, @Property decorators, rozieSpread directive…" paragraph with a one-sentence "canonical LitElement subclass" summary plus a link out. Compress the "Documented edges" three-bullet caveats block into a single pointer paragraph. - `for-vanilla-js-shops.md` — soften the per-target-lowering catalog in "compiler invariants" (drop `this._ref__rozieRoot` on Lit, `getCurrentInstance()?.vnode.el` on Vue, per-target reactive-primitive listing) — the reader doesn't need the implementation-name receipt to trust the claim. Strip the CI test-path internals (`packages/core/tests/engine-examples.compile.test.ts — 14 × 6 = 84`) in favor of a plain claim. Verified: docs build clean. Net -70 lines. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a85afe1 commit f7371bc

4 files changed

Lines changed: 50 additions & 120 deletions

File tree

docs/guide/for-angular-shops.md

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,12 @@ export class SearchInput {
113113
```rozie-src SearchInput
114114
```
115115

116-
#### What the Rozie compiler emits for the `angular` target
117-
118-
This block is compiled live by `@rozie/core` on every docs build. The
119-
source above is the input; the code below is the verbatim output.
120-
121-
```rozie-out SearchInput angular
122-
```
123-
124-
The Rozie source is roughly a third the size and reads top-to-bottom. The
125-
compiled output is **structurally equivalent** to the hand-written
126-
version — the same `signal()` / `input()` / `output()` / `viewChild()` /
127-
`inject(DestroyRef)` / `ngAfterViewInit` machinery, with `@for` / `@if`
128-
blocks and `FormsModule`-backed `r-model` lowering. You don't see it
129-
during authoring. You import it normally:
116+
Roughly a third the size, reads top-to-bottom, no decorator soup. The
117+
compiler emits an idiomatic Angular standalone component using the same
118+
`signal()` / `input()` / `output()` / `viewChild()` / `inject(DestroyRef)`
119+
machinery you'd write by hand — see the
120+
[SearchInput example page](/examples/search-input) for the full Angular
121+
output. You don't see it during authoring. You import it normally:
130122

131123
```ts
132124
// app.component.ts
@@ -227,17 +219,13 @@ delete the `.rozie` source, and the `.ts` works on its own. Zero lock-in.
227219
reference + engine-wrapper examples)
228220
- ChangeDetection: signal-driven, no zone.js round-trips for state updates
229221

230-
### Not in v1 (documented edge)
231-
232-
- **Modifier `valueTransform` must be a pure expression**, not a statement
233-
block — Angular's template parser splices the fragment verbatim into a
234-
binding expression. (`registerModifier()` plugins targeting Angular
235-
should keep the `valueTransform` short and expression-shaped.)
236-
- **`$watch` vs `$onMount` ordering**: immediate `$watch` fires *before*
237-
`$onMount` on Angular (and Vue). React/Svelte/Solid/Lit fire it after.
238-
Engine-wrapper reconcilers should be idempotent no-ops to stay safe.
239-
- **TypeScript floor 5.6+** — Angular 19's own peer dep is `>=5.5.0 <5.9.0`,
240-
which sits inside our floor. Older TS isn't tested.
222+
### Documented edges
223+
224+
A handful of small Angular-specific edges (custom modifier value-transforms
225+
must be pure expressions; immediate `$watch` fires before `$onMount` on
226+
Angular and Vue but after on the other targets; TypeScript 5.6+ required)
227+
are described in [Cross-Framework Parity](/parity) and
228+
[Compatibility](/compatibility).
241229

242230
## Why Angular shops in particular
243231

docs/guide/for-lit-teams.md

Lines changed: 17 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,20 @@ Lit's `<slot>` element projects light-DOM children into shadow DOM by name. It d
2222

2323
Community workarounds: serialize params as JSON into `data-*` attributes, hand-roll a "fillSlot" property API, or expose imperative methods the consumer queries. Each is a bespoke surface in every Lit component.
2424

25-
Rozie's compiler emits the canonical version of one of these (the typed property-fill bridge) on your behalf:
26-
27-
```rozie
28-
<rozie name="ItemList">
29-
<props>
30-
{ items: { type: Array, default: () => [] } }
31-
</props>
32-
<template>
33-
<ul>
34-
<li r-for="item in $props.items" :key="item.id">
35-
<slot name="row" :item="item" />
36-
</li>
37-
</ul>
38-
</template>
39-
</rozie>
40-
```
41-
42-
What the compiler emits:
43-
44-
```ts
45-
// Producer-side (simplified):
46-
@property({ attribute: false })
47-
row?: (scope: { item: Item }) => unknown;
48-
49-
render() {
50-
return html`
51-
<ul>
52-
${repeat(this.items, (item) => item.id, (item) =>
53-
this.row !== undefined
54-
? html`<li>${this.row({ item })}</li>`
55-
: html`<li><slot name="row"></slot></li>`
56-
)}
57-
</ul>
58-
`;
59-
}
60-
```
61-
62-
The consumer authors a typed render function:
63-
64-
```html
65-
<item-list .items=${items} .row=${({ item }) => html`<strong>${item.name}</strong>`}></item-list>
66-
```
67-
68-
Or, for a Rozie-authored consumer, the standard slot-fill syntax — Rozie compiles the bridge call automatically:
25+
Rozie compiles a typed property-fill bridge on your behalf. The producer
26+
declares its slot the normal way; the consumer fills it with the same
27+
syntax used on any other target:
6928

7029
```rozie
7130
<ItemList :items="$data.items">
7231
<template #row="{ item }"><strong>{{ item.name }}</strong></template>
7332
</ItemList>
7433
```
7534

76-
Same authoring shape as a Vue scoped slot. Same emitted compose surface as a hand-rolled Lit fill-API. Zero boilerplate either side.
35+
Same authoring shape as a Vue scoped slot. Same emitted compose surface as
36+
a hand-rolled Lit fill-API. Zero boilerplate either side. Plain-HTML
37+
consumers fill the slot via the corresponding `.row=${(scope) => html``}`
38+
property splice — fully typed off the producer's `.d.ts`.
7739

7840
### Consumer CSS doesn't reach into shadow DOM
7941

@@ -127,19 +89,15 @@ Rozie's compile-time dispatch handles dynamic slot names on every target includi
12789

12890
## What you write vs. what Lit sees
12991

130-
The canonical `examples/SearchInput.rozie` compiles to a working Lit Web Component. Below is the source + the verbatim Lit emit produced by `@rozie/core` on every docs build.
131-
132-
### The Rozie source
92+
The canonical `examples/SearchInput.rozie` compiles to a working Lit Web Component:
13393

13494
```rozie-src SearchInput
13595
```
13696

137-
### What the Rozie compiler emits for the `lit` target
138-
139-
```rozie-out SearchInput lit
140-
```
141-
142-
Note the canonical Lit shape: `@customElement('rozie-search-input')` decorator, `SignalWatcher(LitElement)` from `@lit-labs/preact-signals` for reactivity, `@property` decorators on each declared prop (with `reflect: true` where appropriate), `render()` returning a lit-html template, paired `disconnectedCallback()` cleanup. The `rozieSpread` and `rozieListeners` directives are tiny runtime helpers (~30 LOC each) that handle attribute fallthrough and listener fallthrough at lit-html part level — the same machinery your hand-written components would replicate.
97+
The compiled output is a canonical `LitElement` subclass — `@customElement`
98+
decorator, `@property`-declared reactive properties, `render()` returning a
99+
lit-html template, paired `disconnectedCallback()` cleanup. See the
100+
[SearchInput example page](/examples/search-input) for the full Lit emit.
143101

144102
The compiled file is a valid, ready-to-`customElements.define`, fully-typed Lit component. You import it like any Web Component:
145103

@@ -214,11 +172,11 @@ Three audiences in particular:
214172

215173
## Documented edges
216174

217-
The Lit target has a few v1 edges, documented in the [Cross-Framework Parity](/parity) page:
218-
219-
- **Scoped-slot params transport via a property-fill bridge** (vs. native template slots on the other targets). Same authoring shape via the `<template #name="{ … }">` syntax; the bridge is invisible to authors. The bridge uses `data-rozie-s-<hash>` scoping for consumer-CSS reach (documented above).
220-
- **`$onMount` / `$onUnmount` on always-rendered components**: see [the parity note](/parity#lit-solid-—-lifecycle-hooks-colocated-with-an-always-rendered-component)`connectedCallback` / `disconnectedCallback` semantics differ from React `useEffect`-style mount/unmount in edge cases (a component that's removed from the DOM and re-attached fires both, where React's mount-once contract holds across StrictMode but breaks across portal-detach-and-reattach).
221-
- **Engine DOM mutation** needs `$reconcileAfterDomMutation()` — documented escape hatch.
175+
A few small Lit-target edges (scoped-slot params travel via a property-fill
176+
bridge rather than native template slots; `$onMount` / `$onUnmount` timing
177+
on always-rendered components differs slightly from React; engine DOM
178+
mutation needs the `$reconcileAfterDomMutation()` escape hatch) are
179+
described in [Cross-Framework Parity](/parity).
222180

223181
## Next steps
224182

docs/guide/for-react-teams.md

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ CSS Modules hashes class names at build time. That means any code that reference
132132

133133
### Statically-computed `useEffect` dep arrays
134134

135-
`eslint-plugin-react-hooks/exhaustive-deps` is the lint rule everyone respects and quietly hates. With Rozie, you don't write the dep array — the compiler walks the auto-tracked signal reads inside `$onMount`, `$watch`, and `<listeners>` blocks and emits the correct array. Every reference example passes `exhaustive-deps` cleanly on output.
135+
`eslint-plugin-react-hooks/exhaustive-deps` is the lint rule everyone respects and quietly hates. With Rozie, you don't write the dep array — the compiler emits the correct one from the lifecycle hook's body. Output passes `exhaustive-deps` cleanly.
136136

137137
### StrictMode double-fire safety
138138

139-
`$onMount` returning a cleanup function lowers to one `useEffect` with a cleanup return — the canonical React 18 StrictMode-safe pattern. All reference examples + engine-wrapper demos validated under `<React.StrictMode>`.
139+
`$onMount` returning a cleanup function lowers to one `useEffect` with a cleanup return — the canonical React 18 StrictMode-safe pattern.
140140

141141
### Static error on prop mutation
142142

@@ -150,21 +150,7 @@ The single most common React-component bug class (mutating a prop instead of cal
150150

151151
### Two-way binding that doesn't require a state-management library
152152

153-
The React `useControllableState` pattern lives in every component library that does headlessly-controllable components (Radix, Headless UI, React Aria, etc.) — each implementation is a hand-rolled bit of glue. Rozie compiles `model: true` to the canonical hybrid:
154-
155-
```ts
156-
// What you write:
157-
// <props>{ open: { type: Boolean, default: false, model: true } }</props>
158-
159-
// What Rozie emits for React (simplified):
160-
const [open, setOpen] = useControllableState({
161-
value: props.open,
162-
defaultValue: false,
163-
onChange: props.onOpenChange,
164-
});
165-
```
166-
167-
Consumer code stays vanilla React:
153+
The React `useControllableState` pattern lives in every component library that does headlessly-controllable components (Radix, Headless UI, React Aria) — each one re-implements the same glue. Declare `model: true` on a `<props>` member and Rozie emits the canonical controllable-state pair for you. Consumer code stays vanilla React:
168154

169155
```tsx
170156
// Controlled

docs/guide/for-vanilla-js-shops.md

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,28 @@ import { SortableList } from '@your-org/components';
170170

171171
## The compiler invariants that make this work
172172

173-
A handful of engine-wrapper-specific compiler features have already shipped
173+
A handful of engine-wrapper-specific compiler features have shipped
174174
specifically because vanilla-JS engines are common Rozie use cases. You
175175
can rely on them:
176176

177-
- **`$el` lowers to the right per-target host element**`this._ref__rozieRoot`
178-
on Lit, `el.nativeElement` on Angular, the root JSX ref on
179-
React/Solid/Svelte, `getCurrentInstance()?.vnode.el` on Vue.
177+
- **`$el` lowers to the right per-target host element** so engine
178+
constructors that take a DOM node always get the right one.
180179
- **`$onMount` returning a cleanup function** is the right shape on every
181180
target. No `useEffect` ceremony, no `DestroyRef` boilerplate.
182181
- **`$watch(() => $props.x, (v) => …)`** lowers to each target's idiomatic
183-
reactive primitive (`watch` / `useEffect` / `$effect` / `effect` /
184-
`createEffect` / `updated()`). The callback param is correctly bound on
185-
all six targets.
182+
reactive primitive. The callback fires when the watched expression
183+
changes and the new value is bound correctly on all six targets.
186184
- **`$reconcileAfterDomMutation()`** is the escape hatch when the engine
187185
mutates DOM under the framework's feet — no-op on five targets,
188-
Lit-specific `render(nothing, host) + requestUpdate()` on Lit.
189-
- **`$classSelector('grip')`** survives React's CSS-Modules class hashing
190-
lowers to `"." + styles.grip` on React, `".grip"` literal on every other
191-
target. Engines that take a `handle: '.grip'` option Just Work.
192-
- **Round-trip guards on two-way bound engine state**: when the engine
193-
fires its change event you write `$props.x = newValue`; the wrapper's own
194-
`$watch` then sees the change and would push it back. Common pattern:
195-
guard with `if (newValue !== getCurrentEngineValue()) updateEngine(newValue)`
196-
inside the `$watch`. Documented in [the SortableList example](/examples/sortable-list).
186+
active on Lit where lit-html's `repeat` cache needs the nudge.
187+
- **`$classSelector('grip')`** survives React's CSS-Modules class hashing.
188+
Engines that take a `handle: '.grip'` option Just Work everywhere.
189+
- **Round-trip guards on two-way bound engine state.** When the engine
190+
fires its change event you write `$props.x = newValue`; the wrapper's
191+
own `$watch` then sees the change and would push it back. Common
192+
pattern: guard with `if (newValue !== getCurrentEngineValue())
193+
updateEngine(newValue)` inside the `$watch`. Documented in
194+
[the SortableList example](/examples/sortable-list).
197195
- **TypeScript `as` / `satisfies` annotations** in `<data>` block
198196
initializers — `selected: null as Item | null` keeps the field
199197
discriminated instead of degrading to `null` / `any`.
@@ -213,9 +211,9 @@ test of the compiler's engine-wrapper substrate:
213211
| **Uppy** | Multi-event emit chain, streaming progress, engine-state snapshotting |
214212
| **FullCalendar** | Date-typed array elements, object spread inside engine calls, structured-payload multi-emit, portal-slot fills |
215213

216-
All seven are gated by
217-
`packages/core/tests/engine-examples.compile.test.ts` — 14 `.rozie` files ×
218-
6 targets = 84 byte-identical compiled outputs on every commit.
214+
Each wrapper compiles to byte-identical output across all six targets on
215+
every commit — the engine-wrapper substrate is the most heavily-pinned
216+
surface in the test suite.
219217

220218
## Cross-framework leverage — the math
221219

0 commit comments

Comments
 (0)