Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,48 @@
![rozenite-banner](https://www.rozenite.dev/rozenite-banner.jpg)

### React Native DevTools Plugin Framework
### Plug-and-play DevTools panels for React Native and Lynx

[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![Chat][chat-badge]][chat] [![PRs Welcome][prs-welcome-badge]][prs-welcome]

Rozenite brings plug-and-play panels to React Native DevTools. Install plugins, enable them, and start debugging in minutes — no extra windows, servers, or tabs. Works with Expo and bare React Native, across Metro and Re.Pack.
Rozenite brings plug-and-play panels to React Native DevTools. Install plugins, enable them, and start debugging in minutes — no extra windows, servers, or tabs. Works with Expo and bare React Native across Metro and Re.Pack, with React Native Web and [Lynx](https://lynxjs.org) support in the box.

![Plugin slideshow](https://rozenite.dev/plugin-slideshow.webp)

## Features

- **Plug-and-Play Panels**: Install plugins and they auto-appear in DevTools — start debugging in minutes.
- **Unified DevTools Experience**: All panels live inside React Native DevTools — no extra windows, servers, or browser tabs.
- **Unified DevTools Experience**: Panels live inside DevTools by default — no extra windows, servers, or browser tabs.
- **Real-Time Debugging**: Live updates for network requests, storage and performance as you use your app.
- **Works With Your Setup**: Compatible with Expo and bare React Native projects. Supports Metro and Re.Pack based workflows.
- **Beyond React Native**: The same panels, plugins and CLI reach Lynx apps through rspeedy, and React Native Web apps through the browser extension. Both are experimental.
- **Production-Safe Controls**: Plugins are automatically disabled in production builds — no plugin code ships to your users.
- **Easy to Build Your Own**: When you need something custom, create your own panel with type-safe, isomorphic APIs and great DX.

## Compatibility

| Rozenite | Expo SDK | React Native | Re.Pack |
| -------- | -------- | ------------- | ------- |
| 1.x | 52+ | 0.76+ | 5.2+ |
**React Native**

Bare React Native projects are supported too — match your React Native version to the one shipped by the minimum supported Expo SDK above. See the [Compatibility](https://rozenite.dev/docs/compatibility) page for full details.
| Rozenite | Expo SDK | React Native | Re.Pack |
| ---------------- | -------- | ------------ | ------- |
| 1.13.0, >= 2.0.0 | 52+ | 0.76+ | 5.2+ |

Bare React Native projects are supported too — match your React Native version to the one shipped by the minimum supported Expo SDK above.

**Lynx** (experimental)

| Rozenite | rspeedy | @lynx-js/react |
| -------- | ------- | -------------- |
| >= 2.4.0 | 0.16+ | 0.125+ |

See the [Compatibility](https://rozenite.dev/docs/compatibility) page for full details.

## Documentation

The documentation is available at [rozenite.dev](https://rozenite.dev). You can also use the following links to jump to specific topics:

- [Quick Start](https://rozenite.dev/docs/getting-started)
- [Plugin Directory](https://rozenite.dev/plugin-directory)
- [Rozenite for Lynx](https://rozenite.dev/docs/targets/rozenite-for-lynx)
- [Plugin Development](https://rozenite.dev/docs/plugin-development/overview)

## Made with ❤️ at Callstack
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
![rozenite-banner](https://www.rozenite.dev/rozenite-banner.jpg)

### A command-line interface for creating and managing React Native DevTools plugins.
### A command-line interface for creating and managing Rozenite DevTools plugins.

[![mit licence][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![Chat][chat-badge]][chat] [![PRs Welcome][prs-welcome-badge]][prs-welcome]

The Rozenite CLI is the primary tool for scaffolding, building, and developing React Native DevTools plugins. It provides an intuitive command-line experience with interactive prompts, automated project setup, and development workflows.
The Rozenite CLI is the primary tool for scaffolding, building, and developing Rozenite DevTools plugins. It provides an intuitive command-line experience with interactive prompts, automated project setup, and development workflows.

## Features

- **Plugin Generation**: Create new React Native DevTools plugins with a complete project structure
- **Plugin Generation**: Create new Rozenite DevTools plugins with a complete project structure
- **Interactive Setup**: Guided prompts for plugin configuration and metadata
- **Build System**: Compile plugins for different targets
- **Development Server**: Hot-reload development environment with file watchers
Expand All @@ -28,7 +28,7 @@ npm install rozenite

### Generate a New Plugin

Create a new React Native DevTools plugin:
Create a new Rozenite DevTools plugin:

```bash
npx rozenite generate
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "rozenite",
"version": "2.4.0",
"description": "Command-line interface for creating and managing React Native DevTools plugins",
"description": "Command-line interface for creating and managing Rozenite DevTools plugins",
"keywords": [
"cli",
"devtools",
Expand Down
2 changes: 1 addition & 1 deletion packages/rhf-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ If you omit `id`, the hook uses React’s `useId()` for a dev-only identifier.

## Web (React Native for Web)

When you use [Rozenite for Web](https://rozenite.dev/docs/rozenite-for-web) in development, this plugin loads in the browser like on native. It follows the same `control` wiring as in your React Native app, as long as `react-hook-form` is available in the bundle.
When you use [Rozenite for Web](https://rozenite.dev/docs/targets/rozenite-for-web) in development, this plugin loads in the browser like on native. It follows the same `control` wiring as in your React Native app, as long as `react-hook-form` is available in the bundle.

## Notes

Expand Down
2 changes: 1 addition & 1 deletion packages/storage-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ createAsyncStorageAdapter({

## Web (React Native for Web)

When you use [Rozenite for Web](https://rozenite.dev/docs/rozenite-for-web) in development, this plugin loads in the browser like on native. **AsyncStorage** and **Expo SecureStore** adapters work on web when the underlying libraries do.
When you use [Rozenite for Web](https://rozenite.dev/docs/targets/rozenite-for-web) in development, this plugin loads in the browser like on native. **AsyncStorage** and **Expo SecureStore** adapters work on web when the underlying libraries do.

**MMKV** is not available in typical web bundles. In development on web, `createMMKVStorageAdapter` returns an adapter with **empty `storages`** (same behavior as production builds), so you can keep shared setup code without importing `react-native-mmkv` in the web bundle.

Expand Down
2 changes: 1 addition & 1 deletion packages/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Install the browser extension from the [GitHub releases](https://github.com/call

The documentation is available at [rozenite.dev](https://rozenite.dev). You can also use the following links to jump to specific topics:

- [Rozenite for Web](https://rozenite.dev/docs/rozenite-for-web)
- [Rozenite for Web](https://rozenite.dev/docs/targets/rozenite-for-web)
- [Quick Start](https://rozenite.dev/docs/getting-started)
- [Plugin Directory](https://rozenite.dev/plugin-directory)
- [Plugin Development](https://rozenite.dev/docs/plugin-development/overview)
Expand Down
67 changes: 53 additions & 14 deletions website/landing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,38 +43,53 @@ Three rules are worth knowing before adding anything:
- **Radius.** There are exactly two: `--rz-radius-control` for interactive
elements and chips, `--rz-radius-surface` for panels, cards and code blocks.
- **Colour blocks.** `Section` takes `tint="block"`, which remaps the token set
locally so its children need no dark-specific styling. The page spends that
once, on Rozenite for Agents. A second inverted section would read as a theme
locally so its children need no dark-specific styling. Nothing spends it
today. Spend it at most once: a second inverted section would read as a theme
flip mid-scroll.
- **Tints alternate.** Every section is `subtle` or `default`, strictly
alternating down the page, and the `subtle` ones carry `bordered`. Inserting a
section means retinting its neighbours, not breaking the run.
- **The hero owns the first screen.** It is `min-height: calc(100dvh -
var(--rz-nav-height))`. If the host navigation changes height, retune that
token rather than the section.
token rather than the section. Everything down to the install command has to
fit above the fold, on a laptop and on a phone. Check both after changing
anything in it.

## Conventions the page holds to

These came out of a design review and are easy to break by accident:

- **Eyebrows are rationed.** Only the three product surfaces carry one: official
plugins, Rozenite for Agents, Rozenite for Web. Not every section needs a
label above its headline.
- **Eyebrows are rationed.** Only the product surfaces carry one: official
plugins, Rozenite for Agents, Rozenite for Lynx, Rozenite for Web. Not every
section needs a label above its headline -- the standalone app is a way of
working rather than a surface, so it has none. `eyebrow` takes a `ReactNode`
so a surface can pair its label with a brand mark or a status chip on that
one line. That is its whole remit; it is not a second headline.
- **Section headers stack.** Eyebrow, headline, body, in one column. Rozenite
for Agents is the one exception: it pairs its header with the terminal sample
on the same line, and takes `className` to drop its stacking margin.
- **No layout family repeats.** Hero and Rozenite for Web are the only
text-and-visual splits, and they are not adjacent. Everything else uses a
different composition.
- **No layout family repeats.** Rozenite for Web is the only text-and-visual
split. Everything else uses a different composition: the hero is a centred
stack, Lynx is the page's only ordered rail, and the standalone app is the
only centred header over a full-width figure. The hero and the standalone app
both centre, but they are the two ends of the page and hold nothing else in
common -- one is type over a mark, the other is a caption over a screenshot.
- **Icons come from a library.** `@phosphor-icons/react` for UI glyphs,
`simple-icons` for brand marks via `components/brand-mark`. Nothing is drawn
by hand. Brand marks render in `currentColor`, never in the brand hex, so a
logo cannot smuggle a second accent onto the page.

The Lynx mark is the one exception: `simple-icons` has no Lynx entry, so its
paths are vendored into `components/brand-mark`, which carries the provenance
and the licensing reasoning. Vendor another mark only on the same terms.
- **Grids have no empty cells.** The plugin grid shows `FEATURED_PLUGINS`, six
of the fourteen, which divides evenly at three, two and one column. The rest
are counted in a line of copy underneath rather than drawn. Keep the featured
count divisible by six.

## Motion

Two effects, and adding a third needs an argument.
Three effects, and adding a fourth needs an argument.

`components/reveal` is a fade and lift as a block enters the viewport, so a
section's header lands before its content. It is built on CSS scroll-driven
Expand All @@ -84,10 +99,13 @@ state, and the animation only attaches inside
`prefers-reduced-motion: no-preference`.

`components/connection-diagram` runs two dots along the link between its
endpoints. This is the one looping animation on the page, and it is there
because the subject is traffic and a still line cannot show a direction. Same
progressive-enhancement shape: without `prefers-reduced-motion: no-preference`
the dots simply sit on the line.
endpoints. It is there because the subject is traffic and a still line cannot
show a direction. Same progressive-enhancement shape: without
`prefers-reduced-motion: no-preference` the dots simply sit on the line.

The hero's animated mark is the third. It is a canvas rather than CSS, so its
rules live in `sections/hero/hero.tsx`; like the other two it degrades to a
single static frame under `prefers-reduced-motion`.

Everything else is hover, focus and `:active` feedback.

Expand All @@ -103,3 +121,24 @@ ratio.
`components/connection-diagram` is for the places where no screenshot is coming.
It draws two endpoints and the link between them, which explains an architecture
better than a picture of a panel would. Rozenite for Web uses it.

Nothing uses `components/screenshot` right now -- the standalone app's capture
landed, which is what the slot is for. Keep it around for the next section that
is drafted before its screenshot exists.

## Framing a real capture

The page has one product capture, and how it is framed follows from what the
image already contains rather than from a house style.

`standalone-rozenite.png` is a raw window grab: it has macOS traffic lights but
no border or shadow, and a dark app screen sitting flush on the page reads as a
hole rather than a window. So the section frames it, and that frame is the
page's only elevated surface. See `sections/standalone/standalone.module.css`.

Never draw chrome the capture already has. If a future capture arrives without
traffic lights, that is a reason to recapture it, not to fake a title bar.

`website/landing-rozenite.png` is the hero's old DevTools screenshot. Nothing
imports it since the hero became the animated mark, so it is emitted by no
build; it is kept only in case the hero ever wants a product shot back.
73 changes: 54 additions & 19 deletions website/landing/components/brand-mark/brand-mark.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import { siGooglechrome, siReact } from 'simple-icons';

export type BrandSlug = 'googlechrome' | 'react';
export type BrandSlug = 'googlechrome' | 'react' | 'lynx';

/** Real brand marks, straight from simple-icons. Never redrawn by hand. */
const ICONS = {
googlechrome: siGooglechrome,
react: siReact,
} as const;
type BrandIcon = {
paths: readonly string[];
/** simple-icons is uniformly 24x24; a vendored mark keeps its own box. */
viewBox?: string;
/** Some marks are authored with even-odd holes and look solid without it. */
fillRule?: 'evenodd';
};

/**
* Real brand marks, straight from simple-icons. Never redrawn by hand.
*
* Lynx is the exception: it has no simple-icons entry, so the paths below are
* the mark from lynxjs.org's own header, vendored rather than hotlinked so the
* page pulls nothing from a third-party CDN. It is ByteDance/TikTok's mark,
* used nominatively to identify the Lynx project Rozenite integrates with. The
* Lynx repositories are Apache-2.0, whose section 6 grants no trademark rights,
* so this rests on nominative use rather than on the licence -- which is also
* why it renders in `currentColor` below. The mark identifies; it does not
* imply endorsement.
*/
const ICONS: Record<BrandSlug, BrandIcon> = {
googlechrome: { paths: [siGooglechrome.path] },
react: { paths: [siReact.path] },
lynx: {
paths: [
'M7.56542 6.19594L3.90642 8.7164C3.50877 8.99031 3.23346 9.40191 3.13675 9.86708L2.77902 11.5878C2.76005 11.679 2.71799 11.7642 2.65665 11.8355L0.996306 14.0121C0.772761 14.2722 0.778152 14.9632 1.39044 15.4057C1.62523 15.6103 1.93915 16.0691 2.29622 16.591C3.05224 17.6959 4.00169 19.0836 4.80312 18.9394C5.9372 18.541 7.32544 18.4135 8.39128 18.9394C10.4632 20.7282 9.95449 22.3775 9.22514 24.7421C8.93165 25.6936 8.60243 26.7609 8.39128 27.9997C9.38643 24.3777 11.6242 20.1711 15.6592 18.7437C14.9322 18.1498 13.4486 17.5981 12.1357 17.4653C12.1357 17.4653 16.1691 14.0121 21.1439 12.4254C17.671 4.16205 11.9386 0.213095 11.9386 0.213095C11.6465 -0.148197 11.0566 -0.0296711 10.9349 0.414774C10.8371 1.72112 10.675 2.60942 10.4074 3.44676L8.39128 1.12029C8.18068 0.866399 7.75965 1.013 7.76176 1.33949C8.10312 3.23719 8.05521 4.30239 7.56542 6.19594ZM8.9846 6.02248L8.99663 6.02171C9.02298 6.02002 9.0489 6.01659 9.07424 6.01153L8.9846 6.02248ZM11.7123 1.7617C13.094 4.1491 13.7199 5.5054 13.9322 8.03659C12.4625 7.2017 11.8221 6.98923 10.7413 6.99451C11.3718 5.0773 11.5644 3.92284 11.7123 1.7617Z',
'M20.5926 19.4929C14.9649 20.7806 11.7681 22.7198 9.32324 28.0001C13.712 20.6367 26.9976 21.9536 26.9976 21.9536C26.7494 20.7508 24.1079 18.4571 22.3706 17.0503C22.3706 17.0503 23.7722 15.3272 26.8706 14.455C26.8706 14.455 20.9135 14.8002 17.4455 16.6727C18.569 17.2656 20.0081 18.2663 20.5926 19.4929Z',
],
viewBox: '0 0 27 28',
fillRule: 'evenodd',
},
};

type BrandMarkProps = {
slug: BrandSlug;
Expand All @@ -19,16 +46,24 @@ type BrandMarkProps = {
* Rendered in `currentColor` rather than the brand hex, so a mark sits in the
* page's palette instead of dragging a second accent into it.
*/
export const BrandMark = ({ slug, label, size = 26, className }: BrandMarkProps) => (
<svg
role="img"
aria-label={label}
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
className={className}
>
<path d={ICONS[slug].path} />
</svg>
);
export const BrandMark = ({ slug, label, size = 26, className }: BrandMarkProps) => {
const icon = ICONS[slug];

return (
<svg
role="img"
aria-label={label}
viewBox={icon.viewBox ?? '0 0 24 24'}
width={size}
height={size}
fill="currentColor"
fillRule={icon.fillRule}
clipRule={icon.fillRule}
className={className}
>
{icon.paths.map((path) => (
<path d={path} key={path} />
))}
</svg>
);
};
21 changes: 21 additions & 0 deletions website/landing/components/rozenite-loader/rozenite-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* The one file in `landing/` that reaches outside the website.
*
* `@rozenite/ui` exports only its barrel, and declares no `sideEffects: false`,
* so importing the loader through the package entry point would pull Base UI,
* TanStack Table, Virtuoso and lucide-react into a static docs bundle for the
* sake of one canvas. The loader itself depends on nothing but React.
*
* The website is never published -- it is only ever bundled -- so it can import
* the workspace source directly and skip the package's export map entirely.
* That keeps `@rozenite/ui` unchanged and keeps a single source of truth for
* the animation.
*
* The deep path lives here and nowhere else: sections import from this module,
* so if the package ever grows a `./rozenite-loader` subpath export, this file
* is the only one that changes.
*/
export {
RozeniteLoader,
type RozeniteLoaderProps,
} from '../../../../packages/ui/src/rozenite-loader/rozenite-loader';
8 changes: 5 additions & 3 deletions website/landing/components/section/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export const Section = ({

type SectionHeaderProps = {
/**
* Small label above the headline. Rationed on purpose: only the three product
* surfaces (plugins, agents, web) carry one.
* Small label above the headline. Rationed on purpose: only the product
* surfaces (plugins, agents, Lynx, web) carry one. `ReactNode` rather than
* `string` so a surface can pair its label with a brand mark or a status
* chip on the same line -- not so it can grow into a second headline.
*/
eyebrow?: string;
eyebrow?: ReactNode;
title: ReactNode;
body?: ReactNode;
/** For sections that place the header inside a layout of their own. */
Expand Down
Loading