Skip to content

Commit 8f62d59

Browse files
authored
feat(appkit): /ui visual variant picker (#476)
* feat(ui-variants): visual variant picker with dev-only recorder Build a piece of app UI in several variants, pick one live in the running app, then finalize the chosen variant into source. - `<Variants>`/`<Variant>` (appkit-ui): inline switcher — hover-revealed controls, tick confirm, `accent` and `layout` props — that degrades to a plain viewer when the recorder is absent. The block identifier prop is `blockId` (a semantic key, not a DOM id). - `uiVariants()` recorder plugin (appkit): POST /api/ui-variants/confirm, dev-gated (403), keyed upsert into node_modules/.databricks/appkit/.appkit-ui-choices.jsonl via a swappable ChoiceSink (default FileChoiceSink). Register conditionally so it never mounts in production. - ast-grep lint rule flagging any <Variants> left in source before deploy. - dev-playground demo route + nav. - `/ui` command + guidance reference driving the author → pick → finalize loop. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * docs(ui-variants): regenerate API docs for blockId rename Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * feat: add devOnly manifest flag to gate plugin registration Introduce a declarative `devOnly` field on the plugin manifest. When set, createApp registers the plugin only when NODE_ENV === "development"; in any other environment it is skipped before resource collection (not constructed, no routes, resources not validated). This moves the dev-only guarantee from an app-author-written NODE_ENV ternary into the framework itself. Mark the ui-variants plugin devOnly and simplify the dev-playground registration to a plain uiVariants() call. The in-handler NODE_ENV 403 is retained as fail-safe defense against mount-anyway paths. Adds the field to the Zod schema (source of truth) and PluginManifest type, regenerates the published JSON schema, documents it in the manifest guide, and covers development/production/test/unset in bootstrap tests. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * docs: regenerate API docs for devOnly manifest field Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * refactor(appkit-ui): rename variants dir to ui-variants Match the appkit plugin dir name for cross-package consistency. Files and the public API are unchanged; only the internal directory moves. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * feat: ship /ui skill and add `appkit skills install` command Consolidate the /ui command and its variants guidance into a single self-contained skill shipped inside @databricks/appkit (skills/ui.md), and add an `appkit skills install` CLI command that symlinks shipped skills into a consuming project's .claude/commands/ (--copy for git/deploy portability, --dir, --force). `appkit setup --write` now auto-installs skills, and the dist tarball ships the skills/ dir. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * refactor: rename /ui skill to /ui-variants Align the shipped skill's command name with the ui-variants naming used across the branch (plugin, dir, route). The install command derives the slash-command name from the filename, so this makes it install as /ui-variants. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * docs(ui-variants): drop conditional-registration guidance from skill createApp now skips devOnly plugins automatically, so the skill just says to add uiVariants() to the plugin list. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * refactor(ui-variants): rename shipped skill to appkit-ui-variants Installs as the /appkit-ui-variants slash command (name derived from the file). Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * feat(ui-variants): register the dev-only recorder automatically createApp now injects the ui-variants recorder before the dev-only filter runs, so it is always alive in development and always stripped in production through the same guard as any devOnly plugin. Neither the app author nor their coding agent needs to register it by hand. Also drops the unused ChoiceSink abstraction (folded into an internal FileChoiceStore) and removes uiVariants and the sink types from the public API surface, since manual registration is no longer a path. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * feat(appkit-ui): render only the first variant in production builds If a <Variants> block ever ships without being finalized, users saw the dev-only picker chrome. In a production build the component now drops the chrome and renders the first variant as plain content — a cosmetic safety net on top of the no-variants-in-prod lint; the recorder is already server-gated. Guard sits below the hooks and uses import.meta.env.DEV to match use-query-hmr. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * refactor(appkit-ui): drop the prod-fallback guard comment Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * refactor(cli): remove appkit skills install command Remove the `appkit skills install` CLI command and its invocation during `appkit setup --write`. Skill installation is now delegated to the external `npx skills add` tool (vercel-labs/skills). The shipped skill is kept and restructured to the SKILL.md convention (skills/appkit-ui-variants/SKILL.md with a `name` frontmatter field) so it is discoverable and installable by that tool. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * chore(shared): update postinstall message for skills install flow The postinstall notice referenced the removed `appkit skills install` command and claimed `setup --write` installs skills. Point users at the external `npx skills add ./node_modules/@databricks/appkit` flow instead. Signed-off-by: Galymzhan <zhangazy2004@gmail.com> * chore(shared): drop skills install hint from postinstall message Signed-off-by: Galymzhan <zhangazy2004@gmail.com> --------- Signed-off-by: Galymzhan <zhangazy2004@gmail.com>
1 parent 92cb8bb commit 8f62d59

23 files changed

Lines changed: 1568 additions & 2 deletions

File tree

apps/dev-playground/client/src/lib/nav.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SearchIcon,
1414
ServerIcon,
1515
ShieldIcon,
16+
Wand2Icon,
1617
ZapIcon,
1718
} from "lucide-react";
1819

@@ -162,6 +163,13 @@ export const NAV_GROUPS: ReadonlyArray<NavGroup> = [
162163
"Resilient SSE streams: automatic Last-Event-ID tracking and reconnection.",
163164
icon: RadioIcon,
164165
},
166+
{
167+
to: "/ui-variants",
168+
label: "UI Variants",
169+
description:
170+
"Author UI in variants, pick one live in the browser, and let the agent finalize it into source.",
171+
icon: Wand2Icon,
172+
},
165173
],
166174
},
167175
];

apps/dev-playground/client/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as VectorSearchRouteRouteImport } from './routes/vector-search.route'
13+
import { Route as UiVariantsRouteRouteImport } from './routes/ui-variants.route'
1314
import { Route as TypeSafetyRouteRouteImport } from './routes/type-safety.route'
1415
import { Route as TelemetryRouteRouteImport } from './routes/telemetry.route'
1516
import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route'
@@ -33,6 +34,11 @@ const VectorSearchRouteRoute = VectorSearchRouteRouteImport.update({
3334
path: '/vector-search',
3435
getParentRoute: () => rootRouteImport,
3536
} as any)
37+
const UiVariantsRouteRoute = UiVariantsRouteRouteImport.update({
38+
id: '/ui-variants',
39+
path: '/ui-variants',
40+
getParentRoute: () => rootRouteImport,
41+
} as any)
3642
const TypeSafetyRouteRoute = TypeSafetyRouteRouteImport.update({
3743
id: '/type-safety',
3844
path: '/type-safety',
@@ -137,6 +143,7 @@ export interface FileRoutesByFullPath {
137143
'/sql-helpers': typeof SqlHelpersRouteRoute
138144
'/telemetry': typeof TelemetryRouteRoute
139145
'/type-safety': typeof TypeSafetyRouteRoute
146+
'/ui-variants': typeof UiVariantsRouteRoute
140147
'/vector-search': typeof VectorSearchRouteRoute
141148
}
142149
export interface FileRoutesByTo {
@@ -157,6 +164,7 @@ export interface FileRoutesByTo {
157164
'/sql-helpers': typeof SqlHelpersRouteRoute
158165
'/telemetry': typeof TelemetryRouteRoute
159166
'/type-safety': typeof TypeSafetyRouteRoute
167+
'/ui-variants': typeof UiVariantsRouteRoute
160168
'/vector-search': typeof VectorSearchRouteRoute
161169
}
162170
export interface FileRoutesById {
@@ -178,6 +186,7 @@ export interface FileRoutesById {
178186
'/sql-helpers': typeof SqlHelpersRouteRoute
179187
'/telemetry': typeof TelemetryRouteRoute
180188
'/type-safety': typeof TypeSafetyRouteRoute
189+
'/ui-variants': typeof UiVariantsRouteRoute
181190
'/vector-search': typeof VectorSearchRouteRoute
182191
}
183192
export interface FileRouteTypes {
@@ -200,6 +209,7 @@ export interface FileRouteTypes {
200209
| '/sql-helpers'
201210
| '/telemetry'
202211
| '/type-safety'
212+
| '/ui-variants'
203213
| '/vector-search'
204214
fileRoutesByTo: FileRoutesByTo
205215
to:
@@ -220,6 +230,7 @@ export interface FileRouteTypes {
220230
| '/sql-helpers'
221231
| '/telemetry'
222232
| '/type-safety'
233+
| '/ui-variants'
223234
| '/vector-search'
224235
id:
225236
| '__root__'
@@ -240,6 +251,7 @@ export interface FileRouteTypes {
240251
| '/sql-helpers'
241252
| '/telemetry'
242253
| '/type-safety'
254+
| '/ui-variants'
243255
| '/vector-search'
244256
fileRoutesById: FileRoutesById
245257
}
@@ -261,6 +273,7 @@ export interface RootRouteChildren {
261273
SqlHelpersRouteRoute: typeof SqlHelpersRouteRoute
262274
TelemetryRouteRoute: typeof TelemetryRouteRoute
263275
TypeSafetyRouteRoute: typeof TypeSafetyRouteRoute
276+
UiVariantsRouteRoute: typeof UiVariantsRouteRoute
264277
VectorSearchRouteRoute: typeof VectorSearchRouteRoute
265278
}
266279

@@ -273,6 +286,13 @@ declare module '@tanstack/react-router' {
273286
preLoaderRoute: typeof VectorSearchRouteRouteImport
274287
parentRoute: typeof rootRouteImport
275288
}
289+
'/ui-variants': {
290+
id: '/ui-variants'
291+
path: '/ui-variants'
292+
fullPath: '/ui-variants'
293+
preLoaderRoute: typeof UiVariantsRouteRouteImport
294+
parentRoute: typeof rootRouteImport
295+
}
276296
'/type-safety': {
277297
id: '/type-safety'
278298
path: '/type-safety'
@@ -413,6 +433,7 @@ const rootRouteChildren: RootRouteChildren = {
413433
SqlHelpersRouteRoute: SqlHelpersRouteRoute,
414434
TelemetryRouteRoute: TelemetryRouteRoute,
415435
TypeSafetyRouteRoute: TypeSafetyRouteRoute,
436+
UiVariantsRouteRoute: UiVariantsRouteRoute,
416437
VectorSearchRouteRoute: VectorSearchRouteRoute,
417438
}
418439
export const routeTree = rootRouteImport
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import {
2+
Badge,
3+
Button,
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
Separator,
10+
Variant,
11+
Variants,
12+
} from "@databricks/appkit-ui/react";
13+
import { createFileRoute } from "@tanstack/react-router";
14+
import {
15+
ArrowRight,
16+
Boxes,
17+
Gauge,
18+
Github,
19+
Radio,
20+
Rocket,
21+
ShieldCheck,
22+
Sparkles,
23+
} from "lucide-react";
24+
25+
export const Route = createFileRoute("/ui-variants")({
26+
component: UiVariantsRoute,
27+
});
28+
29+
/**
30+
* Demo of the `<Variants>` picker: a page composed of two independent
31+
* `<Variants>` blocks:
32+
* - `about-hero` — the top hero section (3 treatments)
33+
* - `about-info` — the informational section underneath (3 treatments)
34+
*
35+
* Hover either framed block in the browser to reveal the switcher, flip
36+
* between the variants, and Confirm the one you want. The `/ui` skill then
37+
* finalizes each chosen variant into source, dropping the wrappers.
38+
*/
39+
function UiVariantsRoute() {
40+
return (
41+
<div className="min-h-screen bg-background">
42+
<div className="mx-auto max-w-[1000px] px-6 py-12">
43+
<Variants blockId="about-hero">
44+
<Variant label="Centered">
45+
<section className="flex flex-col items-center px-6 py-20 text-center">
46+
<Badge variant="secondary" className="mb-6">
47+
<Sparkles className="size-3.5" /> About AppKit
48+
</Badge>
49+
<h1 className="text-5xl font-bold tracking-tight text-foreground">
50+
Build Databricks apps, not plumbing
51+
</h1>
52+
<p className="mt-6 max-w-xl text-lg text-muted-foreground">
53+
AppKit is a modular TypeScript SDK with a plugin architecture,
54+
first-class streaming, and built-in observability.
55+
</p>
56+
<div className="mt-10 flex gap-3">
57+
<Button size="lg">
58+
Get started <ArrowRight className="size-4" />
59+
</Button>
60+
<Button size="lg" variant="outline">
61+
<Github className="size-4" /> View source
62+
</Button>
63+
</div>
64+
</section>
65+
</Variant>
66+
67+
<Variant label="Split with stats">
68+
<section className="grid grid-cols-1 items-center gap-10 py-20 md:grid-cols-2">
69+
<div>
70+
<Badge variant="secondary" className="mb-4">
71+
<Rocket className="size-3.5" /> About AppKit
72+
</Badge>
73+
<h1 className="text-4xl font-bold tracking-tight text-foreground">
74+
The SDK for Databricks applications
75+
</h1>
76+
<p className="mt-4 text-lg text-muted-foreground">
77+
A plugin-first toolkit that handles the hard parts — auth,
78+
streaming, telemetry — so your team ships features faster.
79+
</p>
80+
<div className="mt-8">
81+
<Button size="lg">
82+
Get started <ArrowRight className="size-4" />
83+
</Button>
84+
</div>
85+
</div>
86+
<div className="grid grid-cols-2 gap-4">
87+
{[
88+
{ value: "20+", label: "Built-in plugins" },
89+
{ value: "100%", label: "TypeScript" },
90+
{ value: "SSE", label: "Streaming-first" },
91+
{ value: "OTEL", label: "Observability" },
92+
].map((stat) => (
93+
<div
94+
key={stat.label}
95+
className="rounded-lg border bg-card p-5 text-center"
96+
>
97+
<div className="text-3xl font-bold text-foreground">
98+
{stat.value}
99+
</div>
100+
<div className="mt-1 text-sm text-muted-foreground">
101+
{stat.label}
102+
</div>
103+
</div>
104+
))}
105+
</div>
106+
</section>
107+
</Variant>
108+
109+
<Variant label="Minimal">
110+
<section className="border-b py-16">
111+
<h1 className="text-4xl font-bold tracking-tight text-foreground">
112+
About AppKit
113+
</h1>
114+
<p className="mt-4 max-w-2xl text-lg text-muted-foreground">
115+
A modular TypeScript SDK for building Databricks applications
116+
with a plugin-based architecture, streaming, and observability
117+
baked in.
118+
</p>
119+
</section>
120+
</Variant>
121+
</Variants>
122+
123+
<div className="h-12" />
124+
125+
<Variants blockId="about-info">
126+
<Variant label="Feature grid">
127+
<section className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
128+
{[
129+
{
130+
icon: Boxes,
131+
title: "Plugin-first",
132+
body: "Everything is a plugin — compose exactly the capabilities your app needs.",
133+
},
134+
{
135+
icon: Radio,
136+
title: "Streaming-first",
137+
body: "Built-in SSE with automatic reconnection and event replay.",
138+
},
139+
{
140+
icon: Gauge,
141+
title: "Observable",
142+
body: "OpenTelemetry traces, metrics, and logs wired in from day one.",
143+
},
144+
{
145+
icon: ShieldCheck,
146+
title: "Type-safe",
147+
body: "Heavy TypeScript with runtime validation via Zod.",
148+
},
149+
{
150+
icon: Sparkles,
151+
title: "Great DX",
152+
body: "HMR, hot-reload, source maps, and inspection tools out of the box.",
153+
},
154+
{
155+
icon: Rocket,
156+
title: "Production-ready",
157+
body: "Graceful shutdown, retries, timeouts, and caching interceptors.",
158+
},
159+
].map((feature) => (
160+
<Card key={feature.title}>
161+
<CardHeader>
162+
<feature.icon className="size-6 text-primary" />
163+
<CardTitle className="mt-2">{feature.title}</CardTitle>
164+
</CardHeader>
165+
<CardContent>
166+
<p className="text-sm text-muted-foreground">
167+
{feature.body}
168+
</p>
169+
</CardContent>
170+
</Card>
171+
))}
172+
</section>
173+
</Variant>
174+
175+
<Variant label="Two column prose">
176+
<section className="grid grid-cols-1 gap-10 md:grid-cols-3">
177+
<div className="md:col-span-1">
178+
<h2 className="text-2xl font-semibold text-foreground">
179+
Why AppKit?
180+
</h2>
181+
<p className="mt-2 text-sm text-muted-foreground">
182+
Designed for teams building data and AI apps on Databricks.
183+
</p>
184+
</div>
185+
<div className="space-y-6 md:col-span-2">
186+
<div>
187+
<h3 className="font-medium text-foreground">
188+
Modular by design
189+
</h3>
190+
<p className="mt-1 text-sm text-muted-foreground">
191+
A plugin architecture lets you add analytics, agents,
192+
Lakebase, and more without rewiring your app.
193+
</p>
194+
</div>
195+
<Separator />
196+
<div>
197+
<h3 className="font-medium text-foreground">
198+
Built for streaming
199+
</h3>
200+
<p className="mt-1 text-sm text-muted-foreground">
201+
Server-Sent Events with reconnection and per-stream
202+
cancellation come standard.
203+
</p>
204+
</div>
205+
<Separator />
206+
<div>
207+
<h3 className="font-medium text-foreground">
208+
Observable and safe
209+
</h3>
210+
<p className="mt-1 text-sm text-muted-foreground">
211+
OpenTelemetry and Zod validation give you confidence in
212+
production.
213+
</p>
214+
</div>
215+
</div>
216+
</section>
217+
</Variant>
218+
219+
<Variant label="Stat callout">
220+
<Card>
221+
<CardHeader>
222+
<CardTitle>What AppKit gives you</CardTitle>
223+
<CardDescription>
224+
The essentials for shipping Databricks apps quickly.
225+
</CardDescription>
226+
</CardHeader>
227+
<CardContent>
228+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
229+
{[
230+
{
231+
value: "Plugins",
232+
body: "Compose analytics, agents, files, and more.",
233+
},
234+
{
235+
value: "Streaming",
236+
body: "SSE with reconnection built in.",
237+
},
238+
{
239+
value: "Telemetry",
240+
body: "OpenTelemetry traces and metrics.",
241+
},
242+
].map((item) => (
243+
<div key={item.value}>
244+
<div className="text-lg font-semibold text-foreground">
245+
{item.value}
246+
</div>
247+
<p className="mt-1 text-sm text-muted-foreground">
248+
{item.body}
249+
</p>
250+
</div>
251+
))}
252+
</div>
253+
</CardContent>
254+
</Card>
255+
</Variant>
256+
</Variants>
257+
</div>
258+
</div>
259+
);
260+
}

0 commit comments

Comments
 (0)