diff --git a/apps/docs/components/web-demos/index.generated.tsx b/apps/docs/components/web-demos/index.generated.tsx index e47c6d73..1a90d2e6 100644 --- a/apps/docs/components/web-demos/index.generated.tsx +++ b/apps/docs/components/web-demos/index.generated.tsx @@ -431,6 +431,12 @@ export const WEB_DEMOS: Record = { 'switch/pending': dynamic(() => import('./switch/pending')), 'switch/sizes': dynamic(() => import('./switch/sizes')), 'switch/thumb': dynamic(() => import('./switch/thumb')), + 'table-of-contents/contour': dynamic(() => import('./table-of-contents/contour')), + 'table-of-contents/controlled': dynamic(() => import('./table-of-contents/controlled')), + 'table-of-contents/nested': dynamic(() => import('./table-of-contents/nested')), + 'table-of-contents/overview': dynamic(() => import('./table-of-contents/overview')), + 'table-of-contents/scrolling': dynamic(() => import('./table-of-contents/scrolling')), + 'table-of-contents/thumb': dynamic(() => import('./table-of-contents/thumb')), 'table/dense': dynamic(() => import('./table/dense')), 'table/frame': dynamic(() => import('./table/frame')), 'table/overview': dynamic(() => import('./table/overview')), diff --git a/apps/docs/components/web-demos/table-of-contents/contour.tsx b/apps/docs/components/web-demos/table-of-contents/contour.tsx new file mode 100644 index 00000000..21ceec4c --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/contour.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { useRef } from 'react'; +import { TableOfContents } from 'panelui-web/table-of-contents'; + +type Section = { + value: string; + label: string; + depth: 0 | 1; + code?: boolean; + text: string; +}; + +const SECTIONS: Section[] = [ + { + value: 'usage', + label: 'Usage', + depth: 0, + text: 'Import the button and give it a label. It takes the full press, hover and focus treatment from the theme, so a plain call already matches every other control on the page.', + }, + { + value: 'sizes', + label: 'Sizes', + depth: 0, + text: 'Five sizes share one type scale. Pick the one that matches the row it sits in rather than the importance of the action: emphasis is the variant’s job.', + }, + { + value: 'size', + label: 'size', + depth: 1, + code: true, + text: 'From xs to xl. The default is md, which lines up with an input of the same size, so a field and its submit button share a baseline.', + }, + { + value: 'full-width', + label: 'fullWidth', + depth: 1, + code: true, + text: 'Stretches the button across its container, for the single action at the end of a form or the bottom of a sheet.', + }, + { + value: 'api', + label: 'API reference', + depth: 0, + text: 'Every prop is typed and documented in the source. The table below is generated from those types, so it cannot drift from what the component accepts.', + }, + { + value: 'button', + label: 'Button', + depth: 1, + text: 'Accepts every attribute of a native button as well as its own props. A render prop draws it as a link or a router’s link instead.', + }, + { + value: 'notes', + label: 'Notes', + depth: 0, + text: 'A few details that only matter in particular layouts.', + }, + { + value: 'large-text', + label: 'Large text', + depth: 1, + text: 'Labels wrap rather than truncate when the reader has asked for larger text, so an action is never cut down to its first word.', + }, + { + value: 'icons', + label: 'Icon buttons', + depth: 1, + text: 'A button with only an icon needs an aria-label. The icon is decoration to a screen reader, and a button with no name is announced as nothing.', + }, + { + value: 'loading', + label: 'Loading', + depth: 1, + text: 'While loading is set the label stays in place and a spinner covers it, so the button keeps its width and the row around it does not jump.', + }, +]; + +const heading = { 0: "text-base font-semibold", 1: "text-sm font-semibold" }; + +export default function TableOfContentsContour() { + const scroller = useRef(null); + + return ( +
+
+
+
+ {SECTIONS.map((section) => ( +
+

+ {section.code ? {section.label} : section.label} +

+

{section.text}

+
+ ))} +
+
+ + + On this page + + {SECTIONS.map((section) => ( + + + {section.code ? {section.label} : section.label} + + + ))} + + +
+
+ ); +} diff --git a/apps/docs/components/web-demos/table-of-contents/controlled.tsx b/apps/docs/components/web-demos/table-of-contents/controlled.tsx new file mode 100644 index 00000000..b20f2b54 --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/controlled.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from 'panelui-web/button'; +import { TableOfContents } from 'panelui-web/table-of-contents'; + +const STEPS = [ + { + value: 'details', + label: 'Details', + heading: 'Order details', + text: 'Three items, placed on 2 November and shipping to 14 Corfield Road. The invoice is addressed to the card holder.', + }, + { + value: 'delivery', + label: 'Delivery', + heading: 'Delivery', + text: 'Standard delivery, three to five working days. The parcel is with the carrier and the tracking link is live.', + }, + { + value: 'payment', + label: 'Payment', + heading: 'Payment', + text: 'Paid in full with the card ending 4417. Nothing else is due, and the receipt is in the order email.', + }, +]; + +export default function TableOfContentsControlled() { + const [active, setActive] = useState('details'); + const at = Math.max( + 0, + STEPS.findIndex((step) => step.value === active) + ); + const section = STEPS[at]!; + + return ( +
+
+

Invoice 2041

+
+ + +
+
+ +
+ + On this page + + {STEPS.map((step) => ( + + {step.label} + + ))} + + + +
+

+ Step {at + 1} of {STEPS.length} +

+

+ {section.heading} +

+

{section.text}

+
+
+
+ ); +} diff --git a/apps/docs/components/web-demos/table-of-contents/nested.tsx b/apps/docs/components/web-demos/table-of-contents/nested.tsx new file mode 100644 index 00000000..12e1e739 --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/nested.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { TableOfContents } from 'panelui-web/table-of-contents'; + +export default function TableOfContentsNested() { + return ( +
+ + On this page + + + Overview + + + Installation + + + From npm + + + By hand + + + Configuration + + + Presets + + + API reference + + + +
+ ); +} diff --git a/apps/docs/components/web-demos/table-of-contents/overview.tsx b/apps/docs/components/web-demos/table-of-contents/overview.tsx new file mode 100644 index 00000000..817fb296 --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/overview.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useRef } from 'react'; +import { TableOfContents } from 'panelui-web/table-of-contents'; + +const SECTIONS = [ + { + value: 'introduction', + label: 'Introduction', + text: 'Orders leave the warehouse within two working days. What follows is how a parcel gets from there to a doorstep, and what to do when it does not arrive at all.', + }, + { + value: 'shipping', + label: 'Shipping', + text: 'Standard delivery is free over £40 and takes three to five working days. Express orders placed before noon are sent the same day, and a tracking link arrives by email as soon as the carrier has scanned the parcel.', + }, + { + value: 'returns', + label: 'Returns', + text: 'Anything unused can come back within thirty days. Print the label from the order page, put the item in its original box, and leave it at any drop-off point; the refund is issued when it reaches the warehouse.', + }, + { + value: 'exchanges', + label: 'Exchanges', + text: 'For a different size or colour, send the first item back and place a new order. It is quicker than a two-way exchange, and the new parcel is not held while the old one is in transit.', + }, + { + value: 'contact', + label: 'Contact', + text: 'The team answers from 09:00 to 18:00 on weekdays. Include the order number in the first message and the reply can be answered in one go rather than three.', + }, +]; + +export default function TableOfContentsOverview() { + const scroller = useRef(null); + + return ( +
+
+
+

Guide

+

Shipping and returns

+

+ Everything about getting an order to you, and getting it back to us. +

+ + {SECTIONS.map((section) => ( +
+

{section.label}

+

{section.text}

+
+ ))} +
+
+ + + On this page + + {SECTIONS.map((section) => ( + + {section.label} + + ))} + + +
+ ); +} diff --git a/apps/docs/components/web-demos/table-of-contents/scrolling.tsx b/apps/docs/components/web-demos/table-of-contents/scrolling.tsx new file mode 100644 index 00000000..7b9f5f63 --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/scrolling.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { useRef } from 'react'; +import { TableOfContents } from 'panelui-web/table-of-contents'; + +const SECTIONS = [ + { + value: 'highlights', + label: 'Highlights', + text: 'Charts can now stack a series by a second key, tooltips stay inside the plot at its edges, and a legend can be placed on any side. The theme gained a muted surface for panels that sit inside a card.', + }, + { + value: 'fixes', + label: 'Fixes', + text: 'A popover opened from a row no longer closes when the row is scrolled under the pointer. Menus keep their place after a resize, and the focus ring is drawn on the element that takes focus rather than the one that wraps it.', + }, + { + value: 'breaking', + label: 'Breaking changes', + text: 'The `tone` prop on Alert is now `variant`. The old name still works for this release and is removed in 3.0, with a warning in development. Nothing else changes shape.', + }, + { + value: 'upgrade', + label: 'Upgrade guide', + text: 'Upgrade the package, then run the codemod if the project uses `tone`. The theme file needs no edits: every token the release reads already exists in 2.3, and the new surface is an addition rather than a rename.', + }, + { + value: 'next', + label: 'What comes next', + text: 'Work has started on a table that sorts and filters without a data library, and on a command palette that can be opened from anywhere in an app. Both are in the next minor.', + }, +]; + +export default function TableOfContentsScrolling() { + const scroller = useRef(null); + + return ( +
+
+
+
+ Release notes + v2.4 +
+ +
+

What changed in 2.4

+

+ A small release: one new surface, a few fixes, and one rename that will be removed in 3.0. +

+ + {SECTIONS.map((section) => ( +
+

{section.label}

+

{section.text}

+
+ ))} +
+
+ + {/* The bar inside the scroller is why the reading line is pushed down: + without `offset`, a heading would count as read while it was still + behind the bar. */} + + On this page + + {SECTIONS.map((section) => ( + + {section.label} + + ))} + + +
+
+ ); +} diff --git a/apps/docs/components/web-demos/table-of-contents/thumb.tsx b/apps/docs/components/web-demos/table-of-contents/thumb.tsx new file mode 100644 index 00000000..e94f70db --- /dev/null +++ b/apps/docs/components/web-demos/table-of-contents/thumb.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { TableOfContents } from 'panelui-web/table-of-contents'; + +const SECTIONS = [ + { value: 'overview', label: 'Overview' }, + { value: 'start', label: 'Getting started' }, + { value: 'states', label: 'States' }, + { value: 'keyboard', label: 'Keyboard' }, + { value: 'api', label: 'API reference' }, +]; + +export default function TableOfContentsThumb() { + return ( + + On this page + + {SECTIONS.map((section) => ( + + {section.label} + + ))} + + + ); +} diff --git a/apps/docs/content/docs/web/components/index.mdx b/apps/docs/content/docs/web/components/index.mdx index 50fa1a71..c49bf714 100644 --- a/apps/docs/content/docs/web/components/index.mdx +++ b/apps/docs/content/docs/web/components/index.mdx @@ -1,6 +1,6 @@ --- title: Components -description: The 74 components in the web collection, each one running. +description: The 75 components in the web collection, each one running. full: true --- diff --git a/apps/docs/content/docs/web/components/meta.json b/apps/docs/content/docs/web/components/meta.json index 3612a36c..1d4accd6 100644 --- a/apps/docs/content/docs/web/components/meta.json +++ b/apps/docs/content/docs/web/components/meta.json @@ -45,6 +45,7 @@ "stat", "switch", "table", + "table-of-contents", "tabs", "textarea", "tilt-card", diff --git a/apps/docs/content/docs/web/components/table-of-contents.mdx b/apps/docs/content/docs/web/components/table-of-contents.mdx new file mode 100644 index 00000000..00098bbc --- /dev/null +++ b/apps/docs/content/docs/web/components/table-of-contents.mdx @@ -0,0 +1,620 @@ +--- +title: Table of Contents +description: The sections of a long page as a quiet rail, with one mark that moves to what you are reading. +status: new +--- + + + +```tsx lineNumbers +'use client'; + +import { useRef } from 'react'; +import { TableOfContents } from '@/components/panelui/table-of-contents'; + +const SECTIONS = [ + { + value: 'introduction', + label: 'Introduction', + text: 'Orders leave the warehouse within two working days. What follows is how a parcel gets from there to a doorstep, and what to do when it does not arrive at all.', + }, + { + value: 'shipping', + label: 'Shipping', + text: 'Standard delivery is free over £40 and takes three to five working days. Express orders placed before noon are sent the same day, and a tracking link arrives by email as soon as the carrier has scanned the parcel.', + }, + { + value: 'returns', + label: 'Returns', + text: 'Anything unused can come back within thirty days. Print the label from the order page, put the item in its original box, and leave it at any drop-off point; the refund is issued when it reaches the warehouse.', + }, + { + value: 'exchanges', + label: 'Exchanges', + text: 'For a different size or colour, send the first item back and place a new order. It is quicker than a two-way exchange, and the new parcel is not held while the old one is in transit.', + }, + { + value: 'contact', + label: 'Contact', + text: 'The team answers from 09:00 to 18:00 on weekdays. Include the order number in the first message and the reply can be answered in one go rather than three.', + }, +]; + +export default function TableOfContentsOverview() { + const scroller = useRef(null); + + return ( +
+
+
+

Guide

+

Shipping and returns

+

+ Everything about getting an order to you, and getting it back to us. +

+ + {SECTIONS.map((section) => ( +
+

{section.label}

+

{section.text}

+
+ ))} +
+
+ + + On this page + + {SECTIONS.map((section) => ( + + {section.label} + + ))} + + +
+ ); +} +``` + +
+ +Use it beside a long page that is read in sections: a documentation article, a settings screen, a statement of terms. It lists the headings once, quietly, and keeps one mark on the section at the top of the viewport, so a reader can see how far in they are and jump without losing their place. + +It is a navigation, not a printed contents table. Every row is a real link with a hash, so the URL follows the reader and a section can be sent to somebody else, and the current row is marked for a screen reader. Keep the labels to the page's own headings and the list to three levels: a rail of twenty equal rows stops saying where you are and becomes a second page. + +Scroll tracking is on by default. Pass `value` to hold the active section yourself, or a `root` when the page scrolls inside a panel rather than as a window. + +## Installation + + + + + + + +The source is copied into `components/panelui/`, where you can edit it. + + + + + + + +Import from `panelui-web/table-of-contents` instead of `@/components/panelui/table-of-contents`. Tailwind needs one line to read the package's classes, which is on the [installation page](/docs/web/installation#from-npm). + + + + + + + + + +Install the packages it uses: + + + + + + + +Copy the source into `components/panelui/table-of-contents/`, one file for each below: + + + + + + + + + + + +## Usage + +```tsx +import { TableOfContents } from "@/components/panelui/table-of-contents" +``` + +```tsx + + On this page + + + Install + + + Configure + + + API reference + + + +``` + +## Composition + +```tsx + + + + + + + + +``` + +- **`TableOfContents.Title`** — A small heading above the list, such as “On this page”. It is optional: the `