Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6b05678
feat(appkit): add dev-only design plugin and ui-only template
MarioCadenas Oct 20, 2026
ca2e6e9
fix(appkit): make design selections accurate and reliable
MarioCadenas Oct 20, 2026
7f6c23a
fix(appkit-ui): keep positioned elements inside their design screenshot
MarioCadenas Oct 20, 2026
51059ce
fix(appkit-ui): keep the design picker usable over dialogs and during…
MarioCadenas Oct 21, 2026
aedca30
feat(appkit): design canvas posts selections to an embedding app builder
MarioCadenas Oct 21, 2026
da0d9f8
chore: acknowledge bundle size growth from the design canvas
MarioCadenas Oct 21, 2026
29e6bbb
fix(appkit-ui): ship the design canvas from its own react/design subpath
MarioCadenas Oct 26, 2026
c2b7604
fix(appkit-ui): select inside open dialogs and capture SVG icon picks
MarioCadenas Oct 26, 2026
6f907c9
fix(appkit-ui): stop selecting on escape without closing dialogs
MarioCadenas Oct 26, 2026
45b1261
refactor(appkit): make the design plugin serve full-size /design page…
MarioCadenas Oct 27, 2026
e9510c4
fix(appkit): load only the requested design page
MarioCadenas Oct 27, 2026
4af7bed
fix(appkit): ignore .appkit/ in the app template
MarioCadenas Oct 27, 2026
ddedcd1
fix(appkit): declare light and dark color schemes on app and design p…
MarioCadenas Oct 27, 2026
7d52bfc
feat(appkit): design readiness signal and routed multi-page /design
MarioCadenas Oct 28, 2026
6a9f4e7
fix(appkit): no readiness from failed renders, reserve index and __ p…
MarioCadenas Oct 28, 2026
7228d1d
fix(appkit): un-ready /design when any component the page uses fails …
MarioCadenas Oct 28, 2026
07bb4e4
feat(appkit): report /design ready only once the real content is showing
MarioCadenas Oct 28, 2026
b6b5c4c
feat(appkit): appkit:app-ready signal and a marked template home page
MarioCadenas Oct 28, 2026
db09350
feat(appkit): serve HTML drafts with data-appkit blocks rendered by a…
MarioCadenas Oct 29, 2026
9135e1e
fix(appkit): theme-correct draft charts, tested error reporting, bloc…
MarioCadenas Oct 29, 2026
a1b7fd6
fix(appkit): ignore colons inside SQL string literals when finding :p…
MarioCadenas Oct 29, 2026
41ea993
feat(appkit): design status without a browser, drafts theme.css, ship…
MarioCadenas Oct 29, 2026
407f007
fix(appkit): /design compiled checks the page's whole import graph
MarioCadenas Oct 29, 2026
3defad8
feat(appkit-ui): charts fill their container's height by default
MarioCadenas Oct 29, 2026
a2fec12
feat(appkit): stream-friendly draft hydrator with skeletons, fade-in …
MarioCadenas Oct 29, 2026
34b93a4
fix(appkit): writing a draft no longer reloads every page in dev
MarioCadenas Oct 30, 2026
509a53f
fix(appkit-ui): charts draw without an entry animation by default
MarioCadenas Oct 30, 2026
af4c93a
fix(appkit): /design finds the app's react-router behind Vite's pre-b…
MarioCadenas Oct 30, 2026
ed6e9f2
fix(appkit): draft blocks size to their container, title as a heading…
MarioCadenas Oct 30, 2026
15cda03
chore(docs): regenerate appkit-ui styles for the docs site
MarioCadenas Oct 30, 2026
d73671e
revert: charts draw without an entry animation by default
MarioCadenas Oct 30, 2026
ff57bed
chore(docs): regenerate appkit-ui styles for the docs site
MarioCadenas Oct 30, 2026
f559a17
feat(appkit): data-appkit blocks can take their data from drafts/mock…
MarioCadenas Oct 30, 2026
26b6b27
fix(appkit): draft mock datasets, frame-free fade-in, unknown draft e…
MarioCadenas Oct 30, 2026
3e4b35c
fix(appkit): draft status reports go out one at a time, in order
MarioCadenas Oct 30, 2026
b71097f
Merge commit '8f4f8982858cb861614a27c06fc7bafe431db3ca' into work/1-d…
MarioCadenas Nov 3, 2026
928d373
fix(appkit): app-ready also reaches a builder that preloads / hidden
MarioCadenas Nov 3, 2026
5527267
Merge remote-tracking branch 'origin/work/2-appkit-plugin-add-with-au…
MarioCadenas Nov 3, 2026
d4e1611
Merge commit '41483062d4df9e3518413882cba6649efb864123' into work/1-d…
MarioCadenas Nov 3, 2026
0c575ac
Merge origin/main into work/1-design-plugin-dev-only-design-
MarioCadenas Nov 3, 2026
c598db2
feat(appkit): design() takes its builder origin from APPKIT_DESIGN_BU…
MarioCadenas Nov 3, 2026
621f2eb
Merge remote-tracking branch 'origin/work/2-appkit-plugin-add-with-au…
MarioCadenas Nov 3, 2026
64217d3
Merge commit '05c51e20f423284099a46e7fec9192e635775002' into work/1-d…
MarioCadenas Nov 4, 2026
6f69418
fix(appkit): data-src blocks wait for drafts/mock.json and render whe…
MarioCadenas Nov 4, 2026
7773752
feat(appkit): /design/__status reports rendered and errors per page a…
MarioCadenas Nov 4, 2026
5f471a0
fix(appkit): a hot update clears the errors it fixed in /design/__status
MarioCadenas Nov 4, 2026
6de9d0e
fix(appkit): a streamed draft has the app's theme from its first byte
MarioCadenas Nov 4, 2026
9ff9dc6
feat(appkit): serve TSX design drafts next to HTML drafts
MarioCadenas Nov 4, 2026
d816730
refactor(appkit): design plugin only renders TSX drafts, builder writ…
MarioCadenas Nov 4, 2026
b987252
docs(appkit): builders touch index.css after adding a TSX draft page
MarioCadenas Nov 4, 2026
2ce2bfe
feat(appkit): spike streaming partial TSX drafts in the design plugin
MarioCadenas Nov 5, 2026
83f7d84
feat(appkit): design pages render through DesignPage, stubs show skel…
MarioCadenas Nov 5, 2026
1289cd2
fix(appkit): drop react-router from the stream spike app
MarioCadenas Nov 5, 2026
c8fdcdb
refactor(appkit): design plugin keeps TSX drafts only, HTML drafts re…
MarioCadenas Nov 5, 2026
3b9ee88
Merge remote-tracking branch 'origin/work/2-appkit-plugin-add-with-au…
MarioCadenas Nov 5, 2026
1e728c4
refactor(appkit): /design/__status compiles the index page once
MarioCadenas Nov 5, 2026
7a3dc28
refactor(appkit): trim the design plugin's comments to the load-beari…
MarioCadenas Nov 5, 2026
f461108
refactor(appkit): drop the stream spike's measurement hooks
MarioCadenas Nov 5, 2026
b35ff17
Merge remote-tracking branch 'origin/work/2-appkit-plugin-add-with-au…
MarioCadenas Nov 5, 2026
73ad9f2
docs: design 002, database route operations (#683)
ditadi Nov 6, 2026
453a05d
Merge commit '73ad9f2d2ba235fd8aa2a467183451536af2b077' into work/1-d…
MarioCadenas Nov 6, 2026
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
483 changes: 483 additions & 0 deletions designs/002_DatabaseRouteOperations.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/docs/api/appkit/Variable.design.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/docs/api/appkit/index.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/docs/api/appkit/typedoc-sidebar.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions docs/docs/plugins/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,13 @@ When multiple series share one value axis, its ticks use the first `yKey`; each

The `selected` prop adds declarative emphasis only to bar, pie, and donut charts. Line, area, scatter, heatmap, and radar charts ignore it because category-selection semantics are not defined for those chart types.

**Chart size.** By default a chart fills its container's height, so it fits the card or grid cell it's placed in, and is at least 160px tall when the container's height comes from its content. In a fixed-height card, give the chart's parent the remaining space (e.g. `className="flex-1 min-h-0"` on `CardContent` in a `flex flex-col` card). Pass `height` (pixels) for an exact size instead:

```tsx
<BarChart data={rows} xKey="city" yKey="trips" /> {/* fills its container */}
<BarChart data={rows} xKey="city" yKey="trips" height={208} /> {/* exactly 208px */}
```

**[Plotly](https://plotly.com/javascript/)** — pass the numeric specifier as `tickformat` and the literal currency symbol as `tickprefix`. Keeping them separate is necessary because d3's `$` marker is locale-driven and cannot represent arbitrary symbols:

```tsx
Expand Down
228 changes: 228 additions & 0 deletions docs/docs/plugins/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
---
sidebar_position: 20
---

# Design plugin

<!-- AUTO-GENERATED: stability-banner-start -->
:::warning Beta plugin
This plugin is currently **beta**. APIs may change between minor releases. Import from `@databricks/appkit/beta`. See [Plugin Stability Tiers](./stability.md).
:::
<!-- AUTO-GENERATED: stability-banner-end -->

Dev-only design pages, for building a layout before it becomes part of the real app. The plugin only runs with `NODE_ENV=development`. In any other environment it does nothing, and `/design` returns 404.

## Basic usage

```ts
import { createApp, server } from "@databricks/appkit";
import { design } from "@databricks/appkit/beta";

await createApp({
plugins: [server(), design({ builderOrigin: "https://builder.example.com" })],
});
```

## Configuration

| Option | Type | Description |
| ------ | ---- | ----------- |
| `builderOrigin` | `string \| string[]` | Origin(s) of the app builder that embeds the app. The [design](#readiness) and [app](#app-readiness) ready messages are posted only to these origins, and `/design/__status` answers CORS for them. Must be a bare origin like `"https://builder.example.com"`; `"*"` is rejected. Defaults to the `APPKIT_DESIGN_BUILDER_ORIGIN` environment variable (one origin, or several separated by commas), so a builder's runtime can set it for a scaffolded `design()` with no options; an explicit `builderOrigin` wins. Optional: without either, no message is posted, and `/design/__status` still works. |

## Pages

| URL | File |
| --- | ---- |
| `/design` | `client/src/design/index.tsx` |
| `/design/<name>` | `client/src/design/pages/<name>.tsx` |

Each file's default export is the page, rendered on its own at full size with the app's styles (`client/src/index.css`). Anything else under `/design` returns 404. The page names `index` and anything starting with `__` are reserved (`pages/index.tsx` would collide with `/design`, and `__status` is the plugin's own endpoint), so such files aren't served or listed. Pages hot-reload like the rest of the app. The requested page is loaded directly and other pages only when navigated to, so an unfinished draft only breaks its own URL.

When the app has `react-router` (the app template does), design pages share a router whose basename is `/design`. Links between them stay inside `/design`, and keep working when the pages are later promoted into the app's own routes:

```tsx
import { Link } from "react-router";

<Link to="/orders">Orders</Link>; // goes to /design/orders
```

Without `react-router`, each page renders on its own; link with `<a href="/design/orders">`.

```tsx
// client/src/design/index.tsx
import { Button, Card, CardContent, CardHeader, CardTitle } from "@databricks/appkit-ui/react";

export default function DesignPage() {
return (
<Card className="m-10 max-w-sm">
<CardHeader>
<CardTitle>Orders</CardTitle>
</CardHeader>
<CardContent>
<Button>New order</Button>
</CardContent>
</Card>
);
}
```

## Mapping elements to source

In development, the dev server adds `data-source="file:line:col"` to every native JSX element, on design pages and app pages alike. A builder's element inspector can use it to map what was clicked back to the code that renders it.

## Readiness

A builder that embeds `/design` can wait until there is a real design to show, instead of revealing an empty page. `/design` is **ready** once `client/src/design/index.tsx` exists, isn't the template placeholder, and has rendered in a browser since it last changed. **`ready` needs a browser, but not a visible one**: a client has to load `/design`, and a hidden iframe is enough (nothing waits for a painted frame, and nothing looks at pixels). To wait without a browser at all, use `compiled` and `waitingFor` (below). A page that throws while rendering (in the page file or any component it uses) shows the error in place, reports it, and turns `ready` back to `false` until a render succeeds again.

### What "ready" waits for

After a successful render the page waits until it shows its real content before reporting:

- web fonts have loaded (`document.fonts.ready`);
- no `Suspense` fallback is on screen (lazy components and data that suspend);
- no `fetch` started by the page is still in flight (its data, whatever kind);
- the page's DOM hasn't changed for 300 ms (lists filling in, content appearing);
- then two animation frames, or 100 ms without any: a hidden page (a builder's preview kept out of sight) gets no animation frames, and still reports.

It never reports while the page is the template placeholder or is showing a render error. The automatic waits give up after 10 seconds, so a stream that never ends can't hold the page back forever.

### Holding readiness back: `useDesignReady`

For content the page can't detect on its own (a chart that draws on a timer, an animation, data from a source that isn't `fetch`), hold readiness back explicitly. While any mounted component passes `false`, the page isn't ready, with no time limit:

```tsx
import { useDesignReady } from "@databricks/appkit-ui/react/beta";

function TripsPerDay() {
const { data, loading } = useAnalyticsQuery("trips_per_day", {});
useDesignReady(!loading && data != null);
return <LineChart data={data ?? []} />;
}
```

It also holds back [app readiness](#app-readiness) on the app's own pages in development. Elsewhere (production, tests) it does nothing, so it can stay in components that are later promoted into the app.

### Placeholder

The app template's starter page is marked as a placeholder by this export. Delete it when you write the real page:

```tsx
export const designPlaceholder = true;
```

### `GET /design/__status`

```json
{
"ready": true,
"compiled": true,
"pages": [
{ "name": "index", "path": "/design", "compiled": true, "rendered": true, "errors": [], "state": "ready" },
{ "name": "orders", "path": "/design/orders", "compiled": true, "rendered": true, "errors": [{ "error": "Unhandled rejection: save failed" }], "state": "failed" }
],
"app": { "entry": "/src/main.tsx", "compiled": true, "path": "/", "rendered": true, "template": false, "errors": [] }
}
```

`pages` lists every design page, index first, so a builder can show a page switcher. For each, as a browser (hidden is fine) last reported it for the file's current version:

- `rendered`: React mounted the page without a render error and it settled (see above).
- `errors`: what the page hit: its render error (caught by the page's error boundary), uncaught errors (event handlers, timers), unhandled promise rejections and Vite compile errors, each `{ "error": message }`. `null` until a browser has loaded this version of the page (unknown, not "no errors"). Errors are cleared when the page loads again or takes a hot update (also one to a component it imports): what's still wrong is reported again, so a fixed error disappears without a reload.

`app` is the same for the app's own pages (not `/design`):

- `entry`: the app's entry module, from `client/index.html`.
- `compiled`: that module and everything it imports build, checked server-side like `compiled` below (`compileError` when they don't).
- `path`: the page last reported. Each path is tracked on its own, so two tabs on different pages don't reset each other; `rendered`, `template` and `errors` are this path's.
- `rendered`: something is mounted in `#root` and the page settled (a crash outside an error boundary leaves `#root` empty: `false`, and its error is listed).
- `template`: it's the template's default home page.
- `errors`: as for `pages` (`null` until a page has loaded since the dev server started).

The status also carries:

| Field | Meaning |
| ----- | ------- |
| `compiled` | The index page and every module of the app it imports, transitively, build (compiled server-side as a browser would load them, so no browser is needed; dependencies in `node_modules` aren't recompiled). |
| `compileError` | Why it doesn't, prefixed with the failing module (`src/design/Card.tsx: ...`). |
| `waitingFor` | What `ready` is waiting on: `"index"` (no `index.tsx`), `"placeholder"` (still the template page), `"compile"` (build error), `"paint"` (no browser has rendered it yet, visible or not; the name is kept for compatibility), or `null` when ready. |
| `placeholder` | `index.tsx` is still the template placeholder. |

An agent writing the page waits for `compiled: true`; a builder that wants to reveal it loads `/design` (hidden is fine) and waits for `ready` or the `appkit:design-ready` message. To check that a page works, an agent reads `rendered` and `errors` here, for any kind of app (a game, a form, a chat, a dashboard); it never needs the preview to be visible, a resize, or a look at its pixels:

```ts
const s = await (await fetch(`${appUrl}/design/__status`)).json();
const appWorks = s.app.compiled && s.app.rendered && s.app.errors?.length === 0;
const pagesWork = s.pages.every((p) => p.rendered && p.errors?.length === 0);
```

### `appkit:design-ready` message

Once a design page is ready (see above) and again after each hot update that leaves it ready, the page posts this to `window.parent` (or `window.opener`), only to the configured `builderOrigin`s:

```ts
interface DesignReadyMessage {
type: "appkit:design-ready";
version: 1;
page: string; // name of the page that painted ("index" or <name>)
pages: { name: string; path: string }[];
}
```

```ts
// In the app builder:
window.addEventListener("message", (event) => {
if (event.origin !== appOrigin) return;
if (event.data?.type === "appkit:design-ready" && event.data.version === 1) {
showDesign(event.data.pages);
}
});
```

## App readiness

The same idea for the real app, so a builder never reveals the template's default home page. In development the plugin adds a small script to the app's own pages (not `/design`). It always reports to `/design/__status` (`app`, above); with `builderOrigin` set it also posts to the builder. Once a page has settled (the same waits as [design readiness](#what-ready-waits-for), including `useDesignReady`), and again after client-side navigation and hot updates, it posts:

```ts
interface AppReadyMessage {
type: "appkit:app-ready";
version: 1;
path: string; // location.pathname of the page that is ready
}
```

This is the signal for an app that skips `/design` (e.g. one that needs no data): the builder can load the app's `/` in a hidden frame and reveal it when `appkit:app-ready` arrives. It isn't posted while nothing is mounted in `#root` (a crash), and it's sent from a hidden frame too (the final wait for a paint gives up after 100 ms, as on `/design`).

It isn't posted while an element with the `data-appkit-template-home` attribute is on screen. The app template's default home page (`HomePage` in `client/src/App.tsx`) carries it; a builder can also look for it directly. Remove the attribute, or the page, when the app gets its real home page, and the next settle (a hot update counts) posts `appkit:app-ready`.

```ts
// In the app builder:
window.addEventListener("message", (event) => {
if (event.origin !== appOrigin) return;
if (event.data?.type === "appkit:app-ready" && event.data.version === 1) {
showApp(event.data.path);
}
});
```

## TSX drafts

TSX drafts are design pages written with real appkit-ui components. The plugin only renders them; whatever writes them (an app builder) owns the prompt, the model calls, the mock data and the type-check. A builder writes:

- `client/src/design/mock.ts`: the sample data the drafts share (any module works; the pages import it as `../mock`).
- `client/src/design/pages/<name>.tsx` per draft, default-exporting the page, served at `/design/<name>`. Write a page there only once it type-checks (e.g. write elsewhere, then move it in), so a half-written file never shows. Use theme tokens (`bg-background`, `text-foreground`, ...), so drafts work in dark mode.
- After moving a page in, touch `client/src/index.css` (update its modification time). Tailwind's Vite plugin only rescans sources for classes when the stylesheet changes, so until then a new page renders without its Tailwind styles.

Each page renders in its own error boundary and posts `appkit:design-ready` once rendered. Its state is in `GET /design/__status` under `pages` (see above): `compiled` / `compileError` (built server-side, no browser needed), `rendered`, `errors`, and `state`: `compile-error`, `rendering` (builds, not rendered yet), `ready` or `failed` (a browser reported errors).

### Streaming a draft while it's written

A builder can show a draft while it's still being written, instead of only once it's finished:

- Append the page's source, as it arrives, to `client/src/design/.staging/<name>.tsx` (and the shared mock's to `.staging/mock.ts`).
- Show `/design/__stream/<name>`. The dev server keeps the latest renderable version of the staging file under the page's real module (`pages/<name>.tsx`): it closes whatever is still open at the cut (JSX tags, brackets, template literals) and stubs what the page uses that hasn't arrived yet. Components not written yet show a `DraftPlaceholder` skeleton of the right shape. Each version reaches the page as a hot update, so it fills in in place, with no reload and with its state kept.
- Until `mock.ts` exists, the page's `../mock` import is what `.staging/mock.ts` has so far, plus stubs for whatever the page imports from it that isn't there yet.
- When the page type-checks, write it to `pages/<name>.tsx` (and `mock.ts`): the same module then loads from disk. Deleting the staging file starts that draft over.

Every design page, streamed or not, renders through `DesignPage` (from `@databricks/appkit-ui/react/beta`; the plugin adds it, pages don't). It applies the app's theme before the first paint (`?theme=dark` or `?theme=light` on the URL, else the system setting), so a dark page never flashes light. If a render fails mid-stream, it keeps the last good frame on screen instead of a broken page, and holds readiness back until a render succeeds.

Lock-in is a file operation for the builder: keep the chosen page as `client/src/design/index.tsx` (`/design`, repointing `../mock` to `./mock`), or as the app's home page for apps with no data, and delete the other drafts.
12 changes: 12 additions & 0 deletions docs/static/appkit-ui/styles.gen.css
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@
.m-1 {
margin: calc(var(--spacing) * 1);
}
.m-10 {
margin: calc(var(--spacing) * 10);
}
.-mx-1 {
margin-inline: calc(var(--spacing) * -1);
}
Expand Down Expand Up @@ -648,6 +651,9 @@
.h-32 {
height: calc(var(--spacing) * 32);
}
.h-56 {
height: calc(var(--spacing) * 56);
}
.h-72 {
height: calc(var(--spacing) * 72);
}
Expand Down Expand Up @@ -711,6 +717,9 @@
.min-h-\[100vh\] {
min-height: 100vh;
}
.min-h-screen {
min-height: 100vh;
}
.min-h-svh {
min-height: 100svh;
}
Expand All @@ -726,6 +735,9 @@
.w-1\/2 {
width: calc(1/2 * 100%);
}
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-2 {
width: calc(var(--spacing) * 2);
}
Expand Down
Loading
Loading