feat(ui): add menubar, navigation-menu, stepper, timeline, tree, resizable, carousel, data-table and select-native - #844
Open
jim-fung wants to merge 13 commits into
Open
Conversation
…e, carousel, data-table and select-native Wrap the Base UI 1.6 Menubar and NavigationMenu primitives, port Stepper, Timeline, Tree (Headless Tree) and Resizable (react-resizable-panels v3) from Origin UI with useRender instead of Radix Slot, and add Carousel (Embla), Data Table (TanStack Table) and a server-renderable Select Native. Each component ships with a demo particle, docs page, agent skill reference and registry entry, plus new registry categories. New dependencies: embla-carousel-react, react-resizable-panels, @headless-tree/core, @headless-tree/react.
- data-table: wire the pageSize prop into the table's initial pagination state (it was accepted but ignored), add opt-in row selection with controlled/uncontrolled rowSelection + onRowSelectionChange so the pagination selected-count is reachable, and add the missing data-slot="data-table" attribute - stepper: StepperIndicator now honors the render prop through useRender instead of only toggling between children and the default content - tree: drop a no-op Omit that removed a key the props type never had - carousel particle: define the static slide list outside the component per the particle conventions The reviewed resizable imports are correct as-is: apps/ui resolves react-resizable-panels 4.12.3 (Group/Panel/Separator); the 3.0.6 copy at the repo root belongs to apps/origin.
…ages/ui The resizable, carousel, data-table, and tree components sync into packages/ui and import react-resizable-panels, embla-carousel-react, @tanstack/react-table, and @headless-tree. Without declared dependencies, packages/ui resolved the hoisted react-resizable-panels 3.0.6 copy (apps/origin's) instead of the 4.x API those components use, breaking packages/ui's typecheck.
|
@jim-fung is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
…ives Bring the nine new component pages up to the library's documentation standard: an API Reference section with a part-by-part breakdown and prop tables (matching the scroll-area/tabs/combobox format), and an Examples section driven by a second demo particle per component. New example particles: menubar submenu, navigation-menu two-column panel with active link, vertical stepper with descriptions and a loading step, horizontal timeline, checkbox tree (cascading checked state via headless-tree's checkboxesFeature), vertical resizable with visible handle, carousel with multiple visible slides, data-table row selection with indeterminate header checkbox, and select-native multiple + small size. Writing the select-native example surfaced a typing bug: the variant size prop intersected with the native select size attribute (number), so size="sm" was invalid. The component now omits the native key.
ResizablePanel accepts snapPoints (percentages of the group, requires an id) and snapThreshold (percentage distance, default 5). When a resize gesture settles — drag release or a keyboard resize — inside a point's threshold, the panel snaps to it and the largest other panel absorbs the delta. Snapping is settle-based rather than applied mid-gesture: driving resize() during a pointer drag gets overridden by the next pointermove, which made the snap flicker instead of hold. The zone-entry rule compares against the previous settled layout, so stepping away from a snapped point never traps keyboard resizes. Verified in the browser with synthetic pointer drags: zone-free targets (40, 58, 44) stay untouched, in-zone targets (27, 48, 72) settle exactly on 25/50/75, and escaping a zone applies no snap.
…utChanged The settle-based snapping depended on the Group's onLayoutChanged callback, which does not fire reliably for real pointer drags (the library suppresses it while its drag state machine is active and the final emission path proved inconsistent under trusted input). Real drags therefore never snapped. Snapping now runs on the continuous onLayoutChange stream, which is what powers the live panel readouts and demonstrably fires during real drags. A pointer gesture tracker (capture-phase pointerdown on a handle, window pointerup/pointercancel to end the gesture) defers snapping until the gesture ends so mid-drag snaps cannot fight the pointer; keyboard and other single-step resizes snap immediately. The zone-entry rule still guarantees a keyboard nudge off a snapped point escapes instead of being re-captured.
The zone-entry rule applied to every snap decision, so a pointer release inside a snap zone only snapped when the previous settled position was outside it — releasing at 48% snapped when coming from 40% but not from a snapped 50%, which read as arbitrary. Pointer releases now snap unconditionally whenever they settle within snapThreshold of the nearest point (CSS scroll-snap proximity semantics); keyboard steps keep the zone-entry rule because arrow steps are smaller than the threshold and unconditional snapping would trap a nudge trying to leave a point. The snap example also renders tick marks at the snap points so the zones are visible.
Snapping is now mandatory (CSS scroll-snap mandatory semantics): a pointer release always parks the panel on the nearest snap point — there are no free-floating positions in between — instead of only snapping when released within a threshold (proximity semantics). The threshold no longer gates pointer snaps; it only sizes the snap zone for keyboard resizes, which keep the zone-entry rule so arrow-key steps can pass through a point without being trapped on it. The edges (0 and 100) are now always snap points, so a panel dragged to either end of the group settles fully closed or fully open. Edge snaps also exposed a floating-point bug: the absorbed delta equals the full remainder of the layout, whose stream sum carries rounding dust, so a strict negative guard rejected edge snaps at random — replaced with a small tolerance. The snap example renders tick marks at every snap point.
- StepperTrigger: default to type="button" so step clicks never submit an enclosing form - p-tree-2: render tree rows as divs (role=treeitem from headless-tree still applies) to avoid a checkbox button nested inside the item button; stop the checkbox click from also selecting/focusing the row; give the checkbox an accessible name - Data Table: expose sort state via aria-sort on sortable header cells - registry build: prefer PascalCase component exports in the lazy preview resolver so cva variant helpers (selectNativeVariants, buttonVariants, ...) are never picked as the component - docs: correct stale snapping wording — pointer releases snap to the nearest point unconditionally, not within the threshold, and never during the drag
jim-fung
force-pushed
the
feat/nine-new-primitives
branch
from
August 31, 2026 22:17
65e37ca to
0141f46
Compare
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
Adds nine primitives, closing the component gaps against the Origin UI collection and the roadmap's "expand the primitives set" goal.
MenubarTrigger; every popup inside the bar is a regularMenucomposition. NavigationMenu bundles Portal + Positioner + Popup + the morphing viewport intoNavigationMenuPopupwith the library'sportalPropspattern.Slot/asChildto Base UIuseRender+mergeProps.@headless-tree/core/@headless-tree/react(folder chevron, selection/focus/drag states,TreeDragLine).Group/Panel/Separator, percentage-string sizes).CarouselApiaccess and arrow-key support.<select>reusing the Select trigger look.Each ships with a registry entry, two demo particles, a docs page with a full API Reference (part-by-part prop tables) and Examples section — matching the library's documentation standard — plus wiring into
meta.json,llms.txt,PAGES_NEW, an agent-skill reference, and a changelog entry. The tree example doubles as a checkbox tree via headless-tree'scheckboxesFeature(cascading checked state, indeterminate folders). New registry categories:carousel,data table,menubar,navigation menu,resizable,stepper,timeline,tree.New dependencies:
embla-carousel-react,react-resizable-panels,@headless-tree/core,@headless-tree/react(@tanstack/react-tablewas already present). They are declared in bothapps/uiandpackages/ui— the latter matters because the synced copies otherwise resolve the hoistedreact-resizable-panels3.0.6 copy fromapps/origininstead of the 4.x API they use.A deliberate non-addition: no dedicated
multiselectprimitive —Comboboxalready supportsmultiplewith chips natively.Test plan
bun run typecheck(includingpackages/ui), lint, formatregistry:validate-deps,registry:build,ui:sync