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
7 changes: 7 additions & 0 deletions .changeset/witty-tips-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@reactlit/vanilla': minor
'@reactlit/radix': minor
'@reactlit/core': minor
---

Make layout plugin into a view, wrapper functionality
3 changes: 2 additions & 1 deletion apps/reactlit-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default defineConfig({
{ label: 'Installation', slug: 'guides/installation' },
{ label: 'Basics', slug: 'guides/basics' },
{ label: 'Data Fetching', slug: 'guides/data-fetching' },
{ label: 'Layout Plugin', slug: 'guides/layout' },
{ label: 'Wrappers', slug: 'guides/wrappers' },
{ label: 'Layout', slug: 'guides/layout' },
{ label: 'Managed State', slug: 'guides/managed-state' },
{ label: 'Defining Views', slug: 'guides/defining-views' },
],
Expand Down
4 changes: 2 additions & 2 deletions apps/reactlit-docs/src/content/docs/guides/data-fetching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ import ContactListDataFetchApp from '/src/examples/contact-list-data-fetch.tsx';
{ range: '38-39', label: 'get data' },
{ range: '44-50' },
{ range: '51-52', label: 'refetch' },
{ range: '87-98', label: 'update' },
{ range: '102', label: 'disable' },
{ range: '85-96', label: 'update' },
{ range: '100', label: 'disable' },
]}
/>

Expand Down
15 changes: 5 additions & 10 deletions apps/reactlit-docs/src/content/docs/guides/layout.mdx
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
---
title: Layout Plugin
description: How to use the layout plugin
title: Layout
description: How to use the layout view
---

By default, Reactlit uses a simple layout system where each `display` call
By default, Reactlit uses a simple flat layout system where each `display` call
concatenates a `ReactNode` to the DOM. This works great for simple applications,
where you are ok just showing your content in a single column,
but as your application grows, you may want to have more control over the layout.

The plugin provides a `layout` helper function to let you define more custom layouts with
The built-in layout view lets you define more custom layouts with
your Reactlit script.

import { Card, Aside } from '@astrojs/starlight/components';

<Aside>
Currently this plugin is provided in the core package, but this may change in
the future.
</Aside>

The `layout` function works via the [tunnel-rat](https://github.com/pmndrs/tunnel-rat)
The `layout` view works via a modified version of the [tunnel-rat](https://github.com/pmndrs/tunnel-rat)
library. It takes a number of slots to expose and a render function that defines how
those slots are layed out. Then, for each slot, it returns a mini-context with
`view` and `display` functions that render specifically within that slot.
Expand Down
64 changes: 64 additions & 0 deletions apps/reactlit-docs/src/content/docs/guides/wrappers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: Wrappers
---

You might encounter use cases that make Reactlit feel limiting when it comes to the heirarchical nature
of UI. The fact that the render is a flat list of elements can make it hard to build even moderately complex
interfaces.

To solve this, Reactlit provides a way to wrap elements in custom components. These wrappers can be used to
apply styles and add structure to your UI.

Any `display` or `view` call actually allows a varidic number of arguments. Penultmate arguments are considered wrappers
and will automatically `wrap` their successors. The structure can look a bit unusual coming from standard React, but
it does allow for a lot of flexibility in a very succinct format.

Wrappers come in two flavors.

1. They can be static React elements with their children omitted:

```tsx
const name = view(
'name',
<div style={{ padding: '2rem' }} />,
Inputs.Text({ placeholder: 'Enter your name' })
);
display(
<div style={{ padding: '2rem' }} />,
<div>
<h1>Hello World</h1>
</div>
);
```

2. They can react components of type `WrapperComponent`.

define the component builder. This is a function that creates the wrapper component
you want to use.

```tsx
export const Label = (label: string) => {
const LabelComponent: WrapperComponent = ({ children, stateKey }) => {
return (
<div>
<label htmlFor={stateKey}>{label}</label>
{children}
</div>
);
};
return LabelComponent;
};
```

use the component:

```tsx
const name = view(
'name',
Label('Name'),
Inputs.Text({ placeholder: 'Enter your name' })
);
```

This makes views and displays much more composable allowing you to build
custom interfaces without a lot of boilerplate.
18 changes: 8 additions & 10 deletions apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@radix-ui/themes';
import { FormInput, type ReactlitContext } from '@reactlit/core';
import { Inputs } from '@reactlit/radix';
import { type ReactlitContext } from '@reactlit/core';
import { Inputs, Label } from '@reactlit/radix';
import { TopRightLoader } from '../components/loader';
import { ContactsMockService } from '../mocks/contacts';

Expand Down Expand Up @@ -32,15 +32,13 @@ export async function ContactListApp(app: ReactlitContext) {
if (!selectedContact) return;
app.display(<h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3>);
if (app.changed('selectedContact')) {
app.set('updates', selectedContact);
app.set('name', selectedContact.name);
app.set('email', selectedContact.email);
}
const updates = app.view(
'updates',
FormInput({
name: Inputs.Text({ label: 'Name' }),
email: Inputs.Text({ label: 'Email' }),
})
);
const updates = {
name: app.view('name', Label('Name'), Inputs.Text()),
email: app.view('email', Label('Email'), Inputs.Text()),
};
// if you wish, you can use an AsyncButton view for a button that
// has a loading state during async operations
app.view(
Expand Down
22 changes: 10 additions & 12 deletions apps/reactlit-docs/src/examples/apps/contact-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FormInput, type ReactlitContext } from '@reactlit/core';
import { Inputs } from '@reactlit/radix';
import { ContactMockApi as api } from '../mocks/contacts';
import { Button } from '@radix-ui/themes';
import { type ReactlitContext } from '@reactlit/core';
import { Inputs, Label } from '@reactlit/radix';
import { ContactMockApi as api } from '../mocks/contacts';

export async function ContactListApp(app: ReactlitContext) {
const contacts = await api.getContacts();
Expand All @@ -25,16 +25,14 @@ export async function ContactListApp(app: ReactlitContext) {
if (!selectedContact) return;
app.display(<h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3>);
if (app.changed('selectedContact')) {
app.set('updates', selectedContact);
app.set('name', selectedContact.name);
app.set('email', selectedContact.email);
}
// the built-in FormInput allows you to group inputs together
const updates = app.view(
'updates',
FormInput({
name: Inputs.Text({ label: 'Name' }),
email: Inputs.Text({ label: 'Email' }),
})
);
// the built-in FormView allows you to group inputs together
const updates = {
name: app.view('name', Label('Name'), Inputs.Text()),
email: app.view('email', Label('Email'), Inputs.Text()),
};
app.display(
<Button
onClick={async () => {
Expand Down
18 changes: 8 additions & 10 deletions apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
import { DataFetchingPlugin, FormInput, useReactlit } from '@reactlit/core';
import { Inputs } from '@reactlit/radix';
import { DataFetchingPlugin, useReactlit } from '@reactlit/core';
import { Inputs, Label } from '@reactlit/radix';
import { TopRightLoader } from './components/loader';
import { ContactsMockService } from './mocks/contacts';

Expand Down Expand Up @@ -68,15 +68,13 @@ const ContactListApp = () => {
<h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3>
);
if (app.changed('selectedContact')) {
app.set('updates', selectedContact);
app.set('name', selectedContact.name);
app.set('email', selectedContact.email);
}
const updates = app.view(
'updates',
FormInput({
name: Inputs.Text({ label: 'Name' }),
email: Inputs.Text({ label: 'Email' }),
})
);
const updates = {
name: app.view('name', Label('Name'), Inputs.Text()),
email: app.view('email', Label('Email'), Inputs.Text()),
};
app.display(
<Button
onClick={async () => {
Expand Down
6 changes: 6 additions & 0 deletions apps/reactlit-docs/src/examples/contact-list-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ function ContactListApp() {
value={selectedContactId}
setValue={(id) => setSelectedContactId(id)}
getRowId={(c) => c.id}
display={() => {}}
view={() => undefined as any}
/>
{selectedContact && (
<>
Expand All @@ -98,12 +100,16 @@ function ContactListApp() {
label="Name"
value={name}
setValue={setName}
display={() => {}}
view={() => undefined as any}
/>
<TextInputComponent
stateKey="email"
label="Email"
value={email}
setValue={setEmail}
display={() => {}}
view={() => undefined as any}
/>
<Button
disabled={isFetching}
Expand Down
22 changes: 17 additions & 5 deletions apps/reactlit-docs/src/examples/layout-example.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { LayoutPlugin, useReactlit } from '@reactlit/core';
import { LayoutView, useReactlit } from '@reactlit/core';
import { TextInput } from './inputs/basic-text-input';
import { ThreeColumnLayout } from './layouts/three-column-layout';

export default function LayoutExample() {
const Reactlit = useReactlit(LayoutPlugin);
const Reactlit = useReactlit();
return (
<Reactlit>
{async ({ layout }) => {
const [col1, col2, col3] = layout(ThreeColumnLayout);
{async ({ view }) => {
const [col1, col2, col3] = view(
'cols',
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gap: '1rem',
alignItems: 'end',
}}
/>,
// the second argument here wraps each slot in a div so that they show
// up as a single grid column in the layout
LayoutView(3, <div />)
);

col1.display('First Name');
const first = col1.view('first', TextInput);
Expand Down
51 changes: 51 additions & 0 deletions apps/reactlit-examples/src/components/debug-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';
import { Button, Tooltip } from '@radix-ui/themes';
import { Wrapper } from '@reactlit/core';
import { Bug, BugOff } from 'lucide-react';
import { createContext, useContext, useState } from 'react';

type DebugContextType = {
debug: boolean;
setDebug: (debug: boolean) => void;
};

const DebugContext = createContext<DebugContextType>({
debug: false,
setDebug: () => {},
});

export const DebugToggle = () => {
const { debug, setDebug } = useContext(DebugContext);
return (
<Tooltip content="Toggle debug">
<Button size="1" variant="ghost" onClick={() => setDebug(!debug)}>
{debug ? <BugOff /> : <Bug />}
</Button>
</Tooltip>
);
};

export const DebugProvider = ({ children }: { children: React.ReactNode }) => {
const [debug, setDebug] = useState(false);
return (
<DebugContext.Provider value={{ debug, setDebug }}>
{children}
</DebugContext.Provider>
);
};

export function useDebug() {
const { debug } = useContext(DebugContext);
return debug;
}

export const Debug: Wrapper = ({ children, stateKey }) => {
const debug = useDebug();
if (!debug) return children;
return (
<div className="grid grid-cols-[auto_1fr] gap-2 items-center border rounded-md mb-2 overflow-hidden">
<div className="min-w-16 p-2 h-full border-r text-xs">{stateKey}</div>
<div className="flex-auto p-2">{children}</div>
</div>
);
};
4 changes: 4 additions & 0 deletions apps/reactlit-examples/src/components/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Box, Container, Flex, Text } from '@radix-ui/themes';
import { Geist, Geist_Mono } from 'next/font/google';
import Head from 'next/head';
import React from 'react';
import { DebugToggle } from './debug-toggle';
import { Menu } from './menu';
import { ThemeToggle } from './theme-toggle';

Expand Down Expand Up @@ -35,6 +37,7 @@ export function Main({
width="100%"
p="4"
align="center"
gap="4"
style={{
background: 'var(--color-background)',
borderBottom: '1px solid var(--gray-4)',
Expand All @@ -44,6 +47,7 @@ export function Main({
{title}
</Text>
<Box flexGrow={'1'} />
<DebugToggle />
<ThemeToggle />
</Flex>
<Flex flexGrow={'1'} overflow={'hidden'}>
Expand Down
1 change: 1 addition & 0 deletions apps/reactlit-examples/src/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function Menu() {
<MenuItem href="/radix-inputs">Radix Inputs</MenuItem>
<MenuItem href="/todo-list">Todo List</MenuItem>
<MenuItem href="/starter">Starter</MenuItem>
<MenuItem href="/layout-test">Layout Test</MenuItem>
</Flex>
);
}
9 changes: 6 additions & 3 deletions apps/reactlit-examples/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import { Theme } from '@radix-ui/themes';
import { ThemeProvider } from 'next-themes';
import type { AppProps } from 'next/app';
import { Main } from '@/components/main';
import { DebugProvider } from '@/components/debug-toggle';

export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute="class">
<Theme>
<Main title="Reactlit Examples">
<Component {...pageProps} />
</Main>
<DebugProvider>
<Main title="Reactlit Examples">
<Component {...pageProps} />
</Main>
</DebugProvider>
</Theme>
</ThemeProvider>
);
Expand Down
Loading