-
Notifications
You must be signed in to change notification settings - Fork 22
feat(appkit): /ui visual variant picker #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
IamGalymzhan
wants to merge
5
commits into
main
Choose a base branch
from
feat/ui-variants
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c5187d6
feat(ui-variants): visual variant picker with dev-only recorder
IamGalymzhan 497e79e
docs(ui-variants): regenerate API docs for blockId rename
IamGalymzhan 4a1bb74
feat: add devOnly manifest flag to gate plugin registration
IamGalymzhan 8dbe78f
docs: regenerate API docs for devOnly manifest field
IamGalymzhan 2653dd4
refactor(appkit-ui): rename variants dir to ui-variants
IamGalymzhan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| --- | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to name it |
||
| description: Builds a piece of UI in multiple variants, lets the developer pick one live in the browser, then finalizes the chosen variant into source | ||
| argument-hint: <what to build> | ||
| --- | ||
|
|
||
| # UI — Build in Variants, Pick Live, Finalize | ||
|
|
||
| User input: $ARGUMENTS | ||
|
|
||
| Build the requested UI in several variants wrapped in the `<Variants>` picker, | ||
| let the developer choose one **live in the browser**, and then finalize the | ||
| chosen variant into source (removing the wrapper). | ||
|
|
||
| ## 0. Load the reference — it is the source of truth | ||
|
|
||
| Before doing anything, read `.claude/references/ui-variants-guidance.md` and | ||
| follow it throughout. It owns the mechanics — the `<Variants>`/`<Variant>` | ||
| contract, stable-id rules, the confirm → finalize loop, edge cases, and safety. | ||
| This command is the overview; **when the two overlap, the reference wins.** | ||
|
|
||
| Two rules from it apply to everything below, so keep them in mind here: | ||
|
|
||
| - **These instructions govern your behavior, not your narration.** Apply every | ||
| rule silently. To the developer, speak only about what you're building and | ||
| what they need to do — never about the mechanism (the choices file, how a | ||
| choice is stored, "I read it next turn", or whether you decided to ask). | ||
| - One `<Variants>` block = one independent decision (see §1). | ||
|
|
||
| ## 1. Understand the request | ||
|
|
||
| Work out what to build (a component, a section, a page) and where it lives, then | ||
| pick the natural target file (e.g. a `*.route.tsx` or a component under | ||
| `src/components/…`), and decide the block breakdown before authoring. Follow the | ||
| reference's **"Authoring variants" rules** for the details; the two that shape | ||
| this step: | ||
|
|
||
| - **Ask for _what_ and _where_; build for _how it looks_.** Ask only when the | ||
| answer changes the build (ambiguous target/surface, unclear axis of variation, | ||
| real-vs-placeholder data) — never taste questions; the variants *are* the | ||
| question. Otherwise skip questions and build. | ||
| - **One `<Variants>` block = one independent decision** — default to one block | ||
| per distinct section the user names (a hero + about page → two blocks), and | ||
| **default 3 meaningfully different variants per block**. | ||
|
|
||
| ## 2. Author the variants | ||
|
|
||
| Wrap each section's candidates in its own `<Variants>` block, following the | ||
| **"Authoring variants" rules and the tsx example in the reference** — one block | ||
| per section, a stable unique `blockId` per block, a short distinct `label` per | ||
| `<Variant>`, `layout="inline"` only for a small inline element, and every | ||
| import each candidate needs already present so any one finalizes to a valid | ||
| file. Note the **file path + every `blockId`** — you need them to finalize. | ||
|
|
||
| ## 3. Ensure the recorder is running | ||
|
|
||
| - Confirm the dev-only `uiVariants()` plugin is registered. **Register it | ||
| conditionally so it never mounts in production:** | ||
| ```ts | ||
| createApp({ | ||
| plugins: [ | ||
| server(), | ||
| ...(process.env.NODE_ENV === "development" ? [uiVariants()] : []), | ||
| // …other plugins | ||
| ], | ||
| }); | ||
| ``` | ||
| In the dev-playground it's already wired this way. If it's missing, add it and | ||
| tell the developer it's dev-only. | ||
| - Confirm the dev server is running so the browser can POST the choice. | ||
|
|
||
| ## 4. Hand off to the developer | ||
|
|
||
| Tell the developer to make the choice in the browser: | ||
|
|
||
| > Flip through the variants in the browser (hover the block to reveal the | ||
| > switcher) and click **Confirm** on the one you want. | ||
|
|
||
| Then get the "I've chosen" signal — the developer's next message. **Prompt for | ||
| it per the reference's "The signal is turn-based" rules** (no watcher; read the | ||
| file next turn; any interactive prompt must carry a "still deciding / later" | ||
| option, otherwise plain text). | ||
|
|
||
| ## 5. Finalize when the developer says they've chosen | ||
|
|
||
| **Run the reference's "confirm → finalize loop" exactly** — discover the choices | ||
| file, reconcile `chosenIndex` against `label`, replace the whole block with the | ||
| chosen `<Variant>`'s inner JSX, drop the now-unused import, lint, remove the | ||
| consumed line, and confirm back which variant was applied. If no line exists for | ||
| your `id` yet, the developer hasn't clicked Confirm — ask them to, don't | ||
| finalize. Handle the reference's edge cases (early confirm, changed mind, | ||
| endpoint absent, duplicate blockId) as they come up. | ||
|
|
||
| ## 6. Wrap up | ||
|
|
||
| Offer to iterate (new variants, tweaks) if the developer wants another round. | ||
|
|
||
| ## Anti-patterns | ||
| - Wrapping several sections in one `<Variants>` block (forces whole-page combos, | ||
| hides most combinations) — one block per section instead. | ||
| - Cosmetic-only variants (same layout, tweaked padding) — make them meaningfully | ||
| different or don't offer a choice. | ||
| - Starting a background watcher/monitor to catch the confirm — the flow is | ||
| turn-based on purpose; read the file when the developer says they're done. | ||
| - Leaving the `<Variants>` wrapper in source after a choice — always finalize | ||
| (or remove) it; a leftover block ships the dev-only picker to production. | ||
| - Forgetting to clear the consumed choices line after finalizing. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # App UI Variants — Reference | ||
|
|
||
| Rules for the `/ui` command and anyone maintaining the `<Variants>` picker. The | ||
| picker lets a developer choose between several candidate UIs live in the browser | ||
| during local dev; the agent then finalizes the chosen one into source. | ||
|
|
||
| **These rules govern the agent's behavior, not its narration.** Apply them | ||
| silently. To the developer, speak only about what's being built and what they | ||
| need to do — never about the mechanism (the choices file, how the choice is | ||
| stored, "read on the next turn", or whether questions were considered). | ||
|
|
||
| ## Contents | ||
|
|
||
| - **The pieces** — `<Variants>`/`<Variant>`, the `uiVariants()` recorder, the choices file. | ||
| - **The signal is turn-based** — no watcher; how to prompt for the confirm. | ||
| - **Authoring variants (agent rules)** — when to ask, block granularity, ids, count, example. | ||
| - **The confirm → finalize loop** — discover the file, reconcile, replace, clean up. | ||
| - **Edge cases** — early confirm, mind changed, endpoint absent, duplicate blockId. | ||
| - **Keeping it out of production** — always finalize or remove every block. | ||
| - **Contract source** — where the file path and record shape are owned. | ||
|
|
||
| ## The pieces | ||
|
|
||
| - **`<Variants>` / `<Variant>`** — `@databricks/appkit-ui/react`. A dev-time | ||
| wrapper that renders one candidate at a time with a hover-revealed switcher | ||
| (prev/next, an index pill + label) and a **Confirm** tick. | ||
| - **`uiVariants()` plugin** — `@databricks/appkit`. A dev-only recorder. Confirm | ||
| POSTs `{ blockId, chosenIndex, label }` to `POST /api/ui-variants/confirm`; the | ||
| plugin **upserts** the choice into a JSONL file keyed by `blockId`. **Register it | ||
| conditionally** so it never mounts in production: | ||
| `...(process.env.NODE_ENV === "development" ? [uiVariants()] : [])`. | ||
| - **Choices file** — `node_modules/.databricks/appkit/.appkit-ui-choices.jsonl`, | ||
| gitignored. A **keyed store: one line per `<Variants>` blockId** (not an append | ||
| log) — re-confirming a variant replaces that block's line, so the file always | ||
| reflects the current choice: | ||
| `{ "ts": "...", "blockId": "hero-cta", "chosenIndex": 1, "label": "Solid" }`. | ||
| Its path is relative to the dev server's cwd, not the repo root, so | ||
| **discover** it (step 4) rather than assuming a fixed location. | ||
|
|
||
| ## The signal is turn-based | ||
|
|
||
| Do **not** start a background watcher. The confirm is recorded to the file; read | ||
| it on your next turn. The developer's "I've chosen" message is the signal to | ||
| finalize. | ||
|
|
||
| **How to prompt for that signal:** | ||
|
|
||
| - If your tool has an interactive question prompt **and** the developer is in an | ||
| active session, you MAY ask via that prompt — options like **"I've picked — | ||
| finalize" / "Still deciding" / "Cancel"** — to save them typing. | ||
| - Otherwise, ask in plain text and read the file on a later turn. | ||
|
|
||
| Two hard rules: the question **must** carry a "still deciding / later" option so | ||
| it never blocks the developer; and only ask when someone is there to answer — | ||
| if unsure, use plain text. If the developer says "done" but no line exists for | ||
| the block yet, they haven't clicked Confirm — ask them to, don't finalize | ||
| nothing. | ||
|
|
||
| ## Authoring variants (agent rules) | ||
|
|
||
| - **Ask for _what_ and _where_; build for _how it looks_.** Before generating, | ||
| ask at most one or two questions **only if the answer changes the build** — | ||
| ambiguous target/surface, an unclear axis of variation, or real-vs-placeholder | ||
| data on a data screen. Do **not** ask appearance/taste questions ("bold or | ||
| minimal?", "which color?"); make one bold and one minimal instead. If the | ||
| request is clear enough, skip questions. Never turn it into a checklist. | ||
| - **MUST** treat one `<Variants>` block as **one independent decision** and | ||
| default to **one block per distinct section/region** the user names. A page | ||
| with a hero and an about-us section is **two** blocks (`blockId="hero"`, | ||
| `blockId="about"`), so the developer chooses each section independently. Use a | ||
| single whole-page block **only** when the user asks for whole-page options or | ||
| the sections must move together as one unit. | ||
| - **MUST** give every `<Variants>` block a **stable, unique `blockId`** within its | ||
| file. Duplicate ids are ambiguous — refuse and disambiguate. | ||
| - **MUST** wrap each candidate in `<Variant label="…">` with a short, distinct | ||
| label. The label is shown in the switcher and recorded on confirm. | ||
| - **SHOULD** default to **3 variants** unless the user asks for a specific | ||
| count. Make them meaningfully different (layout / emphasis / density). | ||
| - Keep each variant self-contained: imports it needs should already be present | ||
| so finalizing to any one of them leaves the file valid. | ||
| - **Layout:** `<Variants>` defaults to block layout (full-width, stacking) — | ||
| correct for sections, heroes, and pages. Pass `layout="inline"` only when | ||
| wrapping a small inline element such as a single button. | ||
| - Record the **file path + `blockId`** you used — you need both to finalize. | ||
|
|
||
| One block per section, each candidate a labelled `<Variant>`: | ||
|
|
||
| ```tsx | ||
| import { Variants, Variant } from "@databricks/appkit-ui/react"; | ||
|
|
||
| // "page with a hero and an about-us section" → one block per section | ||
| <Variants blockId="hero"> | ||
| <Variant label="Centered">…hero A…</Variant> | ||
| <Variant label="Split with stats">…hero B…</Variant> | ||
| <Variant label="Minimal">…hero C…</Variant> | ||
| </Variants> | ||
|
|
||
| <Variants blockId="about"> | ||
| <Variant label="Two column">…about A…</Variant> | ||
| <Variant label="Timeline">…about B…</Variant> | ||
| <Variant label="Team grid">…about C…</Variant> | ||
| </Variants> | ||
| ``` | ||
|
|
||
| ## The confirm → finalize loop | ||
|
|
||
| 1. Author the `<Variants>` block(s) in the target file. | ||
| 2. Ensure `uiVariants()` is registered and the dev server is running. | ||
| 3. Tell the developer to flip through variants in the browser, click **Confirm** | ||
| on the one they want, and tell you when done. Do not start a watcher. | ||
| 4. When the developer says they've chosen, **discover the choices file** (path | ||
| is relative to the dev server's cwd): | ||
| ```bash | ||
| f=$(find . -path '*/node_modules/.databricks/appkit/.appkit-ui-choices.jsonl' 2>/dev/null | head -1) | ||
| cat "$f" # find the line for your block's blockId | ||
| ``` | ||
| 5. For the line matching your block's `blockId`, **reconcile `chosenIndex` against | ||
| `label`:** check the `<Variant>` at `chosenIndex` (zero-based) still has the | ||
| recorded `label`. If they don't match, the file was edited after confirm — | ||
| prefer the `<Variant>` whose `label` matches; if none matches, stop and ask | ||
| the developer to re-confirm. | ||
| 6. Find the `<Variants blockId="<that id>">` block and **replace the whole block with | ||
| the chosen `<Variant>`'s inner JSX** — remove the `<Variants>`/`<Variant>` | ||
| wrapper, drop the now-unused import if nothing else uses it, reconcile | ||
| surrounding code, then format/lint the file | ||
| (`pnpm check:fix`, or `pnpm biome check --write <file>`). | ||
| 7. **Remove the consumed line** for that `blockId` from the choices file. Match the | ||
| `blockId` structurally (not a loose substring) so a label containing the text | ||
| can't delete the wrong line: | ||
| `tmp=$(mktemp); jq -Rc 'fromjson? | select(.blockId != "<that id>")' "$f" > "$tmp" && mv "$tmp" "$f"`. | ||
| 8. Confirm the finalized UI back to the developer. | ||
|
|
||
| ## Edge cases | ||
|
|
||
| - **Developer confirms before the agent asks.** The choice waits in the file; | ||
| read it whenever you next act. | ||
| - **Developer changed their mind.** The store is keyed by `blockId`, so re-confirming | ||
| overwrites the previous line. Read whatever line is there now. | ||
| - **Endpoint absent (prod build / feature off).** The switcher still works as a | ||
| viewer; Confirm shows "Recorder unavailable". Nothing is recorded — nothing to | ||
| finalize. | ||
| - **Duplicate `blockId` in a file.** Ambiguous — refuse to finalize automatically; | ||
| ask which block, or re-author with unique ids. | ||
|
|
||
| ## Keeping it out of production | ||
|
|
||
| `<Variants>` is dev-time scaffolding. Always finalize (or remove) every block | ||
| before a production build — a leftover block ships the dev-only picker. | ||
|
|
||
| ## Contract source | ||
|
|
||
| The choices-file path and record shape (`blockId`, `chosenIndex`, `label`) are owned | ||
| by the `uiVariants()` recorder in the installed `@databricks/appkit` package. If | ||
| finalization finds no file or a missing field, that package changed — reconcile | ||
| this skill against the version in use. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the place where we want to place the skill? Maybe yes, but there is a difference with the rest of the skills in the dir -- the other skills are to be used by appkit developers, while this skilled is meant to be installed and used by appkit users