Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 415 additions & 0 deletions .storybook/modules/toc.module.css

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions packages/react/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { dirname, resolve } from 'node:path'
import { createHash } from 'node:crypto'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { StorybookConfig } from '@storybook/react-vite'

const __dirname = dirname(fileURLToPath(import.meta.url))
const stylesDir = resolve(__dirname, '../../../.storybook/modules')

const config: StorybookConfig = {
stories: ['../src/components/**/*.stories.tsx', '../src/providers/**/*.stories.tsx'],
Expand All @@ -21,7 +23,15 @@ const config: StorybookConfig = {
config.resolve ??= {}
config.resolve.alias ??= {}
// @ts-expect-error - alias type mismatch
config.resolve.alias.styles = resolve(__dirname, '../../../.storybook/modules')
config.resolve.alias.styles = stylesDir
config.css ??= {}
config.css.modules = {
...config.css.modules,
generateScopedName: (name, filename) => {
const hash = createHash('md5').update(relative(stylesDir, filename)).digest('hex').slice(0, 5)
return `_${name}_${hash}`
},
}
return config
},
}
Expand Down
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"tags input",
"timer",
"toast",
"toc",
"toggle group",
"tooltip",
"tree view",
Expand Down Expand Up @@ -149,6 +150,7 @@
"@zag-js/toggle-group": "1.40.0",
"@zag-js/tooltip": "1.40.0",
"@zag-js/tour": "1.40.0",
"@zag-js/toc": "1.40.0",
"@zag-js/tree-view": "1.40.0",
"@zag-js/types": "1.40.0",
"@zag-js/utils": "1.40.0"
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/anatomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
47 changes: 47 additions & 0 deletions packages/react/src/components/toc/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Toc } from '@ark-ui/react/toc'
import { useRef } from 'react'
import styles from 'styles/toc.module.css'

const items = [
{ value: 'introduction', depth: 2, label: 'Introduction', lines: 12 },
{ value: 'getting-started', depth: 2, label: 'Getting Started', lines: 10 },
{ value: 'installation', depth: 2, label: 'Installation', lines: 8 },
{ value: 'usage', depth: 2, label: 'Usage', lines: 14 },
{ value: 'conclusion', depth: 2, label: 'Conclusion', lines: 10 },
]

export const Basic = () => {
const contentRef = useRef<HTMLDivElement>(null)

return (
<Toc.Root className={styles.Root} items={items} getScrollEl={() => contentRef.current}>
<Toc.Content className={styles.Content} ref={contentRef}>
{items.map((item) => (
<section key={item.value} className={styles.Section}>
<h2 id={item.value} className={styles.Heading} data-depth={item.depth}>
{item.label}
</h2>
<div className={styles.DummyText}>
{Array.from({ length: item.lines }).map((_, i) => (
<div key={i} className={styles.DummyLine} />
))}
</div>
</section>
))}
</Toc.Content>

<Toc.Nav className={styles.Nav}>
<Toc.Title className={styles.Title}>On this page</Toc.Title>
<Toc.List className={styles.List}>
{items.map((item) => (
<Toc.Item className={styles.Item} key={item.value} item={item}>
<Toc.Link className={styles.Link} href={`#${item.value}`}>
{item.label}
</Toc.Link>
</Toc.Item>
))}
</Toc.List>
</Toc.Nav>
</Toc.Root>
)
}
50 changes: 50 additions & 0 deletions packages/react/src/components/toc/examples/nested.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Toc } from '@ark-ui/react/toc'
import { useRef } from 'react'
import styles from 'styles/toc.module.css'

const items = [
{ value: 'importance', depth: 2, label: 'Importance', lines: 10 },
{ value: 'integrations', depth: 2, label: 'Integrations', lines: 12 },
{ value: 'free-blocks', depth: 3, label: 'Free Blocks', lines: 8 },
{ value: 'configuration', depth: 3, label: 'Configuration', lines: 14 },
{ value: 'api-reference', depth: 2, label: 'API Reference', lines: 10 },
{ value: 'hooks', depth: 3, label: 'Hooks', lines: 8 },
{ value: 'components', depth: 3, label: 'Components', lines: 12 },
{ value: 'examples', depth: 2, label: 'Examples', lines: 10 },
]

export const Nested = () => {
const contentRef = useRef<HTMLDivElement>(null)

return (
<Toc.Root className={`${styles.Root}`} items={items} getScrollEl={() => contentRef.current}>
<Toc.Content className={styles.Content} ref={contentRef}>
{items.map((item) => (
<section key={item.value}>
<div id={item.value} className={styles.Heading} data-depth={item.depth}>
{item.label}
</div>
<div className={styles.DummyText}>
{Array.from({ length: item.lines }).map((_, i) => (
<div key={i} className={styles.DummyLine} />
))}
</div>
</section>
))}
</Toc.Content>

<Toc.Nav className={styles.Nav}>
<Toc.Title className={styles.Title}>On this page</Toc.Title>
<Toc.List className={styles.List}>
{items.map((item) => (
<Toc.Item className={item.depth > 2 ? styles.ItemNested : styles.Item} key={item.value} item={item}>
<Toc.Link className={styles.Link} href={`#${item.value}`}>
{item.label}
</Toc.Link>
</Toc.Item>
))}
</Toc.List>
</Toc.Nav>
</Toc.Root>
)
}
55 changes: 55 additions & 0 deletions packages/react/src/components/toc/examples/root-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Toc, useToc } from '@ark-ui/react/toc'
import { useRef } from 'react'
import styles from 'styles/toc.module.css'

const items = [
{ value: 'introduction', depth: 2, label: 'Introduction', lines: 12 },
{ value: 'getting-started', depth: 2, label: 'Getting Started', lines: 10 },
{ value: 'installation', depth: 2, label: 'Installation', lines: 8 },
{ value: 'usage', depth: 2, label: 'Usage', lines: 14 },
{ value: 'conclusion', depth: 2, label: 'Conclusion', lines: 10 },
]

export const RootProvider = () => {
const contentRef = useRef<HTMLDivElement | null>(null)

const toc = useToc({
items,
getScrollEl: () => contentRef.current,
})

return (
<div className="vstack" style={{ width: '100%' }}>
<output>activeIds: {JSON.stringify(toc.activeIds)}</output>
<Toc.RootProvider className={styles.Root} value={toc}>
<Toc.Content className={styles.Content} ref={contentRef}>
{items.map((item) => (
<section key={item.value} className={styles.Section}>
<h2 id={item.value} className={styles.Heading} data-depth={item.depth}>
{item.label}
</h2>
<div className={styles.DummyText}>
{Array.from({ length: item.lines }).map((_, i) => (
<div key={i} className={styles.DummyLine} />
))}
</div>
</section>
))}
</Toc.Content>

<Toc.Nav className={styles.Nav}>
<Toc.Title className={styles.Title}>On this page</Toc.Title>
<Toc.List className={styles.List}>
{items.map((item) => (
<Toc.Item className={styles.Item} key={item.value} item={item}>
<Toc.Link className={styles.Link} href={`#${item.value}`}>
{item.label}
</Toc.Link>
</Toc.Item>
))}
</Toc.List>
</Toc.Nav>
</Toc.RootProvider>
</div>
)
}
107 changes: 107 additions & 0 deletions packages/react/src/components/toc/examples/with-collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Collapsible } from '@ark-ui/react/collapsible'
import { Toc } from '@ark-ui/react/toc'
import { ChevronRightIcon } from 'lucide-react'
import CollapsibleStyles from 'styles/collapsible.module.css'
import styles from 'styles/toc.module.css'
import { useRef } from 'react'

const items = [
{ value: 'overview', depth: 2, label: 'Overview', lines: 8 },
{ value: 'prerequisites', depth: 2, label: 'Prerequisites', lines: 5 },
{ value: 'quick-start', depth: 2, label: 'Quick Start', lines: 20 },
{ value: 'commands', depth: 2, label: 'Core Commands', lines: 15 },
{ value: 'troubleshooting', depth: 2, label: 'Troubleshooting', lines: 12 },
]

export const WithCollapsible = () => {
const contentRef = useRef<HTMLDivElement | null>(null)

return (
<Toc.Root className={styles.Root} data-stacked items={items} getScrollEl={() => contentRef.current}>
<Collapsible.Root className={CollapsibleStyles.Root} style={{ width: '100%' }}>
<Toc.Context>
{({ activeItems }) => {
const activeIndex = items.findIndex((i) => i.value === activeItems[0]?.value)
const activeLabel = items[activeIndex]?.label ?? 'On this page'

return (
<Collapsible.Trigger className={CollapsibleStyles.Trigger}>
<span className={styles.TriggerContent}>
<Ring index={activeIndex} total={items.length} />
<span key={activeLabel} className={styles.TriggerLabel}>
{activeLabel}
</span>
</span>
<Collapsible.Indicator className={CollapsibleStyles.Indicator}>
<ChevronRightIcon />
</Collapsible.Indicator>
</Collapsible.Trigger>
)
}}
</Toc.Context>
<Collapsible.Content className={CollapsibleStyles.Content}>
<Toc.List className={styles.List}>
{items.map((item, index) => (
<Toc.Item className={styles.Item} key={item.value} item={item}>
<Toc.Link className={styles.LinkNumbered} href={`#${item.value}`}>
<span className={styles.Number}>{String(index + 1).padStart(2, '0')}</span>
{item.label}
</Toc.Link>
</Toc.Item>
))}
</Toc.List>
</Collapsible.Content>
</Collapsible.Root>

<Toc.Content className={styles.Content} ref={contentRef}>
{items.map((item) => (
<section key={item.value}>
<h2 id={item.value} className={styles.Heading} data-depth={item.depth}>
{item.label}
</h2>
<div className={styles.DummyText}>
{Array.from({ length: item.lines }).map((_, i) => (
<div key={i} className={styles.DummyLine} />
))}
</div>
</section>
))}
</Toc.Content>
</Toc.Root>
)
}

const Ring = ({ index, total }: { index: number; total: number }) => {
const progress = index >= 0 ? (index + 1) / total : 0
return (
<svg width="28" height="28" viewBox="0 0 36 36" aria-hidden="true" className={styles.ProgressRing}>
<circle cx="18" cy="18" r="14" fill="none" stroke="currentColor" strokeOpacity="0.2" strokeWidth="2.5" />
<circle
cx="18"
cy="18"
r="14"
fill="none"
pathLength="100"
stroke="var(--demo-coral-solid)"
strokeWidth="2.5"
strokeDasharray={`${progress * 100} 100`}
strokeLinecap="round"
transform="rotate(-90 18 18)"
style={{ transition: 'stroke-dasharray 0.4s ease-out' }}
/>
<text
key={index}
x="18"
y="18"
textAnchor="middle"
dominantBaseline="central"
fontSize="10"
fontWeight="600"
fill="currentColor"
className={styles.ProgressIndexText}
>
{index >= 0 ? index + 1 : '—'}
</text>
</svg>
)
}
Loading