@papit/table-of-content 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,139 @@
1
- # Temporary Holding Version
1
+ # @papit/table-of-content
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A page-aware table of contents that builds a tree from the page's headings and highlights the ones currently in view. Clicking an item jumps to its section via the URL hash.
4
+
5
+ ![Logo](https://raw.githubusercontent.com/onkelhoy/papit/refs/heads/main/asset/logo.svg)
6
+
7
+ ---
8
+
9
+ ![Type](https://img.shields.io/badge/Type-atoms-orange)
10
+ [![Tests](https://github.com/onkelhoy/papit/actions/workflows/pull-request.yml/badge.svg)](https://github.com/onkelhoy/papit/actions/workflows/pull-request.yml)
11
+ [![NPM version](https://img.shields.io/npm/v/@papit/table-of-content.svg?logo=npm)](https://www.npmjs.com/package/@papit/table-of-content)
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @papit/table-of-content
19
+ ```
20
+
21
+ ### HTML
22
+
23
+ ```html
24
+ <script type="module" defer>
25
+ import "@papit/table-of-content";
26
+ </script>
27
+
28
+ <pap-table-of-content></pap-table-of-content>
29
+ ```
30
+
31
+ ### JavaScript / TypeScript
32
+
33
+ ```ts
34
+ import "@papit/table-of-content";
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ Place `<pap-table-of-content>` anywhere on the page. By default it scans the **root node** (document or shadow root) for all heading elements and `role="heading"` elements, excluding those with `aria-hidden` or `data-skipped`.
42
+
43
+ ```html
44
+ <pap-table-of-content></pap-table-of-content>
45
+
46
+ <main>
47
+ <h1>Introduction</h1>
48
+ <h2>Getting started</h2>
49
+ <h3>Prerequisites</h3>
50
+ <h2>API reference</h2>
51
+ </main>
52
+ ```
53
+
54
+ As the user scrolls, the items corresponding to visible headings receive the `marked` class and the host gains the `:state(scrolling)` custom state.
55
+
56
+ ### Custom query
57
+
58
+ Override which elements are observed by setting the `query` attribute or property:
59
+
60
+ ```html
61
+ <!-- Only observe h2 and h3 -->
62
+ <pap-table-of-content query="h2, h3"></pap-table-of-content>
63
+ ```
64
+
65
+ ### Icons
66
+
67
+ Add a `data-icon` attribute to a heading to render a `pap-icon` beside its label in the tree:
68
+
69
+ ```html
70
+ <h2 data-icon="star">Highlights</h2>
71
+ ```
72
+
73
+ ### Reading the active set
74
+
75
+ ```ts
76
+ const toc = document.querySelector("pap-table-of-content");
77
+ toc.addEventListener("change", () => {
78
+ console.log(toc.value); // [{ href: '#...', name: '...' }, ...]
79
+ });
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Properties
85
+
86
+ | Property | Attribute | Type | Default | Description |
87
+ | -------- | --------- | ---------------------------------- | --------------------- | --------------------------------------------------------------------- |
88
+ | `query` | `query` | `string` | All heading selectors | CSS selector used to discover headings. Reflected from the attribute. |
89
+ | `links` | — | `Link[]` | `[]` | Internal list of discovered links; not reflected. |
90
+ | `value` | — | `{ href: string; name: string }[]` | — | Read-only. Returns the subset of links currently marked as visible. |
91
+
92
+ ---
93
+
94
+ ## Events
95
+
96
+ | Event | Detail | Description |
97
+ | -------- | ------ | ---------------------------------------------------------------------------------------- |
98
+ | `change` | — | Fired whenever the set of visible headings changes (throttled via IntersectionObserver). |
99
+
100
+ ---
101
+
102
+ ## CSS Parts
103
+
104
+ | Part | Element | Description |
105
+ | -------- | -------------------- | --------------------------------- |
106
+ | `tree` | `pap-treeview` | The root tree element. |
107
+ | `item` | `pap-treeitem` | Each individual tree item. |
108
+ | `anchor` | `<a>` inside an item | The anchor link inside each item. |
109
+
110
+ ---
111
+
112
+ ## Custom States
113
+
114
+ | State | Description |
115
+ | ------------------- | --------------------------------------------------------------------------- |
116
+ | `:state(scrolling)` | Present while the page is scrolling; removed ~500 ms after scrolling stops. |
117
+ | `:state(active)` | Present while the inner `pap-treeview` is focused / active. |
118
+
119
+ ---
120
+
121
+ ## License
122
+
123
+ Licensed under the @Papit License 1.0 — Copyright (c) 2024 Henry Pap (@onkelhoy)
124
+
125
+ **Key points:**
126
+
127
+ - ✅ Free to use in commercial projects
128
+ - ✅ Free to modify and distribute
129
+ - ✅ Attribution required
130
+ - ❌ Cannot resell the component itself as a standalone product
131
+
132
+ See the [LICENSE](https://github.com/onkelhoy/papit/blob/main/LICENSE) file for full details.
133
+
134
+ ---
135
+
136
+ ## Related packages
137
+
138
+ - [@papit/web-component](https://github.com/onkelhoy/papit/tree/main/packages/web/engines/web-component) — Core utilities, decorators, and base component class
139
+ - [@papit/treeview](https://github.com/onkelhoy/papit/tree/main/packages/web/design-system/1-foundations/treeview) — Tree view and tree item components used for rendering
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "region": "GB",
4
+ "language": "en-UK"
5
+ },
6
+ "table-of-content": "table-of-content"
7
+ }
@@ -0,0 +1,66 @@
1
+ import { CustomElementInternals } from '@papit/web-component';
2
+
3
+ declare type Link = {
4
+ href: string;
5
+ name: string;
6
+ icon: string | null;
7
+ reference: HTMLElement;
8
+ marked: boolean;
9
+ level: number;
10
+ };
11
+
12
+ /**
13
+ * `<pap-table-of-content>` — a scroll-aware table of contents web component.
14
+ *
15
+ * Scans the root node for heading elements (or any element matching {@link query}),
16
+ * assigns stable `id` attributes derived from their text content, and renders them
17
+ * as a hierarchical `<pap-treeview>`. Items corresponding to headings that are
18
+ * currently intersecting the viewport are highlighted with the `marked` CSS class.
19
+ *
20
+ * @fires change - Dispatched whenever the set of visible (intersecting) headings
21
+ * changes. Read {@link value} inside the handler to get the current active set.
22
+ *
23
+ * @csspart tree - The root `<pap-treeview>` element.
24
+ * @csspart item - Each `<pap-treeitem>` element.
25
+ * @csspart anchor - The `<a>` element inside each tree item.
26
+ *
27
+ * @cssprop --space-3 - Inline padding applied to each tree item's content area (default `0.75rem`).
28
+ *
29
+ * @example
30
+ * ```html
31
+ * <pap-table-of-content></pap-table-of-content>
32
+ *
33
+ * <main>
34
+ * <h1>Intro</h1>
35
+ * <h2>Details</h2>
36
+ * <h3>Sub-section</h3>
37
+ * </main>
38
+ * ```
39
+ */
40
+ export declare class TableOfContent extends CustomElementInternals {
41
+ static sheet: CSSStyleSheet;
42
+ /**
43
+ * The currently visible headings as plain objects.
44
+ * Useful for driving external navigation UI or breadcrumbs.
45
+ *
46
+ * @returns An array of `{ href, name }` pairs for every link whose
47
+ * `marked` flag is `true`.
48
+ */
49
+ get value(): {
50
+ href: string;
51
+ name: string;
52
+ }[];
53
+ private observer;
54
+ links: Link[];
55
+ query: string;
56
+ private handleobserve;
57
+ private handleenter;
58
+ private handleactive;
59
+ private handleinactive;
60
+ private stopscrolling;
61
+ private buildHierarchy;
62
+ private renderTreeItems;
63
+ render(): Node;
64
+ }
65
+
66
+ export { }
package/lib/bundle.js ADDED
@@ -0,0 +1,66 @@
1
+ var f=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var s=(h,o,t,r)=>{for(var e=r>1?void 0:r?g(o,t):o,n=h.length-1,a;n>=0;n--)(a=h[n])&&(e=(r?a(o,t,e):a(e))||e);return r&&e&&f(o,t,e),e};import{bind as c,CustomElementInternals as b,debounce as w,html as l,property as v,unsafeHTML as k}from"@papit/web-component";import"@papit/treeview";var m=new CSSStyleSheet;m.replaceSync(`:host {
2
+ pap-treeview {
3
+ list-style: none;
4
+ padding: 0;
5
+
6
+ pap-treeitem {
7
+
8
+ a {
9
+ color: currentColor;
10
+ text-decoration: none;
11
+ }
12
+
13
+ &:hover {
14
+ &>a {
15
+ text-decoration: underline;
16
+ }
17
+ }
18
+
19
+ &::part(content) {
20
+ padding-inline: var(--space-3, 0.75rem);
21
+ }
22
+
23
+ &:has(pap-treeitem.marked) {
24
+
25
+ &::part(content) {
26
+ background-color: var(--background);
27
+ }
28
+
29
+ pap-treeitem:not(.marked) {
30
+ &::part(content) {
31
+ background-color: var(--background);
32
+ }
33
+ }
34
+ }
35
+
36
+
37
+ &:hover,
38
+ &.marked {
39
+ &::part(content) {
40
+ background-color: var(--background-dark);
41
+ }
42
+ }
43
+
44
+ }
45
+ }
46
+ }`);var u=m;var i=class extends b{constructor(){super(...arguments);this.observer=new IntersectionObserver(this.handleobserve);this.links=[];this.query=['[role="heading"]',"h1","h2","h3","h4","h5","h6"].map(t=>t+":not([aria-hidden]):not([data-skipped])").join(",")}static{this.sheet=u}get value(){return this.links.filter(t=>t.marked).map(t=>({href:t.href,name:t.name}))}handleobserve(t,r){t.forEach(e=>{let n=this.links.find(a=>a.reference===e.target);n&&(n.marked=("isVisible"in e?e.isVisible:!1)||e.isIntersecting)}),this.dispatchEvent(new Event("change")),this._internals.states.add("scrolling"),this.stopscrolling(),this.throttleUpdate()}handleenter(t){t.target instanceof HTMLElement&&(window.location.hash=t.target.querySelector("a")?.getAttribute("href")??"")}handleactive(){this._internals.states.add("active")}handleinactive(){this._internals.states.delete("active")}stopscrolling(){this._internals.states.delete("scrolling")}buildHierarchy(t){let r=[],e=[];for(let n of t){let a={...n,children:[]};for(;e.length>0&&e[e.length-1].level>=n.level;)e.pop();e.length===0?r.push(a):e[e.length-1].item.children.push(a),e.push({item:a,level:n.level})}return r}renderTreeItems(t){return t.length===0?null:t.map(r=>{let e=r.children.length>0,n=r.marked;return e?l`
47
+ <pap-treeitem part="item" @enter="${this.handleenter}" aria-expanded="true" class="${n?"marked":""}">
48
+ <a part="anchor" tabindex="-1" href="${r.href}">
49
+ ${r.icon&&l`<pap-icon name="${r.icon}"></pap-icon>`}
50
+ ${k(r.name)}
51
+ </a>
52
+ ${this.renderTreeItems(r.children)}
53
+ </pap-treeitem>
54
+ `:l`
55
+ <pap-treeitem part="item" @enter="${this.handleenter}" aria-expanded="true" class="${n?"marked":""}">
56
+ <a part="anchor" tabindex="-1" href="${r.href}">
57
+ ${r.icon&&l`<pap-icon name="${r.icon}"></pap-icon>`}
58
+ ${k(r.name)}
59
+ </a>
60
+ </pap-treeitem>
61
+ `})}render(){let t=this.buildHierarchy(this.links);return l`
62
+ <slot></slot>
63
+ <pap-treeview @active="${this.handleactive}" @inactive="${this.handleinactive}" part="tree">
64
+ ${this.renderTreeItems(t)}
65
+ </pap-treeview>
66
+ `}};s([v({attribute:!1})],i.prototype,"links",2),s([v({reflect:!1,after(){let t=this.getRootNode(),r=[];(t instanceof Document||t instanceof ShadowRoot||t instanceof HTMLElement)&&t.querySelectorAll(this.query).forEach(e=>{if(!(e instanceof HTMLElement))return;let n=e.textContent??e.getAttribute("aria-label");if(!n)return;let a=0;if(e.hasAttribute("aria-level"))a=Number(e.getAttribute("aria-level"));else if(e instanceof HTMLHeadingElement){let d=e.nodeName.match(/H(\d)/i);d&&(a=parseInt(d[1],10))}let p=n.toLowerCase().replace(/\s/g,"-");e.setAttribute("id",p),r.push({reference:e,href:"#"+p,name:n,icon:e.getAttribute("data-icon"),marked:!1,level:a}),this.observer.observe(e)}),this.links=r,this.requestUpdate()}})],i.prototype,"query",2),s([c],i.prototype,"handleobserve",1),s([c],i.prototype,"handleenter",1),s([c],i.prototype,"handleactive",1),s([c],i.prototype,"handleinactive",1),s([w(500)],i.prototype,"stopscrolling",1);if(!window.customElements)throw new Error("Custom Elements not supported");window.customElements.get("pap-table-of-content")||window.customElements.define("pap-table-of-content",i);export{i as TableOfContent};
@@ -0,0 +1,38 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import type { ReactElement } from 'react';
3
+ import type { ReactNode } from 'react';
4
+ import type { Ref } from 'react';
5
+ import type { TableOfContent as TableOfContent_2 } from '@papit/table-of-content';
6
+
7
+ /** React wrapper for the `pap-table-of-content` element. */
8
+ export declare function TableOfContent({ ref, onChange, links, ...props }: TableOfContentProps): ReactElement;
9
+
10
+ export declare interface TableOfContentProps extends Omit<HTMLAttributes<TableOfContent_2>, "links" | "query" | "disabled" | "onChange"> {
11
+ links?: TableOfContent_2["links"];
12
+ query?: TableOfContent_2["query"];
13
+ /**
14
+ * Reactive disabled property.
15
+ * - `rerender: false` → does not trigger a re-render when changed
16
+ * - `type: Boolean` → coerces values to `true`/`false`
17
+ * - `aria: 'aria-disabled'` → syncs to ARIA for accessibility
18
+ * - `removeAttribute: true` → removes `disabled` attribute when false
19
+ */
20
+ disabled?: TableOfContent_2["disabled"];
21
+ /**
22
+ * Dispatched whenever the set of visible (intersecting) headings
23
+ * changes. Read {@link value} inside the handler to get the current active set.
24
+ */
25
+ onChange?: (event: Event) => void;
26
+ ref?: Ref<TableOfContent_2>;
27
+ children?: ReactNode;
28
+ }
29
+
30
+ declare module "react" {
31
+ namespace JSX {
32
+ interface IntrinsicElements {
33
+ "pap-table-of-content": Omit<TableOfContentProps, "onChange" | "links">;
34
+ }
35
+ }
36
+ }
37
+
38
+ export { }
@@ -0,0 +1 @@
1
+ "use client";import{useCallback as u,useEffect as i,useRef as c}from"react";import{jsx as s}from"react/jsx-runtime";typeof window<"u"&&import("@papit/table-of-content");function p({ref:t,onChange:l,links:r,...f}){let o=c(null),a=u(e=>{o.current=e;let n;return typeof t=="function"?n=t(e):t&&(t.current=e),()=>{o.current=null,typeof n=="function"?n():typeof t=="function"?t(null):t&&(t.current=null)}},[t]);return i(()=>{let e=o.current;if(!(!e||!l))return e.addEventListener("change",l),()=>e.removeEventListener("change",l)},[l]),i(()=>{let e=o.current;if(!e)return;let n=!0;return customElements.whenDefined("pap-table-of-content").then(()=>{n&&r!==void 0&&(e.links=r)}),()=>{n=!1}},[r]),s("pap-table-of-content",{suppressHydrationWarning:!0,...f,ref:a})}export{p as TableOfContent};
package/package.json CHANGED
@@ -1,6 +1,104 @@
1
1
  {
2
2
  "name": "@papit/table-of-content",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "A page-aware table of contents that builds a tree from the page's headings and highlights the ones currently in view. Clicking an item jumps to its section via the URL hash.",
4
+ "keywords": [
5
+ "papit",
6
+ "web-component",
7
+ "typescript",
8
+ "table-of-content",
9
+ "toc",
10
+ "navigation",
11
+ "headings"
12
+ ],
13
+ "version": "0.0.2",
14
+ "scripts": {
15
+ "build": "npx @papit/build --error",
16
+ "analyse": "npx @papit/analyse",
17
+ "generate:react": "npx @papit/react",
18
+ "watch": "tsc -w",
19
+ "start": "npx @papit/server --info --live",
20
+ "test": "npx playwright test -c tests/playwright.config.ts $@",
21
+ "preversion": "papit-version",
22
+ "postversion": "papit-version",
23
+ "component": "npm create @papit --component",
24
+ "test:update-snapshots": "npx playwright test --update-snapshots -c tests/playwright.config.ts",
25
+ "test:chrome": "npm test -- --project=chromium"
26
+ },
27
+ "main": "./lib/bundle.js",
28
+ "types": "./lib/bundle.d.ts",
29
+ "type": "module",
30
+ "files": [
31
+ "asset/",
32
+ "lib/"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "import": "./lib/bundle.js",
37
+ "types": "./lib/bundle.d.ts"
38
+ },
39
+ "./react": {
40
+ "import": "./lib/react-wrapper.js",
41
+ "types": "./lib/react-wrapper.d.ts"
42
+ }
43
+ },
44
+ "author": "henry",
45
+ "private": false,
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ },
50
+ "license": "LicenseRef-Papit-1.0",
51
+ "homepage": "https://github.com/onkelhoy/papit/tree/main/packages/web/design-system/atoms/table-of-content",
52
+ "bugs": {
53
+ "url": "https://github.com/onkelhoy/papit/issues"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/onkelhoy/papit.git",
58
+ "directory": "packages/web/design-system/atoms/table-of-content"
59
+ },
60
+ "sideEffects": true,
61
+ "engines": {
62
+ "node": ">=18"
63
+ },
64
+ "dependencies": {
65
+ "@papit/treeview": "0.0.11",
66
+ "@papit/web-component": "0.0.11"
67
+ },
68
+ "peerDependencies": {
69
+ "react": "^19"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "react": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "devDependencies": {
77
+ "@papit/codeblock": "0.1.14",
78
+ "@papit/router": "0.1.7",
79
+ "@papit/tabs": "0.1.7",
80
+ "@papit/theme": "0.0.3",
81
+ "@papit/translator": "0.1.1",
82
+ "@playwright/test": "1.64.1",
83
+ "playwright": "1.64.1",
84
+ "typescript": "5.9.3"
85
+ },
86
+ "browserslist": [
87
+ "defaults",
88
+ "not IE 11",
89
+ "not dead"
90
+ ],
91
+ "papit": {
92
+ "publish": true,
93
+ "type": "web-component",
94
+ "main": "table-of-content",
95
+ "htmlprefix": "pap",
96
+ "components": {
97
+ "table-of-content": {
98
+ "className": "TableOfContent",
99
+ "htmlprefix": "pap"
100
+ }
101
+ }
102
+ },
103
+ "remoteVersion": ""
6
104
  }