Add LLM-native API design guide and audit documentation#117
Draft
developit wants to merge 8 commits into
Draft
Conversation
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
✅ Deploy Preview for kinu-sh ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
Size Change: +1.01 kB (+2.69%) Total Size: 38.7 kB 📦 View Changed
ℹ️ View Unchanged
|
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
dab473a to
d5846f4
Compare
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
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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:{X}Trigger,{X}Content,{X}Item,{X}Close)API_REVIEW.md: A detailed audit of Kinu's current API surface identifying:Itemuniversality, Accordion using<details>)Itemtyping, non-standardkattribute, naming inconsistencies, missing layout/typographic primitives)Implementation Details
Both documents are reference material designed to guide future API changes. They establish:
The documents are paired—
API_REVIEW.mddocuments the current state and why changes are needed;LLM_NATIVE.mdprovides the concrete recommendations.https://claude.ai/code/session_013FYEm9UvGwbB9ybJqm3sfq