Skip to content

Add LLM-native API design guide and audit documentation#117

Draft
developit wants to merge 8 commits into
mainfrom
claude/review-kinu-api-design-21MRF
Draft

Add LLM-native API design guide and audit documentation#117
developit wants to merge 8 commits into
mainfrom
claude/review-kinu-api-design-21MRF

Conversation

@developit

Copy link
Copy Markdown
Owner

Summary

This PR adds comprehensive documentation for making Kinu a predictable, LLM-friendly UI toolkit. It includes two new markdown files that establish design principles and audit the current API surface against industry standards.

Changes

  • LLM_NATIVE.md: A concrete roadmap with 12 prioritized recommendations (R1–R12) for making Kinu so predictable that a 2-sentence description plus a component list is sufficient for an LLM to write correct code on the first try. Covers:

    • Unified compound component notation (both flat and dot-static exports)
    • Standardized subcomponent vocabulary ({X}Trigger, {X}Content, {X}Item, {X}Close)
    • Unified control protocol (native HTML attributes + events, with React fallbacks)
    • Consolidation of overlapping components (Dialog/Sheet/Drawer, Badge/Status/Chip)
    • Layout and typographic primitives (Stack, Row, Grid, Text, Heading)
    • Casing normalization and attribute standardization
    • Documentation improvements and a validation test prompt
  • API_REVIEW.md: A detailed audit of Kinu's current API surface identifying:

    • What Kinu gets right (native HTML events, Item universality, Accordion using <details>)
    • Eight categories of LLM-predictability gaps (mixed compound notation, non-uniform naming, varying state protocols, overlapping components, context-dependent Item typing, non-standard k attribute, naming inconsistencies, missing layout/typographic primitives)
    • Comparison against shadcn/ui, Radix UI, and Mantine
    • A comprehensive rename table and methodology

Implementation Details

Both documents are reference material designed to guide future API changes. They establish:

  1. Three core rules for LLM-native design (every component renders its named element, uses native HTML attributes/events, follows consistent compound patterns)
  2. A prioritized roadmap ordered by "LLM-correctness payoff per unit of breakage"
  3. Detailed rationale for each recommendation, including breaking vs. additive changes
  4. A validation test: a single prompt that should produce working code after all recommendations are implemented

The documents are paired—API_REVIEW.md documents the current state and why changes are needed; LLM_NATIVE.md provides the concrete recommendations.

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq

API_REVIEW.md audits the current component surface against shadcn/ui,
Radix, and Mantine, flagging compound-notation inconsistency, split
state protocols, overlapping components (Badge/Status/Chip,
Dialog/Sheet/Drawer, Progress/ProgressRing/Meter), and the silent
type-narrowing in Item.

LLM_NATIVE.md proposes 12 prioritised changes (R1-R12) - flat+static
exports, a single {X}Trigger/Content/Item/Close vocabulary, a unified
value/onValueChange fallback, layout and typographic primitives, and
the data-k attribute rename - that together let an LLM produce correct
Kinu code from a 2-sentence description plus the component list alone.

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
@netlify

netlify Bot commented Apr 21, 2026

Copy link
Copy Markdown

Deploy Preview for kinu-sh ready!

Name Link
🔨 Latest commit 9642aec
🔍 Latest deploy log https://app.netlify.com/projects/kinu-sh/deploys/6a234faefc52c60008dc338e
😎 Deploy Preview https://deploy-preview-117--kinu-sh.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Size Change: +1.01 kB (+2.69%)

Total Size: 38.7 kB

📦 View Changed
Filename Size Change
benchmarks/size/.tmp/few-components/bundle.css 3.2 kB +170 B (+5.61%) 🔍
benchmarks/size/.tmp/few-components/bundle.js 800 B +163 B (+25.59%) 🚨
benchmarks/size/.tmp/nearly-all-components/bundle.css 11 kB +156 B (+1.43%)
benchmarks/size/.tmp/nearly-all-components/bundle.js 4.33 kB +331 B (+8.28%) 🔍
dist/index.css 11.1 kB +151 B (+1.38%)
dist/index.js 6.39 kB +44 B (+0.69%)
ℹ️ View Unchanged
Filename Size
benchmarks/size/.tmp/one-component/bundle.css 1.52 kB
benchmarks/size/.tmp/one-component/bundle.js 341 B

compressed-size-action

The original Tabs was the one component in the kit that contradicted
Kinu's "DOM is the state machine" rule -- it required users to manage
selection state externally via aria-selected + onClick + useState.

This switch makes Tabs use the same substrate the rest of the
disclosure family (Accordion, Collapsible, Tree.Group) already uses:
native <details name=...>. Tabs sharing a name are mutually exclusive
via the platform; opening one closes the others without any JS.

API:
  <Tabs>
    <Tab name="t" open>
      <TabLabel>Foo</TabLabel>
      <TabPanel>...</TabPanel>
    </Tab>
    <Tab name="t">
      <TabLabel>Bar</TabLabel>
      <TabPanel>...</TabPanel>
    </Tab>
  </Tabs>

- Tabs: <div k="tabs"> grid wrapper.
- Tab: <details k="tab" name=...> with display:contents so its
  <summary> and panel children participate in the parent grid.
- TabLabel: <summary k="tab-label">.
- TabPanel: <div k="tab-panel">, visible only when its parent Tab[open].
- TabList: kept as a deprecated alias for Tabs.

CSS pointer-events:none on the open tab's summary blocks mouse/touch
close. Keyboard close on the active tab is left unaddressed in this
pass (the alternative was a +90B gz global beforetoggle handler).

Doc reconciliation:
- AGENTS.md [p="button"]/--p- -> [k="button"]/--k- to match source.
- ARCHITECTURE.md and README.md bundle-size figures updated to current
  measurements; ARCHITECTURE.md Tabs example updated to the new shape.
- docs/pages/commands.md kinu-command -> command (matches source).

LLM_NATIVE.md is rewritten to clearly separate what shipped from the
broader recommendations in API_REVIEW.md that remain deferred.

Cost: +23 bytes gzipped (22,101 -> 22,172 raw / 6,379 -> 6,402 gz).

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
@developit developit force-pushed the claude/review-kinu-api-design-21MRF branch from dab473a to d5846f4 Compare April 29, 2026 13:30
claude added 6 commits May 1, 2026 11:41
Previous Tabs CSS used CSS anchor-positioning for a sliding indicator
that smoothly slid behind whichever tab was active. I replaced it with
a static box-shadow when refactoring to <details> -- that was sloppy
and lost a deliberate visual feature.

Restored the anchor-positioning indicator, adapted to the new selectors:
- [k="tabs"]::before is the muted strip background, restricted to grid
  row 1 (where labels live). Replaces the bg color the old [k="tablist"]
  had directly.
- [k="tabs"]::after is the sliding indicator, anchored to whichever
  [k="tab"][open] > [k="tab-label"] is active.
- Active label gets anchor-name; the indicator pseudo positions to it
  via position-anchor and animates inset-inline-start / inline-size.
- Fallback (no @supports) keeps the static shadow on the active label.

JS bundle unchanged. CSS size returns to baseline (~10.86kB gz vs
baseline 10.84kB).

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
Previous CSS used `display: grid` with `grid-column: 1 / -1` on the
muted-strip ::before pseudo. Without `grid-template-columns`, `-1`
resolves to the last *explicit* grid line (which is line 1 when no
columns are defined), so the pseudo collapsed to zero width and got
auto-placed in its own column — siblings of the labels rather than
behind them.

Switched to `display: flex; flex-wrap: wrap;` for [k="tabs"]:
- ::before is absolutely-positioned, height fixed at the label-row's
  height (2.25rem) — it's no longer a flex item, so layout doesn't
  fight with the labels.
- Labels remain horizontal flex items in the first wrap line.
- Panels use `flex-basis: 100%` so they wrap to a new line; only the
  open panel renders (display: block on [k="tab"][open] > [k="tab-panel"]).
- Sliding-indicator anchor-positioning unchanged; ::after still
  position-anchors to the active label.

JS bundle unchanged (22,172 / 6,402). CSS unchanged in size.

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
Previous attempt rewrote half the tabs CSS (display:flex/wrap, removed
fit-content, restructured with extra wrappers) while trying to fix the
strip layout. The resulting bundle was wider, the strip went full-width,
and the indicator pseudo logic ended up in different selectors than the
original.

This is the minimal diff against the pre-overhaul tabs CSS:

  [k="tablist"]                   -> [k="tabs"]
  [k="tab"]  (the button styles)  -> [k="tab-label"]
  [k="tab"][aria-selected="true"] -> [k="tab"][open] > [k="tab-label"]
  added: [k="tab"] { display: contents }       (dissolve <details>)
  added: list-style + ::marker hide on label   (kill <summary> marker)
  added: pointer-events:none + cursor:default  (block close-on-click)
  added: tab-panel uses position:absolute      (escape inline-flex strip)

Strip stays inline-flex / fit-content / 2.25rem-tall / muted-bg, exactly
like the original. Sliding indicator (::before, anchor-positioning) is
the same code, just with [k="tabs"] / [k="tab-label"] selectors.

The panel uses position:absolute so it doesn't disrupt the inline-flex
strip (the open <details>'s panel becomes a flex sibling of the labels
because of display:contents on Tab; absolute positioning takes it out
of that flow). Containing block is [k="tabs"] which is position:relative.

JS bundle unchanged (22,172 / 6,402).

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
Replaces the interim <details name> Tabs substrate with <input type="radio">
(native exclusive selection via shared name, no keyboard-close edge case).
Pays for the migration by extending createSimpleComponent's defaultProps to
accept a function (props) => finalProps, then converting 9 small function
components to that form — net change −2 B gzipped vs the <details> version.

Conversions (each gains real ref forwarding):
- DialogContent, PopoverContent, SheetContent, DrawerContent,
  DropdownMenuContent, ContextMenuContent
- CarouselContent, CarouselPrevious, CarouselNext

Container components (Dialog, Popover, Sheet, Drawer, DropdownMenu,
ContextMenu, Carousel) and Trigger/Close components were not converted —
measured larger or impossible due to install* + Provider wraps / child
cloning respectively.

Tabs shape:
  <Tabs>
    <Tab checked>One</Tab>  <TabPanel>...</TabPanel>
    <Tab>Two</Tab>           <TabPanel>...</TabPanel>
  </Tabs>

Props on <Tab> forward to the inner radio (including checked, disabled,
value); the radio is visually hidden with appearance:none + opacity:0,
form="" stops form participation, and selection is queryable via
[k="tab"]:has(> input:checked).

TabLabel is removed (the label is implicit in <Tab>'s children now).
TabList kept as @deprecated alias for Tabs.

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
The a203de2 design placed <TabPanel>s inside <TabList> interleaved with tabs
and absolute-positioned them under the strip. Two problems:

  1. position: absolute broke vertical reflow of the surrounding page
     (panel content didn't push subsequent content down).
  2. The well's `width: fit-content` constrained the panel's containing
     block, so the panel got clipped to the hugging-pill width — wrong in
     the common "narrow tabs, wide panel content" case.

A full-width well would "fix" #2 but makes the pill look stupidly wide.
The structural cause is forcing tabs + panels into one box: hugging pill
wants fit-content, panel wants the parent's width — they can't share a
box and a width.

Decoupling. <TabList> stays the strip (hugging pill, sized to tabs,
horizontally scrollable on its own). <TabPanel>s render *after* TabList
as siblings, in normal block flow at the parent container's width. The
tab-to-panel link is expressed in selectors rather than DOM containment,
by N unrolled rules:

  [k="tablist"]:has([k="tab"]:nth-of-type(N) > input:checked)
    ~ [k="tab-panel"]:nth-of-type(N) { display: block }

TabPanel renders as <section> so :nth-of-type self-isolates from the
<div k="tablist">. The 12 rules compress to roughly nothing (massive
repetition). N=12 covers any realistic tab strip; raise it in one CSS
file if needed.

Cost vs a203de2: +20 B gz (≈+9 CSS rework, ≈+11 for restoring a
`Tabs = TabList` deprecated alias the previous commit dropped). JS
factory side is unchanged.

Verified in a real browser via Playwright on three demo routes:
  - getting-started: pill = 267 px, panel = 960 px (parent's full width)
  - player (narrow sidebar): pill = 208 px, panel = 300 px (sidebar width)
  - dashboard: loads without errors (its "tabs" are nav buttons, not
    radio Tabs — left as plain <button role="tab">).
Click + arrow nav switch selection correctly; sliding indicator intact;
zero page errors anywhere.

Other changes:
  - demo/getting-started, demo/player: un-interleave panels (siblings
    after TabList), drop useState, use defaultChecked.
  - docs/components/tabs.md, docs/examples/tabs.tsx, ARCHITECTURE.md
    updated to document the sibling-panel structure and the up-to-12
    cap.
  - IMPL_COSTS.md and LLM_NATIVE.md updated with the actual deltas and
    the rejected approaches (full-width well, position-absolute, grid
    auto-flow, delegated-JS true tablist).

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
Previous commit incorrectly rewrote the dashboard's mobile tabs as plain
<button>s, claiming the controlled-from-elsewhere pattern wasn't something
radio Tabs supports. Wrong: Tab forwards `value` onto the underlying radio
and bubbled `change` events let a parent <TabList onChange> read
`e.target.value` to drive arbitrary external UI. Native arrow-key nav also
fires change, so one handler covers mouse + keyboard.

dashboard now uses <TabList onChange={…}> + <Tab value=… checked={…}>
(controlled, so external activeTab updates from the desktop sidebar also
sync the mobile tabs when the layout flips).

docs/components/tabs.md documents the change-driven pattern explicitly,
including:
  - reading e.target.value off onChange to drive external UI;
  - using defaultChecked for uncontrolled, checked for two-way sync;
  - the value prop's role on the radio.

Verified in a real browser via Playwright at mobile viewport (420×900):
clicking each mobile tab updates the radio AND the .tab-content panel
correctly; ArrowRight from Goals advances to Settings and switches the
panel; e.target.value matches the Tab's value prop.

No library code change; library size unchanged (6,420 gz).

https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants