facebook / facebook/astryx

Adoption feedback: migrating a real Next.js 16 app to Astryx (0.1.8) — 10 findings + suggestions

Open
#4,276 5 comments 2 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

## Adoption feedback: migrating a real Next.js 16 app to Astryx (0.1.8)

We migrated a production dark-only chat/transcript viewer onto `@astryxdesign/core@0.1.8` — Next.js 16 App Router, React 19, **webpack** build, pnpm 10 (strict `node_modules`), a strict CSP (`style-src 'self' 'unsafe-inline'`, `font-src 'self'`, `connect-src 'self'`), and ~130 Playwright locators against our own CSS classes. Overall it went well, and a few things were genuinely excellent — so first, credit where it's due, then the actionable findings.

Happy to split any of these into individual tracked issues if you'd prefer; consolidating so triage is one read.

### What worked really well

- **`BaseProps` forwards `className` / `style` / `data-*` / `aria-*`.** This is what made an *incremental* migration of an app with ~130 CSS-selector tests possible at all — we could swap components without rewriting the test suite in the same commit. Arguably the single most important adoption property; worth advertising louder.
- **The CLI is a better docs surface than the website.** `astryx docs ` / `component ` / `hook` / `template`, plus `--json` and `--dense`, gave complete, offline, machine-readable docs. We initially tried scraping the docs site (Next RSC payload, fetches poorly) and threw it away once we found the CLI.
- **`astryx docs migration` predicted our worst bug before we hit it** — the "Cascade Layer Safety" section names the exact webpack `@import`-hoisting failure and the `layers.css` remedy. Docs that name the specific silent failure of a specific bundler are rare.
- **Pre-compiled CSS is genuinely build-step-free on Next+webpack**, and CSP-clean (no `@font-face`, no remote `url()` in `reset.css`/`astryx.css`/the built theme).
- **`TextArea`'s iOS auto-zoom guard** (`@media (pointer: coarse) { font-size: max(1rem, …) }`) is pointer-aware — better than the unconditional 16px we had.

---

### Bugs / friction (roughly by cost)

**1. `@stylexjs/stylex` is missing from the documented install line — hard runtime failure on pnpm.**
README Quick Start and `astryx docs getting-started` both say `npm install @astryxdesign/core @astryxdesign/theme-neutral`. But `@stylexjs/stylex` is a *peer dependency* of core, and **202 of `dist/`'s files import it at runtime**:
```
$ grep -rl "@stylexjs/stylex" node_modules/@astryxdesign/core/dist --include="*.js" | wc -l
202
```
npm/yarn auto-install peers, so it's invisible there. **pnpm does not** — under strict `node_modules` every component throws on import. Suggest adding it to the documented install line, or moving it to `dependencies` (consumers who don't author StyleX have no reason to pin its version). An `astryx doctor` check would also catch it.

**2. `astryx theme build`'s default output location is a trap for a `.ts` theme source.**
The default writes `.{css,js,d.ts,variants.d.ts}` *next to* the source, so `remote-claw.js` lands beside `remote-claw.ts`. With `next.config`'s `experimental.extensionAlias: { ".js": [".ts", …] }` (which many TS monorepos set), `import "./theme/remote-claw.js"` then silently resolves to the **source** theme (runtime `` injection) instead of the built one — same export name, same shape, just without `__built: true`. `--out` fixes it and relocates the *whole* set (all four artifacts follow the CSS path's dirname), which we only discovered by reading the CLI source. `astryx theme build --help` describes `-o, --out` as "Output CSS file path", which reads as CSS-only. Suggest: "output directory for all theme artifacts (named by the CSS path)", plus a note in `astryx docs theme` that the default co-locates a `.js` beside a `.ts` source.

**3. Generated artifacts carry a `Generated: <ISO timestamp>` header, so committed output can't be drift-checked.**
We commit the built theme so CI/Vercel never run the CLI. The timestamp makes *every* rebuild a diff, defeating a "did someone edit the built file instead of the source?" gate; we post-process the line away for byte-identical rebuilds. `Source:`/`Command:` already carry the useful provenance — suggest dropping the timestamp or gating it behind a flag (`SOURCE_DATE_EPOCH` / `--no-timestamp`). Reproducible generated output is worth a lot.

**4. The `astryx init` nudge prints on **stdout** of every command.**
`astryx docs theme | head -2` begins with `Next step: run 'pnpm exec astryx init' …`. It's prepended to `docs`/`component`/`hook`/`template` stdout, so anything that pipes/captures has to strip it. `--json` is clean (right instinct) — suggest sending the nudge to **stderr** so it stays visible to a human but disappears from pipes.

**5. Minor: `astryx docs --list` errors** with `unknown option '--list'`, even though bare `astryx docs` prints exactly that list and `component --list` / `template --list` both exist.

---

### Theming / behavior

**6. `defineTheme`'s accent scale **inverts** the accent in dark mode — with no warning at the place you'd look.**
Seeding the family the documented way, `color: { accent: '#5457e8' }`, generates:
```
--color-accent: light-dark(#424BDA, #CBBEFF)
--color-on-accent: light-dark(#FFFFFF, #001F9C)
```
i.e. in dark mode the accent becomes a **pale surface carrying dark text**. Coherent as a system default, but a surprise when the reason you're writing a theme is to carry a fixed brand across — our primary button silently became pale lavender with dark-blue text (measured, not eyeballed; our full test suite stayed green because our design guard asserted the disabled *treatment*, not the hue). The fix is supported — pin **both** halves — but `astryx docs theme` only warns *against* hand-writing `--color-accent` alone, which nudges you toward the scale API and away from the thing you actually need. Suggest documenting that the seed inverts for dark mode, with the pin-both-halves snippet as the supported way to keep a fixed brand fill.

**7. Focus rings come from `--color-accent`, which fights a pinned brand fill — and there's no focus token.**
`astryx.css` draws focus as `outline: 2px solid var(--color-accent)`. A theme whose accent is a dark, saturated *fill* (correct for white-on-accent buttons) therefore gets a dark, low-contrast ring on a dark background (~3.4:1 vs the 5.8:1 our text-accent would give). It still clears WCAG 2.2 SC 1.4.11's 3:1, so nothing is broken — just dimmer than intended, and invisible until you tab through on a dark theme. Suggest a dedicated `--color-focus-ring` defaulting to `--color-accent`, so a dark theme with a saturated brand can set fill and ring independently.

**8. `<Text size>` is silently inert whenever the theme styles that `type`.**
`size` is documented as an override, but on a themed app it does nothing for any `type` the theme emits a rule for:
```tsx
<Text type="supporting" size="xsm">…</Text>
// class="astryx-text supporting xsm …", computed font-size: 12px ← size had no effect
```
It's cascade layers, not specificity: `@layer astryx-theme { .astryx-text.supporting { font-size: … } }` beats the `xsm` size class in `@layer astryx-base` every time, and the `xsm` class is still on the element so it looks like it worked. We only caught it measuring `getComputedStyle`. Suggest emitting the size classes into `astryx-theme` too (or a later layer), or having `size` set a custom property the theme rule reads — or documenting that `size` and a themed `type` don't compose.

---

### Component-fit gaps (where we couldn't adopt, and kept our own)

**9. `ChatComposerInput` can't host a composer with platform-specific Enter behavior.**
It's a `contentEditable` div whose key handler is unconditional:
```js
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); … onSubmit?.(text); }
```
Our composer sends on Enter on a **desktop** but inserts a **newline** on a touch keyboard (a soft-keyboard Return mustn't strand a multi-line prompt), and guards IME composition. `ChatComposerInput` would send on Enter on a phone, with no prop to override it — and adopting it means trading a controlled-`value` + auto-grow `<textarea>` for `contentEditable` + `serialize()`, plus its built-in history/trigger model. A `contentEditable` input with mentions is a reasonable default; it just isn't a host for bespoke platform behavior, and there's no seam. Suggest an `onKeyDown` / `shouldSubmitOnEnter` hook, or letting `ChatComposer` accept a plain controlled `<textarea>` in its `input` slot without the contentEditable assumptions (today the shell's click-to-focus/submit wiring assume the default input). We migrated the composer's *controls* (Send/Mode/attach → `Button`/`IconButton`) cleanly and kept only the input.

**10. `Button` has a `destructive` variant but no constructive/success counterpart.**
`ButtonVariantMap` is `primary | secondary | ghost | destructive` — a red destructive with no green mirror. Migrating a permission grant/deny (approve = green, reject = red — a deliberate security-UX signal), we had `destructive` for Deny but nothing for Allow. `primary` recolors it to the brand and reads as "the main action" rather than "the affirmative/safe one", so we kept a small semantic tint over a `secondary` Button. Suggest a `constructive` (or `success`) variant mirroring `destructive`, drawing from the `--color-success` family the theme already defines — approve/reject, accept/decline, confirm/cancel are common enough that having one side but not the other pushes every consumer to recolor `secondary` by hand or misuse `primary`.

### Related upstream issue

One component-fit gap from this same migration that isn't in the 10 findings above — `CodeBlock` can't render per-line add/remove (diff) backgrounds — is already tracked in #3345. I've [added an external, real-world side-by-side there](https://github.com/facebook/astryx/issues/3345#issuecomment-5078376984) (our production diff viewer vs. `<CodeBlock highlightLines>`, light + dark), and folded the correction into the write-up's "surfaces we KEPT" table.

---

Environment: `@astryxdesign/core@0.1.8`, `@astryxdesign/theme-neutral@0.1.8`, `@astryxdesign/cli@0.1.8`; Next 16.2.7 (webpack), React 19.2.7, pnpm 10.29, Node 22. Each finding above was verified against the installed package. Thanks for open-sourcing this — the incremental-adoption story held up on a real app.

---

**Full write-up** (all findings with repro commands, measurements, before/after screenshots, and the "what worked well" list): [docs/astryx-migration.md](https://github.com/ejc3/remote-claw/blob/main/docs/astryx-migration.md) in the app we migrated.

Contributor guide

Open the contributing guide

Research direction

Start with docs/astryx-migration.md and the linked findings, then choose one of the ten independently scoped problems rather than attempting the whole report. Read the named README Quick Start, `astryx docs getting-started` or `theme` output, `astryx theme build --help`, and the relevant component entry point; done should mean a focused issue or change with a reproducible result and verified behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, playwright, react, typescript, webpack
Domain
cli, design, documentation, frontend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.