From 45251a0e82e8dddb9fd05a52fe5cff87221e9a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Wed, 8 Apr 2026 13:41:44 +0200 Subject: [PATCH 1/7] feat: toc component implementation --- .storybook/modules/toc.module.css | 291 ++++++++++++++++++ bun.lock | 6 + packages/react/package.json | 3 +- packages/react/src/components/index.ts | 1 + .../src/components/toc/examples/basic.tsx | 61 ++++ .../src/components/toc/examples/context.tsx | 54 ++++ .../components/toc/examples/controlled.tsx | 68 ++++ .../toc/examples/custom-rendering.tsx | 36 +++ .../toc/examples/custom-scroll-behavior.tsx | 44 +++ .../src/components/toc/examples/floating.tsx | 59 ++++ .../src/components/toc/examples/grouped.tsx | 63 ++++ .../toc/examples/highlight-on-enter.tsx | 49 +++ .../src/components/toc/examples/nested.tsx | 60 ++++ .../src/components/toc/examples/placement.tsx | 58 ++++ .../toc/examples/progressive-reveal.tsx | 93 ++++++ .../toc/examples/reading-progress.tsx | 75 +++++ .../components/toc/examples/root-provider.tsx | 53 ++++ .../react/src/components/toc/examples/rtl.tsx | 33 ++ .../src/components/toc/examples/theming.tsx | 34 ++ .../toc/examples/with-indicator.tsx | 64 ++++ .../toc/examples/with-scroll-area.tsx | 45 +++ packages/react/src/components/toc/index.ts | 16 + .../react/src/components/toc/tests/basic.tsx | 32 ++ .../src/components/toc/tests/toc.test.tsx | 107 +++++++ .../react/src/components/toc/toc-content.tsx | 11 + .../react/src/components/toc/toc-context.tsx | 8 + .../src/components/toc/toc-indicator.tsx | 16 + .../react/src/components/toc/toc-item.tsx | 26 ++ .../react/src/components/toc/toc-link.tsx | 18 ++ .../react/src/components/toc/toc-list.tsx | 16 + packages/react/src/components/toc/toc-nav.tsx | 19 ++ .../src/components/toc/toc-root-provider.tsx | 27 ++ .../react/src/components/toc/toc-root.tsx | 36 +++ .../react/src/components/toc/toc-title.tsx | 16 + .../react/src/components/toc/toc.anatomy.ts | 1 + .../react/src/components/toc/toc.stories.tsx | 25 ++ packages/react/src/components/toc/toc.ts | 27 ++ .../src/components/toc/use-toc-context.ts | 10 + .../toc/use-toc-item-props-context.ts | 10 + packages/react/src/components/toc/use-toc.ts | 24 ++ packages/solid/package.json | 3 +- packages/svelte/package.json | 3 +- packages/vue/package.json | 3 +- 43 files changed, 1700 insertions(+), 4 deletions(-) create mode 100644 .storybook/modules/toc.module.css create mode 100644 packages/react/src/components/toc/examples/basic.tsx create mode 100644 packages/react/src/components/toc/examples/context.tsx create mode 100644 packages/react/src/components/toc/examples/controlled.tsx create mode 100644 packages/react/src/components/toc/examples/custom-rendering.tsx create mode 100644 packages/react/src/components/toc/examples/custom-scroll-behavior.tsx create mode 100644 packages/react/src/components/toc/examples/floating.tsx create mode 100644 packages/react/src/components/toc/examples/grouped.tsx create mode 100644 packages/react/src/components/toc/examples/highlight-on-enter.tsx create mode 100644 packages/react/src/components/toc/examples/nested.tsx create mode 100644 packages/react/src/components/toc/examples/placement.tsx create mode 100644 packages/react/src/components/toc/examples/progressive-reveal.tsx create mode 100644 packages/react/src/components/toc/examples/reading-progress.tsx create mode 100644 packages/react/src/components/toc/examples/root-provider.tsx create mode 100644 packages/react/src/components/toc/examples/rtl.tsx create mode 100644 packages/react/src/components/toc/examples/theming.tsx create mode 100644 packages/react/src/components/toc/examples/with-indicator.tsx create mode 100644 packages/react/src/components/toc/examples/with-scroll-area.tsx create mode 100644 packages/react/src/components/toc/index.ts create mode 100644 packages/react/src/components/toc/tests/basic.tsx create mode 100644 packages/react/src/components/toc/tests/toc.test.tsx create mode 100644 packages/react/src/components/toc/toc-content.tsx create mode 100644 packages/react/src/components/toc/toc-context.tsx create mode 100644 packages/react/src/components/toc/toc-indicator.tsx create mode 100644 packages/react/src/components/toc/toc-item.tsx create mode 100644 packages/react/src/components/toc/toc-link.tsx create mode 100644 packages/react/src/components/toc/toc-list.tsx create mode 100644 packages/react/src/components/toc/toc-nav.tsx create mode 100644 packages/react/src/components/toc/toc-root-provider.tsx create mode 100644 packages/react/src/components/toc/toc-root.tsx create mode 100644 packages/react/src/components/toc/toc-title.tsx create mode 100644 packages/react/src/components/toc/toc.anatomy.ts create mode 100644 packages/react/src/components/toc/toc.stories.tsx create mode 100644 packages/react/src/components/toc/toc.ts create mode 100644 packages/react/src/components/toc/use-toc-context.ts create mode 100644 packages/react/src/components/toc/use-toc-item-props-context.ts create mode 100644 packages/react/src/components/toc/use-toc.ts diff --git a/.storybook/modules/toc.module.css b/.storybook/modules/toc.module.css new file mode 100644 index 0000000000..5914ae0877 --- /dev/null +++ b/.storybook/modules/toc.module.css @@ -0,0 +1,291 @@ +.Root { + display: flex; + flex-direction: row; + gap: 3rem; + align-items: flex-start; + width: 100%; + max-width: 56rem; +} + +.Nav { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 14rem; + position: sticky; + top: 0; + flex-shrink: 0; +} + +.Title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--demo-neutral-fg-muted); + padding-inline: 0.5rem; +} + +.List { + list-style: none; + padding: 0; + margin: 0; + position: relative; +} + +.Item { + padding-inline-start: calc(var(--depth, 0) * 0.75rem); +} + +.Link { + display: block; + padding: 0.3rem 0.5rem; + font-size: 0.875rem; + color: var(--demo-neutral-fg-muted); + text-decoration: none; + border-radius: 0.375rem; + transition: color 0.15s; + + &:hover { + color: var(--demo-neutral-fg); + } + + &[data-active] { + color: var(--demo-coral-fg); + font-weight: 500; + } + + &:focus-visible { + outline: 2px solid var(--demo-coral-focus-ring); + outline-offset: 2px; + } +} + +.Indicator { + position: absolute; + top: var(--top); + left: 0; + height: var(--height); + width: 2px; + background: var(--demo-coral-solid); + border-radius: 9999px; + transition: + top 0.2s ease, + height 0.2s ease; +} + +.Content { + flex: 1; + min-width: 0; + height: 28rem; + overflow-y: auto; + padding-right: 1rem; + + & h2, + & h3, + & h4 { + color: var(--demo-neutral-fg); + margin-block: 1.5rem 0.5rem; + + &:first-child { + margin-top: 0; + } + } + + & h2 { + font-size: 1.125rem; + font-weight: 700; + } + + & h3 { + font-size: 1rem; + font-weight: 600; + } + + & h4 { + font-size: 0.875rem; + font-weight: 600; + } + + & p { + font-size: 0.875rem; + line-height: 1.7; + color: var(--demo-neutral-fg-muted); + margin-bottom: 0.75rem; + } +} + +.SubList { + list-style: none; + padding: 0; + margin: 0; + overflow: hidden; + max-height: 0; + opacity: 0; + transition: + max-height 0.3s ease, + opacity 0.2s ease; + + &[data-visible] { + max-height: 20rem; + opacity: 1; + } +} + +.Root:has([data-placement='right']) { + flex-direction: row-reverse; +} + +.Controls { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; +} + +.Stepper { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0; + margin-bottom: 0.75rem; + border-bottom: 1px solid var(--demo-neutral-border); +} + +.StepCount { + font-size: 0.8125rem; + color: var(--demo-neutral-fg-muted); + min-width: 3rem; + text-align: center; +} + +.ActiveIds { + font-size: 0.8125rem; + color: var(--demo-neutral-fg-muted); + margin: 0; +} + +.LinkAnimated { + display: block; + padding: 0.3rem 0.5rem; + font-size: 0.875rem; + color: var(--demo-neutral-fg-muted); + text-decoration: none; + border-radius: 0.375rem; + transition: color 0.15s, background-color 0.5s ease-out; + + &:hover { + color: var(--demo-neutral-fg); + } + + &:focus-visible { + outline: 2px solid var(--demo-coral-focus-ring); + outline-offset: 2px; + } + + &[data-active] { + color: var(--demo-coral-fg); + font-weight: 500; + background-color: transparent; + + @starting-style { + background-color: color-mix(in srgb, var(--demo-coral-solid) 15%, transparent); + } + } +} + +.Progress { + height: 2px; + background: var(--demo-neutral-border); + border-radius: 9999px; + overflow: hidden; + margin-bottom: 0.25rem; +} + +.ProgressBar { + height: 100%; + width: 100%; + background: var(--demo-coral-solid); + border-radius: 9999px; + transform: scaleX(var(--progress, 0)); + transform-origin: left; + transition: transform 0.1s ease; +} + +.Group { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.GroupLabel { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--demo-neutral-fg-muted); + padding-inline: 0.5rem; + padding-block: 0.5rem 0.25rem; +} + +.FloatingWrapper { + display: flex; + flex-direction: column; + max-width: 40rem; + width: 100%; +} + +.FloatingHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding-block: 0.5rem; + margin-bottom: 0.75rem; + border-bottom: 1px solid var(--demo-neutral-border); +} + +.FloatingTitle { + font-size: 0.875rem; + font-weight: 600; + color: var(--demo-neutral-fg); +} + +.FloatingNav { + min-width: 14rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + position: relative; +} + +.LinkWithIcon { + display: flex; + align-items: center; + gap: 0.375rem; +} + +.LinkAmber { + color: #d97706; + font-weight: 600; + + &:hover { + color: #b45309; + } +} + +.LinkBlue { + color: #2563eb; + font-weight: 600; + + &:hover { + color: #1d4ed8; + } +} + +.LinkGreen { + color: #059669; + font-weight: 600; + + &:hover { + color: #047857; + } +} diff --git a/bun.lock b/bun.lock index c8ddcf0df7..4a4d8dd192 100644 --- a/bun.lock +++ b/bun.lock @@ -103,6 +103,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", @@ -214,6 +215,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", @@ -321,6 +323,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", @@ -425,6 +428,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", @@ -1907,6 +1911,8 @@ "@zag-js/toast": ["@zag-js/toast@1.39.1", "", { "dependencies": { "@zag-js/anatomy": "1.39.1", "@zag-js/core": "1.39.1", "@zag-js/dismissable": "1.39.1", "@zag-js/dom-query": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1" } }, "sha512-K7ndEfBTKDds10iQKCQUmin74s6V4BEIypAIyQxs18gQB9TCn5+wff886JAzecIKPY97PDQHDKjYR71yzRC7/g=="], + "@zag-js/toc": ["@zag-js/toc@1.39.1", "", { "dependencies": { "@zag-js/anatomy": "1.39.1", "@zag-js/core": "1.39.1", "@zag-js/dom-query": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1" } }, "sha512-IifbYEaM2WJv86eNp/NBAlglxK+i/F5FKKIe3mGmonOTX5Hn6ZQdNg7YjQHAbHgpcCMwmnH+TQr7eJ3nSnEnWA=="], + "@zag-js/toggle": ["@zag-js/toggle@1.39.1", "", { "dependencies": { "@zag-js/anatomy": "1.39.1", "@zag-js/core": "1.39.1", "@zag-js/dom-query": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1" } }, "sha512-0MD11S+y3GLhLAo7d+hudUcLm4LBhCa4zt693Z6fJECnGu9Jxh5ZXqcSLIj2ffeJs1zeGF9lYFEYTNMsQEYCLQ=="], "@zag-js/toggle-group": ["@zag-js/toggle-group@1.39.1", "", { "dependencies": { "@zag-js/anatomy": "1.39.1", "@zag-js/core": "1.39.1", "@zag-js/dom-query": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1" } }, "sha512-KS4Bo17foMKXVBhQjocRf4GQxMV4pMXclTo14IWjldaHs2HIrNJ0Ar0Ri+vo47BBKBNsXs4HuNvfbMdQj94wEA=="], diff --git a/packages/react/package.json b/packages/react/package.json index 927de48bb1..3d8bb4d834 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -110,10 +110,10 @@ "@zag-js/drawer": "1.39.1", "@zag-js/editable": "1.39.1", "@zag-js/file-upload": "1.39.1", - "@zag-js/focus-visible": "1.39.1", "@zag-js/file-utils": "1.39.1", "@zag-js/floating-panel": "1.39.1", "@zag-js/focus-trap": "1.39.1", + "@zag-js/focus-visible": "1.39.1", "@zag-js/highlight-word": "1.39.1", "@zag-js/hover-card": "1.39.1", "@zag-js/i18n-utils": "1.39.1", @@ -145,6 +145,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 9d05042d52..0c13683a0d 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -54,6 +54,7 @@ export * from './tabs' export * from './tags-input' export * from './timer' export * from './toast' +export * from './toc' export * from './toggle' export * from './toggle-group' export * from './tooltip' diff --git a/packages/react/src/components/toc/examples/basic.tsx b/packages/react/src/components/toc/examples/basic.tsx new file mode 100644 index 0000000000..3eb93164e7 --- /dev/null +++ b/packages/react/src/components/toc/examples/basic.tsx @@ -0,0 +1,61 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2 }, + { value: 'getting-started', depth: 2 }, + { value: 'installation', depth: 2 }, + { value: 'usage', depth: 2 }, + { value: 'api-reference', depth: 2 }, +] + +export const Basic = () => ( + + +

Introduction

+

+ A table of contents helps readers navigate long documents by providing quick links to each section. It + automatically highlights the section currently visible in the viewport. +

+

Getting Started

+

+ To get started, pass an array of items to the root component. Each item has a value matching the heading id and + a depth matching the heading level. +

+

Installation

+

+ Install the package using your preferred package manager. The component has no external dependencies beyond the + state machine. +

+

+ You can install it via npm, yarn, pnpm, or bun. The package ships as an ES module and requires a bundler that + supports modern JavaScript. +

+

Usage

+

+ Import the component and compose it using the compound component pattern. The Root component manages the + IntersectionObserver internally. +

+

+ Each link in the TOC corresponds to a heading in the document. When the heading enters the viewport, its link + becomes active. +

+

API Reference

+

+ The full API reference documents all props, events, and methods available on each component part. Refer to it + when customizing behavior. +

+
+ + + On this page + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/context.tsx b/packages/react/src/components/toc/examples/context.tsx new file mode 100644 index 0000000000..005fc68c49 --- /dev/null +++ b/packages/react/src/components/toc/examples/context.tsx @@ -0,0 +1,54 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'background', depth: 2 }, + { value: 'motivation', depth: 2 }, + { value: 'design-decisions', depth: 2 }, + { value: 'future-plans', depth: 2 }, +] + +export const Context = () => ( + + +

Background

+

+ This project started as an internal tool before being open-sourced. The original motivation was to avoid + rebuilding the same components across multiple products. +

+

Motivation

+

+ Existing solutions were either too opinionated about styling or too low-level to use productively. We needed + something in between. +

+

Design Decisions

+

+ We chose state machines as the foundation because they make interaction logic explicit, testable, and shareable + across framework implementations. +

+

Future Plans

+

+ The roadmap includes more components, improved accessibility tooling, and tighter integration with design token + workflows. +

+
+ + + + {(toc) => ( + <> + On this page ({toc.activeIds.length} visible) + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + + )} + + +
+) diff --git a/packages/react/src/components/toc/examples/controlled.tsx b/packages/react/src/components/toc/examples/controlled.tsx new file mode 100644 index 0000000000..0ec0b3d986 --- /dev/null +++ b/packages/react/src/components/toc/examples/controlled.tsx @@ -0,0 +1,68 @@ +import { Toc } from '@ark-ui/react/toc' +import { useState } from 'react' +import button from 'styles/button.module.css' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'setup', depth: 2 }, + { value: 'configuration', depth: 2 }, + { value: 'deployment', depth: 2 }, + { value: 'monitoring', depth: 2 }, +] + +export const Controlled = () => { + const [step, setStep] = useState(0) + + return ( + + +
+ + + {step + 1} / {items.length} + + +
+ +

Setup

+

+ Configure your environment before beginning. You will need Node.js version 18 or higher and a package manager + of your choice. +

+

Configuration

+

+ Edit the configuration file to match your project requirements. Most defaults work well for typical use cases. +

+

Deployment

+

+ Deploy to your hosting provider of choice. The build output is a standard static site that works on any CDN. +

+

Monitoring

+

+ After deploying, set up monitoring to track errors and performance. Connect your preferred observability + platform. +

+
+ + + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+ ) +} diff --git a/packages/react/src/components/toc/examples/custom-rendering.tsx b/packages/react/src/components/toc/examples/custom-rendering.tsx new file mode 100644 index 0000000000..c776f7fd6f --- /dev/null +++ b/packages/react/src/components/toc/examples/custom-rendering.tsx @@ -0,0 +1,36 @@ +import { Toc } from '@ark-ui/react/toc' +import { BookOpenIcon } from 'lucide-react' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2 }, + { value: 'usage', depth: 2 }, + { value: 'api', depth: 2 }, +] + +export const CustomRendering = () => ( + + +

Introduction

+

Custom rendering with icons.

+

Usage

+

How to use custom links in your ToC.

+

API

+

API reference section.

+
+ + + {items.map((item) => ( + + + + + {item.value.charAt(0).toUpperCase() + item.value.slice(1)} + + + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx b/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx new file mode 100644 index 0000000000..0b9939e77f --- /dev/null +++ b/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx @@ -0,0 +1,44 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'alpha', depth: 2 }, + { value: 'beta', depth: 2 }, + { value: 'gamma', depth: 2 }, +] + +export const CustomScrollBehavior = () => { + const scrollToId = (e: React.MouseEvent, id: string) => { + e.preventDefault() + const el = document.getElementById(id) + if (el) { + const y = el.getBoundingClientRect().top + window.scrollY - 64 + window.scrollTo({ top: y, behavior: 'smooth' }) + } + } + + return ( + + +

Alpha

+

Section Alpha content.

+

Beta

+

Section Beta content.

+

Gamma

+

Section Gamma content.

+
+ + On this page + + {items.map((item) => ( + + scrollToId(e, item.value)}> + {item.value.charAt(0).toUpperCase() + item.value.slice(1)} + + + ))} + + +
+ ) +} diff --git a/packages/react/src/components/toc/examples/floating.tsx b/packages/react/src/components/toc/examples/floating.tsx new file mode 100644 index 0000000000..ed0df2a9dc --- /dev/null +++ b/packages/react/src/components/toc/examples/floating.tsx @@ -0,0 +1,59 @@ +import { Popover } from '@ark-ui/react/popover' +import { Portal } from '@ark-ui/react/portal' +import { Toc } from '@ark-ui/react/toc' +import button from 'styles/button.module.css' +import popoverStyles from 'styles/popover.module.css' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2 }, + { value: 'installation', depth: 2 }, + { value: 'usage', depth: 2 }, + { value: 'api-reference', depth: 2 }, +] + +export const Floating = () => ( + +
+
+ Article + + Contents ↓ + + + + + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + + + + + +
+ + +

Introduction

+

A table of contents helps readers navigate long documents by providing quick links to each section.

+

Installation

+

+ Install the package using your preferred package manager. The component has no external dependencies beyond + the state machine. +

+

Usage

+

+ Import and compose using the compound component pattern. The Root manages the IntersectionObserver internally. +

+

API Reference

+

The full API reference documents all props, events, and methods available on each component part.

+
+
+
+) diff --git a/packages/react/src/components/toc/examples/grouped.tsx b/packages/react/src/components/toc/examples/grouped.tsx new file mode 100644 index 0000000000..af43e3081d --- /dev/null +++ b/packages/react/src/components/toc/examples/grouped.tsx @@ -0,0 +1,63 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const groups = [ + { + label: 'Getting Started', + items: [ + { value: 'overview', depth: 2 }, + { value: 'installation', depth: 2 }, + ], + }, + { + label: 'Advanced', + items: [ + { value: 'configuration', depth: 2 }, + { value: 'plugins', depth: 2 }, + ], + }, + { + label: 'Reference', + items: [ + { value: 'api', depth: 2 }, + { value: 'changelog', depth: 2 }, + ], + }, +] + +const allItems = groups.flatMap((g) => g.items) + +export const Grouped = () => ( + + +

Overview

+

Ark UI is a headless component library for building scalable design systems.

+

Installation

+

Install with your preferred package manager. No configuration needed to get started.

+

Configuration

+

Customize behavior through props. Most defaults work well out of the box.

+

Plugins

+

Extend functionality with plugins. Write your own or use community-contributed ones.

+

API

+

The full API reference covers all props, events, and methods on each component part.

+

Changelog

+

All notable changes are documented here following semantic versioning conventions.

+
+ + + + {groups.map((group) => ( +
+ {group.label} + + {group.items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + +
+ ))} +
+
+) diff --git a/packages/react/src/components/toc/examples/highlight-on-enter.tsx b/packages/react/src/components/toc/examples/highlight-on-enter.tsx new file mode 100644 index 0000000000..0026bdc4b3 --- /dev/null +++ b/packages/react/src/components/toc/examples/highlight-on-enter.tsx @@ -0,0 +1,49 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'principles', depth: 2 }, + { value: 'accessibility', depth: 2 }, + { value: 'performance', depth: 2 }, + { value: 'testing', depth: 2 }, + { value: 'tooling', depth: 2 }, +] + +export const HighlightOnEnter = () => ( + + +

Principles

+

+ Good software follows a set of guiding principles that inform every decision from API design to implementation + details. +

+

Accessibility

+

+ Every component ships with ARIA attributes and keyboard navigation baked in. Accessibility is not an + afterthought. +

+

Performance

+

Measure before optimizing. The bottleneck is rarely where you expect. Profile first, then act on real data.

+

Testing

+

+ Tests give you confidence that your code works as intended. A good suite catches regressions before they reach + production. +

+

Tooling

+

+ Great tooling removes friction. Invest in your development environment the same way you invest in your product. +

+
+ + + On this page + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/nested.tsx b/packages/react/src/components/toc/examples/nested.tsx new file mode 100644 index 0000000000..25f0318aad --- /dev/null +++ b/packages/react/src/components/toc/examples/nested.tsx @@ -0,0 +1,60 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'guides', depth: 2 }, + { value: 'quick-start', depth: 3 }, + { value: 'manual-setup', depth: 3 }, + { value: 'core-concepts', depth: 2 }, + { value: 'props', depth: 3 }, + { value: 'events', depth: 3 }, + { value: 'context', depth: 3 }, + { value: 'advanced', depth: 2 }, + { value: 'root-provider', depth: 3 }, + { value: 'custom-rendering', depth: 3 }, +] + +export const Nested = () => ( + + +

Guides

+

Step-by-step guides for getting the most out of Ark UI in your projects.

+

Quick Start

+

Install the package and render your first component in under two minutes. No configuration required.

+

Manual Setup

+

For projects that need fine-grained control over the setup, follow the manual configuration guide.

+

Core Concepts

+

Understanding the core concepts helps you use the library more effectively.

+

Props

+

Props control the machine context. Pass them directly to the Root component or via the hook.

+

Events

+

Callback props like onValueChange fire when the machine transitions to a new state.

+

Context

+

Every component exposes a Context render prop for accessing the machine API without an extra hook.

+

Advanced

+

Advanced patterns for complex use cases.

+

Root Provider

+

+ Use the RootProvider pattern to call the hook at a higher level and share the API with components outside the + tree. +

+

Custom Rendering

+

+ Use the asChild prop to render any component part as a custom element while keeping all aria and event + attributes. +

+
+ + + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/placement.tsx b/packages/react/src/components/toc/examples/placement.tsx new file mode 100644 index 0000000000..94497ce4fd --- /dev/null +++ b/packages/react/src/components/toc/examples/placement.tsx @@ -0,0 +1,58 @@ +import { Toc } from '@ark-ui/react/toc' +import { useState } from 'react' +import button from 'styles/button.module.css' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'overview', depth: 2 }, + { value: 'installation', depth: 2 }, + { value: 'configuration', depth: 2 }, + { value: 'usage', depth: 2 }, +] + +export const Placement = () => { + const [placement, setPlacement] = useState<'left' | 'right'>('left') + + return ( + + +

Overview

+

The placement prop controls whether the navigation sits on the left or right side of the content.

+

Installation

+

Install the package and render your first component. No configuration required to get started.

+

Configuration

+

Edit the configuration file to match your project requirements. Most defaults work for typical use cases.

+

Usage

+

Import the component and compose it using the compound component pattern.

+
+ + +
+ + +
+ On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + +
+
+ ) +} diff --git a/packages/react/src/components/toc/examples/progressive-reveal.tsx b/packages/react/src/components/toc/examples/progressive-reveal.tsx new file mode 100644 index 0000000000..32c26cd045 --- /dev/null +++ b/packages/react/src/components/toc/examples/progressive-reveal.tsx @@ -0,0 +1,93 @@ +import { Toc, type TocActiveChangeDetails } from '@ark-ui/react/toc' +import { useState } from 'react' +import styles from 'styles/toc.module.css' + +const sections = [ + { + item: { value: 'design-system', depth: 2 }, + children: [ + { value: 'typography', depth: 3 }, + { value: 'color-palette', depth: 3 }, + { value: 'spacing', depth: 3 }, + ], + }, + { + item: { value: 'components', depth: 2 }, + children: [ + { value: 'buttons', depth: 3 }, + { value: 'forms', depth: 3 }, + { value: 'navigation', depth: 3 }, + ], + }, + { + item: { value: 'patterns', depth: 2 }, + children: [ + { value: 'feedback', depth: 3 }, + { value: 'data-display', depth: 3 }, + ], + }, +] + +const allItems = sections.flatMap((s) => [s.item, ...s.children]) + +const label = (value: string) => value.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + +export const ProgressiveReveal = () => { + const [activeIds, setActiveIds] = useState([]) + + const activeParent = sections.find( + (s) => activeIds.includes(s.item.value) || s.children.some((c) => activeIds.includes(c.value)), + )?.item.value + + return ( + setActiveIds(details.activeIds)} + > + +

Design System

+

The foundation of visual consistency across products.

+

Typography

+

Type scales, font choices, and heading hierarchy.

+

Color Palette

+

Semantic color tokens for light and dark modes.

+

Spacing

+

Consistent spacing units and layout rhythm.

+

Components

+

Reusable building blocks for interfaces.

+

Buttons

+

Primary, secondary, and tertiary action triggers.

+

Forms

+

Input patterns and validation feedback.

+ +

Menus, breadcrumbs, and wayfinding.

+

Patterns

+

Higher-level interaction and layout patterns.

+

Feedback

+

Toasts, alerts, and progress indicators.

+

Data Display

+

Tables, lists, and structured information.

+
+ + + On this page + + + {sections.map((section) => ( + + {label(section.item.value)} +
    + {section.children.map((child) => ( + + {label(child.value)} + + ))} +
+
+ ))} +
+
+
+ ) +} diff --git a/packages/react/src/components/toc/examples/reading-progress.tsx b/packages/react/src/components/toc/examples/reading-progress.tsx new file mode 100644 index 0000000000..10a7b807aa --- /dev/null +++ b/packages/react/src/components/toc/examples/reading-progress.tsx @@ -0,0 +1,75 @@ +import { Toc } from '@ark-ui/react/toc' +import { useRef, useState } from 'react' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'overview', depth: 2 }, + { value: 'architecture', depth: 2 }, + { value: 'state-machines', depth: 2 }, + { value: 'components', depth: 2 }, + { value: 'theming', depth: 2 }, +] + +export const ReadingProgress = () => { + const contentRef = useRef(null) + const [progress, setProgress] = useState(0) + + const handleScroll = () => { + const el = contentRef.current + if (!el) return + const { scrollTop, scrollHeight, clientHeight } = el + const total = scrollHeight - clientHeight + setProgress(total > 0 ? scrollTop / total : 0) + } + + return ( + + +

Overview

+

+ Ark UI is a headless component library built on Zag.js state machines. It provides unstyled, accessible + components for building design systems. +

+

Architecture

+

+ The library follows a layered architecture. At the base are Zag.js machines handling all interaction logic. + Framework adapters on top expose idiomatic React, Solid, Vue, and Svelte APIs. +

+

+ Each component is a thin wrapper around the machine. Props flow down, events bubble up, and the machine + orchestrates all state transitions. +

+

State Machines

+

+ State machines make interaction logic predictable and testable. Every possible state is explicitly defined, + making undefined states impossible by construction. +

+

Components

+

+ Components use the compound component pattern. A Root component holds state and provides it to all child parts + via React context. +

+

Theming

+

+ Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any + styling solution that works in your stack. +

+
+ + + On this page +
+
+
+ + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + + + ) +} diff --git a/packages/react/src/components/toc/examples/root-provider.tsx b/packages/react/src/components/toc/examples/root-provider.tsx new file mode 100644 index 0000000000..ad3c5c7b64 --- /dev/null +++ b/packages/react/src/components/toc/examples/root-provider.tsx @@ -0,0 +1,53 @@ +import { Toc, useToc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'principles', depth: 2 }, + { value: 'patterns', depth: 2 }, + { value: 'testing', depth: 2 }, + { value: 'performance', depth: 2 }, +] + +export const RootProvider = () => { + const toc = useToc({ items }) + + return ( + + +

Principles

+

+ Good software follows a set of guiding principles. These inform every decision from API design to + implementation details. +

+

Patterns

+

+ Design patterns are reusable solutions to common problems. Learning them helps you recognize familiar + structures in new codebases. +

+

Testing

+

+ Tests give you confidence that your code works as intended. A good test suite catches regressions before they + reach production. +

+

Performance

+

+ Performance is a feature. Measure before optimizing, and optimize the things that matter most to your users. +

+
+ + +

Active: {toc.activeIds.length > 0 ? toc.activeIds.join(', ') : '—'}

+ + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + +
+
+ ) +} diff --git a/packages/react/src/components/toc/examples/rtl.tsx b/packages/react/src/components/toc/examples/rtl.tsx new file mode 100644 index 0000000000..0ee59e5f7a --- /dev/null +++ b/packages/react/src/components/toc/examples/rtl.tsx @@ -0,0 +1,33 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'rtl1', depth: 2 }, + { value: 'rtl2', depth: 2 }, + { value: 'rtl3', depth: 2 }, +] + +export const Rtl = () => ( + + +

مقدمة

+

هذا هو القسم الأول.

+

الاستخدام

+

هذا هو القسم الثاني.

+

الميزات

+

هذا هو القسم الثالث.

+
+ + في هذه الصفحة + + {items.map((item) => ( + + + {item.value === 'rtl1' ? 'مقدمة' : item.value === 'rtl2' ? 'الاستخدام' : 'الميزات'} + + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/theming.tsx b/packages/react/src/components/toc/examples/theming.tsx new file mode 100644 index 0000000000..d400e3c526 --- /dev/null +++ b/packages/react/src/components/toc/examples/theming.tsx @@ -0,0 +1,34 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'custom1', depth: 2 }, + { value: 'custom2', depth: 2 }, + { value: 'custom3', depth: 2 }, +] + +export const Theming = () => ( + + +

Custom Style 1

+

Section with custom theming 1.

+

Custom Style 2

+

Section with custom theming 2.

+

Custom Style 3

+

Section with custom theming 3.

+
+ + + + Custom Style 1 + + + Custom Style 2 + + + Custom Style 3 + + + +
+) diff --git a/packages/react/src/components/toc/examples/with-indicator.tsx b/packages/react/src/components/toc/examples/with-indicator.tsx new file mode 100644 index 0000000000..c08d6fc1a5 --- /dev/null +++ b/packages/react/src/components/toc/examples/with-indicator.tsx @@ -0,0 +1,64 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'overview', depth: 2 }, + { value: 'architecture', depth: 2 }, + { value: 'state-machines', depth: 2 }, + { value: 'components', depth: 2 }, + { value: 'theming', depth: 2 }, + { value: 'accessibility', depth: 2 }, +] + +export const WithIndicator = () => ( + + +

Overview

+

+ Ark UI is a headless component library built on top of Zag.js state machines. It provides unstyled, accessible + components ready for your design system. +

+

Architecture

+

+ The library follows a layered architecture. At the base are the Zag.js machines, which handle all interaction + logic. On top of that, framework adapters expose idiomatic React, Solid, Vue, and Svelte APIs. +

+

+ Each component is a thin wrapper around the machine. Props flow down, events bubble up, and the machine + orchestrates all state transitions. +

+

State Machines

+

+ State machines make the interaction logic predictable and testable. Every possible state is explicitly defined, + making it impossible to end up in an undefined state. +

+

Components

+

+ Components are structured using the compound component pattern. A Root component holds state and provides it to + all child parts via React context. +

+

Theming

+

+ Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any + styling solution that works in your stack. +

+

Accessibility

+

+ All components follow WAI-ARIA patterns and are tested with screen readers. Keyboard navigation is built into + the state machine, not bolted on after the fact. +

+
+ + + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/with-scroll-area.tsx b/packages/react/src/components/toc/examples/with-scroll-area.tsx new file mode 100644 index 0000000000..5a95114f62 --- /dev/null +++ b/packages/react/src/components/toc/examples/with-scroll-area.tsx @@ -0,0 +1,45 @@ +import { Toc } from '@ark-ui/react/toc' +import { ScrollArea } from '@ark-ui/react/scroll-area' +import scrollAreaStyles from 'styles/scroll-area.module.css' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'a', depth: 2 }, + { value: 'b', depth: 2 }, + { value: 'c', depth: 2 }, + { value: 'd', depth: 2 }, + { value: 'e', depth: 2 }, + { value: 'f', depth: 2 }, +] + +export const WithScrollArea = () => ( + + +

Section A

+

Content for section A.

+

Section B

+

Content for section B.

+

Section C

+

Content for section C.

+

Section D

+

Content for section D.

+

Section E

+

Content for section E.

+

Section F

+

Content for section F.

+
+ + + + + {items.map((item) => ( + + Section {item.value.toUpperCase()} + + ))} + + + + +
+) diff --git a/packages/react/src/components/toc/index.ts b/packages/react/src/components/toc/index.ts new file mode 100644 index 0000000000..af93ee014c --- /dev/null +++ b/packages/react/src/components/toc/index.ts @@ -0,0 +1,16 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { TocContent, type TocContentBaseProps, type TocContentProps } from './toc-content' +export { TocContext, type TocContextProps } from './toc-context' +export { TocIndicator, type TocIndicatorBaseProps, type TocIndicatorProps } from './toc-indicator' +export { TocItem, type TocItemBaseProps, type TocItemProps } from './toc-item' +export { TocLink, type TocLinkBaseProps, type TocLinkProps } from './toc-link' +export { TocList, type TocListBaseProps, type TocListProps } from './toc-list' +export { TocNav, type TocNavBaseProps, type TocNavProps } from './toc-nav' +export { TocRoot, type TocRootBaseProps, type TocRootProps } from './toc-root' +export { TocRootProvider, type TocRootProviderBaseProps, type TocRootProviderProps } from './toc-root-provider' +export { TocTitle, type TocTitleBaseProps, type TocTitleProps } from './toc-title' +export { tocAnatomy } from './toc.anatomy' +export { useToc, type UseTocProps, type UseTocReturn } from './use-toc' +export { useTocContext, type UseTocContext } from './use-toc-context' + +export * as Toc from './toc' diff --git a/packages/react/src/components/toc/tests/basic.tsx b/packages/react/src/components/toc/tests/basic.tsx new file mode 100644 index 0000000000..75a88bb49b --- /dev/null +++ b/packages/react/src/components/toc/tests/basic.tsx @@ -0,0 +1,32 @@ +import { Toc } from '../' + +export const items = [ + { value: 'introduction', depth: 2 }, + { value: 'getting-started', depth: 2 }, + { value: 'installation', depth: 2 }, + { value: 'usage', depth: 2 }, + { value: 'api-reference', depth: 2 }, +] + +export const ComponentUnderTest = (props: Omit) => ( + + +

Introduction

+

Getting Started

+

Installation

+

Usage

+

API Reference

+
+ + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+) diff --git a/packages/react/src/components/toc/tests/toc.test.tsx b/packages/react/src/components/toc/tests/toc.test.tsx new file mode 100644 index 0000000000..98d2a8b940 --- /dev/null +++ b/packages/react/src/components/toc/tests/toc.test.tsx @@ -0,0 +1,107 @@ +import { render, screen } from '@testing-library/react' +import { vi } from 'vitest' +import { axe } from 'vitest-axe' +import { Toc } from '../' +import { ComponentUnderTest, items } from './basic' + +class MockIntersectionObserver { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() +} + +beforeAll(() => { + vi.stubGlobal('IntersectionObserver', MockIntersectionObserver) +}) + +afterAll(() => { + vi.unstubAllGlobals() +}) + +describe('Toc', () => { + it('should have no a11y violations', async () => { + const { container } = render() + const results = await axe(container) + expect(results).toHaveNoViolations() + }) + + it('should render Nav as a nav landmark', () => { + render() + expect(screen.getByRole('navigation')).toBeInTheDocument() + }) + + it('should render Content as an article', () => { + const { container } = render() + expect(container.querySelector('article')).toBeInTheDocument() + }) + + it('should render the title', () => { + render() + expect(screen.getByText('On this page')).toBeInTheDocument() + }) + + it('should render all links with correct hrefs', () => { + render() + expect(screen.getByRole('link', { name: 'introduction' })).toHaveAttribute('href', '#introduction') + expect(screen.getByRole('link', { name: 'usage' })).toHaveAttribute('href', '#usage') + expect(screen.getByRole('link', { name: 'api reference' })).toHaveAttribute('href', '#api-reference') + }) + + it('should render the indicator', () => { + render() + expect(screen.getByTestId('indicator')).toHaveAttribute('data-part', 'indicator') + }) + + it('should apply data-part attributes to all parts', () => { + const { container } = render() + expect(container.querySelector('[data-part="title"]')).toBeInTheDocument() + expect(container.querySelector('[data-part="list"]')).toBeInTheDocument() + expect(container.querySelector('[data-part="item"]')).toBeInTheDocument() + expect(container.querySelector('[data-part="link"]')).toBeInTheDocument() + expect(container.querySelector('[data-part="indicator"]')).toBeInTheDocument() + }) + + it('should set data-active on the link matching defaultActiveIds', () => { + const { container } = render() + expect(container.querySelector('[data-part="link"][data-value="usage"]')).toHaveAttribute('data-active', '') + expect(container.querySelector('[data-part="link"][data-value="introduction"]')).not.toHaveAttribute('data-active') + }) + + it('should support multiple defaultActiveIds', () => { + const { container } = render() + expect(container.querySelector('[data-part="link"][data-value="introduction"]')).toHaveAttribute('data-active', '') + expect(container.querySelector('[data-part="link"][data-value="usage"]')).toHaveAttribute('data-active', '') + expect(container.querySelector('[data-part="link"][data-value="installation"]')).not.toHaveAttribute('data-active') + }) + + it('should reflect controlled activeIds', () => { + const { container } = render() + expect(container.querySelector('[data-part="link"][data-value="installation"]')).toHaveAttribute('data-active', '') + expect(container.querySelector('[data-part="link"][data-value="introduction"]')).not.toHaveAttribute('data-active') + }) + + it('should update active link when controlled activeIds change', () => { + const { container, rerender } = render() + expect(container.querySelector('[data-part="link"][data-value="introduction"]')).toHaveAttribute('data-active', '') + + rerender() + expect(container.querySelector('[data-part="link"][data-value="usage"]')).toHaveAttribute('data-active', '') + expect(container.querySelector('[data-part="link"][data-value="introduction"]')).not.toHaveAttribute('data-active') + }) + + it('should expose activeIds via Context render prop', () => { + render( + + + {(ctx) => {ctx.activeIds.length}} + + + introduction + + + + , + ) + expect(screen.getByTestId('active-count')).toHaveTextContent('1') + }) +}) diff --git a/packages/react/src/components/toc/toc-content.tsx b/packages/react/src/components/toc/toc-content.tsx new file mode 100644 index 0000000000..074267e90c --- /dev/null +++ b/packages/react/src/components/toc/toc-content.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' + +export interface TocContentBaseProps extends PolymorphicProps {} +export interface TocContentProps extends HTMLProps<'article'>, TocContentBaseProps {} + +export const TocContent = forwardRef((props, ref) => { + return +}) + +TocContent.displayName = 'TocContent' diff --git a/packages/react/src/components/toc/toc-context.tsx b/packages/react/src/components/toc/toc-context.tsx new file mode 100644 index 0000000000..a977b1a1de --- /dev/null +++ b/packages/react/src/components/toc/toc-context.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' +import { type UseTocContext, useTocContext } from './use-toc-context' + +export interface TocContextProps { + children: (context: UseTocContext) => ReactNode +} + +export const TocContext = (props: TocContextProps) => props.children(useTocContext()) diff --git a/packages/react/src/components/toc/toc-indicator.tsx b/packages/react/src/components/toc/toc-indicator.tsx new file mode 100644 index 0000000000..8209b48c33 --- /dev/null +++ b/packages/react/src/components/toc/toc-indicator.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocIndicatorBaseProps extends PolymorphicProps {} +export interface TocIndicatorProps extends HTMLProps<'div'>, TocIndicatorBaseProps {} + +export const TocIndicator = forwardRef((props, ref) => { + const toc = useTocContext() + const mergedProps = mergeProps(toc.getIndicatorProps(), props) + + return +}) + +TocIndicator.displayName = 'TocIndicator' diff --git a/packages/react/src/components/toc/toc-item.tsx b/packages/react/src/components/toc/toc-item.tsx new file mode 100644 index 0000000000..4c68ab7952 --- /dev/null +++ b/packages/react/src/components/toc/toc-item.tsx @@ -0,0 +1,26 @@ +import { mergeProps } from '@zag-js/react' +import type { ItemProps } from '@zag-js/toc' +import { forwardRef } from 'react' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' +import { TocItemPropsProvider } from './use-toc-item-props-context' + +export interface TocItemBaseProps extends ItemProps, PolymorphicProps {} +export interface TocItemProps extends HTMLProps<'li'>, TocItemBaseProps {} + +const splitItemProps = createSplitProps() + +export const TocItem = forwardRef((props, ref) => { + const [itemProps, localProps] = splitItemProps(props, ['item']) + const toc = useTocContext() + const mergedProps = mergeProps(toc.getItemProps(itemProps), localProps) + + return ( + + + + ) +}) + +TocItem.displayName = 'TocItem' diff --git a/packages/react/src/components/toc/toc-link.tsx b/packages/react/src/components/toc/toc-link.tsx new file mode 100644 index 0000000000..ca090f8f29 --- /dev/null +++ b/packages/react/src/components/toc/toc-link.tsx @@ -0,0 +1,18 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' +import { useTocItemPropsContext } from './use-toc-item-props-context' + +export interface TocLinkBaseProps extends PolymorphicProps {} +export interface TocLinkProps extends HTMLProps<'a'>, TocLinkBaseProps {} + +export const TocLink = forwardRef((props, ref) => { + const toc = useTocContext() + const itemProps = useTocItemPropsContext() + const mergedProps = mergeProps(toc.getLinkProps(itemProps), props) + + return +}) + +TocLink.displayName = 'TocLink' diff --git a/packages/react/src/components/toc/toc-list.tsx b/packages/react/src/components/toc/toc-list.tsx new file mode 100644 index 0000000000..2cd7dceb5a --- /dev/null +++ b/packages/react/src/components/toc/toc-list.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocListBaseProps extends PolymorphicProps {} +export interface TocListProps extends HTMLProps<'ul'>, TocListBaseProps {} + +export const TocList = forwardRef((props, ref) => { + const toc = useTocContext() + const mergedProps = mergeProps(toc.getListProps(), props) + + return +}) + +TocList.displayName = 'TocList' diff --git a/packages/react/src/components/toc/toc-nav.tsx b/packages/react/src/components/toc/toc-nav.tsx new file mode 100644 index 0000000000..3bb8172829 --- /dev/null +++ b/packages/react/src/components/toc/toc-nav.tsx @@ -0,0 +1,19 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocNavBaseProps extends PolymorphicProps { + placement?: 'left' | 'right' +} +export interface TocNavProps extends HTMLProps<'nav'>, TocNavBaseProps {} + +export const TocNav = forwardRef((props, ref) => { + const { placement, ...rest } = props + const toc = useTocContext() + const mergedProps = mergeProps(toc.getRootProps(), rest) + + return +}) + +TocNav.displayName = 'TocNav' diff --git a/packages/react/src/components/toc/toc-root-provider.tsx b/packages/react/src/components/toc/toc-root-provider.tsx new file mode 100644 index 0000000000..1e99b170d3 --- /dev/null +++ b/packages/react/src/components/toc/toc-root-provider.tsx @@ -0,0 +1,27 @@ +import { forwardRef } from 'react' +import type { Assign } from '../../types' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import type { UseTocReturn } from './use-toc' +import { TocProvider } from './use-toc-context' + +interface RootProviderProps { + value: UseTocReturn +} + +export interface TocRootProviderBaseProps extends RootProviderProps, PolymorphicProps {} +export interface TocRootProviderProps extends Assign, TocRootProviderBaseProps> {} + +const splitRootProviderProps = createSplitProps() + +export const TocRootProvider = forwardRef((props, ref) => { + const [{ value: toc }, localProps] = splitRootProviderProps(props, ['value']) + + return ( + + + + ) +}) + +TocRootProvider.displayName = 'TocRootProvider' diff --git a/packages/react/src/components/toc/toc-root.tsx b/packages/react/src/components/toc/toc-root.tsx new file mode 100644 index 0000000000..4d640c10bd --- /dev/null +++ b/packages/react/src/components/toc/toc-root.tsx @@ -0,0 +1,36 @@ +import { forwardRef } from 'react' +import type { Assign } from '../../types' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { type UseTocProps, useToc } from './use-toc' +import { TocProvider } from './use-toc-context' + +export interface TocRootBaseProps extends UseTocProps, PolymorphicProps {} +export interface TocRootProps extends Assign, TocRootBaseProps> {} + +const splitRootProps = createSplitProps() + +export const TocRoot = forwardRef((props, ref) => { + const [useTocProps, localProps] = splitRootProps(props, [ + 'activeIds', + 'autoScroll', + 'defaultActiveIds', + 'getScrollEl', + 'id', + 'ids', + 'items', + 'onActiveChange', + 'rootMargin', + 'scrollBehavior', + 'threshold', + ]) + const toc = useToc(useTocProps) + + return ( + + + + ) +}) + +TocRoot.displayName = 'TocRoot' diff --git a/packages/react/src/components/toc/toc-title.tsx b/packages/react/src/components/toc/toc-title.tsx new file mode 100644 index 0000000000..c092cd67c4 --- /dev/null +++ b/packages/react/src/components/toc/toc-title.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocTitleBaseProps extends PolymorphicProps {} +export interface TocTitleProps extends HTMLProps<'h2'>, TocTitleBaseProps {} + +export const TocTitle = forwardRef((props, ref) => { + const toc = useTocContext() + const mergedProps = mergeProps(toc.getTitleProps(), props) + + return +}) + +TocTitle.displayName = 'TocTitle' diff --git a/packages/react/src/components/toc/toc.anatomy.ts b/packages/react/src/components/toc/toc.anatomy.ts new file mode 100644 index 0000000000..560bfdc21b --- /dev/null +++ b/packages/react/src/components/toc/toc.anatomy.ts @@ -0,0 +1 @@ +export { anatomy as tocAnatomy } from '@zag-js/toc' diff --git a/packages/react/src/components/toc/toc.stories.tsx b/packages/react/src/components/toc/toc.stories.tsx new file mode 100644 index 0000000000..56f65d0e01 --- /dev/null +++ b/packages/react/src/components/toc/toc.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta } from '@storybook/react-vite' + +const meta: Meta = { + title: 'Components / Toc', +} + +export default meta + +export { Basic } from './examples/basic' +export { Context } from './examples/context' +export { Controlled } from './examples/controlled' +export { Nested } from './examples/nested' +export { ProgressiveReveal } from './examples/progressive-reveal' +export { RootProvider } from './examples/root-provider' +export { WithIndicator } from './examples/with-indicator' +export { WithScrollArea } from './examples/with-scroll-area' +export { CustomRendering } from './examples/custom-rendering' +export { CustomScrollBehavior } from './examples/custom-scroll-behavior' +export { HighlightOnEnter } from './examples/highlight-on-enter' +export { ReadingProgress } from './examples/reading-progress' +export { Grouped } from './examples/grouped' +export { Floating } from './examples/floating' +export { Rtl } from './examples/rtl' +export { Theming } from './examples/theming' +export { Placement } from './examples/placement' diff --git a/packages/react/src/components/toc/toc.ts b/packages/react/src/components/toc/toc.ts new file mode 100644 index 0000000000..c5813b27fc --- /dev/null +++ b/packages/react/src/components/toc/toc.ts @@ -0,0 +1,27 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { + TocContent as Content, + type TocContentBaseProps as ContentBaseProps, + type TocContentProps as ContentProps, +} from './toc-content' +export { TocContext as Context, type TocContextProps as ContextProps } from './toc-context' +export { + TocIndicator as Indicator, + type TocIndicatorBaseProps as IndicatorBaseProps, + type TocIndicatorProps as IndicatorProps, +} from './toc-indicator' +export { TocItem as Item, type TocItemBaseProps as ItemBaseProps, type TocItemProps as ItemProps } from './toc-item' +export { TocLink as Link, type TocLinkBaseProps as LinkBaseProps, type TocLinkProps as LinkProps } from './toc-link' +export { TocList as List, type TocListBaseProps as ListBaseProps, type TocListProps as ListProps } from './toc-list' +export { TocNav as Nav, type TocNavBaseProps as NavBaseProps, type TocNavProps as NavProps } from './toc-nav' +export { TocRoot as Root, type TocRootBaseProps as RootBaseProps, type TocRootProps as RootProps } from './toc-root' +export { + TocRootProvider as RootProvider, + type TocRootProviderBaseProps as RootProviderBaseProps, + type TocRootProviderProps as RootProviderProps, +} from './toc-root-provider' +export { + TocTitle as Title, + type TocTitleBaseProps as TitleBaseProps, + type TocTitleProps as TitleProps, +} from './toc-title' diff --git a/packages/react/src/components/toc/use-toc-context.ts b/packages/react/src/components/toc/use-toc-context.ts new file mode 100644 index 0000000000..b59c7764b5 --- /dev/null +++ b/packages/react/src/components/toc/use-toc-context.ts @@ -0,0 +1,10 @@ +import { createContext } from '../../utils/create-context' +import type { UseTocReturn } from './use-toc' + +export interface UseTocContext extends UseTocReturn {} + +export const [TocProvider, useTocContext] = createContext({ + name: 'TocContext', + hookName: 'useTocContext', + providerName: '', +}) diff --git a/packages/react/src/components/toc/use-toc-item-props-context.ts b/packages/react/src/components/toc/use-toc-item-props-context.ts new file mode 100644 index 0000000000..4b63341aa2 --- /dev/null +++ b/packages/react/src/components/toc/use-toc-item-props-context.ts @@ -0,0 +1,10 @@ +import type { ItemProps } from '@zag-js/toc' +import { createContext } from '../../utils/create-context' + +export interface UseTocItemPropsContext extends ItemProps {} + +export const [TocItemPropsProvider, useTocItemPropsContext] = createContext({ + name: 'TocItemPropsContext', + hookName: 'useTocItemPropsContext', + providerName: '', +}) diff --git a/packages/react/src/components/toc/use-toc.ts b/packages/react/src/components/toc/use-toc.ts new file mode 100644 index 0000000000..9a0e96ac61 --- /dev/null +++ b/packages/react/src/components/toc/use-toc.ts @@ -0,0 +1,24 @@ +import * as toc from '@zag-js/toc' +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/react' +import { useId } from 'react' +import { useEnvironmentContext, useLocaleContext } from '../../providers' +import type { Optional } from '../../types' + +export interface UseTocProps extends Optional, 'id'> {} +export interface UseTocReturn extends toc.Api {} + +export const useToc = (props: UseTocProps): UseTocReturn => { + const id = useId() + const { getRootNode } = useEnvironmentContext() + const { dir } = useLocaleContext() + + const context: toc.Props = { + id, + dir, + getRootNode, + ...props, + } + + const service = useMachine(toc.machine, context) + return toc.connect(service, normalizeProps) +} diff --git a/packages/solid/package.json b/packages/solid/package.json index 3ea1ca264c..db13f92334 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -110,9 +110,9 @@ "@zag-js/editable": "1.39.1", "@zag-js/file-upload": "1.39.1", "@zag-js/file-utils": "1.39.1", - "@zag-js/focus-visible": "1.39.1", "@zag-js/floating-panel": "1.39.1", "@zag-js/focus-trap": "1.39.1", + "@zag-js/focus-visible": "1.39.1", "@zag-js/highlight-word": "1.39.1", "@zag-js/hover-card": "1.39.1", "@zag-js/i18n-utils": "1.39.1", @@ -144,6 +144,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ec4e463132..09ff99538d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -167,9 +167,9 @@ "@zag-js/editable": "1.39.1", "@zag-js/file-upload": "1.39.1", "@zag-js/file-utils": "1.39.1", - "@zag-js/focus-visible": "1.39.1", "@zag-js/floating-panel": "1.39.1", "@zag-js/focus-trap": "1.39.1", + "@zag-js/focus-visible": "1.39.1", "@zag-js/highlight-word": "1.39.1", "@zag-js/hover-card": "1.39.1", "@zag-js/i18n-utils": "1.39.1", @@ -201,6 +201,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", diff --git a/packages/vue/package.json b/packages/vue/package.json index f84837fbdd..eedaf16555 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -110,9 +110,9 @@ "@zag-js/editable": "1.39.1", "@zag-js/file-upload": "1.39.1", "@zag-js/file-utils": "1.39.1", - "@zag-js/focus-visible": "1.39.1", "@zag-js/floating-panel": "1.39.1", "@zag-js/focus-trap": "1.39.1", + "@zag-js/focus-visible": "1.39.1", "@zag-js/highlight-word": "1.39.1", "@zag-js/hover-card": "1.39.1", "@zag-js/i18n-utils": "1.39.1", @@ -143,6 +143,7 @@ "@zag-js/tags-input": "1.39.1", "@zag-js/timer": "1.39.1", "@zag-js/toast": "1.39.1", + "@zag-js/toc": "1.39.1", "@zag-js/toggle": "1.39.1", "@zag-js/toggle-group": "1.39.1", "@zag-js/tooltip": "1.39.1", From a0a66af25022ddf481c497e900062cf33de7ba3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Mon, 13 Apr 2026 13:17:00 +0200 Subject: [PATCH 2/7] fix: toc components --- .../toc/examples/with-collapsible.tsx | 108 +++++++++++++ .../components/toc/examples/with-hover.tsx | 84 ++++++++++ .../components/toc/examples/with-numbers.tsx | 39 +++++ .../toc/examples/with-tree-view.tsx | 145 +++++++++++++++++ .../src/components/toc/examples/basic.tsx | 54 +++++++ .../toc/examples/custom-scroll-behavior.tsx | 44 ++++++ .../src/components/toc/examples/grouped.tsx | 63 ++++++++ .../src/components/toc/examples/nested.tsx | 60 +++++++ .../components/toc/examples/root-provider.tsx | 49 ++++++ .../toc/examples/with-collapsible.tsx | 77 +++++++++ .../components/toc/examples/with-hover.tsx | 52 ++++++ .../toc/examples/with-indicator.tsx | 60 +++++++ .../components/toc/examples/with-numbers.tsx | 38 +++++ .../toc/examples/with-tree-view.tsx | 68 ++++++++ packages/solid/src/components/toc/index.tsx | 16 ++ .../solid/src/components/toc/toc-content.tsx | 8 + .../solid/src/components/toc/toc-context.tsx | 8 + .../src/components/toc/toc-indicator.tsx | 12 ++ .../solid/src/components/toc/toc-item.tsx | 22 +++ .../solid/src/components/toc/toc-link.tsx | 14 ++ .../solid/src/components/toc/toc-list.tsx | 13 ++ packages/solid/src/components/toc/toc-nav.tsx | 15 ++ .../src/components/toc/toc-root-provider.tsx | 24 +++ .../solid/src/components/toc/toc-root.tsx | 33 ++++ .../solid/src/components/toc/toc-title.tsx | 13 ++ .../solid/src/components/toc/toc.anatomy.ts | 1 + .../src/components/toc/toc.stories copy.tsx | 17 ++ .../solid/src/components/toc/toc.stories.tsx | 28 ++++ packages/solid/src/components/toc/toc.ts | 27 ++++ .../src/components/toc/use-toc-context.ts | 9 ++ .../toc/use-toc-item-props-context.ts | 10 ++ packages/solid/src/components/toc/use-toc.ts | 25 +++ .../lib/components/toc/examples/basic.svelte | 44 ++++++ .../components/toc/examples/grouped.svelte | 63 ++++++++ .../lib/components/toc/examples/nested.svelte | 41 +++++ .../toc/examples/root-provider.svelte | 37 +++++ .../toc/examples/with-collapsible.svelte | 96 +++++++++++ .../components/toc/examples/with-hover.svelte | 79 ++++++++++ .../toc/examples/with-indicator.svelte | 38 +++++ .../toc/examples/with-numbers.svelte | 39 +++++ .../toc/examples/with-tree-view.svelte | 149 ++++++++++++++++++ .../svelte/src/lib/components/toc/index.ts | 19 +++ .../src/lib/components/toc/toc-content.svelte | 14 ++ .../src/lib/components/toc/toc-context.svelte | 18 +++ .../lib/components/toc/toc-indicator.svelte | 18 +++ .../src/lib/components/toc/toc-item.svelte | 24 +++ .../src/lib/components/toc/toc-link.svelte | 20 +++ .../src/lib/components/toc/toc-list.svelte | 18 +++ .../src/lib/components/toc/toc-nav.svelte | 54 +++++++ .../components/toc/toc-root-provider.svelte | 21 +++ .../src/lib/components/toc/toc-root.svelte | 40 +++++ .../src/lib/components/toc/toc-title.svelte | 18 +++ .../src/lib/components/toc/toc.anatomy.ts | 1 + .../src/lib/components/toc/toc.stories.ts | 70 ++++++++ packages/svelte/src/lib/components/toc/toc.ts | 43 +++++ .../src/lib/components/toc/use-toc-context.ts | 9 ++ .../toc/use-toc-item-props-context.ts | 8 + .../src/lib/components/toc/use-toc.svelte.ts | 27 ++++ .../vue/src/components/toc/examples/basic.vue | 38 +++++ .../src/components/toc/examples/grouped.vue | 58 +++++++ .../src/components/toc/examples/nested.vue | 45 ++++++ .../components/toc/examples/root-provider.vue | 37 +++++ .../components/toc/examples/toc-tree-node.vue | 58 +++++++ .../toc/examples/with-collapsible.vue | 117 ++++++++++++++ .../components/toc/examples/with-hover.vue | 83 ++++++++++ .../toc/examples/with-indicator.vue | 34 ++++ .../components/toc/examples/with-numbers.vue | 35 ++++ .../toc/examples/with-tree-view.vue | 94 +++++++++++ packages/vue/src/components/toc/index.ts | 20 +++ .../vue/src/components/toc/toc-content.vue | 28 ++++ .../vue/src/components/toc/toc-context.vue | 22 +++ .../vue/src/components/toc/toc-indicator.vue | 30 ++++ packages/vue/src/components/toc/toc-item.vue | 36 +++++ packages/vue/src/components/toc/toc-link.vue | 32 ++++ packages/vue/src/components/toc/toc-list.vue | 30 ++++ packages/vue/src/components/toc/toc-nav.vue | 32 ++++ .../src/components/toc/toc-root-provider.vue | 37 +++++ packages/vue/src/components/toc/toc-root.vue | 36 +++++ packages/vue/src/components/toc/toc-title.vue | 30 ++++ .../vue/src/components/toc/toc.anatomy.ts | 1 + .../vue/src/components/toc/toc.stories.ts | 80 ++++++++++ packages/vue/src/components/toc/toc.ts | 32 ++++ packages/vue/src/components/toc/toc.types.ts | 65 ++++++++ .../vue/src/components/toc/use-toc-context.ts | 6 + .../toc/use-toc-item-props-context.ts | 6 + packages/vue/src/components/toc/use-toc.ts | 35 ++++ 86 files changed, 3401 insertions(+) create mode 100644 packages/react/src/components/toc/examples/with-collapsible.tsx create mode 100644 packages/react/src/components/toc/examples/with-hover.tsx create mode 100644 packages/react/src/components/toc/examples/with-numbers.tsx create mode 100644 packages/react/src/components/toc/examples/with-tree-view.tsx create mode 100644 packages/solid/src/components/toc/examples/basic.tsx create mode 100644 packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx create mode 100644 packages/solid/src/components/toc/examples/grouped.tsx create mode 100644 packages/solid/src/components/toc/examples/nested.tsx create mode 100644 packages/solid/src/components/toc/examples/root-provider.tsx create mode 100644 packages/solid/src/components/toc/examples/with-collapsible.tsx create mode 100644 packages/solid/src/components/toc/examples/with-hover.tsx create mode 100644 packages/solid/src/components/toc/examples/with-indicator.tsx create mode 100644 packages/solid/src/components/toc/examples/with-numbers.tsx create mode 100644 packages/solid/src/components/toc/examples/with-tree-view.tsx create mode 100644 packages/solid/src/components/toc/index.tsx create mode 100644 packages/solid/src/components/toc/toc-content.tsx create mode 100644 packages/solid/src/components/toc/toc-context.tsx create mode 100644 packages/solid/src/components/toc/toc-indicator.tsx create mode 100644 packages/solid/src/components/toc/toc-item.tsx create mode 100644 packages/solid/src/components/toc/toc-link.tsx create mode 100644 packages/solid/src/components/toc/toc-list.tsx create mode 100644 packages/solid/src/components/toc/toc-nav.tsx create mode 100644 packages/solid/src/components/toc/toc-root-provider.tsx create mode 100644 packages/solid/src/components/toc/toc-root.tsx create mode 100644 packages/solid/src/components/toc/toc-title.tsx create mode 100644 packages/solid/src/components/toc/toc.anatomy.ts create mode 100644 packages/solid/src/components/toc/toc.stories copy.tsx create mode 100644 packages/solid/src/components/toc/toc.stories.tsx create mode 100644 packages/solid/src/components/toc/toc.ts create mode 100644 packages/solid/src/components/toc/use-toc-context.ts create mode 100644 packages/solid/src/components/toc/use-toc-item-props-context.ts create mode 100644 packages/solid/src/components/toc/use-toc.ts create mode 100644 packages/svelte/src/lib/components/toc/examples/basic.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/grouped.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/nested.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/root-provider.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/with-collapsible.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/with-hover.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/with-indicator.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/with-numbers.svelte create mode 100644 packages/svelte/src/lib/components/toc/examples/with-tree-view.svelte create mode 100644 packages/svelte/src/lib/components/toc/index.ts create mode 100644 packages/svelte/src/lib/components/toc/toc-content.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-context.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-indicator.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-item.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-link.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-list.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-nav.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-root-provider.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-root.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc-title.svelte create mode 100644 packages/svelte/src/lib/components/toc/toc.anatomy.ts create mode 100644 packages/svelte/src/lib/components/toc/toc.stories.ts create mode 100644 packages/svelte/src/lib/components/toc/toc.ts create mode 100644 packages/svelte/src/lib/components/toc/use-toc-context.ts create mode 100644 packages/svelte/src/lib/components/toc/use-toc-item-props-context.ts create mode 100644 packages/svelte/src/lib/components/toc/use-toc.svelte.ts create mode 100644 packages/vue/src/components/toc/examples/basic.vue create mode 100644 packages/vue/src/components/toc/examples/grouped.vue create mode 100644 packages/vue/src/components/toc/examples/nested.vue create mode 100644 packages/vue/src/components/toc/examples/root-provider.vue create mode 100644 packages/vue/src/components/toc/examples/toc-tree-node.vue create mode 100644 packages/vue/src/components/toc/examples/with-collapsible.vue create mode 100644 packages/vue/src/components/toc/examples/with-hover.vue create mode 100644 packages/vue/src/components/toc/examples/with-indicator.vue create mode 100644 packages/vue/src/components/toc/examples/with-numbers.vue create mode 100644 packages/vue/src/components/toc/examples/with-tree-view.vue create mode 100644 packages/vue/src/components/toc/index.ts create mode 100644 packages/vue/src/components/toc/toc-content.vue create mode 100644 packages/vue/src/components/toc/toc-context.vue create mode 100644 packages/vue/src/components/toc/toc-indicator.vue create mode 100644 packages/vue/src/components/toc/toc-item.vue create mode 100644 packages/vue/src/components/toc/toc-link.vue create mode 100644 packages/vue/src/components/toc/toc-list.vue create mode 100644 packages/vue/src/components/toc/toc-nav.vue create mode 100644 packages/vue/src/components/toc/toc-root-provider.vue create mode 100644 packages/vue/src/components/toc/toc-root.vue create mode 100644 packages/vue/src/components/toc/toc-title.vue create mode 100644 packages/vue/src/components/toc/toc.anatomy.ts create mode 100644 packages/vue/src/components/toc/toc.stories.ts create mode 100644 packages/vue/src/components/toc/toc.ts create mode 100644 packages/vue/src/components/toc/toc.types.ts create mode 100644 packages/vue/src/components/toc/use-toc-context.ts create mode 100644 packages/vue/src/components/toc/use-toc-item-props-context.ts create mode 100644 packages/vue/src/components/toc/use-toc.ts diff --git a/packages/react/src/components/toc/examples/with-collapsible.tsx b/packages/react/src/components/toc/examples/with-collapsible.tsx new file mode 100644 index 0000000000..6a213bafc1 --- /dev/null +++ b/packages/react/src/components/toc/examples/with-collapsible.tsx @@ -0,0 +1,108 @@ +import { Collapsible } from '@ark-ui/react/collapsible' +import { Toc } from '@ark-ui/react/toc' +import { loremIpsum } from 'lorem-ipsum' +import { ChevronRightIcon } from 'lucide-react' +import CollapsibleStyles from 'styles/Collapsible.module.css' +import styles from 'styles/toc.module.css' + +const p = loremIpsum({ count: 7, units: 'paragraphs' }) + +const items = [ + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'examples', depth: 2, label: 'Examples' }, +] + +const RADIUS = 14 +const CIRCUMFERENCE = 2 * Math.PI * RADIUS + +export const WithCollapsible = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{p}

+
+ ))} +
+ + + + + {({ activeItems }) => { + const activeIndex = activeItems[0] ? items.findIndex((i) => i.value === activeItems[0].value) : -1 + const activeLabel = activeIndex >= 0 ? items[activeIndex].label : undefined + const progress = activeIndex >= 0 ? (activeIndex + 1) / items.length : 0 + const dashArray = `${progress * CIRCUMFERENCE} ${CIRCUMFERENCE}` + + return ( + + + + + + + {activeLabel ?? 'On this page'} + + + + + + + ) + }} + + + + {items.map((item, index) => ( + + + {String(index + 1).padStart(2, '0')} + {item.label} + + + ))} + + + + +
+) diff --git a/packages/react/src/components/toc/examples/with-hover.tsx b/packages/react/src/components/toc/examples/with-hover.tsx new file mode 100644 index 0000000000..d14c7832c0 --- /dev/null +++ b/packages/react/src/components/toc/examples/with-hover.tsx @@ -0,0 +1,84 @@ +import { Toc } from '@ark-ui/react/toc' +import { Swap } from '@ark-ui/react/swap' +import { Pin, PinOff } from 'lucide-react' +import { useState } from 'react' +import { loremIpsum } from 'lorem-ipsum' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api', depth: 2, label: 'API' }, + { value: 'examples', depth: 2, label: 'Examples' }, +] + +const paragraphs = loremIpsum({ count: 7, units: 'paragraphs' }) + +export const WithHover = () => { + const [pinned, setPinned] = useState(false) + const [hovered, setHovered] = useState(false) + + return ( + +
+ {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + + +
+ {items.map((item) => ( + + ))} +
+
+ +
+ + {items.map((item) => ( + + + {item.label} + + + ))} + +
+
+
+
+
+ ) +} diff --git a/packages/react/src/components/toc/examples/with-numbers.tsx b/packages/react/src/components/toc/examples/with-numbers.tsx new file mode 100644 index 0000000000..e4ee5b3c9a --- /dev/null +++ b/packages/react/src/components/toc/examples/with-numbers.tsx @@ -0,0 +1,39 @@ +import { Toc } from '@ark-ui/react/toc' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const items = [ + { value: 'executive-summary', depth: 2, label: 'Executive Summary' }, + { value: 'methodology', depth: 2, label: 'Methodology' }, + { value: 'findings', depth: 2, label: 'Findings' }, + { value: 'recommendations', depth: 2, label: 'Recommendations' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, +] + +const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) + +export const WithNumbers = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ + Contents + + {items.map((item, index) => ( + + + {String(index + 1).padStart(2, '0')} + {item.label} + + + ))} + + +
+) diff --git a/packages/react/src/components/toc/examples/with-tree-view.tsx b/packages/react/src/components/toc/examples/with-tree-view.tsx new file mode 100644 index 0000000000..fefac3bf41 --- /dev/null +++ b/packages/react/src/components/toc/examples/with-tree-view.tsx @@ -0,0 +1,145 @@ +import { Toc, useTocContext } from '@ark-ui/react/toc' +import { TreeView, createTreeCollection } from '@ark-ui/react/tree-view' +import { loremIpsum } from 'lorem-ipsum' +import { ChevronRightIcon } from 'lucide-react' +import { useState } from 'react' +import tocStyles from 'styles/toc.module.css' +import treeStyles from 'styles/tree-view.module.css' + +const p = loremIpsum({ count: 7, units: 'paragraphs' }) + +type TocNode = { + id: string + name: string + depth: number + children?: TocNode[] +} + +const sections: TocNode[] = [ + { + id: 'guides', + name: 'Guides', + depth: 2, + children: [ + { id: 'quick-start', name: 'Quick Start', depth: 3 }, + { id: 'manual-setup', name: 'Manual Setup', depth: 3 }, + ], + }, + { + id: 'core-concepts', + name: 'Core Concepts', + depth: 2, + children: [ + { id: 'props', name: 'Props', depth: 3 }, + { id: 'events', name: 'Events', depth: 3 }, + { id: 'context', name: 'Context', depth: 3 }, + ], + }, + { + id: 'advanced', + name: 'Advanced', + depth: 2, + children: [ + { id: 'root-provider', name: 'Root Provider', depth: 3 }, + { id: 'custom-rendering', name: 'Custom Rendering', depth: 3 }, + ], + }, +] + +const collection = createTreeCollection({ + nodeToValue: (node) => node.id, + nodeToString: (node) => node.name, + rootNode: { id: 'ROOT', name: '', depth: 0, children: sections }, +}) + +const allItems = sections.flatMap((section) => [ + { value: section.id, depth: section.depth }, + ...(section.children ?? []).map((child) => ({ value: child.id, depth: child.depth })), +]) + +const TocTreeNode = ({ node, indexPath }: TreeView.NodeProviderProps) => { + const toc = useTocContext() + return ( + + {node.children ? ( + + + + + + + + {node.name} + + + + + + {node.children.map((child, index) => ( + + ))} + + + ) : ( + + + + {node.name} + + + + )} + + ) +} + +export const WithTreeView = () => { + const [expandedValue, setExpandedValue] = useState([]) + + return ( + { + const activeIds = new Set(activeItems.map((i) => i.value)) + const next = sections + .filter( + (section) => activeIds.has(section.id) || (section.children ?? []).some((child) => activeIds.has(child.id)), + ) + .map((s) => s.id) + setExpandedValue(next) + }} + > + + {sections.map((section) => ( +
+

{section.name}

+

{p}

+ {section.children?.map((child) => ( +
+

{child.name}

+

{p}

+
+ ))} +
+ ))} +
+ + + On this page + setExpandedValue(next)} + > + + {sections.map((node, index) => ( + + ))} + + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/basic.tsx b/packages/solid/src/components/toc/examples/basic.tsx new file mode 100644 index 0000000000..eab9fff295 --- /dev/null +++ b/packages/solid/src/components/toc/examples/basic.tsx @@ -0,0 +1,54 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2 }, + { value: 'getting-started', depth: 2 }, + { value: 'installation', depth: 2 }, + { value: 'usage', depth: 2 }, + { value: 'api-reference', depth: 2 }, +] + +export default function Basic() { + return ( + + +

Introduction

+

+ A table of contents helps readers navigate long documents by providing quick links to each section. It + automatically highlights the section currently visible in the viewport. +

+

Getting Started

+

+ To get started, pass an array of items to the root component. Each item has a value matching the heading id + and a depth matching the heading level. +

+

Installation

+

+ Install the package using your preferred package manager. The component has no external dependencies beyond + the state machine. +

+

Usage

+

+ Import the component and compose it using the compound component pattern. The Root component manages the + IntersectionObserver internally. +

+

API Reference

+

+ The full API reference documents all props, events, and methods available on each component part. Refer to it + when customizing behavior. +

+
+ + On this page + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx b/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx new file mode 100644 index 0000000000..84e27b320d --- /dev/null +++ b/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx @@ -0,0 +1,44 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'alpha', depth: 2 }, + { value: 'beta', depth: 2 }, + { value: 'gamma', depth: 2 }, +] + +export default function CustomScrollBehavior() { + const scrollToId = (e: MouseEvent, id: string) => { + e.preventDefault() + const el = document.getElementById(id) + if (el) { + const y = el.getBoundingClientRect().top + window.scrollY - 64 + window.scrollTo({ top: y, behavior: 'smooth' }) + } + } + + return ( + + +

Alpha

+

Section Alpha content.

+

Beta

+

Section Beta content.

+

Gamma

+

Section Gamma content.

+
+ + On this page + + {items.map((item) => ( + + + {item.value.charAt(0).toUpperCase() + item.value.slice(1)} + + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/grouped.tsx b/packages/solid/src/components/toc/examples/grouped.tsx new file mode 100644 index 0000000000..defb28cad7 --- /dev/null +++ b/packages/solid/src/components/toc/examples/grouped.tsx @@ -0,0 +1,63 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const groups = [ + { + label: 'Getting Started', + items: [ + { value: 'overview', depth: 2 }, + { value: 'installation', depth: 2 }, + ], + }, + { + label: 'Advanced', + items: [ + { value: 'configuration', depth: 2 }, + { value: 'plugins', depth: 2 }, + ], + }, + { + label: 'Reference', + items: [ + { value: 'api', depth: 2 }, + { value: 'changelog', depth: 2 }, + ], + }, +] + +const allItems = groups.flatMap((g) => g.items) + +export default function Grouped() { + return ( + + +

Overview

+

Ark UI is a headless component library for building scalable design systems.

+

Installation

+

Install with your preferred package manager. No configuration needed to get started.

+

Configuration

+

Customize behavior through props. Most defaults work well out of the box.

+

Plugins

+

Extend functionality with plugins. Write your own or use community-contributed ones.

+

API

+

The full API reference covers all props, events, and methods on each component part.

+

Changelog

+

All notable changes are documented here following semantic versioning conventions.

+
+ + {groups.map((group) => ( +
+

{group.label}

+ + {group.items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + +
+ ))} +
+
+ ) +} diff --git a/packages/solid/src/components/toc/examples/nested.tsx b/packages/solid/src/components/toc/examples/nested.tsx new file mode 100644 index 0000000000..17dff9a3f2 --- /dev/null +++ b/packages/solid/src/components/toc/examples/nested.tsx @@ -0,0 +1,60 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'guides', depth: 2 }, + { value: 'quick-start', depth: 3 }, + { value: 'manual-setup', depth: 3 }, + { value: 'core-concepts', depth: 2 }, + { value: 'props', depth: 3 }, + { value: 'events', depth: 3 }, + { value: 'context', depth: 3 }, + { value: 'advanced', depth: 2 }, + { value: 'root-provider', depth: 3 }, + { value: 'custom-rendering', depth: 3 }, +] + +export default function Nested() { + return ( + + +

Guides

+

Step-by-step guides for getting the most out of Ark UI in your projects.

+

Quick Start

+

Install the package and render your first component in under two minutes. No configuration required.

+

Manual Setup

+

For projects that need fine-grained control over the setup, follow the manual configuration guide.

+

Core Concepts

+

Understanding the core concepts helps you use the library more effectively.

+

Props

+

Props control the machine context. Pass them directly to the Root component or via the hook.

+

Events

+

Callback props like onValueChange fire when the machine transitions to a new state.

+

Context

+

Every component exposes a Context render prop for accessing the machine API without an extra hook.

+

Advanced

+

Advanced patterns for complex use cases.

+

Root Provider

+

+ Use the RootProvider pattern to call the hook at a higher level and share the API with components outside the + tree. +

+

Custom Rendering

+

+ Use the asChild prop to render any component part as a custom element while keeping all aria and event + attributes. +

+
+ + On this page + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/root-provider.tsx b/packages/solid/src/components/toc/examples/root-provider.tsx new file mode 100644 index 0000000000..9e1b9a1836 --- /dev/null +++ b/packages/solid/src/components/toc/examples/root-provider.tsx @@ -0,0 +1,49 @@ +import { Toc, useToc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'principles', depth: 2 }, + { value: 'patterns', depth: 2 }, + { value: 'testing', depth: 2 }, + { value: 'performance', depth: 2 }, +] + +export default function RootProvider() { + const toc = useToc({ items }) + return ( + + +

Principles

+

+ Good software follows a set of guiding principles. These inform every decision from API design to + implementation details. +

+

Patterns

+

+ Design patterns are reusable solutions to common problems. Learning them helps you recognize familiar + structures in new codebases. +

+

Testing

+

+ Tests give you confidence that your code works as intended. A good test suite catches regressions before they + reach production. +

+

Performance

+

+ Performance is a feature. Measure before optimizing, and optimize the things that matter most to your users. +

+
+ + On this page + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-collapsible.tsx b/packages/solid/src/components/toc/examples/with-collapsible.tsx new file mode 100644 index 0000000000..6d7b2cd2c2 --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-collapsible.tsx @@ -0,0 +1,77 @@ +import { Toc } from '@ark-ui/solid/toc' +import { createSignal } from 'solid-js' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'examples', depth: 2, label: 'Examples' }, +] + +export default function WithCollapsible() { + const [open, setOpen] = createSignal(false) + const [progress, setProgress] = createSignal(0) + + // Simulate progress for demo + // In a real app, calculate based on scroll or active section + const handleToggle = () => { + setOpen((prev) => !prev) + setProgress((prev) => (prev >= 1 ? 0 : prev + 0.2)) + } + + const RADIUS = 14 + const CIRCUMFERENCE = 2 * Math.PI * RADIUS + const dashArray = () => `${progress() * CIRCUMFERENCE} ${CIRCUMFERENCE}` + + return ( + + + {items.map((item) => ( +
+

{item.label}

+

Section content for {item.label}.

+
+ ))} +
+ + + {open() && ( + + {items.map((item) => ( + + {item.label} + + ))} + + )} + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-hover.tsx b/packages/solid/src/components/toc/examples/with-hover.tsx new file mode 100644 index 0000000000..171b8b16ce --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-hover.tsx @@ -0,0 +1,52 @@ +import { Toc } from '@ark-ui/solid/toc' +import { createSignal } from 'solid-js' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api', depth: 2, label: 'API' }, + { value: 'examples', depth: 2, label: 'Examples' }, +] + +export default function WithHover() { + const [pinned, setPinned] = createSignal(false) + const [hovered, setHovered] = createSignal(false) + + return ( + + + {items.map((item) => ( +
+

{item.label}

+

Section content for {item.label}.

+
+ ))} +
+ setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + {items.map((item) => ( + + {item.label} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-indicator.tsx b/packages/solid/src/components/toc/examples/with-indicator.tsx new file mode 100644 index 0000000000..69800af8f4 --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-indicator.tsx @@ -0,0 +1,60 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'overview', depth: 2 }, + { value: 'architecture', depth: 2 }, + { value: 'state-machines', depth: 2 }, + { value: 'components', depth: 2 }, + { value: 'theming', depth: 2 }, + { value: 'accessibility', depth: 2 }, +] + +export default function WithIndicator() { + return ( + + +

Overview

+

+ Ark UI is a headless component library built on top of Zag.js state machines. It provides unstyled, accessible + components ready for your design system. +

+

Architecture

+

+ The library follows a layered architecture. At the base are the Zag.js machines, which handle all interaction + logic. On top of that, framework adapters expose idiomatic React, Solid, Vue, and Svelte APIs. +

+

State Machines

+

+ State machines make the interaction logic predictable and testable. Every possible state is explicitly + defined, making it impossible to end up in an undefined state. +

+

Components

+

+ Components are structured using the compound component pattern. A Root component holds state and provides it + to all child parts via context. +

+

Theming

+

+ Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any + styling solution that works in your stack. +

+

Accessibility

+

+ All components follow WAI-ARIA patterns and are tested with screen readers. Keyboard navigation is built into + the state machine, not bolted on after the fact. +

+
+ + + + {items.map((item) => ( + + {item.value.replace(/-/g, ' ')} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-numbers.tsx b/packages/solid/src/components/toc/examples/with-numbers.tsx new file mode 100644 index 0000000000..a6c49f99a2 --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-numbers.tsx @@ -0,0 +1,38 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const items = [ + { value: 'executive-summary', depth: 2, label: 'Executive Summary' }, + { value: 'methodology', depth: 2, label: 'Methodology' }, + { value: 'findings', depth: 2, label: 'Findings' }, + { value: 'recommendations', depth: 2, label: 'Recommendations' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, +] + +export default function WithNumbers() { + return ( + + + {items.map((item) => ( +
+

{item.label}

+

Section content for {item.label}.

+
+ ))} +
+ + Contents + + {items.map((item, index) => ( + + + {String(index + 1).padStart(2, '0')} + {item.label} + + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-tree-view.tsx b/packages/solid/src/components/toc/examples/with-tree-view.tsx new file mode 100644 index 0000000000..aa25802fcc --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-tree-view.tsx @@ -0,0 +1,68 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' + +const sections = [ + { + id: 'guides', + name: 'Guides', + depth: 2, + children: [ + { id: 'quick-start', name: 'Quick Start', depth: 3 }, + { id: 'manual-setup', name: 'Manual Setup', depth: 3 }, + ], + }, + { + id: 'core-concepts', + name: 'Core Concepts', + depth: 2, + children: [ + { id: 'props', name: 'Props', depth: 3 }, + { id: 'events', name: 'Events', depth: 3 }, + { id: 'context', name: 'Context', depth: 3 }, + ], + }, + { + id: 'advanced', + name: 'Advanced', + depth: 2, + children: [ + { id: 'root-provider', name: 'Root Provider', depth: 3 }, + { id: 'custom-rendering', name: 'Custom Rendering', depth: 3 }, + ], + }, +] + +const flattenSections = (nodes) => + nodes.flatMap((node) => [node, ...(node.children ? flattenSections(node.children) : [])]) + +const items = flattenSections(sections).map(({ id, name, depth }) => ({ value: id, label: name, depth })) + +export default function WithTreeView() { + return ( + + + {sections.map((section) => ( +
+

{section.name}

+ {section.children && + section.children.map((child) => ( +

+ {child.name} +

+ ))} +
+ ))} +
+ + On this page + + {items.map((item) => ( + + {item.label} + + ))} + + +
+ ) +} diff --git a/packages/solid/src/components/toc/index.tsx b/packages/solid/src/components/toc/index.tsx new file mode 100644 index 0000000000..af93ee014c --- /dev/null +++ b/packages/solid/src/components/toc/index.tsx @@ -0,0 +1,16 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { TocContent, type TocContentBaseProps, type TocContentProps } from './toc-content' +export { TocContext, type TocContextProps } from './toc-context' +export { TocIndicator, type TocIndicatorBaseProps, type TocIndicatorProps } from './toc-indicator' +export { TocItem, type TocItemBaseProps, type TocItemProps } from './toc-item' +export { TocLink, type TocLinkBaseProps, type TocLinkProps } from './toc-link' +export { TocList, type TocListBaseProps, type TocListProps } from './toc-list' +export { TocNav, type TocNavBaseProps, type TocNavProps } from './toc-nav' +export { TocRoot, type TocRootBaseProps, type TocRootProps } from './toc-root' +export { TocRootProvider, type TocRootProviderBaseProps, type TocRootProviderProps } from './toc-root-provider' +export { TocTitle, type TocTitleBaseProps, type TocTitleProps } from './toc-title' +export { tocAnatomy } from './toc.anatomy' +export { useToc, type UseTocProps, type UseTocReturn } from './use-toc' +export { useTocContext, type UseTocContext } from './use-toc-context' + +export * as Toc from './toc' diff --git a/packages/solid/src/components/toc/toc-content.tsx b/packages/solid/src/components/toc/toc-content.tsx new file mode 100644 index 0000000000..00c700d2bb --- /dev/null +++ b/packages/solid/src/components/toc/toc-content.tsx @@ -0,0 +1,8 @@ +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' + +export interface TocContentBaseProps extends PolymorphicProps {} +export interface TocContentProps extends HTMLProps<'article'>, TocContentBaseProps {} + +export const TocContent = (props: TocContentProps) => { + return +} diff --git a/packages/solid/src/components/toc/toc-context.tsx b/packages/solid/src/components/toc/toc-context.tsx new file mode 100644 index 0000000000..3f99e01247 --- /dev/null +++ b/packages/solid/src/components/toc/toc-context.tsx @@ -0,0 +1,8 @@ +import type { JSX } from 'solid-js' +import { type UseTocContext, useTocContext } from './use-toc-context' + +export interface TocContextProps { + children: (context: UseTocContext) => JSX.Element +} + +export const TocContext = (props: TocContextProps) => props.children(useTocContext()) diff --git a/packages/solid/src/components/toc/toc-indicator.tsx b/packages/solid/src/components/toc/toc-indicator.tsx new file mode 100644 index 0000000000..396790d65d --- /dev/null +++ b/packages/solid/src/components/toc/toc-indicator.tsx @@ -0,0 +1,12 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocIndicatorBaseProps extends PolymorphicProps {} +export interface TocIndicatorProps extends HTMLProps<'div'>, TocIndicatorBaseProps {} + +export const TocIndicator = (props: TocIndicatorProps) => { + const toc = useTocContext() + const mergedProps = mergeProps(() => toc.getIndicatorProps(), props) + return +} diff --git a/packages/solid/src/components/toc/toc-item.tsx b/packages/solid/src/components/toc/toc-item.tsx new file mode 100644 index 0000000000..cc3b03338f --- /dev/null +++ b/packages/solid/src/components/toc/toc-item.tsx @@ -0,0 +1,22 @@ +import { mergeProps } from '@zag-js/solid' +import type { ItemProps } from '@zag-js/toc' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' +import { TocItemPropsProvider } from './use-toc-item-props-context' + +export interface TocItemBaseProps extends ItemProps, PolymorphicProps {} +export interface TocItemProps extends HTMLProps<'li'>, TocItemBaseProps {} + +const splitItemProps = createSplitProps() + +export const TocItem = (props: TocItemProps) => { + const [itemProps, localProps] = splitItemProps(props, ['item']) + const toc = useTocContext() + const mergedProps = mergeProps(() => toc.getItemProps(itemProps), localProps) + return ( + + + + ) +} diff --git a/packages/solid/src/components/toc/toc-link.tsx b/packages/solid/src/components/toc/toc-link.tsx new file mode 100644 index 0000000000..93ec06766d --- /dev/null +++ b/packages/solid/src/components/toc/toc-link.tsx @@ -0,0 +1,14 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' +import { useTocItemPropsContext } from './use-toc-item-props-context' + +export interface TocLinkBaseProps extends PolymorphicProps {} +export interface TocLinkProps extends HTMLProps<'a'>, TocLinkBaseProps {} + +export const TocLink = (props: TocLinkProps) => { + const toc = useTocContext() + const itemProps = useTocItemPropsContext() + const mergedProps = mergeProps(() => toc.getLinkProps(itemProps), props) + return +} diff --git a/packages/solid/src/components/toc/toc-list.tsx b/packages/solid/src/components/toc/toc-list.tsx new file mode 100644 index 0000000000..926f020a3b --- /dev/null +++ b/packages/solid/src/components/toc/toc-list.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocListBaseProps extends PolymorphicProps<'ul'> {} +export interface TocListProps extends HTMLProps<'ul'>, TocListBaseProps {} + +export const TocList = (props: TocListProps) => { + const api = useTocContext() + const mergedProps = mergeProps(() => api().getListProps(), props) + + return +} diff --git a/packages/solid/src/components/toc/toc-nav.tsx b/packages/solid/src/components/toc/toc-nav.tsx new file mode 100644 index 0000000000..e13e8de25f --- /dev/null +++ b/packages/solid/src/components/toc/toc-nav.tsx @@ -0,0 +1,15 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocNavBaseProps extends PolymorphicProps { + placement?: 'left' | 'right' +} +export interface TocNavProps extends HTMLProps<'nav'>, TocNavBaseProps {} + +export const TocNav = (props: TocNavProps) => { + const { placement, ...rest } = props + const toc = useTocContext() + const mergedProps = mergeProps(() => toc.getRootProps(), rest) + return +} diff --git a/packages/solid/src/components/toc/toc-root-provider.tsx b/packages/solid/src/components/toc/toc-root-provider.tsx new file mode 100644 index 0000000000..672f95ed35 --- /dev/null +++ b/packages/solid/src/components/toc/toc-root-provider.tsx @@ -0,0 +1,24 @@ +import type { Assign } from '../../types' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import type { UseTocReturn } from './use-toc' +import { TocProvider } from './use-toc-context' + +interface RootProviderProps { + value: UseTocReturn +} + +export interface TocRootProviderBaseProps extends RootProviderProps, PolymorphicProps {} +export interface TocRootProviderProps extends Assign, TocRootProviderBaseProps> {} + +const splitRootProviderProps = createSplitProps() + +export const TocRootProvider = (props: TocRootProviderProps) => { + const [{ value: toc }, localProps] = splitRootProviderProps(props, ['value']) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/toc/toc-root.tsx b/packages/solid/src/components/toc/toc-root.tsx new file mode 100644 index 0000000000..c98160f0cf --- /dev/null +++ b/packages/solid/src/components/toc/toc-root.tsx @@ -0,0 +1,33 @@ +import type { Assign } from '../../types' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { type UseTocProps, useToc } from './use-toc' +import { TocProvider } from './use-toc-context' + +export interface TocRootBaseProps extends UseTocProps, PolymorphicProps {} +export interface TocRootProps extends Assign, TocRootBaseProps> {} + +const splitRootProps = createSplitProps() + +export const TocRoot = (props: TocRootProps) => { + const [useTocProps, localProps] = splitRootProps(props, [ + 'activeIds', + 'autoScroll', + 'defaultActiveIds', + 'getScrollEl', + 'id', + 'ids', + 'items', + 'onActiveChange', + 'rootMargin', + 'scrollBehavior', + 'threshold', + ]) + const toc = useToc({ ...useTocProps, id: useTocProps.id ?? undefined }) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/toc/toc-title.tsx b/packages/solid/src/components/toc/toc-title.tsx new file mode 100644 index 0000000000..e2218c06e7 --- /dev/null +++ b/packages/solid/src/components/toc/toc-title.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { splitProps } from 'solid-js' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTocContext } from './use-toc-context' + +export interface TocTitleBaseProps extends PolymorphicProps {} +export interface TocTitleProps extends HTMLProps<'h2'>, TocTitleBaseProps {} + +export const TocTitle = (props: TocTitleProps) => { + const toc = useTocContext() + const mergedProps = mergeProps(() => toc.getTitleProps(), props) + return +} diff --git a/packages/solid/src/components/toc/toc.anatomy.ts b/packages/solid/src/components/toc/toc.anatomy.ts new file mode 100644 index 0000000000..560bfdc21b --- /dev/null +++ b/packages/solid/src/components/toc/toc.anatomy.ts @@ -0,0 +1 @@ +export { anatomy as tocAnatomy } from '@zag-js/toc' diff --git a/packages/solid/src/components/toc/toc.stories copy.tsx b/packages/solid/src/components/toc/toc.stories copy.tsx new file mode 100644 index 0000000000..d971a821d1 --- /dev/null +++ b/packages/solid/src/components/toc/toc.stories copy.tsx @@ -0,0 +1,17 @@ +import type { Meta } from '@storybook/react-vite' + +const meta: Meta = { + title: 'Components / Toc', +} + +export default meta + +export { Basic } from './examples/basic' +export { Grouped } from './examples/grouped' +export { Nested } from './examples/nested' +export { RootProvider } from './examples/root-provider' +export { WithCollapsible } from './examples/with-collapsible' +export { WithHover } from './examples/with-hover' +export { WithIndicator } from './examples/with-indicator' +export { WithNumbers } from './examples/with-numbers' +export { WithTreeView } from './examples/with-tree-view' diff --git a/packages/solid/src/components/toc/toc.stories.tsx b/packages/solid/src/components/toc/toc.stories.tsx new file mode 100644 index 0000000000..eb2fbabd6c --- /dev/null +++ b/packages/solid/src/components/toc/toc.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta } from 'storybook-solidjs-vite' +import Basic from './examples/basic' +import Grouped from './examples/grouped' +import Nested from './examples/nested' +import CustomScrollBehavior from './examples/custom-scroll-behavior' +import RootProvider from './examples/root-provider' +import WithIndicator from './examples/with-indicator' +import WithTreeView from './examples/with-tree-view' +import WithCollapsible from './examples/with-collapsible' +import WithHover from './examples/with-hover' +import WithNumbers from './examples/with-numbers' + +const meta: Meta = { + title: 'Components / Toc', +} + +export default meta + +export const BasicExample = () => +export const GroupedExample = () => +export const NestedExample = () => +export const CustomScrollBehaviorExample = () => +export const RootProviderExample = () => +export const WithIndicatorExample = () => +export const WithTreeViewExample = () => +export const WithCollapsibleExample = () => +export const WithHoverExample = () => +export const WithNumbersExample = () => diff --git a/packages/solid/src/components/toc/toc.ts b/packages/solid/src/components/toc/toc.ts new file mode 100644 index 0000000000..c5813b27fc --- /dev/null +++ b/packages/solid/src/components/toc/toc.ts @@ -0,0 +1,27 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { + TocContent as Content, + type TocContentBaseProps as ContentBaseProps, + type TocContentProps as ContentProps, +} from './toc-content' +export { TocContext as Context, type TocContextProps as ContextProps } from './toc-context' +export { + TocIndicator as Indicator, + type TocIndicatorBaseProps as IndicatorBaseProps, + type TocIndicatorProps as IndicatorProps, +} from './toc-indicator' +export { TocItem as Item, type TocItemBaseProps as ItemBaseProps, type TocItemProps as ItemProps } from './toc-item' +export { TocLink as Link, type TocLinkBaseProps as LinkBaseProps, type TocLinkProps as LinkProps } from './toc-link' +export { TocList as List, type TocListBaseProps as ListBaseProps, type TocListProps as ListProps } from './toc-list' +export { TocNav as Nav, type TocNavBaseProps as NavBaseProps, type TocNavProps as NavProps } from './toc-nav' +export { TocRoot as Root, type TocRootBaseProps as RootBaseProps, type TocRootProps as RootProps } from './toc-root' +export { + TocRootProvider as RootProvider, + type TocRootProviderBaseProps as RootProviderBaseProps, + type TocRootProviderProps as RootProviderProps, +} from './toc-root-provider' +export { + TocTitle as Title, + type TocTitleBaseProps as TitleBaseProps, + type TocTitleProps as TitleProps, +} from './toc-title' diff --git a/packages/solid/src/components/toc/use-toc-context.ts b/packages/solid/src/components/toc/use-toc-context.ts new file mode 100644 index 0000000000..e8228bea43 --- /dev/null +++ b/packages/solid/src/components/toc/use-toc-context.ts @@ -0,0 +1,9 @@ +import { createContext } from '../../utils/create-context' +import type { UseTocReturn } from './use-toc' + +export interface UseTocContext extends UseTocReturn {} + +export const [TocProvider, useTocContext] = createContext({ + hookName: 'useTocContext', + providerName: '', +}) diff --git a/packages/solid/src/components/toc/use-toc-item-props-context.ts b/packages/solid/src/components/toc/use-toc-item-props-context.ts new file mode 100644 index 0000000000..4b63341aa2 --- /dev/null +++ b/packages/solid/src/components/toc/use-toc-item-props-context.ts @@ -0,0 +1,10 @@ +import type { ItemProps } from '@zag-js/toc' +import { createContext } from '../../utils/create-context' + +export interface UseTocItemPropsContext extends ItemProps {} + +export const [TocItemPropsProvider, useTocItemPropsContext] = createContext({ + name: 'TocItemPropsContext', + hookName: 'useTocItemPropsContext', + providerName: '', +}) diff --git a/packages/solid/src/components/toc/use-toc.ts b/packages/solid/src/components/toc/use-toc.ts new file mode 100644 index 0000000000..6a818d22a9 --- /dev/null +++ b/packages/solid/src/components/toc/use-toc.ts @@ -0,0 +1,25 @@ +import * as toc from '@zag-js/toc' +import { normalizeProps, useMachine, type PropTypes } from '@zag-js/solid' +import { createMemo, createUniqueId, type Accessor } from 'solid-js' +import { useEnvironmentContext } from '../../providers/environment' +import { useLocaleContext } from '../../providers/locale' +import { runIfFn } from '../../utils/run-if-fn' + +export interface UseTocProps extends Omit {} +export interface UseTocReturn extends Accessor> {} + +export const useToc = (props: UseTocProps) => { + const id = createUniqueId() + const locale = useLocaleContext() + const environment = useEnvironmentContext() + + const machineProps = createMemo(() => ({ + id, + dir: locale().dir, + getRootNode: environment().getRootNode, + ...runIfFn(props), + })) + + const service = useMachine(toc.machine, machineProps) + return toc.connect(service, normalizeProps) +} diff --git a/packages/svelte/src/lib/components/toc/examples/basic.svelte b/packages/svelte/src/lib/components/toc/examples/basic.svelte new file mode 100644 index 0000000000..9a2c6eb8f9 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/basic.svelte @@ -0,0 +1,44 @@ + + + + + {#each items as item (item.value)} +
+

{item.label}

+

{paragraphs}

+
+ {/each} +
+ + On this page + + {#each items as item (item.value)} + + + {item.label} + + + {/each} + + +
+ diff --git a/packages/svelte/src/lib/components/toc/examples/grouped.svelte b/packages/svelte/src/lib/components/toc/examples/grouped.svelte new file mode 100644 index 0000000000..2aeddc28b2 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/grouped.svelte @@ -0,0 +1,63 @@ + + +
+
+ {#each groups as group (group.label)} +
+

{group.label}

+ {#each group.items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ {/each} +
+ + {#each groups as group (group.label)} +
+ {group.label} + + {#each group.items as item (item.value)} + + {item.label} + + {/each} + +
+ {/each} +
+
diff --git a/packages/svelte/src/lib/components/toc/examples/nested.svelte b/packages/svelte/src/lib/components/toc/examples/nested.svelte new file mode 100644 index 0000000000..78af00d936 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/nested.svelte @@ -0,0 +1,41 @@ + + +
+
+ {#each items as item (item.value)} +
+ + {item.label} + +

{p}

+
+ {/each} +
+ + On this page + + {#each items as item (item.value)} + + {item.label} + + {/each} + + +
diff --git a/packages/svelte/src/lib/components/toc/examples/root-provider.svelte b/packages/svelte/src/lib/components/toc/examples/root-provider.svelte new file mode 100644 index 0000000000..c6ae8c820e --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/root-provider.svelte @@ -0,0 +1,37 @@ + + +
+
+ {#each items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ + On this page + + {#each items as item (item.value)} + + {item.label} + + {/each} + + +
diff --git a/packages/svelte/src/lib/components/toc/examples/with-collapsible.svelte b/packages/svelte/src/lib/components/toc/examples/with-collapsible.svelte new file mode 100644 index 0000000000..145562ca09 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/with-collapsible.svelte @@ -0,0 +1,96 @@ + + +
+
+ {#each items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ + + + {#snippet render(toc)} + {@const activeIndex = toc().activeItems[0] + ? items.findIndex((i) => i.value === toc().activeItems[0].value) + : -1} + {@const progress = activeIndex >= 0 ? (activeIndex + 1) / items.length : 0} + {@const dashArray = `${progress * CIRCUMFERENCE} ${CIRCUMFERENCE}`} + + + + + + + {activeIndex >= 0 ? items[activeIndex].label : 'On this page'} + + + + + + + {/snippet} + + + + {#each items as item, index (item.value)} + + + {String(index + 1).padStart(2, '0')} + {item.label} + + + {/each} + + + + +
diff --git a/packages/svelte/src/lib/components/toc/examples/with-hover.svelte b/packages/svelte/src/lib/components/toc/examples/with-hover.svelte new file mode 100644 index 0000000000..a8fb3144f8 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/with-hover.svelte @@ -0,0 +1,79 @@ + + +
+
+ {#each items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ (hovered = true)} + onmouseleave={() => (hovered = false)} + > + + + + +
+ {#each items as item (item.value)} + + {/each} +
+
+ +
+ + {#each items as item (item.value)} + + {item.label} + + {/each} + +
+
+
+
+
diff --git a/packages/svelte/src/lib/components/toc/examples/with-indicator.svelte b/packages/svelte/src/lib/components/toc/examples/with-indicator.svelte new file mode 100644 index 0000000000..d1991b3cf0 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/with-indicator.svelte @@ -0,0 +1,38 @@ + + +
+
+ {#each items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ + On this page + + + {#each items as item (item.value)} + + {item.label} + + {/each} + + +
diff --git a/packages/svelte/src/lib/components/toc/examples/with-numbers.svelte b/packages/svelte/src/lib/components/toc/examples/with-numbers.svelte new file mode 100644 index 0000000000..4789158c8e --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/with-numbers.svelte @@ -0,0 +1,39 @@ + + +
+
+ {#each items as item (item.value)} +
+

{item.label}

+

{p}

+
+ {/each} +
+ + Contents + + {#each items as item, index (item.value)} + + + {String(index + 1).padStart(2, '0')} + {item.label} + + + {/each} + + +
diff --git a/packages/svelte/src/lib/components/toc/examples/with-tree-view.svelte b/packages/svelte/src/lib/components/toc/examples/with-tree-view.svelte new file mode 100644 index 0000000000..b61d852db9 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/examples/with-tree-view.svelte @@ -0,0 +1,149 @@ + + +
+
+ {#each sections as section (section.id)} +
+

{section.name}

+

{p}

+ {#each section.children ?? [] as child (child.id)} +
+

{child.name}

+

{p}

+
+ {/each} +
+ {/each} +
+ { + const activeIds = new Set(activeItems.map((i) => i.value)) + expandedValue = sections + .filter( + (section) => + activeIds.has(section.id) || + (section.children ?? []).some((child) => activeIds.has(child.id)), + ) + .map((s) => s.id) + }} + > + On this page + (expandedValue = next)} + > + + {#each sections as node, index (node.id)} + {@render renderNode(node, [index])} + {/each} + + + +
+ +{#snippet renderNode(node: TocNode, indexPath: number[])} + + {#if node.children} + + + + + + + + {#snippet render(toc)} + {node.name} + {/snippet} + + + + + + {#each node.children as child, i (child.id)} + {@render renderNode(child, [...indexPath, i])} + {/each} + + + {:else} + + + + {#snippet render(toc)} + {node.name} + {/snippet} + + + + {/if} + +{/snippet} diff --git a/packages/svelte/src/lib/components/toc/index.ts b/packages/svelte/src/lib/components/toc/index.ts new file mode 100644 index 0000000000..fc11164268 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/index.ts @@ -0,0 +1,19 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { default as TocContent, type TocContentBaseProps, type TocContentProps } from './toc-content.svelte' +export { default as TocContext, type TocContextProps } from './toc-context.svelte' +export { default as TocIndicator, type TocIndicatorBaseProps, type TocIndicatorProps } from './toc-indicator.svelte' +export { default as TocItem, type TocItemBaseProps, type TocItemProps } from './toc-item.svelte' +export { default as TocLink, type TocLinkBaseProps, type TocLinkProps } from './toc-link.svelte' +export { default as TocList, type TocListBaseProps, type TocListProps } from './toc-list.svelte' +export { default as TocNav, type TocNavBaseProps, type TocNavProps } from './toc-nav.svelte' +export { default as TocRoot, type TocRootBaseProps, type TocRootProps } from './toc-root.svelte' +export { + default as TocRootProvider, + type TocRootProviderBaseProps, + type TocRootProviderProps, +} from './toc-root-provider.svelte' +export { default as TocTitle, type TocTitleBaseProps, type TocTitleProps } from './toc-title.svelte' +export { tocAnatomy } from './toc.anatomy' +export { useToc, type UseTocProps, type UseTocReturn } from './use-toc.svelte' +export { useTocContext, type UseTocContext } from './use-toc-context' +export * as Toc from './toc' diff --git a/packages/svelte/src/lib/components/toc/toc-content.svelte b/packages/svelte/src/lib/components/toc/toc-content.svelte new file mode 100644 index 0000000000..9bcc6d9623 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-content.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-context.svelte b/packages/svelte/src/lib/components/toc/toc-context.svelte new file mode 100644 index 0000000000..7a5e676692 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-context.svelte @@ -0,0 +1,18 @@ + + + + +{@render render(toc)} diff --git a/packages/svelte/src/lib/components/toc/toc-indicator.svelte b/packages/svelte/src/lib/components/toc/toc-indicator.svelte new file mode 100644 index 0000000000..67b8636190 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-indicator.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-item.svelte b/packages/svelte/src/lib/components/toc/toc-item.svelte new file mode 100644 index 0000000000..992ea90b7c --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-item.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-link.svelte b/packages/svelte/src/lib/components/toc/toc-link.svelte new file mode 100644 index 0000000000..6dc3cc789d --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-link.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-list.svelte b/packages/svelte/src/lib/components/toc/toc-list.svelte new file mode 100644 index 0000000000..2f27fb303b --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-list.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-nav.svelte b/packages/svelte/src/lib/components/toc/toc-nav.svelte new file mode 100644 index 0000000000..fe1fdb4d00 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-nav.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-root-provider.svelte b/packages/svelte/src/lib/components/toc/toc-root-provider.svelte new file mode 100644 index 0000000000..86c24cf87d --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-root-provider.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-root.svelte b/packages/svelte/src/lib/components/toc/toc-root.svelte new file mode 100644 index 0000000000..687a94ba04 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-root.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc-title.svelte b/packages/svelte/src/lib/components/toc/toc-title.svelte new file mode 100644 index 0000000000..c3fac0a325 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc-title.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/svelte/src/lib/components/toc/toc.anatomy.ts b/packages/svelte/src/lib/components/toc/toc.anatomy.ts new file mode 100644 index 0000000000..560bfdc21b --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc.anatomy.ts @@ -0,0 +1 @@ +export { anatomy as tocAnatomy } from '@zag-js/toc' diff --git a/packages/svelte/src/lib/components/toc/toc.stories.ts b/packages/svelte/src/lib/components/toc/toc.stories.ts new file mode 100644 index 0000000000..281cd70436 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc.stories.ts @@ -0,0 +1,70 @@ +import type { Meta } from '@storybook/svelte' +import BasicExample from './examples/basic.svelte' +// import WithIndicatorExample from './examples/with-indicator.svelte' +// import GroupedExample from './examples/grouped.svelte' +// import RootProviderExample from './examples/root-provider.svelte' +// import WithNumbersExample from './examples/with-numbers.svelte' +// import NestedExample from './examples/nested.svelte' +// import WithHoverExample from './examples/with-hover.svelte' +// import WithTreeViewExample from './examples/with-tree-view.svelte' +// import WithCollapsibleExample from './examples/with-collapsible.svelte' + +const meta: Meta = { + title: 'Components/Toc', +} + +export default meta + +export const Basic = { + render: () => ({ + Component: BasicExample, + }), +} + +// export const Nested = { +// render: () => ({ +// Component: NestedExample, +// }), +// } + +// export const Grouped = { +// render: () => ({ +// Component: GroupedExample, +// }), +// } + +// export const WithHover = { +// render: () => ({ +// Component: WithHoverExample, +// }), +// } + +// export const WithNumbers = { +// render: () => ({ +// Component: WithNumbersExample, +// }), +// } + +// export const RootProvider = { +// render: () => ({ +// Component: RootProviderExample, +// }), +// } + +// export const WithTreeView = { +// render: () => ({ +// Component: WithTreeViewExample, +// }), +// } + +// export const WithIndicator = { +// render: () => ({ +// Component: WithIndicatorExample, +// }), +// } + +// export const WithCollapsible = { +// render: () => ({ +// Component: WithCollapsibleExample, +// }), +// } diff --git a/packages/svelte/src/lib/components/toc/toc.ts b/packages/svelte/src/lib/components/toc/toc.ts new file mode 100644 index 0000000000..f12300212b --- /dev/null +++ b/packages/svelte/src/lib/components/toc/toc.ts @@ -0,0 +1,43 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { + default as Content, + type TocContentBaseProps as ContentBaseProps, + type TocContentProps as ContentProps, +} from './toc-content.svelte' +export { default as Context, type TocContextProps as ContextProps } from './toc-context.svelte' +export { + default as Indicator, + type TocIndicatorBaseProps as IndicatorBaseProps, + type TocIndicatorProps as IndicatorProps, +} from './toc-indicator.svelte' +export { + default as Item, + type TocItemBaseProps as ItemBaseProps, + type TocItemProps as ItemProps, +} from './toc-item.svelte' +export { + default as Link, + type TocLinkBaseProps as LinkBaseProps, + type TocLinkProps as LinkProps, +} from './toc-link.svelte' +export { + default as List, + type TocListBaseProps as ListBaseProps, + type TocListProps as ListProps, +} from './toc-list.svelte' +export { default as Nav, type TocNavBaseProps as NavBaseProps, type TocNavProps as NavProps } from './toc-nav.svelte' +export { + default as Root, + type TocRootBaseProps as RootBaseProps, + type TocRootProps as RootProps, +} from './toc-root.svelte' +export { + default as RootProvider, + type TocRootProviderBaseProps as RootProviderBaseProps, + type TocRootProviderProps as RootProviderProps, +} from './toc-root-provider.svelte' +export { + default as Title, + type TocTitleBaseProps as TitleBaseProps, + type TocTitleProps as TitleProps, +} from './toc-title.svelte' diff --git a/packages/svelte/src/lib/components/toc/use-toc-context.ts b/packages/svelte/src/lib/components/toc/use-toc-context.ts new file mode 100644 index 0000000000..4b210911af --- /dev/null +++ b/packages/svelte/src/lib/components/toc/use-toc-context.ts @@ -0,0 +1,9 @@ +import { createContext } from '../../utils/create-context' +import type { UseTocReturn } from './use-toc.svelte' + +export interface UseTocContext extends UseTocReturn {} +export const [TocProvider, useTocContext, TocContextId] = createContext({ + name: 'TocContext', + hookName: 'useTocContext', + providerName: '', +}) diff --git a/packages/svelte/src/lib/components/toc/use-toc-item-props-context.ts b/packages/svelte/src/lib/components/toc/use-toc-item-props-context.ts new file mode 100644 index 0000000000..d01bbcdd33 --- /dev/null +++ b/packages/svelte/src/lib/components/toc/use-toc-item-props-context.ts @@ -0,0 +1,8 @@ +import { createContext } from '../../utils/create-context' +import type { ItemProps } from '@zag-js/toc' + +export const [TocItemPropsProvider, useTocItemPropsContext] = createContext<() => ItemProps>({ + name: 'TocItemPropsContext', + hookName: 'useTocItemPropsContext', + providerName: '', +}) diff --git a/packages/svelte/src/lib/components/toc/use-toc.svelte.ts b/packages/svelte/src/lib/components/toc/use-toc.svelte.ts new file mode 100644 index 0000000000..5eaec5808f --- /dev/null +++ b/packages/svelte/src/lib/components/toc/use-toc.svelte.ts @@ -0,0 +1,27 @@ +import { useEnvironmentContext } from '$lib/providers/environment' +import { useLocaleContext } from '$lib/providers/locale' +import type { Accessor, Optional } from '$lib/types' +import * as toc from '@zag-js/toc' +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/svelte' +import { type MaybeFunction, runIfFn } from '@zag-js/utils' + +export interface UseTocProps extends Optional, 'id'> {} +export interface UseTocReturn extends Accessor> {} + +export const useToc = (props?: MaybeFunction): UseTocReturn => { + const env = useEnvironmentContext() + const locale = useLocaleContext() + + const machineProps = $derived.by(() => { + const resolvedProps = runIfFn(props) + return { + dir: locale().dir, + getRootNode: env().getRootNode, + ...resolvedProps, + } + }) + + const service = useMachine(toc.machine, () => machineProps) + const api = $derived(toc.connect(service, normalizeProps)) + return () => api +} diff --git a/packages/vue/src/components/toc/examples/basic.vue b/packages/vue/src/components/toc/examples/basic.vue new file mode 100644 index 0000000000..0ae4fb9a43 --- /dev/null +++ b/packages/vue/src/components/toc/examples/basic.vue @@ -0,0 +1,38 @@ + + diff --git a/packages/vue/src/components/toc/examples/grouped.vue b/packages/vue/src/components/toc/examples/grouped.vue new file mode 100644 index 0000000000..46bcee1a18 --- /dev/null +++ b/packages/vue/src/components/toc/examples/grouped.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/vue/src/components/toc/examples/nested.vue b/packages/vue/src/components/toc/examples/nested.vue new file mode 100644 index 0000000000..eb8ee04138 --- /dev/null +++ b/packages/vue/src/components/toc/examples/nested.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/vue/src/components/toc/examples/root-provider.vue b/packages/vue/src/components/toc/examples/root-provider.vue new file mode 100644 index 0000000000..31276642b0 --- /dev/null +++ b/packages/vue/src/components/toc/examples/root-provider.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/vue/src/components/toc/examples/toc-tree-node.vue b/packages/vue/src/components/toc/examples/toc-tree-node.vue new file mode 100644 index 0000000000..d4f6e7f706 --- /dev/null +++ b/packages/vue/src/components/toc/examples/toc-tree-node.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/vue/src/components/toc/examples/with-collapsible.vue b/packages/vue/src/components/toc/examples/with-collapsible.vue new file mode 100644 index 0000000000..352305153f --- /dev/null +++ b/packages/vue/src/components/toc/examples/with-collapsible.vue @@ -0,0 +1,117 @@ + + + diff --git a/packages/vue/src/components/toc/examples/with-hover.vue b/packages/vue/src/components/toc/examples/with-hover.vue new file mode 100644 index 0000000000..5d427f4dc1 --- /dev/null +++ b/packages/vue/src/components/toc/examples/with-hover.vue @@ -0,0 +1,83 @@ + + + diff --git a/packages/vue/src/components/toc/examples/with-indicator.vue b/packages/vue/src/components/toc/examples/with-indicator.vue new file mode 100644 index 0000000000..e8d74165e8 --- /dev/null +++ b/packages/vue/src/components/toc/examples/with-indicator.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/vue/src/components/toc/examples/with-numbers.vue b/packages/vue/src/components/toc/examples/with-numbers.vue new file mode 100644 index 0000000000..46aaa64484 --- /dev/null +++ b/packages/vue/src/components/toc/examples/with-numbers.vue @@ -0,0 +1,35 @@ + + diff --git a/packages/vue/src/components/toc/examples/with-tree-view.vue b/packages/vue/src/components/toc/examples/with-tree-view.vue new file mode 100644 index 0000000000..dd864ba29c --- /dev/null +++ b/packages/vue/src/components/toc/examples/with-tree-view.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/vue/src/components/toc/index.ts b/packages/vue/src/components/toc/index.ts new file mode 100644 index 0000000000..56cc03e13d --- /dev/null +++ b/packages/vue/src/components/toc/index.ts @@ -0,0 +1,20 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { default as TocContent, type TocContentBaseProps, type TocContentProps } from './toc-content.vue' +export { default as TocContext, type TocContextProps } from './toc-context.vue' +export { default as TocIndicator, type TocIndicatorBaseProps, type TocIndicatorProps } from './toc-indicator.vue' +export { default as TocItem, type TocItemBaseProps, type TocItemProps } from './toc-item.vue' +export { default as TocLink, type TocLinkBaseProps, type TocLinkProps } from './toc-link.vue' +export { default as TocList, type TocListBaseProps, type TocListProps } from './toc-list.vue' +export { default as TocNav, type TocNavBaseProps, type TocNavProps } from './toc-nav.vue' +export { default as TocRoot, type TocRootBaseProps, type TocRootEmits, type TocRootProps } from './toc-root.vue' +export { + default as TocRootProvider, + type TocRootProviderBaseProps, + type TocRootProviderProps, +} from './toc-root-provider.vue' +export { default as TocTitle, type TocTitleBaseProps, type TocTitleProps } from './toc-title.vue' +export { tocAnatomy } from './toc.anatomy' +export { useToc, type UseTocProps, type UseTocReturn } from './use-toc' +export { useTocContext, type UseTocContext } from './use-toc-context' + +export * as Toc from './toc' diff --git a/packages/vue/src/components/toc/toc-content.vue b/packages/vue/src/components/toc/toc-content.vue new file mode 100644 index 0000000000..d307cbcfd3 --- /dev/null +++ b/packages/vue/src/components/toc/toc-content.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-context.vue b/packages/vue/src/components/toc/toc-context.vue new file mode 100644 index 0000000000..994a8a9492 --- /dev/null +++ b/packages/vue/src/components/toc/toc-context.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-indicator.vue b/packages/vue/src/components/toc/toc-indicator.vue new file mode 100644 index 0000000000..c20b5f37e0 --- /dev/null +++ b/packages/vue/src/components/toc/toc-indicator.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-item.vue b/packages/vue/src/components/toc/toc-item.vue new file mode 100644 index 0000000000..cc8962af6f --- /dev/null +++ b/packages/vue/src/components/toc/toc-item.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-link.vue b/packages/vue/src/components/toc/toc-link.vue new file mode 100644 index 0000000000..cb66bda1ac --- /dev/null +++ b/packages/vue/src/components/toc/toc-link.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-list.vue b/packages/vue/src/components/toc/toc-list.vue new file mode 100644 index 0000000000..6f0cdeb899 --- /dev/null +++ b/packages/vue/src/components/toc/toc-list.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-nav.vue b/packages/vue/src/components/toc/toc-nav.vue new file mode 100644 index 0000000000..30e6aa92ef --- /dev/null +++ b/packages/vue/src/components/toc/toc-nav.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-root-provider.vue b/packages/vue/src/components/toc/toc-root-provider.vue new file mode 100644 index 0000000000..45e6a42555 --- /dev/null +++ b/packages/vue/src/components/toc/toc-root-provider.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-root.vue b/packages/vue/src/components/toc/toc-root.vue new file mode 100644 index 0000000000..4c292077fb --- /dev/null +++ b/packages/vue/src/components/toc/toc-root.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc-title.vue b/packages/vue/src/components/toc/toc-title.vue new file mode 100644 index 0000000000..33712a4231 --- /dev/null +++ b/packages/vue/src/components/toc/toc-title.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue/src/components/toc/toc.anatomy.ts b/packages/vue/src/components/toc/toc.anatomy.ts new file mode 100644 index 0000000000..560bfdc21b --- /dev/null +++ b/packages/vue/src/components/toc/toc.anatomy.ts @@ -0,0 +1 @@ +export { anatomy as tocAnatomy } from '@zag-js/toc' diff --git a/packages/vue/src/components/toc/toc.stories.ts b/packages/vue/src/components/toc/toc.stories.ts new file mode 100644 index 0000000000..494a989dc1 --- /dev/null +++ b/packages/vue/src/components/toc/toc.stories.ts @@ -0,0 +1,80 @@ +import type { Meta } from '@storybook/vue3-vite' + +import BasicExample from './examples/basic.vue' +import GroupedExample from './examples/grouped.vue' +import NestedExample from './examples/nested.vue' +import RootProviderExample from './examples/root-provider.vue' +import WithCollapsibleExample from './examples/with-collapsible.vue' +import WithHoverExample from './examples/with-hover.vue' +import WithIndicatorExample from './examples/with-indicator.vue' +import WithNumbersExample from './examples/with-numbers.vue' +import WithTreeViewExample from './examples/with-tree-view.vue' + +const meta: Meta = { + title: 'Components / Toc', +} + +export default meta + +export const Basic = { + render: () => ({ + components: { Component: BasicExample }, + template: '', + }), +} + +export const Grouped = { + render: () => ({ + components: { Component: GroupedExample }, + template: '', + }), +} + +export const Nested = { + render: () => ({ + components: { Component: NestedExample }, + template: '', + }), +} + +export const RootProvider = { + render: () => ({ + components: { Component: RootProviderExample }, + template: '', + }), +} + +export const WithCollapsible = { + render: () => ({ + components: { Component: WithCollapsibleExample }, + template: '', + }), +} + +export const WithHover = { + render: () => ({ + components: { Component: WithHoverExample }, + template: '', + }), +} + +export const WithIndicator = { + render: () => ({ + components: { Component: WithIndicatorExample }, + template: '', + }), +} + +export const WithNumbers = { + render: () => ({ + components: { Component: WithNumbersExample }, + template: '', + }), +} + +export const WithTreeView = { + render: () => ({ + components: { Component: WithTreeViewExample }, + template: '', + }), +} diff --git a/packages/vue/src/components/toc/toc.ts b/packages/vue/src/components/toc/toc.ts new file mode 100644 index 0000000000..dd96d3f34f --- /dev/null +++ b/packages/vue/src/components/toc/toc.ts @@ -0,0 +1,32 @@ +export type { ActiveChangeDetails as TocActiveChangeDetails, TocItem as TocItemData } from '@zag-js/toc' +export { + default as Content, + type TocContentBaseProps as ContentBaseProps, + type TocContentProps as ContentProps, +} from './toc-content.vue' +export { default as Context, type TocContextProps as ContextProps } from './toc-context.vue' +export { + default as Indicator, + type TocIndicatorBaseProps as IndicatorBaseProps, + type TocIndicatorProps as IndicatorProps, +} from './toc-indicator.vue' +export { default as Item, type TocItemBaseProps as ItemBaseProps, type TocItemProps as ItemProps } from './toc-item.vue' +export { default as Link, type TocLinkBaseProps as LinkBaseProps, type TocLinkProps as LinkProps } from './toc-link.vue' +export { default as List, type TocListBaseProps as ListBaseProps, type TocListProps as ListProps } from './toc-list.vue' +export { default as Nav, type TocNavBaseProps as NavBaseProps, type TocNavProps as NavProps } from './toc-nav.vue' +export { + default as Root, + type TocRootBaseProps as RootBaseProps, + type TocRootEmits as RootEmits, + type TocRootProps as RootProps, +} from './toc-root.vue' +export { + default as RootProvider, + type TocRootProviderBaseProps as RootProviderBaseProps, + type TocRootProviderProps as RootProviderProps, +} from './toc-root-provider.vue' +export { + default as Title, + type TocTitleBaseProps as TitleBaseProps, + type TocTitleProps as TitleProps, +} from './toc-title.vue' diff --git a/packages/vue/src/components/toc/toc.types.ts b/packages/vue/src/components/toc/toc.types.ts new file mode 100644 index 0000000000..c5762cdf0b --- /dev/null +++ b/packages/vue/src/components/toc/toc.types.ts @@ -0,0 +1,65 @@ +import type * as toc from '@zag-js/toc' + +export interface RootProps { + /** + * Whether to auto-scroll the TOC container so the first active item + * is visible when active headings change. + * @default true + */ + autoScroll?: boolean + /** + * The controlled active heading ids. + */ + activeIds?: string[] + /** + * The default active heading ids when rendered. + * Use when you don't need to control the active headings. + */ + defaultActiveIds?: string[] + /** + * Function that returns the scroll container element to observe within. + * Defaults to the document/viewport. + */ + getScrollEl?: () => HTMLElement | null + /** + * The unique identifier of the machine. + */ + id?: string + /** + * The ids of the elements in the TOC. Useful for composition. + */ + ids?: Partial<{ + root: string + title: string + list: string + item: (value: string) => string + link: (value: string) => string + indicator: string + }> + /** + * The TOC items with `value` (slug/id) and `depth` (heading level). + */ + items: toc.TocItem[] + /** + * The root margin for the IntersectionObserver. + * @default "-20px 0px -40% 0px" + */ + rootMargin?: string + /** + * The scroll behavior for auto-scrolling the TOC container. + * @default "smooth" + */ + scrollBehavior?: ScrollBehavior + /** + * The IntersectionObserver threshold. + * @default 0 + */ + threshold?: number | number[] +} + +export type RootEmits = { + /** + * Callback when the active (visible) headings change. + */ + activeChange: [details: toc.ActiveChangeDetails] +} diff --git a/packages/vue/src/components/toc/use-toc-context.ts b/packages/vue/src/components/toc/use-toc-context.ts new file mode 100644 index 0000000000..92211cf6ce --- /dev/null +++ b/packages/vue/src/components/toc/use-toc-context.ts @@ -0,0 +1,6 @@ +import { createContext } from '../../utils/create-context' +import type { UseTocReturn } from './use-toc' + +export interface UseTocContext extends UseTocReturn {} + +export const [TocProvider, useTocContext] = createContext('TocContext') diff --git a/packages/vue/src/components/toc/use-toc-item-props-context.ts b/packages/vue/src/components/toc/use-toc-item-props-context.ts new file mode 100644 index 0000000000..e803b5a64a --- /dev/null +++ b/packages/vue/src/components/toc/use-toc-item-props-context.ts @@ -0,0 +1,6 @@ +import type { ItemProps } from '@zag-js/toc' +import type { ComputedRef } from 'vue' +import { createContext } from '../../utils/create-context' + +export const [TocItemPropsProvider, useTocItemPropsContext] = + createContext>('TocItemPropsContext') diff --git a/packages/vue/src/components/toc/use-toc.ts b/packages/vue/src/components/toc/use-toc.ts new file mode 100644 index 0000000000..88d3dcb97f --- /dev/null +++ b/packages/vue/src/components/toc/use-toc.ts @@ -0,0 +1,35 @@ +import * as toc from '@zag-js/toc' +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/vue' +import { type ComputedRef, type MaybeRef, computed, toValue, useId } from 'vue' +import { DEFAULT_ENVIRONMENT, DEFAULT_LOCALE, useEnvironmentContext, useLocaleContext } from '../../providers' +import type { EmitFn, Optional } from '../../types' +import { cleanProps } from '../../utils/clean-props' +import type { RootEmits } from './toc.types' + +export interface UseTocProps extends Optional, 'id'> {} + +export interface UseTocReturn extends ComputedRef> {} + +export const useToc = (props: MaybeRef, emits?: EmitFn): UseTocReturn => { + const id = useId() + const env = useEnvironmentContext(DEFAULT_ENVIRONMENT) + const locale = useLocaleContext(DEFAULT_LOCALE) + + const context = computed(() => { + const localProps = toValue(props) + + return { + id, + dir: locale.value.dir, + getRootNode: env?.value.getRootNode, + ...cleanProps(localProps), + onActiveChange: (details) => { + emits?.('activeChange', details) + localProps.onActiveChange?.(details) + }, + } + }) + + const service = useMachine(toc.machine, context) + return computed(() => toc.connect(service, normalizeProps)) +} From 3c7f2c198cdc0b926279d991a1b939b3477c4ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Mon, 13 Apr 2026 14:06:20 +0200 Subject: [PATCH 3/7] implemented minor changes on the svelte --- packages/react/package.json | 1 + packages/solid/package.json | 1 + packages/svelte/.storybook/main.ts | 5 +++++ packages/svelte/.storybook/preview.ts | 4 ++++ packages/svelte/package.json | 1 + packages/vue/package.json | 1 + 6 files changed, 13 insertions(+) diff --git a/packages/react/package.json b/packages/react/package.json index 3d8bb4d834..dd1a087fc9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -41,6 +41,7 @@ "tags input", "timer", "toast", + "toc", "toggle group", "tooltip", "tree view", diff --git a/packages/solid/package.json b/packages/solid/package.json index db13f92334..af690f7d88 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -39,6 +39,7 @@ "tabs", "tags input", "toast", + "toc", "toggle group", "tooltip", "tour", diff --git a/packages/svelte/.storybook/main.ts b/packages/svelte/.storybook/main.ts index 5511b75842..f9818917c6 100644 --- a/packages/svelte/.storybook/main.ts +++ b/packages/svelte/.storybook/main.ts @@ -16,6 +16,11 @@ const config: StorybookConfig = { config.resolve.alias ??= {} // @ts-expect-error - alias type mismatch config.resolve.alias['styles'] = resolve(__dirname, '../../../.storybook/modules') + + // Prevent Vite from pre-bundling .svelte files through esbuild + config.optimizeDeps ??= {} + config.optimizeDeps.noDiscovery = true + return config }, } diff --git a/packages/svelte/.storybook/preview.ts b/packages/svelte/.storybook/preview.ts index 85ac1f120a..76c4009dc6 100644 --- a/packages/svelte/.storybook/preview.ts +++ b/packages/svelte/.storybook/preview.ts @@ -15,6 +15,10 @@ const preview: Preview = { controls: { disable: true }, backgrounds: { disable: true }, viewport: { disable: true }, + sveltekit_experimental: { + // Disable the experimental SvelteKit mocks that use Svelte 5 runes + skip: true, + }, }, } diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 09ff99538d..8f9fe75232 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -41,6 +41,7 @@ "tags input", "timer", "toast", + "toc", "toggle group", "tooltip", "tree view", diff --git a/packages/vue/package.json b/packages/vue/package.json index eedaf16555..6bddcd823a 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -39,6 +39,7 @@ "tabs", "tags input", "toast", + "toc", "toggle group", "tooltip", "tour", From dda6b59714f8ca10996ad2687770d41ad40f9b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Mon, 13 Apr 2026 14:52:47 +0200 Subject: [PATCH 4/7] rerfactored react components --- .storybook/modules/toc.module.css | 441 ++++++++++++------ bun.lock | 10 +- packages/react/src/components/anatomy.ts | 1 + .../src/components/toc/examples/basic.tsx | 62 +-- .../src/components/toc/examples/context.tsx | 54 --- .../components/toc/examples/controlled.tsx | 68 --- .../toc/examples/custom-scroll-behavior.tsx | 44 -- .../src/components/toc/examples/floating.tsx | 59 --- .../src/components/toc/examples/grouped.tsx | 38 +- .../src/components/toc/examples/nested.tsx | 63 +-- .../src/components/toc/examples/placement.tsx | 58 --- .../toc/examples/progressive-reveal.tsx | 93 ---- .../toc/examples/reading-progress.tsx | 75 --- .../components/toc/examples/root-provider.tsx | 47 +- .../react/src/components/toc/examples/rtl.tsx | 33 -- .../src/components/toc/examples/theming.tsx | 34 -- .../components/toc/examples/with-combobox.tsx | 83 ++++ .../toc/examples/with-indicator.tsx | 62 +-- .../toc/examples/with-scroll-area.tsx | 45 -- .../react/src/components/toc/toc.stories.tsx | 18 +- packages/solid/src/components/anatomy.ts | 1 + packages/solid/src/components/index.tsx | 1 + .../src/components/toc/examples/context.tsx | 45 ++ .../toc/examples/custom-rendering.tsx | 21 +- .../src/components/toc/examples/floating.tsx | 60 +++ .../toc/examples/highlight-on-enter.tsx | 26 +- .../src/components/toc/examples/placement.tsx | 61 +++ .../toc/examples/reading-progress.tsx | 58 +++ .../src/components/toc/examples/report.tsx | 46 ++ .../components/toc/examples/with-combobox.tsx | 92 ++++ .../components/toc/examples/with-steps.tsx | 69 +++ packages/svelte/package.json | 3 +- packages/svelte/src/lib/components/anatomy.ts | 1 + packages/svelte/src/lib/components/index.ts | 1 + packages/vue/package.json | 3 +- packages/vue/src/components/anatomy.ts | 1 + packages/vue/src/components/index.ts | 1 + 37 files changed, 952 insertions(+), 926 deletions(-) delete mode 100644 packages/react/src/components/toc/examples/context.tsx delete mode 100644 packages/react/src/components/toc/examples/controlled.tsx delete mode 100644 packages/react/src/components/toc/examples/custom-scroll-behavior.tsx delete mode 100644 packages/react/src/components/toc/examples/floating.tsx delete mode 100644 packages/react/src/components/toc/examples/placement.tsx delete mode 100644 packages/react/src/components/toc/examples/progressive-reveal.tsx delete mode 100644 packages/react/src/components/toc/examples/reading-progress.tsx delete mode 100644 packages/react/src/components/toc/examples/rtl.tsx delete mode 100644 packages/react/src/components/toc/examples/theming.tsx create mode 100644 packages/react/src/components/toc/examples/with-combobox.tsx delete mode 100644 packages/react/src/components/toc/examples/with-scroll-area.tsx create mode 100644 packages/solid/src/components/toc/examples/context.tsx rename packages/{react => solid}/src/components/toc/examples/custom-rendering.tsx (65%) create mode 100644 packages/solid/src/components/toc/examples/floating.tsx rename packages/{react => solid}/src/components/toc/examples/highlight-on-enter.tsx (57%) create mode 100644 packages/solid/src/components/toc/examples/placement.tsx create mode 100644 packages/solid/src/components/toc/examples/reading-progress.tsx create mode 100644 packages/solid/src/components/toc/examples/report.tsx create mode 100644 packages/solid/src/components/toc/examples/with-combobox.tsx create mode 100644 packages/solid/src/components/toc/examples/with-steps.tsx diff --git a/.storybook/modules/toc.module.css b/.storybook/modules/toc.module.css index 5914ae0877..9ea699e95e 100644 --- a/.storybook/modules/toc.module.css +++ b/.storybook/modules/toc.module.css @@ -1,24 +1,14 @@ .Root { display: flex; flex-direction: row; - gap: 3rem; + gap: 2rem; align-items: flex-start; width: 100%; max-width: 56rem; } -.Nav { - display: flex; - flex-direction: column; - gap: 0.5rem; - width: 14rem; - position: sticky; - top: 0; - flex-shrink: 0; -} - .Title { - font-size: 0.75rem; + font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; @@ -34,17 +24,194 @@ } .Item { - padding-inline-start: calc(var(--depth, 0) * 0.75rem); + padding-inline-start: 0; +} + +.ItemNested { + padding-inline-start: calc((var(--depth) - 2) * 1rem); +} + +.Nav { + display: flex; + flex-direction: column; + gap: 0.75rem; + position: sticky; + top: 0; + width: 12rem; + align-self: flex-start; +} + +.HoverRoot { + gap: 0; + + & .Content { + padding-right: 2rem; + } +} + +.NavHover { + position: fixed; + right: 1.5rem; + top: 50%; + transform: translateY(-50%); + width: 0.875rem; + overflow: hidden; + border-radius: 0.5rem; + z-index: 50; + transition: + width 0.2s cubic-bezier(0.4, 0, 0.2, 1), + background 0.2s, + box-shadow 0.2s, + padding 0.15s; + + &[data-expanded] { + width: 11rem; + background: var(--demo-bg, white); + box-shadow: + 0 0 0 1px var(--demo-neutral-border, rgba(0, 0, 0, 0.08)), + 0 4px 16px rgba(0, 0, 0, 0.07); + padding: 0.375rem 0.5rem; + } +} + +.HoverList { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.HoverSwap { + display: grid; + perspective: 300px; + + & > * { + grid-area: 1 / 1; + backface-visibility: hidden; + transform-origin: top center; + } + + & > *[data-state='open'] { + animation: hover-flip-in 0.25s cubic-bezier(0.4, 0, 0.2, 1) 0.06s both; + } + + & > *[data-state='closed'] { + animation: hover-flip-out 0.15s cubic-bezier(0.4, 0, 0.2, 1) both; + } +} + +.HoverSkeletons { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.125rem 0.25rem; + color: var(--demo-neutral-fg-muted); +} + +.HoverLink { + display: block; + padding: 0.2rem 0.25rem; + font-size: 0.8125rem; + text-decoration: none; + border-radius: 0.3rem; + white-space: nowrap; + color: var(--demo-neutral-fg-muted); + transition: + color 0.15s, + background 0.15s; + + &:hover { + background: var(--demo-neutral-subtle); + } + + &[data-active] { + color: var(--demo-coral-fg); + font-weight: 500; + } +} +.HoverSwap [data-state] { + animation: none !important; + transition: none !important; } -.Link { +.SkeletonBar { display: block; + height: 2px; + border-radius: 9999px; + background: currentColor; + opacity: 0.3; +} + +.PinButton { + display: none; + align-items: center; + justify-content: flex-end; + width: 100%; + padding: 0 0.125rem 0.25rem; + background: none; + border: none; + cursor: pointer; + color: var(--demo-neutral-fg-muted); + + .NavHover[data-expanded] & { + display: flex; + } + + &:hover { + color: var(--demo-neutral-fg); + } +} + +@keyframes hover-flip-in { + from { + opacity: 0; + transform: rotateX(-20deg) translateY(-4px); + } + to { + opacity: 1; + transform: rotateX(0deg) translateY(0); + } +} + +@keyframes hover-flip-out { + from { + opacity: 1; + transform: rotateX(0deg) translateY(0); + } + to { + opacity: 0; + transform: rotateX(20deg) translateY(4px); + } +} + +.RootStacked { + flex-direction: column; + + & .Nav { + width: 100%; + order: -1; + z-index: 100; + background: var(--demo-bg, white); + padding-block: 0.5rem; + } + + & .Content { + height: auto; + overflow-y: visible; + padding-right: 0; + } +} + +.Link, +.LinkAnimated, +.LinkNumbered { padding: 0.3rem 0.5rem; font-size: 0.875rem; color: var(--demo-neutral-fg-muted); text-decoration: none; border-radius: 0.375rem; - transition: color 0.15s; &:hover { color: var(--demo-neutral-fg); @@ -61,6 +228,58 @@ } } +.Link, +.LinkAnimated { + display: block; + transition: color 0.15s; +} + +.LinkAnimated { + transition: + color 0.15s, + transform 0.2s; + + &[data-active] { + background-color: transparent; + transform: translateX(0); + transition: + color 0.15s, + background-color 0.7s ease-out, + transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + + @starting-style { + background-color: color-mix(in srgb, var(--demo-coral-solid) 28%, transparent); + transform: translateX(-8px); + } + } +} + +.TreeLink { + text-decoration: none; + color: inherit; + transition: color 0.15s; + + &[data-active] { + color: var(--demo-coral-fg); + font-weight: 500; + } +} + +.LinkNumbered { + display: flex; + align-items: center; + gap: 0.5rem; + transition: color 0.15s; +} + +.Number { + font-variant-numeric: tabular-nums; + font-size: 0.75rem; + opacity: 0.5; + min-width: 1.25rem; + flex-shrink: 0; +} + .Indicator { position: absolute; top: var(--top); @@ -77,7 +296,7 @@ .Content { flex: 1; min-width: 0; - height: 28rem; + height: 42rem; overflow-y: auto; padding-right: 1rem; @@ -115,102 +334,6 @@ } } -.SubList { - list-style: none; - padding: 0; - margin: 0; - overflow: hidden; - max-height: 0; - opacity: 0; - transition: - max-height 0.3s ease, - opacity 0.2s ease; - - &[data-visible] { - max-height: 20rem; - opacity: 1; - } -} - -.Root:has([data-placement='right']) { - flex-direction: row-reverse; -} - -.Controls { - display: flex; - flex-wrap: wrap; - gap: 0.375rem; -} - -.Stepper { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.625rem 0; - margin-bottom: 0.75rem; - border-bottom: 1px solid var(--demo-neutral-border); -} - -.StepCount { - font-size: 0.8125rem; - color: var(--demo-neutral-fg-muted); - min-width: 3rem; - text-align: center; -} - -.ActiveIds { - font-size: 0.8125rem; - color: var(--demo-neutral-fg-muted); - margin: 0; -} - -.LinkAnimated { - display: block; - padding: 0.3rem 0.5rem; - font-size: 0.875rem; - color: var(--demo-neutral-fg-muted); - text-decoration: none; - border-radius: 0.375rem; - transition: color 0.15s, background-color 0.5s ease-out; - - &:hover { - color: var(--demo-neutral-fg); - } - - &:focus-visible { - outline: 2px solid var(--demo-coral-focus-ring); - outline-offset: 2px; - } - - &[data-active] { - color: var(--demo-coral-fg); - font-weight: 500; - background-color: transparent; - - @starting-style { - background-color: color-mix(in srgb, var(--demo-coral-solid) 15%, transparent); - } - } -} - -.Progress { - height: 2px; - background: var(--demo-neutral-border); - border-radius: 9999px; - overflow: hidden; - margin-bottom: 0.25rem; -} - -.ProgressBar { - height: 100%; - width: 100%; - background: var(--demo-coral-solid); - border-radius: 9999px; - transform: scaleX(var(--progress, 0)); - transform-origin: left; - transition: transform 0.1s ease; -} - .Group { display: flex; flex-direction: column; @@ -227,65 +350,73 @@ padding-block: 0.5rem 0.25rem; } -.FloatingWrapper { - display: flex; - flex-direction: column; - max-width: 40rem; - width: 100%; -} - -.FloatingHeader { +.TriggerContent { display: flex; align-items: center; - justify-content: space-between; - padding-block: 0.5rem; - margin-bottom: 0.75rem; - border-bottom: 1px solid var(--demo-neutral-border); + justify-content: flex-start; + gap: 0.5rem; + flex: 1; + min-width: 0; } -.FloatingTitle { - font-size: 0.875rem; - font-weight: 600; - color: var(--demo-neutral-fg); +.ProgressRing { + flex-shrink: 0; + color: var(--demo-neutral-fg-muted); } -.FloatingNav { - min-width: 14rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - position: relative; +.TriggerLabel { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + animation: toc-label-in 0.25s ease; } -.LinkWithIcon { - display: flex; - align-items: center; - gap: 0.375rem; +.ProgressIndexText { + transform-origin: center; + transform-box: fill-box; + animation: toc-index-pop 0.2s ease-out; } -.LinkAmber { - color: #d97706; - font-weight: 600; - - &:hover { - color: #b45309; +@keyframes toc-label-in { + from { + opacity: 0; + transform: translateX(-4px); + } + to { + opacity: 1; + transform: translateX(0); } } -.LinkBlue { - color: #2563eb; - font-weight: 600; - - &:hover { - color: #1d4ed8; +@keyframes toc-index-pop { + from { + opacity: 0; + transform: scale(0.75); + } + to { + opacity: 1; + transform: scale(1); } } -.LinkGreen { - color: #059669; - font-weight: 600; +@media (max-width: 640px) { + .Root { + flex-direction: column; + } - &:hover { - color: #047857; + .Nav { + width: 100%; + order: -1; + position: sticky; + top: 0; + z-index: 100; + background: var(--demo-bg, white); + padding-block: 0.5rem; + } + + .Content { + height: auto; + overflow-y: visible; + padding-right: 0; } } diff --git a/bun.lock b/bun.lock index 4a4d8dd192..93dfbf53e0 100644 --- a/bun.lock +++ b/bun.lock @@ -331,6 +331,7 @@ "@zag-js/tree-view": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1", + "lorem-ipsum": "2.0.8", }, "devDependencies": { "@storybook/addon-a11y": "10.3.3", @@ -437,6 +438,7 @@ "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1", "@zag-js/vue": "1.39.1", + "lorem-ipsum": "2.0.8", }, "devDependencies": { "@biomejs/biome": "2.4.9", @@ -2183,7 +2185,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], @@ -2947,6 +2949,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lorem-ipsum": ["lorem-ipsum@2.0.8", "", { "dependencies": { "commander": "^9.3.0" }, "bin": { "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js" } }, "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], @@ -4213,6 +4217,8 @@ "@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "@bomb.sh/tab/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "@changesets/apply-release-plan/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -4809,6 +4815,8 @@ "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], diff --git a/packages/react/src/components/anatomy.ts b/packages/react/src/components/anatomy.ts index 70a0da37c5..e24f5109d0 100644 --- a/packages/react/src/components/anatomy.ts +++ b/packages/react/src/components/anatomy.ts @@ -45,6 +45,7 @@ export { tabsAnatomy } from './tabs/tabs.anatomy' export { tagsInputAnatomy } from './tags-input/tags-input.anatomy' export { timerAnatomy } from './timer/timer.anatomy' export { toastAnatomy } from './toast/toast.anatomy' +export { tocAnatomy } from './toc/toc.anatomy' export { toggleAnatomy } from './toggle/toggle.anatomy' export { toggleGroupAnatomy } from './toggle-group/toggle-group.anatomy' export { tooltipAnatomy } from './tooltip/tooltip.anatomy' diff --git a/packages/react/src/components/toc/examples/basic.tsx b/packages/react/src/components/toc/examples/basic.tsx index 3eb93164e7..e4a887908c 100644 --- a/packages/react/src/components/toc/examples/basic.tsx +++ b/packages/react/src/components/toc/examples/basic.tsx @@ -1,58 +1,40 @@ import { Toc } from '@ark-ui/react/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'introduction', depth: 2 }, - { value: 'getting-started', depth: 2 }, - { value: 'installation', depth: 2 }, - { value: 'usage', depth: 2 }, - { value: 'api-reference', depth: 2 }, + { value: 'introduction', depth: 5, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'migration', depth: 2, label: 'Migration' }, + { value: 'faq', depth: 2, label: 'FAQ' }, + { value: 'troubleshooting', depth: 2, label: 'Troubleshooting' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + export const Basic = () => ( -

Introduction

-

- A table of contents helps readers navigate long documents by providing quick links to each section. It - automatically highlights the section currently visible in the viewport. -

-

Getting Started

-

- To get started, pass an array of items to the root component. Each item has a value matching the heading id and - a depth matching the heading level. -

-

Installation

-

- Install the package using your preferred package manager. The component has no external dependencies beyond the - state machine. -

-

- You can install it via npm, yarn, pnpm, or bun. The package ships as an ES module and requires a bundler that - supports modern JavaScript. -

-

Usage

-

- Import the component and compose it using the compound component pattern. The Root component manages the - IntersectionObserver internally. -

-

- Each link in the TOC corresponds to a heading in the document. When the heading enters the viewport, its link - becomes active. -

-

API Reference

-

- The full API reference documents all props, events, and methods available on each component part. Refer to it - when customizing behavior. -

+ {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))}
- On this page {items.map((item) => ( - {item.value.replace(/-/g, ' ')} + + {item.label} + ))} diff --git a/packages/react/src/components/toc/examples/context.tsx b/packages/react/src/components/toc/examples/context.tsx deleted file mode 100644 index 005fc68c49..0000000000 --- a/packages/react/src/components/toc/examples/context.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'background', depth: 2 }, - { value: 'motivation', depth: 2 }, - { value: 'design-decisions', depth: 2 }, - { value: 'future-plans', depth: 2 }, -] - -export const Context = () => ( - - -

Background

-

- This project started as an internal tool before being open-sourced. The original motivation was to avoid - rebuilding the same components across multiple products. -

-

Motivation

-

- Existing solutions were either too opinionated about styling or too low-level to use productively. We needed - something in between. -

-

Design Decisions

-

- We chose state machines as the foundation because they make interaction logic explicit, testable, and shareable - across framework implementations. -

-

Future Plans

-

- The roadmap includes more components, improved accessibility tooling, and tighter integration with design token - workflows. -

-
- - - - {(toc) => ( - <> - On this page ({toc.activeIds.length} visible) - - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - - )} - - -
-) diff --git a/packages/react/src/components/toc/examples/controlled.tsx b/packages/react/src/components/toc/examples/controlled.tsx deleted file mode 100644 index 0ec0b3d986..0000000000 --- a/packages/react/src/components/toc/examples/controlled.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import { useState } from 'react' -import button from 'styles/button.module.css' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'setup', depth: 2 }, - { value: 'configuration', depth: 2 }, - { value: 'deployment', depth: 2 }, - { value: 'monitoring', depth: 2 }, -] - -export const Controlled = () => { - const [step, setStep] = useState(0) - - return ( - - -
- - - {step + 1} / {items.length} - - -
- -

Setup

-

- Configure your environment before beginning. You will need Node.js version 18 or higher and a package manager - of your choice. -

-

Configuration

-

- Edit the configuration file to match your project requirements. Most defaults work well for typical use cases. -

-

Deployment

-

- Deploy to your hosting provider of choice. The build output is a standard static site that works on any CDN. -

-

Monitoring

-

- After deploying, set up monitoring to track errors and performance. Connect your preferred observability - platform. -

-
- - - On this page - - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - -
- ) -} diff --git a/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx b/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx deleted file mode 100644 index 0b9939e77f..0000000000 --- a/packages/react/src/components/toc/examples/custom-scroll-behavior.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'alpha', depth: 2 }, - { value: 'beta', depth: 2 }, - { value: 'gamma', depth: 2 }, -] - -export const CustomScrollBehavior = () => { - const scrollToId = (e: React.MouseEvent, id: string) => { - e.preventDefault() - const el = document.getElementById(id) - if (el) { - const y = el.getBoundingClientRect().top + window.scrollY - 64 - window.scrollTo({ top: y, behavior: 'smooth' }) - } - } - - return ( - - -

Alpha

-

Section Alpha content.

-

Beta

-

Section Beta content.

-

Gamma

-

Section Gamma content.

-
- - On this page - - {items.map((item) => ( - - scrollToId(e, item.value)}> - {item.value.charAt(0).toUpperCase() + item.value.slice(1)} - - - ))} - - -
- ) -} diff --git a/packages/react/src/components/toc/examples/floating.tsx b/packages/react/src/components/toc/examples/floating.tsx deleted file mode 100644 index ed0df2a9dc..0000000000 --- a/packages/react/src/components/toc/examples/floating.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Popover } from '@ark-ui/react/popover' -import { Portal } from '@ark-ui/react/portal' -import { Toc } from '@ark-ui/react/toc' -import button from 'styles/button.module.css' -import popoverStyles from 'styles/popover.module.css' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'introduction', depth: 2 }, - { value: 'installation', depth: 2 }, - { value: 'usage', depth: 2 }, - { value: 'api-reference', depth: 2 }, -] - -export const Floating = () => ( - -
-
- Article - - Contents ↓ - - - - - On this page - - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - - - - - -
- - -

Introduction

-

A table of contents helps readers navigate long documents by providing quick links to each section.

-

Installation

-

- Install the package using your preferred package manager. The component has no external dependencies beyond - the state machine. -

-

Usage

-

- Import and compose using the compound component pattern. The Root manages the IntersectionObserver internally. -

-

API Reference

-

The full API reference documents all props, events, and methods available on each component part.

-
-
-
-) diff --git a/packages/react/src/components/toc/examples/grouped.tsx b/packages/react/src/components/toc/examples/grouped.tsx index af43e3081d..08f5e6bd2f 100644 --- a/packages/react/src/components/toc/examples/grouped.tsx +++ b/packages/react/src/components/toc/examples/grouped.tsx @@ -1,58 +1,56 @@ import { Toc } from '@ark-ui/react/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const groups = [ { label: 'Getting Started', items: [ - { value: 'overview', depth: 2 }, - { value: 'installation', depth: 2 }, + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'installation', depth: 2, label: 'Installation' }, ], }, { label: 'Advanced', items: [ - { value: 'configuration', depth: 2 }, - { value: 'plugins', depth: 2 }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'plugins', depth: 2, label: 'Plugins' }, ], }, { label: 'Reference', items: [ - { value: 'api', depth: 2 }, - { value: 'changelog', depth: 2 }, + { value: 'api', depth: 2, label: 'API' }, + { value: 'changelog', depth: 2, label: 'Changelog' }, ], }, ] +const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) + const allItems = groups.flatMap((g) => g.items) export const Grouped = () => ( -

Overview

-

Ark UI is a headless component library for building scalable design systems.

-

Installation

-

Install with your preferred package manager. No configuration needed to get started.

-

Configuration

-

Customize behavior through props. Most defaults work well out of the box.

-

Plugins

-

Extend functionality with plugins. Write your own or use community-contributed ones.

-

API

-

The full API reference covers all props, events, and methods on each component part.

-

Changelog

-

All notable changes are documented here following semantic versioning conventions.

+ {allItems.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))}
- {groups.map((group) => (
{group.label} {group.items.map((item) => ( - {item.value.replace(/-/g, ' ')} + + {item.label} + ))} diff --git a/packages/react/src/components/toc/examples/nested.tsx b/packages/react/src/components/toc/examples/nested.tsx index 25f0318aad..6f3977b42f 100644 --- a/packages/react/src/components/toc/examples/nested.tsx +++ b/packages/react/src/components/toc/examples/nested.tsx @@ -1,57 +1,42 @@ import { Toc } from '@ark-ui/react/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) const items = [ - { value: 'guides', depth: 2 }, - { value: 'quick-start', depth: 3 }, - { value: 'manual-setup', depth: 3 }, - { value: 'core-concepts', depth: 2 }, - { value: 'props', depth: 3 }, - { value: 'events', depth: 3 }, - { value: 'context', depth: 3 }, - { value: 'advanced', depth: 2 }, - { value: 'root-provider', depth: 3 }, - { value: 'custom-rendering', depth: 3 }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 3, label: 'Installation' }, + { value: 'configuration', depth: 3, label: 'Configuration' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'hooks', depth: 3, label: 'Hooks' }, + { value: 'components', depth: 3, label: 'Components' }, + { value: 'examples', depth: 2, label: 'Examples' }, ] export const Nested = () => ( -

Guides

-

Step-by-step guides for getting the most out of Ark UI in your projects.

-

Quick Start

-

Install the package and render your first component in under two minutes. No configuration required.

-

Manual Setup

-

For projects that need fine-grained control over the setup, follow the manual configuration guide.

-

Core Concepts

-

Understanding the core concepts helps you use the library more effectively.

-

Props

-

Props control the machine context. Pass them directly to the Root component or via the hook.

-

Events

-

Callback props like onValueChange fire when the machine transitions to a new state.

-

Context

-

Every component exposes a Context render prop for accessing the machine API without an extra hook.

-

Advanced

-

Advanced patterns for complex use cases.

-

Root Provider

-

- Use the RootProvider pattern to call the hook at a higher level and share the API with components outside the - tree. -

-

Custom Rendering

-

- Use the asChild prop to render any component part as a custom element while keeping all aria and event - attributes. -

+ {items.map((item) => { + const Heading = item.depth === 2 ? 'h2' : 'h3' + return ( +
+ {item.label} +

{paragraphs}

+
+ ) + })}
On this page - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} + 2 ? styles.ItemNested : styles.Item} key={item.value} item={item}> + + {item.label} + ))} diff --git a/packages/react/src/components/toc/examples/placement.tsx b/packages/react/src/components/toc/examples/placement.tsx deleted file mode 100644 index 94497ce4fd..0000000000 --- a/packages/react/src/components/toc/examples/placement.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import { useState } from 'react' -import button from 'styles/button.module.css' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'overview', depth: 2 }, - { value: 'installation', depth: 2 }, - { value: 'configuration', depth: 2 }, - { value: 'usage', depth: 2 }, -] - -export const Placement = () => { - const [placement, setPlacement] = useState<'left' | 'right'>('left') - - return ( - - -

Overview

-

The placement prop controls whether the navigation sits on the left or right side of the content.

-

Installation

-

Install the package and render your first component. No configuration required to get started.

-

Configuration

-

Edit the configuration file to match your project requirements. Most defaults work for typical use cases.

-

Usage

-

Import the component and compose it using the compound component pattern.

-
- - -
- - -
- On this page - - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - -
-
- ) -} diff --git a/packages/react/src/components/toc/examples/progressive-reveal.tsx b/packages/react/src/components/toc/examples/progressive-reveal.tsx deleted file mode 100644 index 32c26cd045..0000000000 --- a/packages/react/src/components/toc/examples/progressive-reveal.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Toc, type TocActiveChangeDetails } from '@ark-ui/react/toc' -import { useState } from 'react' -import styles from 'styles/toc.module.css' - -const sections = [ - { - item: { value: 'design-system', depth: 2 }, - children: [ - { value: 'typography', depth: 3 }, - { value: 'color-palette', depth: 3 }, - { value: 'spacing', depth: 3 }, - ], - }, - { - item: { value: 'components', depth: 2 }, - children: [ - { value: 'buttons', depth: 3 }, - { value: 'forms', depth: 3 }, - { value: 'navigation', depth: 3 }, - ], - }, - { - item: { value: 'patterns', depth: 2 }, - children: [ - { value: 'feedback', depth: 3 }, - { value: 'data-display', depth: 3 }, - ], - }, -] - -const allItems = sections.flatMap((s) => [s.item, ...s.children]) - -const label = (value: string) => value.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - -export const ProgressiveReveal = () => { - const [activeIds, setActiveIds] = useState([]) - - const activeParent = sections.find( - (s) => activeIds.includes(s.item.value) || s.children.some((c) => activeIds.includes(c.value)), - )?.item.value - - return ( - setActiveIds(details.activeIds)} - > - -

Design System

-

The foundation of visual consistency across products.

-

Typography

-

Type scales, font choices, and heading hierarchy.

-

Color Palette

-

Semantic color tokens for light and dark modes.

-

Spacing

-

Consistent spacing units and layout rhythm.

-

Components

-

Reusable building blocks for interfaces.

-

Buttons

-

Primary, secondary, and tertiary action triggers.

-

Forms

-

Input patterns and validation feedback.

- -

Menus, breadcrumbs, and wayfinding.

-

Patterns

-

Higher-level interaction and layout patterns.

-

Feedback

-

Toasts, alerts, and progress indicators.

-

Data Display

-

Tables, lists, and structured information.

-
- - - On this page - - - {sections.map((section) => ( - - {label(section.item.value)} -
    - {section.children.map((child) => ( - - {label(child.value)} - - ))} -
-
- ))} -
-
-
- ) -} diff --git a/packages/react/src/components/toc/examples/reading-progress.tsx b/packages/react/src/components/toc/examples/reading-progress.tsx deleted file mode 100644 index 10a7b807aa..0000000000 --- a/packages/react/src/components/toc/examples/reading-progress.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import { useRef, useState } from 'react' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'overview', depth: 2 }, - { value: 'architecture', depth: 2 }, - { value: 'state-machines', depth: 2 }, - { value: 'components', depth: 2 }, - { value: 'theming', depth: 2 }, -] - -export const ReadingProgress = () => { - const contentRef = useRef(null) - const [progress, setProgress] = useState(0) - - const handleScroll = () => { - const el = contentRef.current - if (!el) return - const { scrollTop, scrollHeight, clientHeight } = el - const total = scrollHeight - clientHeight - setProgress(total > 0 ? scrollTop / total : 0) - } - - return ( - - -

Overview

-

- Ark UI is a headless component library built on Zag.js state machines. It provides unstyled, accessible - components for building design systems. -

-

Architecture

-

- The library follows a layered architecture. At the base are Zag.js machines handling all interaction logic. - Framework adapters on top expose idiomatic React, Solid, Vue, and Svelte APIs. -

-

- Each component is a thin wrapper around the machine. Props flow down, events bubble up, and the machine - orchestrates all state transitions. -

-

State Machines

-

- State machines make interaction logic predictable and testable. Every possible state is explicitly defined, - making undefined states impossible by construction. -

-

Components

-

- Components use the compound component pattern. A Root component holds state and provides it to all child parts - via React context. -

-

Theming

-

- Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any - styling solution that works in your stack. -

-
- - - On this page -
-
-
- - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - - - ) -} diff --git a/packages/react/src/components/toc/examples/root-provider.tsx b/packages/react/src/components/toc/examples/root-provider.tsx index ad3c5c7b64..1732b1dbe1 100644 --- a/packages/react/src/components/toc/examples/root-provider.tsx +++ b/packages/react/src/components/toc/examples/root-provider.tsx @@ -1,49 +1,46 @@ import { Toc, useToc } from '@ark-ui/react/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'principles', depth: 2 }, - { value: 'patterns', depth: 2 }, - { value: 'testing', depth: 2 }, - { value: 'performance', depth: 2 }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'migration', depth: 2, label: 'Migration' }, + { value: 'faq', depth: 2, label: 'FAQ' }, + { value: 'troubleshooting', depth: 2, label: 'Troubleshooting' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + export const RootProvider = () => { const toc = useToc({ items }) return ( -

Principles

-

- Good software follows a set of guiding principles. These inform every decision from API design to - implementation details. -

-

Patterns

-

- Design patterns are reusable solutions to common problems. Learning them helps you recognize familiar - structures in new codebases. -

-

Testing

-

- Tests give you confidence that your code works as intended. A good test suite catches regressions before they - reach production. -

-

Performance

-

- Performance is a feature. Measure before optimizing, and optimize the things that matter most to your users. -

+ {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))}

Active: {toc.activeIds.length > 0 ? toc.activeIds.join(', ') : '—'}

- On this page {items.map((item) => ( - {item.value.replace(/-/g, ' ')} + + {item.label} + ))} diff --git a/packages/react/src/components/toc/examples/rtl.tsx b/packages/react/src/components/toc/examples/rtl.tsx deleted file mode 100644 index 0ee59e5f7a..0000000000 --- a/packages/react/src/components/toc/examples/rtl.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'rtl1', depth: 2 }, - { value: 'rtl2', depth: 2 }, - { value: 'rtl3', depth: 2 }, -] - -export const Rtl = () => ( - - -

مقدمة

-

هذا هو القسم الأول.

-

الاستخدام

-

هذا هو القسم الثاني.

-

الميزات

-

هذا هو القسم الثالث.

-
- - في هذه الصفحة - - {items.map((item) => ( - - - {item.value === 'rtl1' ? 'مقدمة' : item.value === 'rtl2' ? 'الاستخدام' : 'الميزات'} - - - ))} - - -
-) diff --git a/packages/react/src/components/toc/examples/theming.tsx b/packages/react/src/components/toc/examples/theming.tsx deleted file mode 100644 index d400e3c526..0000000000 --- a/packages/react/src/components/toc/examples/theming.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'custom1', depth: 2 }, - { value: 'custom2', depth: 2 }, - { value: 'custom3', depth: 2 }, -] - -export const Theming = () => ( - - -

Custom Style 1

-

Section with custom theming 1.

-

Custom Style 2

-

Section with custom theming 2.

-

Custom Style 3

-

Section with custom theming 3.

-
- - - - Custom Style 1 - - - Custom Style 2 - - - Custom Style 3 - - - -
-) diff --git a/packages/react/src/components/toc/examples/with-combobox.tsx b/packages/react/src/components/toc/examples/with-combobox.tsx new file mode 100644 index 0000000000..7edaf1f25d --- /dev/null +++ b/packages/react/src/components/toc/examples/with-combobox.tsx @@ -0,0 +1,83 @@ +import { Combobox, useListCollection } from '@ark-ui/react/combobox' +import { useFilter } from '@ark-ui/react/locale' +import { Portal } from '@ark-ui/react/portal' +import { Toc } from '@ark-ui/react/toc' +import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react' +import comboboxStyles from 'styles/combobox.module.css' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 6, units: 'paragraphs' }) + +const items = [ + { value: 'introduction', depth: 1, label: 'Introduction' }, + { value: 'installation', depth: 1, label: 'Installation' }, + { value: 'usage', depth: 1, label: 'Usage' }, + { value: 'api-reference', depth: 1, label: 'API Reference' }, + { value: 'examples', depth: 1, label: 'Examples' }, +] + +export const WithCombobox = () => { + const { contains } = useFilter({ sensitivity: 'base' }) + const { collection, filter } = useListCollection({ + initialItems: items.map(({ label, value }) => ({ label, value })), + filter: contains, + }) + + return ( + + + {items.map((item) => ( +
+

{item.label}

+

{p}

+
+ ))} +
+ + + filter(d.inputValue)} + onValueChange={(d) => { + document.getElementById(d.value[0])?.scrollIntoView({ behavior: 'smooth' }) + }} + > + + +
+ + + + + + +
+
+ + + + {collection.items.map((item) => ( + + {item.label} + + + + + ))} + + + +
+ + {items.map((item) => ( + + {item.label} + + ))} + +
+
+ ) +} diff --git a/packages/react/src/components/toc/examples/with-indicator.tsx b/packages/react/src/components/toc/examples/with-indicator.tsx index c08d6fc1a5..496a0272c8 100644 --- a/packages/react/src/components/toc/examples/with-indicator.tsx +++ b/packages/react/src/components/toc/examples/with-indicator.tsx @@ -1,61 +1,39 @@ import { Toc } from '@ark-ui/react/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'overview', depth: 2 }, - { value: 'architecture', depth: 2 }, - { value: 'state-machines', depth: 2 }, - { value: 'components', depth: 2 }, - { value: 'theming', depth: 2 }, - { value: 'accessibility', depth: 2 }, + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'architecture', depth: 2, label: 'Architecture' }, + { value: 'state-machines', depth: 2, label: 'State Machines' }, + { value: 'components', depth: 2, label: 'Components' }, + { value: 'theming', depth: 2, label: 'Theming' }, + { value: 'accessibility', depth: 2, label: 'Accessibility' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + export const WithIndicator = () => ( -

Overview

-

- Ark UI is a headless component library built on top of Zag.js state machines. It provides unstyled, accessible - components ready for your design system. -

-

Architecture

-

- The library follows a layered architecture. At the base are the Zag.js machines, which handle all interaction - logic. On top of that, framework adapters expose idiomatic React, Solid, Vue, and Svelte APIs. -

-

- Each component is a thin wrapper around the machine. Props flow down, events bubble up, and the machine - orchestrates all state transitions. -

-

State Machines

-

- State machines make the interaction logic predictable and testable. Every possible state is explicitly defined, - making it impossible to end up in an undefined state. -

-

Components

-

- Components are structured using the compound component pattern. A Root component holds state and provides it to - all child parts via React context. -

-

Theming

-

- Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any - styling solution that works in your stack. -

-

Accessibility

-

- All components follow WAI-ARIA patterns and are tested with screen readers. Keyboard navigation is built into - the state machine, not bolted on after the fact. -

+ {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))}
On this page - + {items.map((item) => ( - {item.value.replace(/-/g, ' ')} + + {item.label} + ))} diff --git a/packages/react/src/components/toc/examples/with-scroll-area.tsx b/packages/react/src/components/toc/examples/with-scroll-area.tsx deleted file mode 100644 index 5a95114f62..0000000000 --- a/packages/react/src/components/toc/examples/with-scroll-area.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Toc } from '@ark-ui/react/toc' -import { ScrollArea } from '@ark-ui/react/scroll-area' -import scrollAreaStyles from 'styles/scroll-area.module.css' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'a', depth: 2 }, - { value: 'b', depth: 2 }, - { value: 'c', depth: 2 }, - { value: 'd', depth: 2 }, - { value: 'e', depth: 2 }, - { value: 'f', depth: 2 }, -] - -export const WithScrollArea = () => ( - - -

Section A

-

Content for section A.

-

Section B

-

Content for section B.

-

Section C

-

Content for section C.

-

Section D

-

Content for section D.

-

Section E

-

Content for section E.

-

Section F

-

Content for section F.

-
- - - - - {items.map((item) => ( - - Section {item.value.toUpperCase()} - - ))} - - - - -
-) diff --git a/packages/react/src/components/toc/toc.stories.tsx b/packages/react/src/components/toc/toc.stories.tsx index 56f65d0e01..d971a821d1 100644 --- a/packages/react/src/components/toc/toc.stories.tsx +++ b/packages/react/src/components/toc/toc.stories.tsx @@ -7,19 +7,11 @@ const meta: Meta = { export default meta export { Basic } from './examples/basic' -export { Context } from './examples/context' -export { Controlled } from './examples/controlled' +export { Grouped } from './examples/grouped' export { Nested } from './examples/nested' -export { ProgressiveReveal } from './examples/progressive-reveal' export { RootProvider } from './examples/root-provider' +export { WithCollapsible } from './examples/with-collapsible' +export { WithHover } from './examples/with-hover' export { WithIndicator } from './examples/with-indicator' -export { WithScrollArea } from './examples/with-scroll-area' -export { CustomRendering } from './examples/custom-rendering' -export { CustomScrollBehavior } from './examples/custom-scroll-behavior' -export { HighlightOnEnter } from './examples/highlight-on-enter' -export { ReadingProgress } from './examples/reading-progress' -export { Grouped } from './examples/grouped' -export { Floating } from './examples/floating' -export { Rtl } from './examples/rtl' -export { Theming } from './examples/theming' -export { Placement } from './examples/placement' +export { WithNumbers } from './examples/with-numbers' +export { WithTreeView } from './examples/with-tree-view' diff --git a/packages/solid/src/components/anatomy.ts b/packages/solid/src/components/anatomy.ts index 70a0da37c5..e24f5109d0 100644 --- a/packages/solid/src/components/anatomy.ts +++ b/packages/solid/src/components/anatomy.ts @@ -45,6 +45,7 @@ export { tabsAnatomy } from './tabs/tabs.anatomy' export { tagsInputAnatomy } from './tags-input/tags-input.anatomy' export { timerAnatomy } from './timer/timer.anatomy' export { toastAnatomy } from './toast/toast.anatomy' +export { tocAnatomy } from './toc/toc.anatomy' export { toggleAnatomy } from './toggle/toggle.anatomy' export { toggleGroupAnatomy } from './toggle-group/toggle-group.anatomy' export { tooltipAnatomy } from './tooltip/tooltip.anatomy' diff --git a/packages/solid/src/components/index.tsx b/packages/solid/src/components/index.tsx index 9f918231ff..2d45002b92 100644 --- a/packages/solid/src/components/index.tsx +++ b/packages/solid/src/components/index.tsx @@ -53,6 +53,7 @@ export * from './tabs' export * from './tags-input' export * from './timer' export * from './toast' +export * from './toc' export * from './toggle-group' export * from './toggle' export * from './tooltip' diff --git a/packages/solid/src/components/toc/examples/context.tsx b/packages/solid/src/components/toc/examples/context.tsx new file mode 100644 index 0000000000..e644645689 --- /dev/null +++ b/packages/solid/src/components/toc/examples/context.tsx @@ -0,0 +1,45 @@ +import { Toc } from '@ark-ui/solid/toc' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) + +const items = [ + { value: 'background', depth: 2, label: 'Background' }, + { value: 'motivation', depth: 2, label: 'Motivation' }, + { value: 'design-decisions', depth: 2, label: 'Design Decisions' }, + { value: 'future-plans', depth: 2, label: 'Future Plans' }, +] + +export const Context = () => ( + + +

Background

+

{p}

+

Motivation

+

{p}

+

Design Decisions

+

{p}

+

Future Plans

+

{p}

+
+ + + + {(toc) => ( + <> + On this page ({toc.activeIds.length} visible) + + + {items.map((item) => ( + + {item.label} + + ))} + + + )} + + +
+) diff --git a/packages/react/src/components/toc/examples/custom-rendering.tsx b/packages/solid/src/components/toc/examples/custom-rendering.tsx similarity index 65% rename from packages/react/src/components/toc/examples/custom-rendering.tsx rename to packages/solid/src/components/toc/examples/custom-rendering.tsx index c776f7fd6f..abf0d767e8 100644 --- a/packages/react/src/components/toc/examples/custom-rendering.tsx +++ b/packages/solid/src/components/toc/examples/custom-rendering.tsx @@ -1,22 +1,25 @@ -import { Toc } from '@ark-ui/react/toc' -import { BookOpenIcon } from 'lucide-react' +import { Toc } from '@ark-ui/solid/toc' +import { BookOpenIcon } from 'lucide-solid' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) const items = [ - { value: 'introduction', depth: 2 }, - { value: 'usage', depth: 2 }, - { value: 'api', depth: 2 }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api', depth: 2, label: 'API' }, ] export const CustomRendering = () => (

Introduction

-

Custom rendering with icons.

+

{p}

Usage

-

How to use custom links in your ToC.

+

{p}

API

-

API reference section.

+

{p}

@@ -25,7 +28,7 @@ export const CustomRendering = () => ( - {item.value.charAt(0).toUpperCase() + item.value.slice(1)} + {item.label} diff --git a/packages/solid/src/components/toc/examples/floating.tsx b/packages/solid/src/components/toc/examples/floating.tsx new file mode 100644 index 0000000000..9aed9f9a4f --- /dev/null +++ b/packages/solid/src/components/toc/examples/floating.tsx @@ -0,0 +1,60 @@ +import { Popover } from '@ark-ui/solid/popover' +import { Portal } from '@ark-ui/solid/portal' +import { Toc } from '@ark-ui/solid/toc' +import { For } from 'solid-js' +import button from 'styles/button.module.css' +import popoverStyles from 'styles/popover.module.css' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 6, units: 'paragraphs' }) + +const items = [ + { value: 'introduction', depth: 1, label: 'Introduction' }, + { value: 'installation', depth: 1, label: 'Installation' }, + { value: 'usage', depth: 1, label: 'Usage' }, + { value: 'api-reference', depth: 1, label: 'API Reference' }, +] + +export const Floating = () => ( + +
+
+ Article + + Contents ↓ + + + + + On this page + + + + {(item) => ( + + {item.label} + + )} + + + + + + + +
+ + + + {(item) => ( +
+

{item.label}

+

{p}

+
+ )} +
+
+
+
+) diff --git a/packages/react/src/components/toc/examples/highlight-on-enter.tsx b/packages/solid/src/components/toc/examples/highlight-on-enter.tsx similarity index 57% rename from packages/react/src/components/toc/examples/highlight-on-enter.tsx rename to packages/solid/src/components/toc/examples/highlight-on-enter.tsx index 0026bdc4b3..93fdf82f6d 100644 --- a/packages/react/src/components/toc/examples/highlight-on-enter.tsx +++ b/packages/solid/src/components/toc/examples/highlight-on-enter.tsx @@ -1,5 +1,8 @@ -import { Toc } from '@ark-ui/react/toc' +import { Toc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) const items = [ { value: 'principles', depth: 2 }, @@ -13,26 +16,15 @@ export const HighlightOnEnter = () => (

Principles

-

- Good software follows a set of guiding principles that inform every decision from API design to implementation - details. -

+

{p}

Accessibility

-

- Every component ships with ARIA attributes and keyboard navigation baked in. Accessibility is not an - afterthought. -

+

{p}

Performance

-

Measure before optimizing. The bottleneck is rarely where you expect. Profile first, then act on real data.

+

{p}

Testing

-

- Tests give you confidence that your code works as intended. A good suite catches regressions before they reach - production. -

+

{p}

Tooling

-

- Great tooling removes friction. Invest in your development environment the same way you invest in your product. -

+

{p}

diff --git a/packages/solid/src/components/toc/examples/placement.tsx b/packages/solid/src/components/toc/examples/placement.tsx new file mode 100644 index 0000000000..8f13366ffd --- /dev/null +++ b/packages/solid/src/components/toc/examples/placement.tsx @@ -0,0 +1,61 @@ +import { Toc } from '@ark-ui/solid/toc' +import { createSignal } from 'solid-js' +import button from 'styles/button.module.css' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) + +const items = [ + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'usage', depth: 2, label: 'Usage' }, +] + +export const Placement = () => { + const [placement, setPlacement] = createSignal<'left' | 'right'>('left') + + return ( + + +

Overview

+

{p}

+

Installation

+

{p}

+

Configuration

+

{p}

+

Usage

+

{p}

+
+ + +
+ + +
+ On this page + + + {items.map((item) => ( + + {item.label} + + ))} + +
+
+ ) +} diff --git a/packages/solid/src/components/toc/examples/reading-progress.tsx b/packages/solid/src/components/toc/examples/reading-progress.tsx new file mode 100644 index 0000000000..ad9d5a891e --- /dev/null +++ b/packages/solid/src/components/toc/examples/reading-progress.tsx @@ -0,0 +1,58 @@ +import { Toc } from '@ark-ui/solid/toc' +import { createSignal } from 'solid-js' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) + +const items = [ + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'architecture', depth: 2, label: 'Architecture' }, + { value: 'state-machines', depth: 2, label: 'State Machines' }, + { value: 'components', depth: 2, label: 'Components' }, + { value: 'theming', depth: 2, label: 'Theming' }, +] + +export const ReadingProgress = () => { + let contentEl: HTMLElement | undefined + const [progress, setProgress] = createSignal(0) + + const handleScroll = () => { + if (!contentEl) return + const { scrollTop, scrollHeight, clientHeight } = contentEl + const total = scrollHeight - clientHeight + setProgress(total > 0 ? scrollTop / total : 0) + } + + return ( + + +

Overview

+

{p}

+

Architecture

+

{p}

+

State Machines

+

{p}

+

Components

+

{p}

+

Theming

+

{p}

+
+ + + On this page +
+
+
+ + + {items.map((item) => ( + + {item.label} + + ))} + + + + ) +} diff --git a/packages/solid/src/components/toc/examples/report.tsx b/packages/solid/src/components/toc/examples/report.tsx new file mode 100644 index 0000000000..3ab1b8d314 --- /dev/null +++ b/packages/solid/src/components/toc/examples/report.tsx @@ -0,0 +1,46 @@ +import { Toc } from '@ark-ui/solid/toc' +import { For } from 'solid-js' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 6, units: 'paragraphs' }) + +const items = [ + { value: 'executive-summary', depth: 2, label: 'Executive Summary' }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'methodology', depth: 2, label: 'Methodology' }, + { value: 'findings', depth: 2, label: 'Findings' }, + { value: 'recommendations', depth: 2, label: 'Recommendations' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, +] + +export const Report = () => ( + + + Table of Contents + + + {(item, index) => ( + + + {String(index() + 1).padStart(2, '0')} + {item.label} + + + )} + + + + + + + {(item) => ( +
+

{item.label}

+

{p}

+
+ )} +
+
+
+) diff --git a/packages/solid/src/components/toc/examples/with-combobox.tsx b/packages/solid/src/components/toc/examples/with-combobox.tsx new file mode 100644 index 0000000000..7b38007407 --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-combobox.tsx @@ -0,0 +1,92 @@ +import { Combobox, useListCollection } from '@ark-ui/solid/combobox' +import { useFilter } from '@ark-ui/solid/locale' +import { Toc } from '@ark-ui/solid/toc' +import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-solid' +import { For } from 'solid-js' +import { Portal } from 'solid-js/web' +import comboboxStyles from 'styles/combobox.module.css' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 1, units: 'paragraphs' }) + +const items = [ + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'examples', depth: 2, label: 'Examples' }, +] + +export const WithCombobox = () => { + const filterFn = useFilter({ sensitivity: 'base' }) + const { collection, filter } = useListCollection({ + initialItems: items.map(({ label, value }) => ({ label, value })), + filter: filterFn().contains, + }) + + return ( + + +

Introduction

+

{p}

+

Installation

+

{p}

+

Usage

+

{p}

+

API Reference

+

{p}

+

Examples

+

{p}

+
+ + + filter(d.inputValue)} + onValueChange={(d) => { + document.getElementById(d.value[0])?.scrollIntoView({ behavior: 'smooth' }) + }} + > + + +
+ + + + + + +
+
+ + + + + {(item) => ( + + {item.label} + + + + + )} + + + + +
+ + + {(item) => ( + + {item.label} + + )} + + +
+
+ ) +} diff --git a/packages/solid/src/components/toc/examples/with-steps.tsx b/packages/solid/src/components/toc/examples/with-steps.tsx new file mode 100644 index 0000000000..3d27a533a7 --- /dev/null +++ b/packages/solid/src/components/toc/examples/with-steps.tsx @@ -0,0 +1,69 @@ +import { Steps } from '@ark-ui/solid/steps' +import { Toc, type TocActiveChangeDetails } from '@ark-ui/solid/toc' +import { createSignal, For } from 'solid-js' +import button from 'styles/button.module.css' +import stepsStyles from 'styles/steps.module.css' +import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const p = loremIpsum({ count: 6, units: 'paragraphs' }) + +const items = [ + { value: 'setup', depth: 2, label: 'Setup' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'deployment', depth: 2, label: 'Deployment' }, + { value: 'monitoring', depth: 2, label: 'Monitoring' }, +] + +export const WithSteps = () => { + const [step, setStep] = createSignal(0) + + const handleStepChange = (d: { step: number }) => { + setStep(d.step) + document.getElementById(items[d.step]?.value)?.scrollIntoView({ behavior: 'smooth' }) + } + + const handleActiveChange = (d: TocActiveChangeDetails) => { + const index = items.findIndex((item) => item.value === d.activeIds[0]) + if (index >= 0) setStep(index) + } + + return ( + + + + {(item) => ( +
+

{item.label}

+

{p}

+
+ )} +
+
+ + + + + + {(item, index) => ( + + + {index() + 1} + {item.label} + + + + )} + + +
+ ← Back + + Next → + +
+
+
+
+ ) +} diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8f9fe75232..84d7b440b8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -209,7 +209,8 @@ "@zag-js/tour": "1.39.1", "@zag-js/tree-view": "1.39.1", "@zag-js/types": "1.39.1", - "@zag-js/utils": "1.39.1" + "@zag-js/utils": "1.39.1", + "lorem-ipsum": "2.0.8" }, "devDependencies": { "@storybook/addon-a11y": "10.3.3", diff --git a/packages/svelte/src/lib/components/anatomy.ts b/packages/svelte/src/lib/components/anatomy.ts index 70a0da37c5..e24f5109d0 100644 --- a/packages/svelte/src/lib/components/anatomy.ts +++ b/packages/svelte/src/lib/components/anatomy.ts @@ -45,6 +45,7 @@ export { tabsAnatomy } from './tabs/tabs.anatomy' export { tagsInputAnatomy } from './tags-input/tags-input.anatomy' export { timerAnatomy } from './timer/timer.anatomy' export { toastAnatomy } from './toast/toast.anatomy' +export { tocAnatomy } from './toc/toc.anatomy' export { toggleAnatomy } from './toggle/toggle.anatomy' export { toggleGroupAnatomy } from './toggle-group/toggle-group.anatomy' export { tooltipAnatomy } from './tooltip/tooltip.anatomy' diff --git a/packages/svelte/src/lib/components/index.ts b/packages/svelte/src/lib/components/index.ts index 59ce9945a1..6675fee575 100644 --- a/packages/svelte/src/lib/components/index.ts +++ b/packages/svelte/src/lib/components/index.ts @@ -54,6 +54,7 @@ export * from './tabs' export * from './tags-input' export * from './timer' export * from './toast' +export * from './toc' export * from './toggle' export * from './toggle-group' export * from './tooltip' diff --git a/packages/vue/package.json b/packages/vue/package.json index 6bddcd823a..0b3db4ebb5 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -152,7 +152,8 @@ "@zag-js/tree-view": "1.39.1", "@zag-js/types": "1.39.1", "@zag-js/utils": "1.39.1", - "@zag-js/vue": "1.39.1" + "@zag-js/vue": "1.39.1", + "lorem-ipsum": "2.0.8" }, "devDependencies": { "@biomejs/biome": "2.4.9", diff --git a/packages/vue/src/components/anatomy.ts b/packages/vue/src/components/anatomy.ts index a54556895e..827134bf7d 100644 --- a/packages/vue/src/components/anatomy.ts +++ b/packages/vue/src/components/anatomy.ts @@ -45,6 +45,7 @@ export { tabsAnatomy } from './tabs/tabs.anatomy' export { tagsInputAnatomy } from './tags-input/tags-input.anatomy' export { timerAnatomy } from './timer/timer.anatomy' export { toastAnatomy } from './toast/toast.anatomy' +export { tocAnatomy } from './toc/toc.anatomy' export { toggleGroupAnatomy } from './toggle-group/toggle-group.anatomy' export { toggleAnatomy } from './toggle/toggle.anatomy' export { tooltipAnatomy } from './tooltip/tooltip.anatomy' diff --git a/packages/vue/src/components/index.ts b/packages/vue/src/components/index.ts index 196d48a08b..fd584e61e3 100644 --- a/packages/vue/src/components/index.ts +++ b/packages/vue/src/components/index.ts @@ -53,6 +53,7 @@ export * from './tabs' export * from './tags-input' export * from './timer' export * from './toast' +export * from './toc' export * from './toggle' export * from './toggle-group' export * from './tooltip' From 956bf8ae4829f7edb34a06fea68ea2354c738eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Mon, 13 Apr 2026 15:31:41 +0200 Subject: [PATCH 5/7] fix: lint and test errors --- packages/solid/src/components/popover/popover-root.tsx | 2 -- packages/solid/src/components/toc/toc-indicator.tsx | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/solid/src/components/popover/popover-root.tsx b/packages/solid/src/components/popover/popover-root.tsx index 3ae62ff7ba..622b17359c 100644 --- a/packages/solid/src/components/popover/popover-root.tsx +++ b/packages/solid/src/components/popover/popover-root.tsx @@ -17,7 +17,6 @@ export const PopoverRoot = (props: PopoverRootProps) => { 'closeOnEscape', 'closeOnInteractOutside', 'defaultOpen', - 'finalFocusEl', 'id', 'ids', 'initialFocusEl', @@ -32,7 +31,6 @@ export const PopoverRoot = (props: PopoverRootProps) => { 'persistentElements', 'portalled', 'positioning', - 'restoreFocus', 'translations', 'triggerValue', 'defaultTriggerValue', diff --git a/packages/solid/src/components/toc/toc-indicator.tsx b/packages/solid/src/components/toc/toc-indicator.tsx index 396790d65d..03d459574c 100644 --- a/packages/solid/src/components/toc/toc-indicator.tsx +++ b/packages/solid/src/components/toc/toc-indicator.tsx @@ -2,11 +2,11 @@ import { mergeProps } from '@zag-js/solid' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { useTocContext } from './use-toc-context' -export interface TocIndicatorBaseProps extends PolymorphicProps {} +export interface TocIndicatorBaseProps extends PolymorphicProps<'div'> {} export interface TocIndicatorProps extends HTMLProps<'div'>, TocIndicatorBaseProps {} export const TocIndicator = (props: TocIndicatorProps) => { const toc = useTocContext() - const mergedProps = mergeProps(() => toc.getIndicatorProps(), props) + const mergedProps = mergeProps(() => toc.getRootProps(), props) return } From a2d26740ae9e4dab68e63e6f5be0aeaef7edb81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Mon, 13 Apr 2026 15:34:45 +0200 Subject: [PATCH 6/7] chore: update lockfile --- bun.lock | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index f6e4b97ab8..9555c8c296 100644 --- a/bun.lock +++ b/bun.lock @@ -2177,7 +2177,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], @@ -2941,8 +2941,6 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lorem-ipsum": ["lorem-ipsum@2.0.8", "", { "dependencies": { "commander": "^9.3.0" }, "bin": { "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js" } }, "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], @@ -4209,8 +4207,6 @@ "@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - "@bomb.sh/tab/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "@changesets/apply-release-plan/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -4807,8 +4803,6 @@ "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], From f52a53516b3a5bec6557600c3319cd003b3f871c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Dev=20=F0=9F=87=B7=F0=9F=87=BC?= Date: Tue, 14 Apr 2026 15:40:08 +0200 Subject: [PATCH 7/7] storybook fixes --- bun.lock | 9 +- packages/react/src/components/toc/use-toc.ts | 14 +- packages/solid/package.json | 5 +- .../src/components/popover/popover-root.tsx | 2 +- .../src/components/toc/examples/basic.tsx | 85 +++++----- .../src/components/toc/examples/context.tsx | 45 ----- .../toc/examples/custom-rendering.tsx | 39 ----- .../toc/examples/custom-scroll-behavior.tsx | 44 ----- .../src/components/toc/examples/floating.tsx | 60 ------- .../src/components/toc/examples/grouped.tsx | 78 +++++---- .../toc/examples/highlight-on-enter.tsx | 41 ----- .../src/components/toc/examples/nested.tsx | 93 +++++------ .../src/components/toc/examples/placement.tsx | 61 ------- .../toc/examples/reading-progress.tsx | 58 ------- .../src/components/toc/examples/report.tsx | 46 ------ .../components/toc/examples/root-provider.tsx | 52 +++--- .../toc/examples/with-collapsible.tsx | 156 +++++++++++------- .../components/toc/examples/with-combobox.tsx | 92 ----------- .../components/toc/examples/with-hover.tsx | 59 +++++-- .../toc/examples/with-indicator.tsx | 88 ++++------ .../components/toc/examples/with-numbers.tsx | 53 +++--- .../components/toc/examples/with-steps.tsx | 69 -------- .../toc/examples/with-tree-view.tsx | 130 ++++++++++++--- .../solid/src/components/toc/toc-content.tsx | 2 +- .../src/components/toc/toc-indicator.tsx | 2 +- .../solid/src/components/toc/toc-item.tsx | 4 +- .../solid/src/components/toc/toc-link.tsx | 7 +- packages/solid/src/components/toc/toc-nav.tsx | 4 +- .../src/components/toc/toc-root-provider.tsx | 13 +- .../solid/src/components/toc/toc-root.tsx | 8 +- .../solid/src/components/toc/toc-title.tsx | 5 +- .../src/components/toc/toc.stories copy.tsx | 17 -- .../solid/src/components/toc/toc.stories.tsx | 29 +--- .../toc/use-toc-item-props-context.ts | 1 - packages/solid/src/components/toc/use-toc.ts | 33 ++-- packages/svelte/.storybook/main.ts | 1 + 36 files changed, 519 insertions(+), 986 deletions(-) delete mode 100644 packages/solid/src/components/toc/examples/context.tsx delete mode 100644 packages/solid/src/components/toc/examples/custom-rendering.tsx delete mode 100644 packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx delete mode 100644 packages/solid/src/components/toc/examples/floating.tsx delete mode 100644 packages/solid/src/components/toc/examples/highlight-on-enter.tsx delete mode 100644 packages/solid/src/components/toc/examples/placement.tsx delete mode 100644 packages/solid/src/components/toc/examples/reading-progress.tsx delete mode 100644 packages/solid/src/components/toc/examples/report.tsx delete mode 100644 packages/solid/src/components/toc/examples/with-combobox.tsx delete mode 100644 packages/solid/src/components/toc/examples/with-steps.tsx delete mode 100644 packages/solid/src/components/toc/toc.stories copy.tsx diff --git a/bun.lock b/bun.lock index 9555c8c296..74c8265da6 100644 --- a/bun.lock +++ b/bun.lock @@ -221,6 +221,7 @@ "@zag-js/tree-view": "1.40.0", "@zag-js/types": "1.40.0", "@zag-js/utils": "1.40.0", + "lorem-ipsum": "2.0.8", }, "devDependencies": { "@biomejs/biome": "2.4.9", @@ -2177,7 +2178,7 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], @@ -2941,6 +2942,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lorem-ipsum": ["lorem-ipsum@2.0.8", "", { "dependencies": { "commander": "^9.3.0" }, "bin": { "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js" } }, "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], @@ -4207,6 +4210,8 @@ "@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "@bomb.sh/tab/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "@changesets/apply-release-plan/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -4803,6 +4808,8 @@ "stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], diff --git a/packages/react/src/components/toc/use-toc.ts b/packages/react/src/components/toc/use-toc.ts index 9a0e96ac61..c5b047968d 100644 --- a/packages/react/src/components/toc/use-toc.ts +++ b/packages/react/src/components/toc/use-toc.ts @@ -1,24 +1,26 @@ -import * as toc from '@zag-js/toc' import { type PropTypes, normalizeProps, useMachine } from '@zag-js/react' +import * as toc from '@zag-js/toc' import { useId } from 'react' import { useEnvironmentContext, useLocaleContext } from '../../providers' import type { Optional } from '../../types' export interface UseTocProps extends Optional, 'id'> {} + export interface UseTocReturn extends toc.Api {} -export const useToc = (props: UseTocProps): UseTocReturn => { +export const useToc = (props?: UseTocProps): UseTocReturn => { const id = useId() const { getRootNode } = useEnvironmentContext() const { dir } = useLocaleContext() - const context: toc.Props = { + const machineProps = { id, dir, getRootNode, + items: [], ...props, - } + } as toc.Props - const service = useMachine(toc.machine, context) - return toc.connect(service, normalizeProps) + const service = useMachine(toc.machine as any, machineProps) + return toc.connect(service as any, normalizeProps) } diff --git a/packages/solid/package.json b/packages/solid/package.json index da775bb500..bc8a70367f 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -111,9 +111,9 @@ "@zag-js/editable": "1.40.0", "@zag-js/file-upload": "1.40.0", "@zag-js/file-utils": "1.40.0", - "@zag-js/focus-visible": "1.40.0", "@zag-js/floating-panel": "1.40.0", "@zag-js/focus-trap": "1.40.0", + "@zag-js/focus-visible": "1.40.0", "@zag-js/highlight-word": "1.40.0", "@zag-js/hover-card": "1.40.0", "@zag-js/i18n-utils": "1.40.0", @@ -151,7 +151,8 @@ "@zag-js/tour": "1.40.0", "@zag-js/tree-view": "1.40.0", "@zag-js/types": "1.40.0", - "@zag-js/utils": "1.40.0" + "@zag-js/utils": "1.40.0", + "lorem-ipsum": "2.0.8" }, "devDependencies": { "@biomejs/biome": "2.4.9", diff --git a/packages/solid/src/components/popover/popover-root.tsx b/packages/solid/src/components/popover/popover-root.tsx index 622b17359c..ca37530201 100644 --- a/packages/solid/src/components/popover/popover-root.tsx +++ b/packages/solid/src/components/popover/popover-root.tsx @@ -35,7 +35,7 @@ export const PopoverRoot = (props: PopoverRootProps) => { 'triggerValue', 'defaultTriggerValue', 'onTriggerValueChange', - ]) + ] as const) const api = usePopover(usePopoverProps) const apiPresence = usePresence(mergeProps(presenceProps, () => ({ present: api().open }))) diff --git a/packages/solid/src/components/toc/examples/basic.tsx b/packages/solid/src/components/toc/examples/basic.tsx index eab9fff295..07d653dde4 100644 --- a/packages/solid/src/components/toc/examples/basic.tsx +++ b/packages/solid/src/components/toc/examples/basic.tsx @@ -1,54 +1,43 @@ import { Toc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'introduction', depth: 2 }, - { value: 'getting-started', depth: 2 }, - { value: 'installation', depth: 2 }, - { value: 'usage', depth: 2 }, - { value: 'api-reference', depth: 2 }, + { value: 'introduction', depth: 5, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'migration', depth: 2, label: 'Migration' }, + { value: 'faq', depth: 2, label: 'FAQ' }, + { value: 'troubleshooting', depth: 2, label: 'Troubleshooting' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] -export default function Basic() { - return ( - - -

Introduction

-

- A table of contents helps readers navigate long documents by providing quick links to each section. It - automatically highlights the section currently visible in the viewport. -

-

Getting Started

-

- To get started, pass an array of items to the root component. Each item has a value matching the heading id - and a depth matching the heading level. -

-

Installation

-

- Install the package using your preferred package manager. The component has no external dependencies beyond - the state machine. -

-

Usage

-

- Import the component and compose it using the compound component pattern. The Root component manages the - IntersectionObserver internally. -

-

API Reference

-

- The full API reference documents all props, events, and methods available on each component part. Refer to it - when customizing behavior. -

-
- - On this page - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - -
- ) -} +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + +export const Basic = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ + On this page + + {items.map((item) => ( + + + {item.label} + + + ))} + + +
+) diff --git a/packages/solid/src/components/toc/examples/context.tsx b/packages/solid/src/components/toc/examples/context.tsx deleted file mode 100644 index e644645689..0000000000 --- a/packages/solid/src/components/toc/examples/context.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'background', depth: 2, label: 'Background' }, - { value: 'motivation', depth: 2, label: 'Motivation' }, - { value: 'design-decisions', depth: 2, label: 'Design Decisions' }, - { value: 'future-plans', depth: 2, label: 'Future Plans' }, -] - -export const Context = () => ( - - -

Background

-

{p}

-

Motivation

-

{p}

-

Design Decisions

-

{p}

-

Future Plans

-

{p}

-
- - - - {(toc) => ( - <> - On this page ({toc.activeIds.length} visible) - - - {items.map((item) => ( - - {item.label} - - ))} - - - )} - - -
-) diff --git a/packages/solid/src/components/toc/examples/custom-rendering.tsx b/packages/solid/src/components/toc/examples/custom-rendering.tsx deleted file mode 100644 index abf0d767e8..0000000000 --- a/packages/solid/src/components/toc/examples/custom-rendering.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import { BookOpenIcon } from 'lucide-solid' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'introduction', depth: 2, label: 'Introduction' }, - { value: 'usage', depth: 2, label: 'Usage' }, - { value: 'api', depth: 2, label: 'API' }, -] - -export const CustomRendering = () => ( - - -

Introduction

-

{p}

-

Usage

-

{p}

-

API

-

{p}

-
- - - {items.map((item) => ( - - - - - {item.label} - - - - ))} - - -
-) diff --git a/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx b/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx deleted file mode 100644 index 84e27b320d..0000000000 --- a/packages/solid/src/components/toc/examples/custom-scroll-behavior.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import styles from 'styles/toc.module.css' - -const items = [ - { value: 'alpha', depth: 2 }, - { value: 'beta', depth: 2 }, - { value: 'gamma', depth: 2 }, -] - -export default function CustomScrollBehavior() { - const scrollToId = (e: MouseEvent, id: string) => { - e.preventDefault() - const el = document.getElementById(id) - if (el) { - const y = el.getBoundingClientRect().top + window.scrollY - 64 - window.scrollTo({ top: y, behavior: 'smooth' }) - } - } - - return ( - - -

Alpha

-

Section Alpha content.

-

Beta

-

Section Beta content.

-

Gamma

-

Section Gamma content.

-
- - On this page - - {items.map((item) => ( - - - {item.value.charAt(0).toUpperCase() + item.value.slice(1)} - - - ))} - - -
- ) -} diff --git a/packages/solid/src/components/toc/examples/floating.tsx b/packages/solid/src/components/toc/examples/floating.tsx deleted file mode 100644 index 9aed9f9a4f..0000000000 --- a/packages/solid/src/components/toc/examples/floating.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Popover } from '@ark-ui/solid/popover' -import { Portal } from '@ark-ui/solid/portal' -import { Toc } from '@ark-ui/solid/toc' -import { For } from 'solid-js' -import button from 'styles/button.module.css' -import popoverStyles from 'styles/popover.module.css' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 6, units: 'paragraphs' }) - -const items = [ - { value: 'introduction', depth: 1, label: 'Introduction' }, - { value: 'installation', depth: 1, label: 'Installation' }, - { value: 'usage', depth: 1, label: 'Usage' }, - { value: 'api-reference', depth: 1, label: 'API Reference' }, -] - -export const Floating = () => ( - -
-
- Article - - Contents ↓ - - - - - On this page - - - - {(item) => ( - - {item.label} - - )} - - - - - - - -
- - - - {(item) => ( -
-

{item.label}

-

{p}

-
- )} -
-
-
-
-) diff --git a/packages/solid/src/components/toc/examples/grouped.tsx b/packages/solid/src/components/toc/examples/grouped.tsx index defb28cad7..36e5f52577 100644 --- a/packages/solid/src/components/toc/examples/grouped.tsx +++ b/packages/solid/src/components/toc/examples/grouped.tsx @@ -1,63 +1,61 @@ import { Toc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const groups = [ { label: 'Getting Started', items: [ - { value: 'overview', depth: 2 }, - { value: 'installation', depth: 2 }, + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'installation', depth: 2, label: 'Installation' }, ], }, { label: 'Advanced', items: [ - { value: 'configuration', depth: 2 }, - { value: 'plugins', depth: 2 }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'plugins', depth: 2, label: 'Plugins' }, ], }, { label: 'Reference', items: [ - { value: 'api', depth: 2 }, - { value: 'changelog', depth: 2 }, + { value: 'api', depth: 2, label: 'API' }, + { value: 'changelog', depth: 2, label: 'Changelog' }, ], }, ] +const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) + const allItems = groups.flatMap((g) => g.items) -export default function Grouped() { - return ( - - -

Overview

-

Ark UI is a headless component library for building scalable design systems.

-

Installation

-

Install with your preferred package manager. No configuration needed to get started.

-

Configuration

-

Customize behavior through props. Most defaults work well out of the box.

-

Plugins

-

Extend functionality with plugins. Write your own or use community-contributed ones.

-

API

-

The full API reference covers all props, events, and methods on each component part.

-

Changelog

-

All notable changes are documented here following semantic versioning conventions.

-
- - {groups.map((group) => ( -
-

{group.label}

- - {group.items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - -
- ))} -
-
- ) -} +export const Grouped = () => ( + + + {allItems.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ + + {groups.map((group) => ( +
+ {group.label} + + {group.items.map((item) => ( + + + {item.label} + + + ))} + +
+ ))} +
+
+) diff --git a/packages/solid/src/components/toc/examples/highlight-on-enter.tsx b/packages/solid/src/components/toc/examples/highlight-on-enter.tsx deleted file mode 100644 index 93fdf82f6d..0000000000 --- a/packages/solid/src/components/toc/examples/highlight-on-enter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'principles', depth: 2 }, - { value: 'accessibility', depth: 2 }, - { value: 'performance', depth: 2 }, - { value: 'testing', depth: 2 }, - { value: 'tooling', depth: 2 }, -] - -export const HighlightOnEnter = () => ( - - -

Principles

-

{p}

-

Accessibility

-

{p}

-

Performance

-

{p}

-

Testing

-

{p}

-

Tooling

-

{p}

-
- - - On this page - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - -
-) diff --git a/packages/solid/src/components/toc/examples/nested.tsx b/packages/solid/src/components/toc/examples/nested.tsx index 17dff9a3f2..63a157cc31 100644 --- a/packages/solid/src/components/toc/examples/nested.tsx +++ b/packages/solid/src/components/toc/examples/nested.tsx @@ -1,60 +1,45 @@ import { Toc } from '@ark-ui/solid/toc' +import { Dynamic } from 'solid-js/web' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' + +const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) const items = [ - { value: 'guides', depth: 2 }, - { value: 'quick-start', depth: 3 }, - { value: 'manual-setup', depth: 3 }, - { value: 'core-concepts', depth: 2 }, - { value: 'props', depth: 3 }, - { value: 'events', depth: 3 }, - { value: 'context', depth: 3 }, - { value: 'advanced', depth: 2 }, - { value: 'root-provider', depth: 3 }, - { value: 'custom-rendering', depth: 3 }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 3, label: 'Installation' }, + { value: 'configuration', depth: 3, label: 'Configuration' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'hooks', depth: 3, label: 'Hooks' }, + { value: 'components', depth: 3, label: 'Components' }, + { value: 'examples', depth: 2, label: 'Examples' }, ] -export default function Nested() { - return ( - - -

Guides

-

Step-by-step guides for getting the most out of Ark UI in your projects.

-

Quick Start

-

Install the package and render your first component in under two minutes. No configuration required.

-

Manual Setup

-

For projects that need fine-grained control over the setup, follow the manual configuration guide.

-

Core Concepts

-

Understanding the core concepts helps you use the library more effectively.

-

Props

-

Props control the machine context. Pass them directly to the Root component or via the hook.

-

Events

-

Callback props like onValueChange fire when the machine transitions to a new state.

-

Context

-

Every component exposes a Context render prop for accessing the machine API without an extra hook.

-

Advanced

-

Advanced patterns for complex use cases.

-

Root Provider

-

- Use the RootProvider pattern to call the hook at a higher level and share the API with components outside the - tree. -

-

Custom Rendering

-

- Use the asChild prop to render any component part as a custom element while keeping all aria and event - attributes. -

-
- - On this page - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - -
- ) -} +export const Nested = () => ( + + + {items.map((item) => ( +
+ + {item.label} + +

{paragraphs}

+
+ ))} +
+ + + On this page + + {items.map((item) => ( + 2 ? styles.ItemNested : styles.Item} item={item}> + + {item.label} + + + ))} + + +
+) diff --git a/packages/solid/src/components/toc/examples/placement.tsx b/packages/solid/src/components/toc/examples/placement.tsx deleted file mode 100644 index 8f13366ffd..0000000000 --- a/packages/solid/src/components/toc/examples/placement.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import { createSignal } from 'solid-js' -import button from 'styles/button.module.css' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'overview', depth: 2, label: 'Overview' }, - { value: 'installation', depth: 2, label: 'Installation' }, - { value: 'configuration', depth: 2, label: 'Configuration' }, - { value: 'usage', depth: 2, label: 'Usage' }, -] - -export const Placement = () => { - const [placement, setPlacement] = createSignal<'left' | 'right'>('left') - - return ( - - -

Overview

-

{p}

-

Installation

-

{p}

-

Configuration

-

{p}

-

Usage

-

{p}

-
- - -
- - -
- On this page - - - {items.map((item) => ( - - {item.label} - - ))} - -
-
- ) -} diff --git a/packages/solid/src/components/toc/examples/reading-progress.tsx b/packages/solid/src/components/toc/examples/reading-progress.tsx deleted file mode 100644 index ad9d5a891e..0000000000 --- a/packages/solid/src/components/toc/examples/reading-progress.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import { createSignal } from 'solid-js' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'overview', depth: 2, label: 'Overview' }, - { value: 'architecture', depth: 2, label: 'Architecture' }, - { value: 'state-machines', depth: 2, label: 'State Machines' }, - { value: 'components', depth: 2, label: 'Components' }, - { value: 'theming', depth: 2, label: 'Theming' }, -] - -export const ReadingProgress = () => { - let contentEl: HTMLElement | undefined - const [progress, setProgress] = createSignal(0) - - const handleScroll = () => { - if (!contentEl) return - const { scrollTop, scrollHeight, clientHeight } = contentEl - const total = scrollHeight - clientHeight - setProgress(total > 0 ? scrollTop / total : 0) - } - - return ( - - -

Overview

-

{p}

-

Architecture

-

{p}

-

State Machines

-

{p}

-

Components

-

{p}

-

Theming

-

{p}

-
- - - On this page -
-
-
- - - {items.map((item) => ( - - {item.label} - - ))} - - - - ) -} diff --git a/packages/solid/src/components/toc/examples/report.tsx b/packages/solid/src/components/toc/examples/report.tsx deleted file mode 100644 index 3ab1b8d314..0000000000 --- a/packages/solid/src/components/toc/examples/report.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Toc } from '@ark-ui/solid/toc' -import { For } from 'solid-js' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 6, units: 'paragraphs' }) - -const items = [ - { value: 'executive-summary', depth: 2, label: 'Executive Summary' }, - { value: 'introduction', depth: 2, label: 'Introduction' }, - { value: 'methodology', depth: 2, label: 'Methodology' }, - { value: 'findings', depth: 2, label: 'Findings' }, - { value: 'recommendations', depth: 2, label: 'Recommendations' }, - { value: 'conclusion', depth: 2, label: 'Conclusion' }, -] - -export const Report = () => ( - - - Table of Contents - - - {(item, index) => ( - - - {String(index() + 1).padStart(2, '0')} - {item.label} - - - )} - - - - - - - {(item) => ( -
-

{item.label}

-

{p}

-
- )} -
-
-
-) diff --git a/packages/solid/src/components/toc/examples/root-provider.tsx b/packages/solid/src/components/toc/examples/root-provider.tsx index 9e1b9a1836..6a10f4b8cd 100644 --- a/packages/solid/src/components/toc/examples/root-provider.tsx +++ b/packages/solid/src/components/toc/examples/root-provider.tsx @@ -1,45 +1,45 @@ import { Toc, useToc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'principles', depth: 2 }, - { value: 'patterns', depth: 2 }, - { value: 'testing', depth: 2 }, - { value: 'performance', depth: 2 }, + { value: 'introduction', depth: 2, label: 'Introduction' }, + { value: 'getting-started', depth: 2, label: 'Getting Started' }, + { value: 'installation', depth: 2, label: 'Installation' }, + { value: 'usage', depth: 2, label: 'Usage' }, + { value: 'configuration', depth: 2, label: 'Configuration' }, + { value: 'migration', depth: 2, label: 'Migration' }, + { value: 'faq', depth: 2, label: 'FAQ' }, + { value: 'troubleshooting', depth: 2, label: 'Troubleshooting' }, + { value: 'api-reference', depth: 2, label: 'API Reference' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] -export default function RootProvider() { +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + +export const RootProvider = () => { const toc = useToc({ items }) + return ( -

Principles

-

- Good software follows a set of guiding principles. These inform every decision from API design to - implementation details. -

-

Patterns

-

- Design patterns are reusable solutions to common problems. Learning them helps you recognize familiar - structures in new codebases. -

-

Testing

-

- Tests give you confidence that your code works as intended. A good test suite catches regressions before they - reach production. -

-

Performance

-

- Performance is a feature. Measure before optimizing, and optimize the things that matter most to your users. -

+ {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))}
+ +

Active: {toc().activeIds.length > 0 ? toc().activeIds.join(', ') : '—'}

On this page - {items.map((item) => ( - {item.value.replace(/-/g, ' ')} + + {item.label} + ))} diff --git a/packages/solid/src/components/toc/examples/with-collapsible.tsx b/packages/solid/src/components/toc/examples/with-collapsible.tsx index 6d7b2cd2c2..3439a9dba1 100644 --- a/packages/solid/src/components/toc/examples/with-collapsible.tsx +++ b/packages/solid/src/components/toc/examples/with-collapsible.tsx @@ -1,7 +1,13 @@ +import { Collapsible } from '@ark-ui/solid/collapsible' import { Toc } from '@ark-ui/solid/toc' -import { createSignal } from 'solid-js' +import { loremIpsum } from 'lorem-ipsum' +import { ChevronRightIcon } from 'lucide-solid' +import { createMemo } from 'solid-js' +import CollapsibleStyles from 'styles/Collapsible.module.css' import styles from 'styles/toc.module.css' +const p = loremIpsum({ count: 7, units: 'paragraphs' }) + const items = [ { value: 'introduction', depth: 2, label: 'Introduction' }, { value: 'installation', depth: 2, label: 'Installation' }, @@ -10,68 +16,100 @@ const items = [ { value: 'examples', depth: 2, label: 'Examples' }, ] -export default function WithCollapsible() { - const [open, setOpen] = createSignal(false) - const [progress, setProgress] = createSignal(0) - - // Simulate progress for demo - // In a real app, calculate based on scroll or active section - const handleToggle = () => { - setOpen((prev) => !prev) - setProgress((prev) => (prev >= 1 ? 0 : prev + 0.2)) - } +const RADIUS = 14 +const CIRCUMFERENCE = 2 * Math.PI * RADIUS - const RADIUS = 14 - const CIRCUMFERENCE = 2 * Math.PI * RADIUS - const dashArray = () => `${progress() * CIRCUMFERENCE} ${CIRCUMFERENCE}` +export const WithCollapsible = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{p}

+
+ ))} +
- return ( - - - {items.map((item) => ( -
-

{item.label}

-

Section content for {item.label}.

-
- ))} -
- - - {open() && ( + + + + {(toc) => { + const api = toc() + const activeIndex = createMemo(() => { + const items_ = api.activeItems + return items_[0] ? items.findIndex((i) => i.value === items_[0].value) : -1 + }) + const activeLabel = createMemo(() => { + const idx = activeIndex() + return idx >= 0 ? items[idx].label : undefined + }) + const progress = createMemo(() => { + const idx = activeIndex() + return idx >= 0 ? (idx + 1) / items.length : 0 + }) + const dashArray = createMemo(() => `${progress() * CIRCUMFERENCE} ${CIRCUMFERENCE}`) + return ( + + + + + + {activeLabel() ?? 'On this page'} + + + + + + ) + }} + + - {items.map((item) => ( + {items.map((item, index) => ( - {item.label} + + {String(index + 1).padStart(2, '0')} + {item.label} + ))} - )} - -
- ) -} + + +
+ +) diff --git a/packages/solid/src/components/toc/examples/with-combobox.tsx b/packages/solid/src/components/toc/examples/with-combobox.tsx deleted file mode 100644 index 7b38007407..0000000000 --- a/packages/solid/src/components/toc/examples/with-combobox.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Combobox, useListCollection } from '@ark-ui/solid/combobox' -import { useFilter } from '@ark-ui/solid/locale' -import { Toc } from '@ark-ui/solid/toc' -import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-solid' -import { For } from 'solid-js' -import { Portal } from 'solid-js/web' -import comboboxStyles from 'styles/combobox.module.css' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 1, units: 'paragraphs' }) - -const items = [ - { value: 'introduction', depth: 2, label: 'Introduction' }, - { value: 'installation', depth: 2, label: 'Installation' }, - { value: 'usage', depth: 2, label: 'Usage' }, - { value: 'api-reference', depth: 2, label: 'API Reference' }, - { value: 'examples', depth: 2, label: 'Examples' }, -] - -export const WithCombobox = () => { - const filterFn = useFilter({ sensitivity: 'base' }) - const { collection, filter } = useListCollection({ - initialItems: items.map(({ label, value }) => ({ label, value })), - filter: filterFn().contains, - }) - - return ( - - -

Introduction

-

{p}

-

Installation

-

{p}

-

Usage

-

{p}

-

API Reference

-

{p}

-

Examples

-

{p}

-
- - - filter(d.inputValue)} - onValueChange={(d) => { - document.getElementById(d.value[0])?.scrollIntoView({ behavior: 'smooth' }) - }} - > - - -
- - - - - - -
-
- - - - - {(item) => ( - - {item.label} - - - - - )} - - - - -
- - - {(item) => ( - - {item.label} - - )} - - -
-
- ) -} diff --git a/packages/solid/src/components/toc/examples/with-hover.tsx b/packages/solid/src/components/toc/examples/with-hover.tsx index 171b8b16ce..9931a2d4d0 100644 --- a/packages/solid/src/components/toc/examples/with-hover.tsx +++ b/packages/solid/src/components/toc/examples/with-hover.tsx @@ -1,5 +1,8 @@ import { Toc } from '@ark-ui/solid/toc' +import { Swap } from '@ark-ui/solid/swap' +import { Pin, PinOff } from 'lucide-solid' import { createSignal } from 'solid-js' +import { loremIpsum } from 'lorem-ipsum' import styles from 'styles/toc.module.css' const items = [ @@ -11,20 +14,22 @@ const items = [ { value: 'examples', depth: 2, label: 'Examples' }, ] -export default function WithHover() { +const paragraphs = loremIpsum({ count: 7, units: 'paragraphs' }) + +export const WithHover = () => { const [pinned, setPinned] = createSignal(false) const [hovered, setHovered] = createSignal(false) return ( - - + +
{items.map((item) => (

{item.label}

-

Section content for {item.label}.

+

{paragraphs}

))} - +
setPinned((p) => !p)} + onClick={() => setPinned((v) => !v)} aria-label={pinned() ? 'Unpin navigation' : 'Pin navigation'} > - {pinned() ? 'Unpin' : 'Pin'} + + + + + + + + - - {items.map((item) => ( - - {item.label} - - ))} - + + + +
+ {items.map((item) => ( + + ))} +
+
+ +
+ + {items.map((item) => ( + + + {item.label} + + + ))} + +
+
+
) diff --git a/packages/solid/src/components/toc/examples/with-indicator.tsx b/packages/solid/src/components/toc/examples/with-indicator.tsx index 69800af8f4..35e4a8dc3c 100644 --- a/packages/solid/src/components/toc/examples/with-indicator.tsx +++ b/packages/solid/src/components/toc/examples/with-indicator.tsx @@ -1,60 +1,42 @@ import { Toc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ - { value: 'overview', depth: 2 }, - { value: 'architecture', depth: 2 }, - { value: 'state-machines', depth: 2 }, - { value: 'components', depth: 2 }, - { value: 'theming', depth: 2 }, - { value: 'accessibility', depth: 2 }, + { value: 'overview', depth: 2, label: 'Overview' }, + { value: 'architecture', depth: 2, label: 'Architecture' }, + { value: 'state-machines', depth: 2, label: 'State Machines' }, + { value: 'components', depth: 2, label: 'Components' }, + { value: 'theming', depth: 2, label: 'Theming' }, + { value: 'accessibility', depth: 2, label: 'Accessibility' }, + { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] -export default function WithIndicator() { - return ( - - -

Overview

-

- Ark UI is a headless component library built on top of Zag.js state machines. It provides unstyled, accessible - components ready for your design system. -

-

Architecture

-

- The library follows a layered architecture. At the base are the Zag.js machines, which handle all interaction - logic. On top of that, framework adapters expose idiomatic React, Solid, Vue, and Svelte APIs. -

-

State Machines

-

- State machines make the interaction logic predictable and testable. Every possible state is explicitly - defined, making it impossible to end up in an undefined state. -

-

Components

-

- Components are structured using the compound component pattern. A Root component holds state and provides it - to all child parts via context. -

-

Theming

-

- Because components ship without styles, you own every pixel. Bring your own CSS, CSS modules, Tailwind, or any - styling solution that works in your stack. -

-

Accessibility

-

- All components follow WAI-ARIA patterns and are tested with screen readers. Keyboard navigation is built into - the state machine, not bolted on after the fact. -

-
- +const paragraphs = loremIpsum({ count: 6, units: 'paragraphs' }) + +export const WithIndicator = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ + + On this page + - - {items.map((item) => ( - - {item.value.replace(/-/g, ' ')} - - ))} - - -
- ) -} + {items.map((item) => ( + + + {item.label} + + + ))} + +
+
+) diff --git a/packages/solid/src/components/toc/examples/with-numbers.tsx b/packages/solid/src/components/toc/examples/with-numbers.tsx index a6c49f99a2..1f9d86a4c3 100644 --- a/packages/solid/src/components/toc/examples/with-numbers.tsx +++ b/packages/solid/src/components/toc/examples/with-numbers.tsx @@ -1,5 +1,6 @@ import { Toc } from '@ark-ui/solid/toc' import styles from 'styles/toc.module.css' +import { loremIpsum } from 'lorem-ipsum' const items = [ { value: 'executive-summary', depth: 2, label: 'Executive Summary' }, @@ -9,30 +10,30 @@ const items = [ { value: 'conclusion', depth: 2, label: 'Conclusion' }, ] -export default function WithNumbers() { - return ( - - - {items.map((item) => ( -
-

{item.label}

-

Section content for {item.label}.

-
+const paragraphs = loremIpsum({ count: 5, units: 'paragraphs' }) + +export const WithNumbers = () => ( + + + {items.map((item) => ( +
+

{item.label}

+

{paragraphs}

+
+ ))} +
+ + Contents + + {items.map((item, index) => ( + + + {String(index + 1).padStart(2, '0')} + {item.label} + + ))} -
- - Contents - - {items.map((item, index) => ( - - - {String(index + 1).padStart(2, '0')} - {item.label} - - - ))} - - -
- ) -} + + +
+) diff --git a/packages/solid/src/components/toc/examples/with-steps.tsx b/packages/solid/src/components/toc/examples/with-steps.tsx deleted file mode 100644 index 3d27a533a7..0000000000 --- a/packages/solid/src/components/toc/examples/with-steps.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Steps } from '@ark-ui/solid/steps' -import { Toc, type TocActiveChangeDetails } from '@ark-ui/solid/toc' -import { createSignal, For } from 'solid-js' -import button from 'styles/button.module.css' -import stepsStyles from 'styles/steps.module.css' -import styles from 'styles/toc.module.css' -import { loremIpsum } from 'lorem-ipsum' - -const p = loremIpsum({ count: 6, units: 'paragraphs' }) - -const items = [ - { value: 'setup', depth: 2, label: 'Setup' }, - { value: 'configuration', depth: 2, label: 'Configuration' }, - { value: 'deployment', depth: 2, label: 'Deployment' }, - { value: 'monitoring', depth: 2, label: 'Monitoring' }, -] - -export const WithSteps = () => { - const [step, setStep] = createSignal(0) - - const handleStepChange = (d: { step: number }) => { - setStep(d.step) - document.getElementById(items[d.step]?.value)?.scrollIntoView({ behavior: 'smooth' }) - } - - const handleActiveChange = (d: TocActiveChangeDetails) => { - const index = items.findIndex((item) => item.value === d.activeIds[0]) - if (index >= 0) setStep(index) - } - - return ( - - - - {(item) => ( -
-

{item.label}

-

{p}

-
- )} -
-
- - - - - - {(item, index) => ( - - - {index() + 1} - {item.label} - - - - )} - - -
- ← Back - - Next → - -
-
-
-
- ) -} diff --git a/packages/solid/src/components/toc/examples/with-tree-view.tsx b/packages/solid/src/components/toc/examples/with-tree-view.tsx index aa25802fcc..52130f3d04 100644 --- a/packages/solid/src/components/toc/examples/with-tree-view.tsx +++ b/packages/solid/src/components/toc/examples/with-tree-view.tsx @@ -1,7 +1,21 @@ -import { Toc } from '@ark-ui/solid/toc' -import styles from 'styles/toc.module.css' +import { Toc, useTocContext } from '@ark-ui/solid/toc' +import { TreeView, createTreeCollection } from '@ark-ui/solid/tree-view' +import { loremIpsum } from 'lorem-ipsum' +import { ChevronRightIcon } from 'lucide-solid' +import { createSignal } from 'solid-js' +import tocStyles from 'styles/toc.module.css' +import treeStyles from 'styles/tree-view.module.css' -const sections = [ +const p = loremIpsum({ count: 7, units: 'paragraphs' }) + +type TocNode = { + id: string + name: string + depth: number + children?: TocNode[] +} + +const sections: TocNode[] = [ { id: 'guides', name: 'Guides', @@ -32,36 +46,100 @@ const sections = [ }, ] -const flattenSections = (nodes) => - nodes.flatMap((node) => [node, ...(node.children ? flattenSections(node.children) : [])]) +const collection = createTreeCollection({ + nodeToValue: (node) => node.id, + nodeToString: (node) => node.name, + rootNode: { id: 'ROOT', name: '', depth: 0, children: sections }, +}) -const items = flattenSections(sections).map(({ id, name, depth }) => ({ value: id, label: name, depth })) +const allItems = sections.flatMap((section) => [ + { value: section.id, depth: section.depth }, + ...(section.children ?? []).map((child) => ({ value: child.id, depth: child.depth })), +]) -export default function WithTreeView() { +const TocTreeNode = ({ node, indexPath }: TreeView.NodeProviderProps) => { + const toc = useTocContext() return ( - - + + {node.children ? ( + + + + + + + + {node.name} + + + + + + {node.children.map((child, index) => ( + + ))} + + + ) : ( + + + + {node.name} + + + + )} + + ) +} + +export const WithTreeView = () => { + const [expandedValue, setExpandedValue] = createSignal([]) + + return ( + { + const activeIds = new Set(activeItems.map((i) => i.value)) + const next = sections + .filter( + (section) => activeIds.has(section.id) || (section.children ?? []).some((child) => activeIds.has(child.id)), + ) + .map((s) => s.id) + setExpandedValue(next) + }} + > + {sections.map((section) => ( -
+

{section.name}

- {section.children && - section.children.map((child) => ( -

- {child.name} -

- ))} -
+

{p}

+ {section.children?.map((child) => ( +
+

{child.name}

+

{p}

+
+ ))} + ))}
- - On this page - - {items.map((item) => ( - - {item.label} - - ))} - + + + On this page + setExpandedValue(next)} + > + + {sections.map((node, index) => ( + + ))} + +
) diff --git a/packages/solid/src/components/toc/toc-content.tsx b/packages/solid/src/components/toc/toc-content.tsx index 00c700d2bb..c5ea251590 100644 --- a/packages/solid/src/components/toc/toc-content.tsx +++ b/packages/solid/src/components/toc/toc-content.tsx @@ -1,6 +1,6 @@ import { type HTMLProps, type PolymorphicProps, ark } from '../factory' -export interface TocContentBaseProps extends PolymorphicProps {} +export interface TocContentBaseProps extends PolymorphicProps<'article'> {} export interface TocContentProps extends HTMLProps<'article'>, TocContentBaseProps {} export const TocContent = (props: TocContentProps) => { diff --git a/packages/solid/src/components/toc/toc-indicator.tsx b/packages/solid/src/components/toc/toc-indicator.tsx index 03d459574c..d8f95812be 100644 --- a/packages/solid/src/components/toc/toc-indicator.tsx +++ b/packages/solid/src/components/toc/toc-indicator.tsx @@ -7,6 +7,6 @@ export interface TocIndicatorProps extends HTMLProps<'div'>, TocIndicatorBasePro export const TocIndicator = (props: TocIndicatorProps) => { const toc = useTocContext() - const mergedProps = mergeProps(() => toc.getRootProps(), props) + const mergedProps = mergeProps(() => toc().getIndicatorProps(), props) return } diff --git a/packages/solid/src/components/toc/toc-item.tsx b/packages/solid/src/components/toc/toc-item.tsx index cc3b03338f..a393a23bc8 100644 --- a/packages/solid/src/components/toc/toc-item.tsx +++ b/packages/solid/src/components/toc/toc-item.tsx @@ -5,7 +5,7 @@ import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { useTocContext } from './use-toc-context' import { TocItemPropsProvider } from './use-toc-item-props-context' -export interface TocItemBaseProps extends ItemProps, PolymorphicProps {} +export interface TocItemBaseProps extends ItemProps, PolymorphicProps<'li'> {} export interface TocItemProps extends HTMLProps<'li'>, TocItemBaseProps {} const splitItemProps = createSplitProps() @@ -13,7 +13,7 @@ const splitItemProps = createSplitProps() export const TocItem = (props: TocItemProps) => { const [itemProps, localProps] = splitItemProps(props, ['item']) const toc = useTocContext() - const mergedProps = mergeProps(() => toc.getItemProps(itemProps), localProps) + const mergedProps = mergeProps(() => toc().getItemProps(itemProps), localProps) return ( diff --git a/packages/solid/src/components/toc/toc-link.tsx b/packages/solid/src/components/toc/toc-link.tsx index 93ec06766d..4c9d8e2c49 100644 --- a/packages/solid/src/components/toc/toc-link.tsx +++ b/packages/solid/src/components/toc/toc-link.tsx @@ -2,13 +2,14 @@ import { mergeProps } from '@zag-js/solid' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { useTocContext } from './use-toc-context' import { useTocItemPropsContext } from './use-toc-item-props-context' +import type { Assign } from 'src/types' -export interface TocLinkBaseProps extends PolymorphicProps {} -export interface TocLinkProps extends HTMLProps<'a'>, TocLinkBaseProps {} +export interface TocLinkBaseProps extends PolymorphicProps<'a'> {} +export interface TocLinkProps extends Assign, TocLinkBaseProps> {} export const TocLink = (props: TocLinkProps) => { const toc = useTocContext() const itemProps = useTocItemPropsContext() - const mergedProps = mergeProps(() => toc.getLinkProps(itemProps), props) + const mergedProps = mergeProps(() => toc().getLinkProps(itemProps), props) return } diff --git a/packages/solid/src/components/toc/toc-nav.tsx b/packages/solid/src/components/toc/toc-nav.tsx index e13e8de25f..98c56a166f 100644 --- a/packages/solid/src/components/toc/toc-nav.tsx +++ b/packages/solid/src/components/toc/toc-nav.tsx @@ -2,7 +2,7 @@ import { mergeProps } from '@zag-js/solid' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { useTocContext } from './use-toc-context' -export interface TocNavBaseProps extends PolymorphicProps { +export interface TocNavBaseProps extends PolymorphicProps<'nav'> { placement?: 'left' | 'right' } export interface TocNavProps extends HTMLProps<'nav'>, TocNavBaseProps {} @@ -10,6 +10,6 @@ export interface TocNavProps extends HTMLProps<'nav'>, TocNavBaseProps {} export const TocNav = (props: TocNavProps) => { const { placement, ...rest } = props const toc = useTocContext() - const mergedProps = mergeProps(() => toc.getRootProps(), rest) + const mergedProps = mergeProps(() => toc().getRootProps(), rest) return } diff --git a/packages/solid/src/components/toc/toc-root-provider.tsx b/packages/solid/src/components/toc/toc-root-provider.tsx index 672f95ed35..2d8917838f 100644 --- a/packages/solid/src/components/toc/toc-root-provider.tsx +++ b/packages/solid/src/components/toc/toc-root-provider.tsx @@ -1,4 +1,4 @@ -import type { Assign } from '../../types' +import { mergeProps } from '@zag-js/solid' import { createSplitProps } from '../../utils/create-split-props' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import type { UseTocReturn } from './use-toc' @@ -8,17 +8,16 @@ interface RootProviderProps { value: UseTocReturn } -export interface TocRootProviderBaseProps extends RootProviderProps, PolymorphicProps {} -export interface TocRootProviderProps extends Assign, TocRootProviderBaseProps> {} - -const splitRootProviderProps = createSplitProps() +export interface TocRootProviderBaseProps extends RootProviderProps, PolymorphicProps<'div'> {} +export interface TocRootProviderProps extends HTMLProps<'div'>, TocRootProviderBaseProps {} export const TocRootProvider = (props: TocRootProviderProps) => { - const [{ value: toc }, localProps] = splitRootProviderProps(props, ['value']) + const [{ value: toc }, localProps] = createSplitProps()(props, ['value']) + const mergedProps = mergeProps(() => toc().getRootProps(), localProps) return ( - + ) } diff --git a/packages/solid/src/components/toc/toc-root.tsx b/packages/solid/src/components/toc/toc-root.tsx index c98160f0cf..88d227dd02 100644 --- a/packages/solid/src/components/toc/toc-root.tsx +++ b/packages/solid/src/components/toc/toc-root.tsx @@ -1,10 +1,11 @@ +import { mergeProps } from '@zag-js/solid' import type { Assign } from '../../types' import { createSplitProps } from '../../utils/create-split-props' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { type UseTocProps, useToc } from './use-toc' import { TocProvider } from './use-toc-context' -export interface TocRootBaseProps extends UseTocProps, PolymorphicProps {} +export interface TocRootBaseProps extends UseTocProps, PolymorphicProps<'div'> {} export interface TocRootProps extends Assign, TocRootBaseProps> {} const splitRootProps = createSplitProps() @@ -23,11 +24,12 @@ export const TocRoot = (props: TocRootProps) => { 'scrollBehavior', 'threshold', ]) - const toc = useToc({ ...useTocProps, id: useTocProps.id ?? undefined }) + const toc = useToc(useTocProps) + const mergedProps = mergeProps(() => toc().getRootProps(), localProps) return ( - + ) } diff --git a/packages/solid/src/components/toc/toc-title.tsx b/packages/solid/src/components/toc/toc-title.tsx index e2218c06e7..958f8e032c 100644 --- a/packages/solid/src/components/toc/toc-title.tsx +++ b/packages/solid/src/components/toc/toc-title.tsx @@ -1,13 +1,12 @@ import { mergeProps } from '@zag-js/solid' -import { splitProps } from 'solid-js' import { type HTMLProps, type PolymorphicProps, ark } from '../factory' import { useTocContext } from './use-toc-context' -export interface TocTitleBaseProps extends PolymorphicProps {} +export interface TocTitleBaseProps extends PolymorphicProps<'h2'> {} export interface TocTitleProps extends HTMLProps<'h2'>, TocTitleBaseProps {} export const TocTitle = (props: TocTitleProps) => { const toc = useTocContext() - const mergedProps = mergeProps(() => toc.getTitleProps(), props) + const mergedProps = mergeProps(() => toc().getTitleProps(), props) return } diff --git a/packages/solid/src/components/toc/toc.stories copy.tsx b/packages/solid/src/components/toc/toc.stories copy.tsx deleted file mode 100644 index d971a821d1..0000000000 --- a/packages/solid/src/components/toc/toc.stories copy.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta } from '@storybook/react-vite' - -const meta: Meta = { - title: 'Components / Toc', -} - -export default meta - -export { Basic } from './examples/basic' -export { Grouped } from './examples/grouped' -export { Nested } from './examples/nested' -export { RootProvider } from './examples/root-provider' -export { WithCollapsible } from './examples/with-collapsible' -export { WithHover } from './examples/with-hover' -export { WithIndicator } from './examples/with-indicator' -export { WithNumbers } from './examples/with-numbers' -export { WithTreeView } from './examples/with-tree-view' diff --git a/packages/solid/src/components/toc/toc.stories.tsx b/packages/solid/src/components/toc/toc.stories.tsx index eb2fbabd6c..6109b4cae2 100644 --- a/packages/solid/src/components/toc/toc.stories.tsx +++ b/packages/solid/src/components/toc/toc.stories.tsx @@ -1,14 +1,4 @@ import type { Meta } from 'storybook-solidjs-vite' -import Basic from './examples/basic' -import Grouped from './examples/grouped' -import Nested from './examples/nested' -import CustomScrollBehavior from './examples/custom-scroll-behavior' -import RootProvider from './examples/root-provider' -import WithIndicator from './examples/with-indicator' -import WithTreeView from './examples/with-tree-view' -import WithCollapsible from './examples/with-collapsible' -import WithHover from './examples/with-hover' -import WithNumbers from './examples/with-numbers' const meta: Meta = { title: 'Components / Toc', @@ -16,13 +6,12 @@ const meta: Meta = { export default meta -export const BasicExample = () => -export const GroupedExample = () => -export const NestedExample = () => -export const CustomScrollBehaviorExample = () => -export const RootProviderExample = () => -export const WithIndicatorExample = () => -export const WithTreeViewExample = () => -export const WithCollapsibleExample = () => -export const WithHoverExample = () => -export const WithNumbersExample = () => +export { Basic } from './examples/basic' +export { Grouped } from './examples/grouped' +export { Nested } from './examples/nested' +export { RootProvider } from './examples/root-provider' +export { WithCollapsible } from './examples/with-collapsible' +export { WithHover } from './examples/with-hover' +export { WithIndicator } from './examples/with-indicator' +export { WithNumbers } from './examples/with-numbers' +export { WithTreeView } from './examples/with-tree-view' diff --git a/packages/solid/src/components/toc/use-toc-item-props-context.ts b/packages/solid/src/components/toc/use-toc-item-props-context.ts index 4b63341aa2..fdca2143e6 100644 --- a/packages/solid/src/components/toc/use-toc-item-props-context.ts +++ b/packages/solid/src/components/toc/use-toc-item-props-context.ts @@ -4,7 +4,6 @@ import { createContext } from '../../utils/create-context' export interface UseTocItemPropsContext extends ItemProps {} export const [TocItemPropsProvider, useTocItemPropsContext] = createContext({ - name: 'TocItemPropsContext', hookName: 'useTocItemPropsContext', providerName: '', }) diff --git a/packages/solid/src/components/toc/use-toc.ts b/packages/solid/src/components/toc/use-toc.ts index 6a818d22a9..5e0b39c9a4 100644 --- a/packages/solid/src/components/toc/use-toc.ts +++ b/packages/solid/src/components/toc/use-toc.ts @@ -1,25 +1,28 @@ +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/solid' import * as toc from '@zag-js/toc' -import { normalizeProps, useMachine, type PropTypes } from '@zag-js/solid' -import { createMemo, createUniqueId, type Accessor } from 'solid-js' -import { useEnvironmentContext } from '../../providers/environment' -import { useLocaleContext } from '../../providers/locale' -import { runIfFn } from '../../utils/run-if-fn' +import { type Accessor, createMemo, createUniqueId } from 'solid-js' +import { useEnvironmentContext, useLocaleContext } from '../../providers' +import type { Optional } from '../../types' -export interface UseTocProps extends Omit {} +export interface UseTocProps extends Optional, 'id'> {} export interface UseTocReturn extends Accessor> {} -export const useToc = (props: UseTocProps) => { +export const useToc = (props?: UseTocProps): UseTocReturn => { const id = createUniqueId() const locale = useLocaleContext() const environment = useEnvironmentContext() - const machineProps = createMemo(() => ({ - id, - dir: locale().dir, - getRootNode: environment().getRootNode, - ...runIfFn(props), - })) + const machineProps = createMemo( + () => + ({ + id: props?.id ?? id, + dir: locale().dir, + getRootNode: environment().getRootNode, + items: props?.items ?? [], + ...props, + }) as toc.Props, + ) - const service = useMachine(toc.machine, machineProps) - return toc.connect(service, normalizeProps) + const service = useMachine(toc.machine as any, machineProps) + return createMemo(() => toc.connect(service as any, normalizeProps)) } diff --git a/packages/svelte/.storybook/main.ts b/packages/svelte/.storybook/main.ts index f9818917c6..b6fcdd3736 100644 --- a/packages/svelte/.storybook/main.ts +++ b/packages/svelte/.storybook/main.ts @@ -20,6 +20,7 @@ const config: StorybookConfig = { // Prevent Vite from pre-bundling .svelte files through esbuild config.optimizeDeps ??= {} config.optimizeDeps.noDiscovery = true + config.optimizeDeps.exclude = ['svelte', '@sveltejs/kit', '@ark-ui/svelte'] return config },