morphing-toc 0.1.0
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/MorphingToc.d.mts +6 -0
- package/dist/MorphingToc.d.ts +6 -0
- package/dist/MorphingToc.js +229 -0
- package/dist/MorphingToc.js.map +1 -0
- package/dist/MorphingToc.mjs +227 -0
- package/dist/MorphingToc.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scrollToSection.d.mts +3 -0
- package/dist/scrollToSection.d.ts +3 -0
- package/dist/scrollToSection.js +17 -0
- package/dist/scrollToSection.js.map +1 -0
- package/dist/scrollToSection.mjs +15 -0
- package/dist/scrollToSection.mjs.map +1 -0
- package/dist/types-Dol3SnRo.d.mts +62 -0
- package/dist/types-Dol3SnRo.d.ts +62 -0
- package/dist/useTocItems.d.mts +5 -0
- package/dist/useTocItems.d.ts +5 -0
- package/dist/useTocItems.js +60 -0
- package/dist/useTocItems.js.map +1 -0
- package/dist/useTocItems.mjs +58 -0
- package/dist/useTocItems.mjs.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine Pirard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# morphing-toc
|
|
2
|
+
|
|
3
|
+
A React table of contents component that displays minimal vertical lines and morphs into a full navigation menu on hover.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Minimal visual footprint with vertical lines
|
|
8
|
+
- Smooth morph animation on hover
|
|
9
|
+
- Auto-extracts headings from DOM
|
|
10
|
+
- Generates unique IDs for headings
|
|
11
|
+
- Smooth scrolling with configurable offset
|
|
12
|
+
- Fully customizable colors and sizes
|
|
13
|
+
- TypeScript support
|
|
14
|
+
- Works with any React framework
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install morphing-toc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Peer Dependencies
|
|
23
|
+
|
|
24
|
+
This package requires the following peer dependencies:
|
|
25
|
+
|
|
26
|
+
- `react` >= 18.0.0
|
|
27
|
+
- `react-dom` >= 18.0.0
|
|
28
|
+
- `motion` >= 11.0.0
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { MorphingToc } from 'morphing-toc';
|
|
34
|
+
|
|
35
|
+
function BlogPost() {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<MorphingToc />
|
|
39
|
+
<article>
|
|
40
|
+
<h1>My Blog Post</h1>
|
|
41
|
+
<h2>Introduction</h2>
|
|
42
|
+
<p>...</p>
|
|
43
|
+
<h2>Main Content</h2>
|
|
44
|
+
<h3>Subsection</h3>
|
|
45
|
+
<p>...</p>
|
|
46
|
+
</article>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
|------|------|---------|-------------|
|
|
56
|
+
| `className` | `string` | `''` | Custom CSS class for the container |
|
|
57
|
+
| `scrollOffset` | `number` | `80` | Offset from top when scrolling (px) |
|
|
58
|
+
| `headingLevels` | `number[]` | `[2, 3, 4]` | Which heading levels to include |
|
|
59
|
+
| `skipFirstH1` | `boolean` | `true` | Skip the first h1 (page title) |
|
|
60
|
+
| `containerSelector` | `string` | `undefined` | CSS selector to scope heading search |
|
|
61
|
+
| `colors` | `MorphingTocColors` | See below | Custom color configuration |
|
|
62
|
+
| `sizes` | `MorphingTocSizes` | See below | Custom size configuration |
|
|
63
|
+
|
|
64
|
+
## Customization
|
|
65
|
+
|
|
66
|
+
### Custom Colors
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<MorphingToc
|
|
70
|
+
colors={{
|
|
71
|
+
line: {
|
|
72
|
+
h1: '#1e40af',
|
|
73
|
+
h2: '#3b82f6',
|
|
74
|
+
h3: '#93c5fd',
|
|
75
|
+
},
|
|
76
|
+
menu: {
|
|
77
|
+
background: 'rgba(30, 64, 175, 0.95)',
|
|
78
|
+
border: 'rgba(59, 130, 246, 0.3)',
|
|
79
|
+
text: '#ffffff',
|
|
80
|
+
textHover: '#bfdbfe',
|
|
81
|
+
itemHover: 'rgba(59, 130, 246, 0.2)',
|
|
82
|
+
},
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom Sizes
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<MorphingToc
|
|
91
|
+
sizes={{
|
|
92
|
+
lineWidth: {
|
|
93
|
+
h1: '2rem',
|
|
94
|
+
h2: '1.5rem',
|
|
95
|
+
h3: '1rem',
|
|
96
|
+
},
|
|
97
|
+
lineHeight: '2px',
|
|
98
|
+
lineGap: '0.75rem',
|
|
99
|
+
menuWidth: '20rem',
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Scoped to Container
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<MorphingToc
|
|
108
|
+
containerSelector="#article-content"
|
|
109
|
+
headingLevels={[2, 3]}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Exports
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import {
|
|
117
|
+
MorphingToc, // Main component
|
|
118
|
+
useTocItems, // Hook for heading extraction
|
|
119
|
+
scrollToSection, // Scroll utility
|
|
120
|
+
} from 'morphing-toc';
|
|
121
|
+
|
|
122
|
+
// Type exports
|
|
123
|
+
import type {
|
|
124
|
+
MorphingTocProps,
|
|
125
|
+
MorphingTocColors,
|
|
126
|
+
MorphingTocSizes,
|
|
127
|
+
TocItem,
|
|
128
|
+
UseTocItemsOptions,
|
|
129
|
+
} from 'morphing-toc';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## useTocItems Hook
|
|
133
|
+
|
|
134
|
+
For custom implementations, you can use the `useTocItems` hook directly:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useTocItems, scrollToSection } from 'morphing-toc';
|
|
138
|
+
|
|
139
|
+
function CustomToc() {
|
|
140
|
+
const items = useTocItems({
|
|
141
|
+
headingLevels: [2, 3],
|
|
142
|
+
skipFirstH1: true,
|
|
143
|
+
containerSelector: '#content',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<nav>
|
|
148
|
+
{items.map((item) => (
|
|
149
|
+
<button
|
|
150
|
+
key={item.id}
|
|
151
|
+
onClick={() => scrollToSection(item.id, 80)}
|
|
152
|
+
>
|
|
153
|
+
{item.title}
|
|
154
|
+
</button>
|
|
155
|
+
))}
|
|
156
|
+
</nav>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Browser Support
|
|
162
|
+
|
|
163
|
+
- Chrome (latest)
|
|
164
|
+
- Firefox (latest)
|
|
165
|
+
- Safari (latest)
|
|
166
|
+
- Edge (latest)
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
171
|
+
|
|
172
|
+
## Author
|
|
173
|
+
|
|
174
|
+
[Antoine Pirard](https://antoinepirard.com)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { M as MorphingTocProps } from './types-Dol3SnRo.mjs';
|
|
3
|
+
|
|
4
|
+
declare function MorphingToc({ className, scrollOffset, headingLevels, skipFirstH1, containerSelector, colors, sizes, }: MorphingTocProps): react_jsx_runtime.JSX.Element | null;
|
|
5
|
+
|
|
6
|
+
export { MorphingToc };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { M as MorphingTocProps } from './types-Dol3SnRo.js';
|
|
3
|
+
|
|
4
|
+
declare function MorphingToc({ className, scrollOffset, headingLevels, skipFirstH1, containerSelector, colors, sizes, }: MorphingTocProps): react_jsx_runtime.JSX.Element | null;
|
|
5
|
+
|
|
6
|
+
export { MorphingToc };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var react$1 = require('motion/react');
|
|
7
|
+
var useTocItems = require('./useTocItems');
|
|
8
|
+
var scrollToSection = require('./scrollToSection');
|
|
9
|
+
|
|
10
|
+
function MenuItem({ item, indent, colors, onClick }) {
|
|
11
|
+
const [isHovered, setIsHovered] = react.useState(false);
|
|
12
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13
|
+
"button",
|
|
14
|
+
{
|
|
15
|
+
onClick,
|
|
16
|
+
onMouseEnter: () => setIsHovered(true),
|
|
17
|
+
onMouseLeave: () => setIsHovered(false),
|
|
18
|
+
className: "block w-full text-left px-2 py-1 rounded text-sm cursor-pointer",
|
|
19
|
+
style: {
|
|
20
|
+
paddingLeft: `${8 + indent}px`,
|
|
21
|
+
color: isHovered ? colors.textHover : colors.text,
|
|
22
|
+
backgroundColor: isHovered ? colors.itemHover : "transparent"
|
|
23
|
+
},
|
|
24
|
+
tabIndex: -1,
|
|
25
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate", children: item.title })
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const defaultColors = {
|
|
30
|
+
line: {
|
|
31
|
+
h1: "#475569",
|
|
32
|
+
h2: "#94a3b8",
|
|
33
|
+
h3: "#cbd5e1",
|
|
34
|
+
h4: "#cbd5e1",
|
|
35
|
+
default: "#cbd5e1"
|
|
36
|
+
},
|
|
37
|
+
menu: {
|
|
38
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
39
|
+
border: "rgba(203, 213, 225, 0.3)",
|
|
40
|
+
text: "#475569",
|
|
41
|
+
textHover: "#0f172a",
|
|
42
|
+
itemHover: "#f8fafc"
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const defaultSizes = {
|
|
46
|
+
lineWidth: {
|
|
47
|
+
h1: "1.5rem",
|
|
48
|
+
h2: "1rem",
|
|
49
|
+
h3: "0.75rem",
|
|
50
|
+
h4: "0.5rem",
|
|
51
|
+
default: "0.5rem"
|
|
52
|
+
},
|
|
53
|
+
lineHeight: "1px",
|
|
54
|
+
lineGap: "0.5rem",
|
|
55
|
+
menuWidth: "16rem"
|
|
56
|
+
};
|
|
57
|
+
function getLineColor(level, colors = {}) {
|
|
58
|
+
const merged = { ...defaultColors.line, ...colors };
|
|
59
|
+
switch (level) {
|
|
60
|
+
case 1:
|
|
61
|
+
return merged.h1;
|
|
62
|
+
case 2:
|
|
63
|
+
return merged.h2;
|
|
64
|
+
case 3:
|
|
65
|
+
return merged.h3;
|
|
66
|
+
case 4:
|
|
67
|
+
return merged.h4;
|
|
68
|
+
default:
|
|
69
|
+
return merged.default;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getLineWidth(level, sizes = {}) {
|
|
73
|
+
const merged = { ...defaultSizes.lineWidth, ...sizes };
|
|
74
|
+
switch (level) {
|
|
75
|
+
case 1:
|
|
76
|
+
return merged.h1;
|
|
77
|
+
case 2:
|
|
78
|
+
return merged.h2;
|
|
79
|
+
case 3:
|
|
80
|
+
return merged.h3;
|
|
81
|
+
case 4:
|
|
82
|
+
return merged.h4;
|
|
83
|
+
default:
|
|
84
|
+
return merged.default;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function MorphingToc({
|
|
88
|
+
className = "",
|
|
89
|
+
scrollOffset = 80,
|
|
90
|
+
headingLevels = [2, 3, 4],
|
|
91
|
+
skipFirstH1 = true,
|
|
92
|
+
containerSelector,
|
|
93
|
+
colors = {},
|
|
94
|
+
sizes = {}
|
|
95
|
+
}) {
|
|
96
|
+
const [isHovered, setIsHovered] = react.useState(false);
|
|
97
|
+
const tocItems = useTocItems.useTocItems({
|
|
98
|
+
headingLevels,
|
|
99
|
+
skipFirstH1,
|
|
100
|
+
containerSelector
|
|
101
|
+
});
|
|
102
|
+
const mergedColors = {
|
|
103
|
+
line: { ...defaultColors.line, ...colors.line },
|
|
104
|
+
menu: { ...defaultColors.menu, ...colors.menu }
|
|
105
|
+
};
|
|
106
|
+
const mergedSizes = {
|
|
107
|
+
lineWidth: { ...defaultSizes.lineWidth, ...sizes.lineWidth },
|
|
108
|
+
lineHeight: sizes.lineHeight ?? defaultSizes.lineHeight,
|
|
109
|
+
lineGap: sizes.lineGap ?? defaultSizes.lineGap,
|
|
110
|
+
menuWidth: sizes.menuWidth ?? defaultSizes.menuWidth
|
|
111
|
+
};
|
|
112
|
+
if (tocItems.length === 0) return null;
|
|
113
|
+
const handleClick = (id) => {
|
|
114
|
+
scrollToSection.scrollToSection(id, scrollOffset);
|
|
115
|
+
};
|
|
116
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `fixed left-0 top-1/2 -translate-y-1/2 z-40 hidden lg:block ${className}`, children: [
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
className: "absolute -left-20 top-1/2 -translate-y-1/2 w-32 h-96",
|
|
121
|
+
onMouseEnter: () => setIsHovered(true),
|
|
122
|
+
onMouseLeave: () => setIsHovered(false)
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
126
|
+
react$1.motion.div,
|
|
127
|
+
{
|
|
128
|
+
className: "relative",
|
|
129
|
+
initial: { opacity: 0, x: -20 },
|
|
130
|
+
animate: { opacity: 1, x: 0 },
|
|
131
|
+
transition: { duration: 0.3, delay: 0.5 },
|
|
132
|
+
onMouseEnter: () => setIsHovered(true),
|
|
133
|
+
onMouseLeave: () => setIsHovered(false),
|
|
134
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pl-2 md:pl-4 lg:pl-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.AnimatePresence, { children: !isHovered && /* @__PURE__ */ jsxRuntime.jsx(
|
|
136
|
+
react$1.motion.div,
|
|
137
|
+
{
|
|
138
|
+
initial: { opacity: 0 },
|
|
139
|
+
animate: { opacity: 1 },
|
|
140
|
+
exit: {
|
|
141
|
+
opacity: 0,
|
|
142
|
+
x: 20,
|
|
143
|
+
transition: { duration: 0.15, ease: "easeOut" }
|
|
144
|
+
},
|
|
145
|
+
transition: { duration: 0.15 },
|
|
146
|
+
className: "absolute top-1/2 left-0 -translate-y-1/2",
|
|
147
|
+
style: { display: "flex", flexDirection: "column", gap: mergedSizes.lineGap },
|
|
148
|
+
children: tocItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
149
|
+
react$1.motion.button,
|
|
150
|
+
{
|
|
151
|
+
onClick: () => handleClick(item.id),
|
|
152
|
+
className: "block rounded-sm transition-opacity duration-150 hover:opacity-70",
|
|
153
|
+
style: {
|
|
154
|
+
width: getLineWidth(item.level, mergedSizes.lineWidth),
|
|
155
|
+
height: mergedSizes.lineHeight,
|
|
156
|
+
backgroundColor: getLineColor(item.level, mergedColors.line)
|
|
157
|
+
},
|
|
158
|
+
whileTap: { scale: 0.95 },
|
|
159
|
+
tabIndex: -1,
|
|
160
|
+
animate: {
|
|
161
|
+
x: isHovered ? 20 : 0,
|
|
162
|
+
transition: {
|
|
163
|
+
duration: 0.15,
|
|
164
|
+
delay: index * 0.02,
|
|
165
|
+
ease: "easeOut"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
item.id
|
|
170
|
+
))
|
|
171
|
+
}
|
|
172
|
+
) }),
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.AnimatePresence, { children: isHovered && /* @__PURE__ */ jsxRuntime.jsx(
|
|
174
|
+
react$1.motion.div,
|
|
175
|
+
{
|
|
176
|
+
initial: {
|
|
177
|
+
opacity: 0,
|
|
178
|
+
x: -20,
|
|
179
|
+
scale: 0.9,
|
|
180
|
+
borderRadius: 0
|
|
181
|
+
},
|
|
182
|
+
animate: {
|
|
183
|
+
opacity: 1,
|
|
184
|
+
x: 0,
|
|
185
|
+
scale: 1,
|
|
186
|
+
borderRadius: 6
|
|
187
|
+
},
|
|
188
|
+
exit: {
|
|
189
|
+
opacity: 0,
|
|
190
|
+
x: -20,
|
|
191
|
+
scale: 0.9,
|
|
192
|
+
transition: { duration: 0.15, ease: "easeOut" }
|
|
193
|
+
},
|
|
194
|
+
transition: {
|
|
195
|
+
duration: 0.2,
|
|
196
|
+
delay: 0.05,
|
|
197
|
+
ease: "easeOut"
|
|
198
|
+
},
|
|
199
|
+
className: "absolute top-1/2 left-0 -translate-y-1/2 backdrop-blur-sm shadow-md p-3",
|
|
200
|
+
style: {
|
|
201
|
+
width: mergedSizes.menuWidth,
|
|
202
|
+
backgroundColor: mergedColors.menu.background,
|
|
203
|
+
boxShadow: `0 0 0 1px ${mergedColors.menu.border}`
|
|
204
|
+
},
|
|
205
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "space-y-0.5", children: tocItems.map((item) => {
|
|
206
|
+
const baseLevel = Math.min(...headingLevels);
|
|
207
|
+
const indent = (item.level - baseLevel) * 12;
|
|
208
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
209
|
+
MenuItem,
|
|
210
|
+
{
|
|
211
|
+
item,
|
|
212
|
+
indent,
|
|
213
|
+
colors: mergedColors.menu,
|
|
214
|
+
onClick: () => handleClick(item.id)
|
|
215
|
+
},
|
|
216
|
+
item.id
|
|
217
|
+
);
|
|
218
|
+
}) })
|
|
219
|
+
}
|
|
220
|
+
) })
|
|
221
|
+
] }) })
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
] });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exports.MorphingToc = MorphingToc;
|
|
228
|
+
//# sourceMappingURL=MorphingToc.js.map
|
|
229
|
+
//# sourceMappingURL=MorphingToc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MorphingToc.tsx"],"names":["useState","jsx","useTocItems","scrollToSection","jsxs","motion","AnimatePresence"],"mappings":"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","file":"MorphingToc.js","sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { useTocItems } from './useTocItems';\nimport { scrollToSection } from './scrollToSection';\nimport type { MorphingTocProps, MorphingTocColors, MorphingTocSizes, TocItem } from './types';\n\ninterface MenuItemProps {\n item: TocItem;\n indent: number;\n colors: NonNullable<MorphingTocColors['menu']>;\n onClick: () => void;\n}\n\nfunction MenuItem({ item, indent, colors, onClick }: MenuItemProps) {\n const [isHovered, setIsHovered] = useState(false);\n\n return (\n <button\n onClick={onClick}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className=\"block w-full text-left px-2 py-1 rounded text-sm cursor-pointer\"\n style={{\n paddingLeft: `${8 + indent}px`,\n color: isHovered ? colors.textHover : colors.text,\n backgroundColor: isHovered ? colors.itemHover : 'transparent',\n }}\n tabIndex={-1}\n >\n <span className=\"block truncate\">{item.title}</span>\n </button>\n );\n}\n\nconst defaultColors: Required<MorphingTocColors> = {\n line: {\n h1: '#475569',\n h2: '#94a3b8',\n h3: '#cbd5e1',\n h4: '#cbd5e1',\n default: '#cbd5e1',\n },\n menu: {\n background: 'rgba(255, 255, 255, 0.95)',\n border: 'rgba(203, 213, 225, 0.3)',\n text: '#475569',\n textHover: '#0f172a',\n itemHover: '#f8fafc',\n },\n};\n\nconst defaultSizes: Required<MorphingTocSizes> = {\n lineWidth: {\n h1: '1.5rem',\n h2: '1rem',\n h3: '0.75rem',\n h4: '0.5rem',\n default: '0.5rem',\n },\n lineHeight: '1px',\n lineGap: '0.5rem',\n menuWidth: '16rem',\n};\n\nfunction getLineColor(level: number, colors: MorphingTocColors['line'] = {}): string {\n const merged = { ...defaultColors.line, ...colors };\n switch (level) {\n case 1:\n return merged.h1!;\n case 2:\n return merged.h2!;\n case 3:\n return merged.h3!;\n case 4:\n return merged.h4!;\n default:\n return merged.default!;\n }\n}\n\nfunction getLineWidth(level: number, sizes: MorphingTocSizes['lineWidth'] = {}): string {\n const merged = { ...defaultSizes.lineWidth, ...sizes };\n switch (level) {\n case 1:\n return merged.h1!;\n case 2:\n return merged.h2!;\n case 3:\n return merged.h3!;\n case 4:\n return merged.h4!;\n default:\n return merged.default!;\n }\n}\n\nexport function MorphingToc({\n className = '',\n scrollOffset = 80,\n headingLevels = [2, 3, 4],\n skipFirstH1 = true,\n containerSelector,\n colors = {},\n sizes = {},\n}: MorphingTocProps) {\n const [isHovered, setIsHovered] = useState(false);\n\n const tocItems = useTocItems({\n headingLevels,\n skipFirstH1,\n containerSelector,\n });\n\n const mergedColors = {\n line: { ...defaultColors.line, ...colors.line },\n menu: { ...defaultColors.menu, ...colors.menu },\n };\n\n const mergedSizes = {\n lineWidth: { ...defaultSizes.lineWidth, ...sizes.lineWidth },\n lineHeight: sizes.lineHeight ?? defaultSizes.lineHeight,\n lineGap: sizes.lineGap ?? defaultSizes.lineGap,\n menuWidth: sizes.menuWidth ?? defaultSizes.menuWidth,\n };\n\n if (tocItems.length === 0) return null;\n\n const handleClick = (id: string) => {\n scrollToSection(id, scrollOffset);\n };\n\n return (\n <div className={`fixed left-0 top-1/2 -translate-y-1/2 z-40 hidden lg:block ${className}`}>\n {/* Invisible hover area for better UX */}\n <div\n className=\"absolute -left-20 top-1/2 -translate-y-1/2 w-32 h-96\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n />\n\n <motion.div\n className=\"relative\"\n initial={{ opacity: 0, x: -20 }}\n animate={{ opacity: 1, x: 0 }}\n transition={{ duration: 0.3, delay: 0.5 }}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <div className=\"pl-2 md:pl-4 lg:pl-6\">\n <div className=\"relative\">\n {/* Collapsed state: minimal lines */}\n <AnimatePresence>\n {!isHovered && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{\n opacity: 0,\n x: 20,\n transition: { duration: 0.15, ease: 'easeOut' },\n }}\n transition={{ duration: 0.15 }}\n className=\"absolute top-1/2 left-0 -translate-y-1/2\"\n style={{ display: 'flex', flexDirection: 'column', gap: mergedSizes.lineGap }}\n >\n {tocItems.map((item, index) => (\n <motion.button\n key={item.id}\n onClick={() => handleClick(item.id)}\n className=\"block rounded-sm transition-opacity duration-150 hover:opacity-70\"\n style={{\n width: getLineWidth(item.level, mergedSizes.lineWidth),\n height: mergedSizes.lineHeight,\n backgroundColor: getLineColor(item.level, mergedColors.line),\n }}\n whileTap={{ scale: 0.95 }}\n tabIndex={-1}\n animate={{\n x: isHovered ? 20 : 0,\n transition: {\n duration: 0.15,\n delay: index * 0.02,\n ease: 'easeOut',\n },\n }}\n />\n ))}\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* Expanded state: full menu */}\n <AnimatePresence>\n {isHovered && (\n <motion.div\n initial={{\n opacity: 0,\n x: -20,\n scale: 0.9,\n borderRadius: 0,\n }}\n animate={{\n opacity: 1,\n x: 0,\n scale: 1,\n borderRadius: 6,\n }}\n exit={{\n opacity: 0,\n x: -20,\n scale: 0.9,\n transition: { duration: 0.15, ease: 'easeOut' },\n }}\n transition={{\n duration: 0.2,\n delay: 0.05,\n ease: 'easeOut',\n }}\n className=\"absolute top-1/2 left-0 -translate-y-1/2 backdrop-blur-sm shadow-md p-3\"\n style={{\n width: mergedSizes.menuWidth,\n backgroundColor: mergedColors.menu.background,\n boxShadow: `0 0 0 1px ${mergedColors.menu.border}`,\n }}\n >\n <nav className=\"space-y-0.5\">\n {tocItems.map((item) => {\n const baseLevel = Math.min(...headingLevels);\n const indent = (item.level - baseLevel) * 12;\n\n return (\n <MenuItem\n key={item.id}\n item={item}\n indent={indent}\n colors={mergedColors.menu}\n onClick={() => handleClick(item.id)}\n />\n );\n })}\n </nav>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n </div>\n </motion.div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
5
|
+
import { useTocItems } from './useTocItems';
|
|
6
|
+
import { scrollToSection } from './scrollToSection';
|
|
7
|
+
|
|
8
|
+
function MenuItem({ item, indent, colors, onClick }) {
|
|
9
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
"button",
|
|
12
|
+
{
|
|
13
|
+
onClick,
|
|
14
|
+
onMouseEnter: () => setIsHovered(true),
|
|
15
|
+
onMouseLeave: () => setIsHovered(false),
|
|
16
|
+
className: "block w-full text-left px-2 py-1 rounded text-sm cursor-pointer",
|
|
17
|
+
style: {
|
|
18
|
+
paddingLeft: `${8 + indent}px`,
|
|
19
|
+
color: isHovered ? colors.textHover : colors.text,
|
|
20
|
+
backgroundColor: isHovered ? colors.itemHover : "transparent"
|
|
21
|
+
},
|
|
22
|
+
tabIndex: -1,
|
|
23
|
+
children: /* @__PURE__ */ jsx("span", { className: "block truncate", children: item.title })
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const defaultColors = {
|
|
28
|
+
line: {
|
|
29
|
+
h1: "#475569",
|
|
30
|
+
h2: "#94a3b8",
|
|
31
|
+
h3: "#cbd5e1",
|
|
32
|
+
h4: "#cbd5e1",
|
|
33
|
+
default: "#cbd5e1"
|
|
34
|
+
},
|
|
35
|
+
menu: {
|
|
36
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
37
|
+
border: "rgba(203, 213, 225, 0.3)",
|
|
38
|
+
text: "#475569",
|
|
39
|
+
textHover: "#0f172a",
|
|
40
|
+
itemHover: "#f8fafc"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const defaultSizes = {
|
|
44
|
+
lineWidth: {
|
|
45
|
+
h1: "1.5rem",
|
|
46
|
+
h2: "1rem",
|
|
47
|
+
h3: "0.75rem",
|
|
48
|
+
h4: "0.5rem",
|
|
49
|
+
default: "0.5rem"
|
|
50
|
+
},
|
|
51
|
+
lineHeight: "1px",
|
|
52
|
+
lineGap: "0.5rem",
|
|
53
|
+
menuWidth: "16rem"
|
|
54
|
+
};
|
|
55
|
+
function getLineColor(level, colors = {}) {
|
|
56
|
+
const merged = { ...defaultColors.line, ...colors };
|
|
57
|
+
switch (level) {
|
|
58
|
+
case 1:
|
|
59
|
+
return merged.h1;
|
|
60
|
+
case 2:
|
|
61
|
+
return merged.h2;
|
|
62
|
+
case 3:
|
|
63
|
+
return merged.h3;
|
|
64
|
+
case 4:
|
|
65
|
+
return merged.h4;
|
|
66
|
+
default:
|
|
67
|
+
return merged.default;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function getLineWidth(level, sizes = {}) {
|
|
71
|
+
const merged = { ...defaultSizes.lineWidth, ...sizes };
|
|
72
|
+
switch (level) {
|
|
73
|
+
case 1:
|
|
74
|
+
return merged.h1;
|
|
75
|
+
case 2:
|
|
76
|
+
return merged.h2;
|
|
77
|
+
case 3:
|
|
78
|
+
return merged.h3;
|
|
79
|
+
case 4:
|
|
80
|
+
return merged.h4;
|
|
81
|
+
default:
|
|
82
|
+
return merged.default;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function MorphingToc({
|
|
86
|
+
className = "",
|
|
87
|
+
scrollOffset = 80,
|
|
88
|
+
headingLevels = [2, 3, 4],
|
|
89
|
+
skipFirstH1 = true,
|
|
90
|
+
containerSelector,
|
|
91
|
+
colors = {},
|
|
92
|
+
sizes = {}
|
|
93
|
+
}) {
|
|
94
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
95
|
+
const tocItems = useTocItems({
|
|
96
|
+
headingLevels,
|
|
97
|
+
skipFirstH1,
|
|
98
|
+
containerSelector
|
|
99
|
+
});
|
|
100
|
+
const mergedColors = {
|
|
101
|
+
line: { ...defaultColors.line, ...colors.line },
|
|
102
|
+
menu: { ...defaultColors.menu, ...colors.menu }
|
|
103
|
+
};
|
|
104
|
+
const mergedSizes = {
|
|
105
|
+
lineWidth: { ...defaultSizes.lineWidth, ...sizes.lineWidth },
|
|
106
|
+
lineHeight: sizes.lineHeight ?? defaultSizes.lineHeight,
|
|
107
|
+
lineGap: sizes.lineGap ?? defaultSizes.lineGap,
|
|
108
|
+
menuWidth: sizes.menuWidth ?? defaultSizes.menuWidth
|
|
109
|
+
};
|
|
110
|
+
if (tocItems.length === 0) return null;
|
|
111
|
+
const handleClick = (id) => {
|
|
112
|
+
scrollToSection(id, scrollOffset);
|
|
113
|
+
};
|
|
114
|
+
return /* @__PURE__ */ jsxs("div", { className: `fixed left-0 top-1/2 -translate-y-1/2 z-40 hidden lg:block ${className}`, children: [
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className: "absolute -left-20 top-1/2 -translate-y-1/2 w-32 h-96",
|
|
119
|
+
onMouseEnter: () => setIsHovered(true),
|
|
120
|
+
onMouseLeave: () => setIsHovered(false)
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
motion.div,
|
|
125
|
+
{
|
|
126
|
+
className: "relative",
|
|
127
|
+
initial: { opacity: 0, x: -20 },
|
|
128
|
+
animate: { opacity: 1, x: 0 },
|
|
129
|
+
transition: { duration: 0.3, delay: 0.5 },
|
|
130
|
+
onMouseEnter: () => setIsHovered(true),
|
|
131
|
+
onMouseLeave: () => setIsHovered(false),
|
|
132
|
+
children: /* @__PURE__ */ jsx("div", { className: "pl-2 md:pl-4 lg:pl-6", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
133
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: !isHovered && /* @__PURE__ */ jsx(
|
|
134
|
+
motion.div,
|
|
135
|
+
{
|
|
136
|
+
initial: { opacity: 0 },
|
|
137
|
+
animate: { opacity: 1 },
|
|
138
|
+
exit: {
|
|
139
|
+
opacity: 0,
|
|
140
|
+
x: 20,
|
|
141
|
+
transition: { duration: 0.15, ease: "easeOut" }
|
|
142
|
+
},
|
|
143
|
+
transition: { duration: 0.15 },
|
|
144
|
+
className: "absolute top-1/2 left-0 -translate-y-1/2",
|
|
145
|
+
style: { display: "flex", flexDirection: "column", gap: mergedSizes.lineGap },
|
|
146
|
+
children: tocItems.map((item, index) => /* @__PURE__ */ jsx(
|
|
147
|
+
motion.button,
|
|
148
|
+
{
|
|
149
|
+
onClick: () => handleClick(item.id),
|
|
150
|
+
className: "block rounded-sm transition-opacity duration-150 hover:opacity-70",
|
|
151
|
+
style: {
|
|
152
|
+
width: getLineWidth(item.level, mergedSizes.lineWidth),
|
|
153
|
+
height: mergedSizes.lineHeight,
|
|
154
|
+
backgroundColor: getLineColor(item.level, mergedColors.line)
|
|
155
|
+
},
|
|
156
|
+
whileTap: { scale: 0.95 },
|
|
157
|
+
tabIndex: -1,
|
|
158
|
+
animate: {
|
|
159
|
+
x: isHovered ? 20 : 0,
|
|
160
|
+
transition: {
|
|
161
|
+
duration: 0.15,
|
|
162
|
+
delay: index * 0.02,
|
|
163
|
+
ease: "easeOut"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
item.id
|
|
168
|
+
))
|
|
169
|
+
}
|
|
170
|
+
) }),
|
|
171
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: isHovered && /* @__PURE__ */ jsx(
|
|
172
|
+
motion.div,
|
|
173
|
+
{
|
|
174
|
+
initial: {
|
|
175
|
+
opacity: 0,
|
|
176
|
+
x: -20,
|
|
177
|
+
scale: 0.9,
|
|
178
|
+
borderRadius: 0
|
|
179
|
+
},
|
|
180
|
+
animate: {
|
|
181
|
+
opacity: 1,
|
|
182
|
+
x: 0,
|
|
183
|
+
scale: 1,
|
|
184
|
+
borderRadius: 6
|
|
185
|
+
},
|
|
186
|
+
exit: {
|
|
187
|
+
opacity: 0,
|
|
188
|
+
x: -20,
|
|
189
|
+
scale: 0.9,
|
|
190
|
+
transition: { duration: 0.15, ease: "easeOut" }
|
|
191
|
+
},
|
|
192
|
+
transition: {
|
|
193
|
+
duration: 0.2,
|
|
194
|
+
delay: 0.05,
|
|
195
|
+
ease: "easeOut"
|
|
196
|
+
},
|
|
197
|
+
className: "absolute top-1/2 left-0 -translate-y-1/2 backdrop-blur-sm shadow-md p-3",
|
|
198
|
+
style: {
|
|
199
|
+
width: mergedSizes.menuWidth,
|
|
200
|
+
backgroundColor: mergedColors.menu.background,
|
|
201
|
+
boxShadow: `0 0 0 1px ${mergedColors.menu.border}`
|
|
202
|
+
},
|
|
203
|
+
children: /* @__PURE__ */ jsx("nav", { className: "space-y-0.5", children: tocItems.map((item) => {
|
|
204
|
+
const baseLevel = Math.min(...headingLevels);
|
|
205
|
+
const indent = (item.level - baseLevel) * 12;
|
|
206
|
+
return /* @__PURE__ */ jsx(
|
|
207
|
+
MenuItem,
|
|
208
|
+
{
|
|
209
|
+
item,
|
|
210
|
+
indent,
|
|
211
|
+
colors: mergedColors.menu,
|
|
212
|
+
onClick: () => handleClick(item.id)
|
|
213
|
+
},
|
|
214
|
+
item.id
|
|
215
|
+
);
|
|
216
|
+
}) })
|
|
217
|
+
}
|
|
218
|
+
) })
|
|
219
|
+
] }) })
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { MorphingToc };
|
|
226
|
+
//# sourceMappingURL=MorphingToc.mjs.map
|
|
227
|
+
//# sourceMappingURL=MorphingToc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MorphingToc.tsx"],"names":[],"mappings":";;;;;;AAeA,SAAS,SAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAA,EAAQ,SAAQ,EAAkB;AAClE,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,KAAK,CAAA;AAEhD,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,OAAA;AAAA,MACA,YAAA,EAAc,MAAM,YAAA,CAAa,IAAI,CAAA;AAAA,MACrC,YAAA,EAAc,MAAM,YAAA,CAAa,KAAK,CAAA;AAAA,MACtC,SAAA,EAAU,iEAAA;AAAA,MACV,KAAA,EAAO;AAAA,QACL,WAAA,EAAa,CAAA,EAAG,CAAA,GAAI,MAAM,CAAA,EAAA,CAAA;AAAA,QAC1B,KAAA,EAAO,SAAA,GAAY,MAAA,CAAO,SAAA,GAAY,MAAA,CAAO,IAAA;AAAA,QAC7C,eAAA,EAAiB,SAAA,GAAY,MAAA,CAAO,SAAA,GAAY;AAAA,OAClD;AAAA,MACA,QAAA,EAAU,EAAA;AAAA,MAEV,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,gBAAA,EAAkB,eAAK,KAAA,EAAM;AAAA;AAAA,GAC/C;AAEJ;AAEA,MAAM,aAAA,GAA6C;AAAA,EACjD,IAAA,EAAM;AAAA,IACJ,EAAA,EAAI,SAAA;AAAA,IACJ,EAAA,EAAI,SAAA;AAAA,IACJ,EAAA,EAAI,SAAA;AAAA,IACJ,EAAA,EAAI,SAAA;AAAA,IACJ,OAAA,EAAS;AAAA,GACX;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,UAAA,EAAY,2BAAA;AAAA,IACZ,MAAA,EAAQ,0BAAA;AAAA,IACR,IAAA,EAAM,SAAA;AAAA,IACN,SAAA,EAAW,SAAA;AAAA,IACX,SAAA,EAAW;AAAA;AAEf,CAAA;AAEA,MAAM,YAAA,GAA2C;AAAA,EAC/C,SAAA,EAAW;AAAA,IACT,EAAA,EAAI,QAAA;AAAA,IACJ,EAAA,EAAI,MAAA;AAAA,IACJ,EAAA,EAAI,SAAA;AAAA,IACJ,EAAA,EAAI,QAAA;AAAA,IACJ,OAAA,EAAS;AAAA,GACX;AAAA,EACA,UAAA,EAAY,KAAA;AAAA,EACZ,OAAA,EAAS,QAAA;AAAA,EACT,SAAA,EAAW;AACb,CAAA;AAEA,SAAS,YAAA,CAAa,KAAA,EAAe,MAAA,GAAoC,EAAC,EAAW;AACnF,EAAA,MAAM,SAAS,EAAE,GAAG,aAAA,CAAc,IAAA,EAAM,GAAG,MAAA,EAAO;AAClD,EAAA,QAAQ,KAAA;AAAO,IACb,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB;AACE,MAAA,OAAO,MAAA,CAAO,OAAA;AAAA;AAEpB;AAEA,SAAS,YAAA,CAAa,KAAA,EAAe,KAAA,GAAuC,EAAC,EAAW;AACtF,EAAA,MAAM,SAAS,EAAE,GAAG,YAAA,CAAa,SAAA,EAAW,GAAG,KAAA,EAAM;AACrD,EAAA,QAAQ,KAAA;AAAO,IACb,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB,KAAK,CAAA;AACH,MAAA,OAAO,MAAA,CAAO,EAAA;AAAA,IAChB;AACE,MAAA,OAAO,MAAA,CAAO,OAAA;AAAA;AAEpB;AAEO,SAAS,WAAA,CAAY;AAAA,EAC1B,SAAA,GAAY,EAAA;AAAA,EACZ,YAAA,GAAe,EAAA;AAAA,EACf,aAAA,GAAgB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EACxB,WAAA,GAAc,IAAA;AAAA,EACd,iBAAA;AAAA,EACA,SAAS,EAAC;AAAA,EACV,QAAQ;AACV,CAAA,EAAqB;AACnB,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,KAAK,CAAA;AAEhD,EAAA,MAAM,WAAW,WAAA,CAAY;AAAA,IAC3B,aAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,MAAM,YAAA,GAAe;AAAA,IACnB,MAAM,EAAE,GAAG,cAAc,IAAA,EAAM,GAAG,OAAO,IAAA,EAAK;AAAA,IAC9C,MAAM,EAAE,GAAG,cAAc,IAAA,EAAM,GAAG,OAAO,IAAA;AAAK,GAChD;AAEA,EAAA,MAAM,WAAA,GAAc;AAAA,IAClB,WAAW,EAAE,GAAG,aAAa,SAAA,EAAW,GAAG,MAAM,SAAA,EAAU;AAAA,IAC3D,UAAA,EAAY,KAAA,CAAM,UAAA,IAAc,YAAA,CAAa,UAAA;AAAA,IAC7C,OAAA,EAAS,KAAA,CAAM,OAAA,IAAW,YAAA,CAAa,OAAA;AAAA,IACvC,SAAA,EAAW,KAAA,CAAM,SAAA,IAAa,YAAA,CAAa;AAAA,GAC7C;AAEA,EAAA,IAAI,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAElC,EAAA,MAAM,WAAA,GAAc,CAAC,EAAA,KAAe;AAClC,IAAA,eAAA,CAAgB,IAAI,YAAY,CAAA;AAAA,EAClC,CAAA;AAEA,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,CAAA,2DAAA,EAA8D,SAAS,CAAA,CAAA,EAErF,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,sDAAA;AAAA,QACV,YAAA,EAAc,MAAM,YAAA,CAAa,IAAI,CAAA;AAAA,QACrC,YAAA,EAAc,MAAM,YAAA,CAAa,KAAK;AAAA;AAAA,KACxC;AAAA,oBAEA,GAAA;AAAA,MAAC,MAAA,CAAO,GAAA;AAAA,MAAP;AAAA,QACC,SAAA,EAAU,UAAA;AAAA,QACV,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAG,GAAG,GAAA,EAAI;AAAA,QAC9B,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAG,GAAG,CAAA,EAAE;AAAA,QAC5B,UAAA,EAAY,EAAE,QAAA,EAAU,GAAA,EAAK,OAAO,GAAA,EAAI;AAAA,QACxC,YAAA,EAAc,MAAM,YAAA,CAAa,IAAI,CAAA;AAAA,QACrC,YAAA,EAAc,MAAM,YAAA,CAAa,KAAK,CAAA;AAAA,QAEtC,8BAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBACb,QAAA,kBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,UAAA,EAEb,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,eAAA,EAAA,EACE,WAAC,SAAA,oBACA,GAAA;AAAA,YAAC,MAAA,CAAO,GAAA;AAAA,YAAP;AAAA,cACC,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,cACtB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,cACtB,IAAA,EAAM;AAAA,gBACJ,OAAA,EAAS,CAAA;AAAA,gBACT,CAAA,EAAG,EAAA;AAAA,gBACH,UAAA,EAAY,EAAE,QAAA,EAAU,IAAA,EAAM,MAAM,SAAA;AAAU,eAChD;AAAA,cACA,UAAA,EAAY,EAAE,QAAA,EAAU,IAAA,EAAK;AAAA,cAC7B,SAAA,EAAU,0CAAA;AAAA,cACV,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,eAAe,QAAA,EAAU,GAAA,EAAK,YAAY,OAAA,EAAQ;AAAA,cAE3E,QAAA,EAAA,QAAA,CAAS,GAAA,CAAI,CAAC,IAAA,EAAM,KAAA,qBACnB,GAAA;AAAA,gBAAC,MAAA,CAAO,MAAA;AAAA,gBAAP;AAAA,kBAEC,OAAA,EAAS,MAAM,WAAA,CAAY,IAAA,CAAK,EAAE,CAAA;AAAA,kBAClC,SAAA,EAAU,mEAAA;AAAA,kBACV,KAAA,EAAO;AAAA,oBACL,KAAA,EAAO,YAAA,CAAa,IAAA,CAAK,KAAA,EAAO,YAAY,SAAS,CAAA;AAAA,oBACrD,QAAQ,WAAA,CAAY,UAAA;AAAA,oBACpB,eAAA,EAAiB,YAAA,CAAa,IAAA,CAAK,KAAA,EAAO,aAAa,IAAI;AAAA,mBAC7D;AAAA,kBACA,QAAA,EAAU,EAAE,KAAA,EAAO,IAAA,EAAK;AAAA,kBACxB,QAAA,EAAU,EAAA;AAAA,kBACV,OAAA,EAAS;AAAA,oBACP,CAAA,EAAG,YAAY,EAAA,GAAK,CAAA;AAAA,oBACpB,UAAA,EAAY;AAAA,sBACV,QAAA,EAAU,IAAA;AAAA,sBACV,OAAO,KAAA,GAAQ,IAAA;AAAA,sBACf,IAAA,EAAM;AAAA;AACR;AACF,iBAAA;AAAA,gBAjBK,IAAA,CAAK;AAAA,eAmBb;AAAA;AAAA,WACH,EAEJ,CAAA;AAAA,0BAGA,GAAA,CAAC,mBACE,QAAA,EAAA,SAAA,oBACC,GAAA;AAAA,YAAC,MAAA,CAAO,GAAA;AAAA,YAAP;AAAA,cACC,OAAA,EAAS;AAAA,gBACP,OAAA,EAAS,CAAA;AAAA,gBACT,CAAA,EAAG,GAAA;AAAA,gBACH,KAAA,EAAO,GAAA;AAAA,gBACP,YAAA,EAAc;AAAA,eAChB;AAAA,cACA,OAAA,EAAS;AAAA,gBACP,OAAA,EAAS,CAAA;AAAA,gBACT,CAAA,EAAG,CAAA;AAAA,gBACH,KAAA,EAAO,CAAA;AAAA,gBACP,YAAA,EAAc;AAAA,eAChB;AAAA,cACA,IAAA,EAAM;AAAA,gBACJ,OAAA,EAAS,CAAA;AAAA,gBACT,CAAA,EAAG,GAAA;AAAA,gBACH,KAAA,EAAO,GAAA;AAAA,gBACP,UAAA,EAAY,EAAE,QAAA,EAAU,IAAA,EAAM,MAAM,SAAA;AAAU,eAChD;AAAA,cACA,UAAA,EAAY;AAAA,gBACV,QAAA,EAAU,GAAA;AAAA,gBACV,KAAA,EAAO,IAAA;AAAA,gBACP,IAAA,EAAM;AAAA,eACR;AAAA,cACA,SAAA,EAAU,yEAAA;AAAA,cACV,KAAA,EAAO;AAAA,gBACL,OAAO,WAAA,CAAY,SAAA;AAAA,gBACnB,eAAA,EAAiB,aAAa,IAAA,CAAK,UAAA;AAAA,gBACnC,SAAA,EAAW,CAAA,UAAA,EAAa,YAAA,CAAa,IAAA,CAAK,MAAM,CAAA;AAAA,eAClD;AAAA,cAEA,8BAAC,KAAA,EAAA,EAAI,SAAA,EAAU,eACZ,QAAA,EAAA,QAAA,CAAS,GAAA,CAAI,CAAC,IAAA,KAAS;AACtB,gBAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,GAAG,aAAa,CAAA;AAC3C,gBAAA,MAAM,MAAA,GAAA,CAAU,IAAA,CAAK,KAAA,GAAQ,SAAA,IAAa,EAAA;AAE1C,gBAAA,uBACE,GAAA;AAAA,kBAAC,QAAA;AAAA,kBAAA;AAAA,oBAEC,IAAA;AAAA,oBACA,MAAA;AAAA,oBACA,QAAQ,YAAA,CAAa,IAAA;AAAA,oBACrB,OAAA,EAAS,MAAM,WAAA,CAAY,IAAA,CAAK,EAAE;AAAA,mBAAA;AAAA,kBAJ7B,IAAA,CAAK;AAAA,iBAKZ;AAAA,cAEJ,CAAC,CAAA,EACH;AAAA;AAAA,WACF,EAEJ;AAAA,SAAA,EACF,CAAA,EACF;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ","file":"MorphingToc.mjs","sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { useTocItems } from './useTocItems';\nimport { scrollToSection } from './scrollToSection';\nimport type { MorphingTocProps, MorphingTocColors, MorphingTocSizes, TocItem } from './types';\n\ninterface MenuItemProps {\n item: TocItem;\n indent: number;\n colors: NonNullable<MorphingTocColors['menu']>;\n onClick: () => void;\n}\n\nfunction MenuItem({ item, indent, colors, onClick }: MenuItemProps) {\n const [isHovered, setIsHovered] = useState(false);\n\n return (\n <button\n onClick={onClick}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n className=\"block w-full text-left px-2 py-1 rounded text-sm cursor-pointer\"\n style={{\n paddingLeft: `${8 + indent}px`,\n color: isHovered ? colors.textHover : colors.text,\n backgroundColor: isHovered ? colors.itemHover : 'transparent',\n }}\n tabIndex={-1}\n >\n <span className=\"block truncate\">{item.title}</span>\n </button>\n );\n}\n\nconst defaultColors: Required<MorphingTocColors> = {\n line: {\n h1: '#475569',\n h2: '#94a3b8',\n h3: '#cbd5e1',\n h4: '#cbd5e1',\n default: '#cbd5e1',\n },\n menu: {\n background: 'rgba(255, 255, 255, 0.95)',\n border: 'rgba(203, 213, 225, 0.3)',\n text: '#475569',\n textHover: '#0f172a',\n itemHover: '#f8fafc',\n },\n};\n\nconst defaultSizes: Required<MorphingTocSizes> = {\n lineWidth: {\n h1: '1.5rem',\n h2: '1rem',\n h3: '0.75rem',\n h4: '0.5rem',\n default: '0.5rem',\n },\n lineHeight: '1px',\n lineGap: '0.5rem',\n menuWidth: '16rem',\n};\n\nfunction getLineColor(level: number, colors: MorphingTocColors['line'] = {}): string {\n const merged = { ...defaultColors.line, ...colors };\n switch (level) {\n case 1:\n return merged.h1!;\n case 2:\n return merged.h2!;\n case 3:\n return merged.h3!;\n case 4:\n return merged.h4!;\n default:\n return merged.default!;\n }\n}\n\nfunction getLineWidth(level: number, sizes: MorphingTocSizes['lineWidth'] = {}): string {\n const merged = { ...defaultSizes.lineWidth, ...sizes };\n switch (level) {\n case 1:\n return merged.h1!;\n case 2:\n return merged.h2!;\n case 3:\n return merged.h3!;\n case 4:\n return merged.h4!;\n default:\n return merged.default!;\n }\n}\n\nexport function MorphingToc({\n className = '',\n scrollOffset = 80,\n headingLevels = [2, 3, 4],\n skipFirstH1 = true,\n containerSelector,\n colors = {},\n sizes = {},\n}: MorphingTocProps) {\n const [isHovered, setIsHovered] = useState(false);\n\n const tocItems = useTocItems({\n headingLevels,\n skipFirstH1,\n containerSelector,\n });\n\n const mergedColors = {\n line: { ...defaultColors.line, ...colors.line },\n menu: { ...defaultColors.menu, ...colors.menu },\n };\n\n const mergedSizes = {\n lineWidth: { ...defaultSizes.lineWidth, ...sizes.lineWidth },\n lineHeight: sizes.lineHeight ?? defaultSizes.lineHeight,\n lineGap: sizes.lineGap ?? defaultSizes.lineGap,\n menuWidth: sizes.menuWidth ?? defaultSizes.menuWidth,\n };\n\n if (tocItems.length === 0) return null;\n\n const handleClick = (id: string) => {\n scrollToSection(id, scrollOffset);\n };\n\n return (\n <div className={`fixed left-0 top-1/2 -translate-y-1/2 z-40 hidden lg:block ${className}`}>\n {/* Invisible hover area for better UX */}\n <div\n className=\"absolute -left-20 top-1/2 -translate-y-1/2 w-32 h-96\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n />\n\n <motion.div\n className=\"relative\"\n initial={{ opacity: 0, x: -20 }}\n animate={{ opacity: 1, x: 0 }}\n transition={{ duration: 0.3, delay: 0.5 }}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <div className=\"pl-2 md:pl-4 lg:pl-6\">\n <div className=\"relative\">\n {/* Collapsed state: minimal lines */}\n <AnimatePresence>\n {!isHovered && (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{\n opacity: 0,\n x: 20,\n transition: { duration: 0.15, ease: 'easeOut' },\n }}\n transition={{ duration: 0.15 }}\n className=\"absolute top-1/2 left-0 -translate-y-1/2\"\n style={{ display: 'flex', flexDirection: 'column', gap: mergedSizes.lineGap }}\n >\n {tocItems.map((item, index) => (\n <motion.button\n key={item.id}\n onClick={() => handleClick(item.id)}\n className=\"block rounded-sm transition-opacity duration-150 hover:opacity-70\"\n style={{\n width: getLineWidth(item.level, mergedSizes.lineWidth),\n height: mergedSizes.lineHeight,\n backgroundColor: getLineColor(item.level, mergedColors.line),\n }}\n whileTap={{ scale: 0.95 }}\n tabIndex={-1}\n animate={{\n x: isHovered ? 20 : 0,\n transition: {\n duration: 0.15,\n delay: index * 0.02,\n ease: 'easeOut',\n },\n }}\n />\n ))}\n </motion.div>\n )}\n </AnimatePresence>\n\n {/* Expanded state: full menu */}\n <AnimatePresence>\n {isHovered && (\n <motion.div\n initial={{\n opacity: 0,\n x: -20,\n scale: 0.9,\n borderRadius: 0,\n }}\n animate={{\n opacity: 1,\n x: 0,\n scale: 1,\n borderRadius: 6,\n }}\n exit={{\n opacity: 0,\n x: -20,\n scale: 0.9,\n transition: { duration: 0.15, ease: 'easeOut' },\n }}\n transition={{\n duration: 0.2,\n delay: 0.05,\n ease: 'easeOut',\n }}\n className=\"absolute top-1/2 left-0 -translate-y-1/2 backdrop-blur-sm shadow-md p-3\"\n style={{\n width: mergedSizes.menuWidth,\n backgroundColor: mergedColors.menu.background,\n boxShadow: `0 0 0 1px ${mergedColors.menu.border}`,\n }}\n >\n <nav className=\"space-y-0.5\">\n {tocItems.map((item) => {\n const baseLevel = Math.min(...headingLevels);\n const indent = (item.level - baseLevel) * 12;\n\n return (\n <MenuItem\n key={item.id}\n item={item}\n indent={indent}\n colors={mergedColors.menu}\n onClick={() => handleClick(item.id)}\n />\n );\n })}\n </nav>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n </div>\n </motion.div>\n </div>\n );\n}\n"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MorphingToc } from './MorphingToc.mjs';
|
|
2
|
+
export { useTocItems } from './useTocItems.mjs';
|
|
3
|
+
export { scrollToSection } from './scrollToSection.mjs';
|
|
4
|
+
export { a as MorphingTocColors, M as MorphingTocProps, b as MorphingTocSizes, T as TocItem, U as UseTocItemsOptions } from './types-Dol3SnRo.mjs';
|
|
5
|
+
import 'react/jsx-runtime';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MorphingToc } from './MorphingToc.js';
|
|
2
|
+
export { useTocItems } from './useTocItems.js';
|
|
3
|
+
export { scrollToSection } from './scrollToSection.js';
|
|
4
|
+
export { a as MorphingTocColors, M as MorphingTocProps, b as MorphingTocSizes, T as TocItem, U as UseTocItemsOptions } from './types-Dol3SnRo.js';
|
|
5
|
+
import 'react/jsx-runtime';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var MorphingToc = require('./MorphingToc');
|
|
5
|
+
var useTocItems = require('./useTocItems');
|
|
6
|
+
var scrollToSection = require('./scrollToSection');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(exports, "MorphingToc", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () { return MorphingToc.MorphingToc; }
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(exports, "useTocItems", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return useTocItems.useTocItems; }
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "scrollToSection", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return scrollToSection.scrollToSection; }
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js","sourcesContent":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.mjs","sourcesContent":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function scrollToSection(id, offset = 80) {
|
|
4
|
+
const element = document.getElementById(id);
|
|
5
|
+
if (element) {
|
|
6
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
7
|
+
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
|
8
|
+
window.scrollTo({
|
|
9
|
+
top: offsetPosition,
|
|
10
|
+
behavior: "smooth"
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.scrollToSection = scrollToSection;
|
|
16
|
+
//# sourceMappingURL=scrollToSection.js.map
|
|
17
|
+
//# sourceMappingURL=scrollToSection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scrollToSection.ts"],"names":[],"mappings":";;AAAO,SAAS,eAAA,CAAgB,EAAA,EAAY,MAAA,GAAiB,EAAA,EAAU;AACrE,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA;AAE1C,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,MAAM,eAAA,GAAkB,OAAA,CAAQ,qBAAA,EAAsB,CAAE,GAAA;AACxD,IAAA,MAAM,cAAA,GAAiB,eAAA,GAAkB,MAAA,CAAO,WAAA,GAAc,MAAA;AAE9D,IAAA,MAAA,CAAO,QAAA,CAAS;AAAA,MACd,GAAA,EAAK,cAAA;AAAA,MACL,QAAA,EAAU;AAAA,KACX,CAAA;AAAA,EACH;AACF","file":"scrollToSection.js","sourcesContent":["export function scrollToSection(id: string, offset: number = 80): void {\n const element = document.getElementById(id);\n\n if (element) {\n const elementPosition = element.getBoundingClientRect().top;\n const offsetPosition = elementPosition + window.pageYOffset - offset;\n\n window.scrollTo({\n top: offsetPosition,\n behavior: 'smooth',\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function scrollToSection(id, offset = 80) {
|
|
2
|
+
const element = document.getElementById(id);
|
|
3
|
+
if (element) {
|
|
4
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
5
|
+
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
|
6
|
+
window.scrollTo({
|
|
7
|
+
top: offsetPosition,
|
|
8
|
+
behavior: "smooth"
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { scrollToSection };
|
|
14
|
+
//# sourceMappingURL=scrollToSection.mjs.map
|
|
15
|
+
//# sourceMappingURL=scrollToSection.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scrollToSection.ts"],"names":[],"mappings":"AAAO,SAAS,eAAA,CAAgB,EAAA,EAAY,MAAA,GAAiB,EAAA,EAAU;AACrE,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA;AAE1C,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,MAAM,eAAA,GAAkB,OAAA,CAAQ,qBAAA,EAAsB,CAAE,GAAA;AACxD,IAAA,MAAM,cAAA,GAAiB,eAAA,GAAkB,MAAA,CAAO,WAAA,GAAc,MAAA;AAE9D,IAAA,MAAA,CAAO,QAAA,CAAS;AAAA,MACd,GAAA,EAAK,cAAA;AAAA,MACL,QAAA,EAAU;AAAA,KACX,CAAA;AAAA,EACH;AACF","file":"scrollToSection.mjs","sourcesContent":["export function scrollToSection(id: string, offset: number = 80): void {\n const element = document.getElementById(id);\n\n if (element) {\n const elementPosition = element.getBoundingClientRect().top;\n const offsetPosition = elementPosition + window.pageYOffset - offset;\n\n window.scrollTo({\n top: offsetPosition,\n behavior: 'smooth',\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface TocItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
level: number;
|
|
5
|
+
}
|
|
6
|
+
interface MorphingTocColors {
|
|
7
|
+
/** Colors for the collapsed state (lines) */
|
|
8
|
+
line?: {
|
|
9
|
+
h1?: string;
|
|
10
|
+
h2?: string;
|
|
11
|
+
h3?: string;
|
|
12
|
+
h4?: string;
|
|
13
|
+
default?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Colors for the expanded state (menu) */
|
|
16
|
+
menu?: {
|
|
17
|
+
background?: string;
|
|
18
|
+
border?: string;
|
|
19
|
+
text?: string;
|
|
20
|
+
textHover?: string;
|
|
21
|
+
itemHover?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface MorphingTocSizes {
|
|
25
|
+
/** Line widths for each heading level */
|
|
26
|
+
lineWidth?: {
|
|
27
|
+
h1?: string;
|
|
28
|
+
h2?: string;
|
|
29
|
+
h3?: string;
|
|
30
|
+
h4?: string;
|
|
31
|
+
default?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Height of each line */
|
|
34
|
+
lineHeight?: string;
|
|
35
|
+
/** Spacing between lines */
|
|
36
|
+
lineGap?: string;
|
|
37
|
+
/** Width of the expanded menu */
|
|
38
|
+
menuWidth?: string;
|
|
39
|
+
}
|
|
40
|
+
interface MorphingTocProps {
|
|
41
|
+
/** CSS class name for the root container */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Scroll offset from top in pixels (accounts for fixed headers) */
|
|
44
|
+
scrollOffset?: number;
|
|
45
|
+
/** Which heading levels to include (default: [2, 3, 4]) */
|
|
46
|
+
headingLevels?: number[];
|
|
47
|
+
/** Whether to skip the first h1 element (typically the page title) */
|
|
48
|
+
skipFirstH1?: boolean;
|
|
49
|
+
/** CSS selector to scope heading search (default: searches entire document) */
|
|
50
|
+
containerSelector?: string;
|
|
51
|
+
/** Custom color configuration */
|
|
52
|
+
colors?: MorphingTocColors;
|
|
53
|
+
/** Custom size configuration */
|
|
54
|
+
sizes?: MorphingTocSizes;
|
|
55
|
+
}
|
|
56
|
+
interface UseTocItemsOptions {
|
|
57
|
+
headingLevels?: number[];
|
|
58
|
+
skipFirstH1?: boolean;
|
|
59
|
+
containerSelector?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type { MorphingTocProps as M, TocItem as T, UseTocItemsOptions as U, MorphingTocColors as a, MorphingTocSizes as b };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface TocItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
level: number;
|
|
5
|
+
}
|
|
6
|
+
interface MorphingTocColors {
|
|
7
|
+
/** Colors for the collapsed state (lines) */
|
|
8
|
+
line?: {
|
|
9
|
+
h1?: string;
|
|
10
|
+
h2?: string;
|
|
11
|
+
h3?: string;
|
|
12
|
+
h4?: string;
|
|
13
|
+
default?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Colors for the expanded state (menu) */
|
|
16
|
+
menu?: {
|
|
17
|
+
background?: string;
|
|
18
|
+
border?: string;
|
|
19
|
+
text?: string;
|
|
20
|
+
textHover?: string;
|
|
21
|
+
itemHover?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface MorphingTocSizes {
|
|
25
|
+
/** Line widths for each heading level */
|
|
26
|
+
lineWidth?: {
|
|
27
|
+
h1?: string;
|
|
28
|
+
h2?: string;
|
|
29
|
+
h3?: string;
|
|
30
|
+
h4?: string;
|
|
31
|
+
default?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Height of each line */
|
|
34
|
+
lineHeight?: string;
|
|
35
|
+
/** Spacing between lines */
|
|
36
|
+
lineGap?: string;
|
|
37
|
+
/** Width of the expanded menu */
|
|
38
|
+
menuWidth?: string;
|
|
39
|
+
}
|
|
40
|
+
interface MorphingTocProps {
|
|
41
|
+
/** CSS class name for the root container */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Scroll offset from top in pixels (accounts for fixed headers) */
|
|
44
|
+
scrollOffset?: number;
|
|
45
|
+
/** Which heading levels to include (default: [2, 3, 4]) */
|
|
46
|
+
headingLevels?: number[];
|
|
47
|
+
/** Whether to skip the first h1 element (typically the page title) */
|
|
48
|
+
skipFirstH1?: boolean;
|
|
49
|
+
/** CSS selector to scope heading search (default: searches entire document) */
|
|
50
|
+
containerSelector?: string;
|
|
51
|
+
/** Custom color configuration */
|
|
52
|
+
colors?: MorphingTocColors;
|
|
53
|
+
/** Custom size configuration */
|
|
54
|
+
sizes?: MorphingTocSizes;
|
|
55
|
+
}
|
|
56
|
+
interface UseTocItemsOptions {
|
|
57
|
+
headingLevels?: number[];
|
|
58
|
+
skipFirstH1?: boolean;
|
|
59
|
+
containerSelector?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type { MorphingTocProps as M, TocItem as T, UseTocItemsOptions as U, MorphingTocColors as a, MorphingTocSizes as b };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
const defaultOptions = {
|
|
6
|
+
headingLevels: [2, 3, 4],
|
|
7
|
+
skipFirstH1: true,
|
|
8
|
+
containerSelector: ""
|
|
9
|
+
};
|
|
10
|
+
function useTocItems(options = {}) {
|
|
11
|
+
const [tocItems, setTocItems] = react.useState([]);
|
|
12
|
+
const { headingLevels, skipFirstH1, containerSelector } = {
|
|
13
|
+
...defaultOptions,
|
|
14
|
+
...options
|
|
15
|
+
};
|
|
16
|
+
const headingLevelsKey = react.useMemo(() => headingLevels.join(","), [headingLevels]);
|
|
17
|
+
react.useEffect(() => {
|
|
18
|
+
const container = containerSelector ? document.querySelector(containerSelector) : document;
|
|
19
|
+
if (!container) {
|
|
20
|
+
setTocItems([]);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const selector = headingLevels.map((level) => `h${level}`).join(", ");
|
|
24
|
+
const headings = container.querySelectorAll(selector);
|
|
25
|
+
const items = [];
|
|
26
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
27
|
+
let skippedFirstH1 = false;
|
|
28
|
+
headings.forEach((heading) => {
|
|
29
|
+
const level = parseInt(heading.tagName.charAt(1));
|
|
30
|
+
const title = heading.textContent || "";
|
|
31
|
+
if (skipFirstH1 && level === 1 && !skippedFirstH1) {
|
|
32
|
+
skippedFirstH1 = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let id = heading.id;
|
|
36
|
+
if (!id) {
|
|
37
|
+
const baseId = title.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").trim();
|
|
38
|
+
id = baseId;
|
|
39
|
+
let counter = 1;
|
|
40
|
+
while (usedIds.has(id)) {
|
|
41
|
+
id = `${baseId}-${counter}`;
|
|
42
|
+
counter++;
|
|
43
|
+
}
|
|
44
|
+
heading.id = id;
|
|
45
|
+
}
|
|
46
|
+
usedIds.add(id);
|
|
47
|
+
items.push({
|
|
48
|
+
id,
|
|
49
|
+
title,
|
|
50
|
+
level
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
setTocItems(items);
|
|
54
|
+
}, [headingLevelsKey, skipFirstH1, containerSelector]);
|
|
55
|
+
return tocItems;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.useTocItems = useTocItems;
|
|
59
|
+
//# sourceMappingURL=useTocItems.js.map
|
|
60
|
+
//# sourceMappingURL=useTocItems.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useTocItems.ts"],"names":["useState","useMemo","useEffect"],"mappings":";;;;AAGA,MAAM,cAAA,GAA+C;AAAA,EACnD,aAAA,EAAe,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EACvB,WAAA,EAAa,IAAA;AAAA,EACb,iBAAA,EAAmB;AACrB,CAAA;AAEO,SAAS,WAAA,CAAY,OAAA,GAA8B,EAAC,EAAc;AACvE,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIA,cAAA,CAAoB,EAAE,CAAA;AAEtD,EAAA,MAAM,EAAE,aAAA,EAAe,WAAA,EAAa,iBAAA,EAAkB,GAAI;AAAA,IACxD,GAAG,cAAA;AAAA,IACH,GAAG;AAAA,GACL;AAGA,EAAA,MAAM,gBAAA,GAAmBC,cAAQ,MAAM,aAAA,CAAc,KAAK,GAAG,CAAA,EAAG,CAAC,aAAa,CAAC,CAAA;AAE/E,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAA,GAAY,iBAAA,GACd,QAAA,CAAS,aAAA,CAAc,iBAAiB,CAAA,GACxC,QAAA;AAEJ,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,WAAA,CAAY,EAAE,CAAA;AACd,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,aAAA,CAAc,GAAA,CAAI,CAAC,KAAA,KAAU,IAAI,KAAK,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AACpE,IAAA,MAAM,QAAA,GAAW,SAAA,CAAU,gBAAA,CAAiB,QAAQ,CAAA;AACpD,IAAA,MAAM,QAAmB,EAAC;AAC1B,IAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,IAAA,IAAI,cAAA,GAAiB,KAAA;AAErB,IAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC5B,MAAA,MAAM,QAAQ,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAC,CAAA;AAChD,MAAA,MAAM,KAAA,GAAQ,QAAQ,WAAA,IAAe,EAAA;AAGrC,MAAA,IAAI,WAAA,IAAe,KAAA,KAAU,CAAA,IAAK,CAAC,cAAA,EAAgB;AACjD,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,KAAK,OAAA,CAAQ,EAAA;AACjB,MAAA,IAAI,CAAC,EAAA,EAAI;AACP,QAAA,MAAM,MAAA,GAAS,KAAA,CACZ,WAAA,EAAY,CACZ,OAAA,CAAQ,WAAA,EAAa,EAAE,CAAA,CACvB,OAAA,CAAQ,MAAA,EAAQ,GAAG,CAAA,CACnB,IAAA,EAAK;AAER,QAAA,EAAA,GAAK,MAAA;AACL,QAAA,IAAI,OAAA,GAAU,CAAA;AACd,QAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACtB,UAAA,EAAA,GAAK,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA;AACzB,UAAA,OAAA,EAAA;AAAA,QACF;AAEA,QAAA,OAAA,CAAQ,EAAA,GAAK,EAAA;AAAA,MACf;AAEA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAEd,MAAA,KAAA,CAAM,IAAA,CAAK;AAAA,QACT,EAAA;AAAA,QACA,KAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AAED,IAAA,WAAA,CAAY,KAAK,CAAA;AAAA,EACnB,CAAA,EAAG,CAAC,gBAAA,EAAkB,WAAA,EAAa,iBAAiB,CAAC,CAAA;AAErD,EAAA,OAAO,QAAA;AACT","file":"useTocItems.js","sourcesContent":["import { useState, useEffect, useMemo } from 'react';\nimport type { TocItem, UseTocItemsOptions } from './types';\n\nconst defaultOptions: Required<UseTocItemsOptions> = {\n headingLevels: [2, 3, 4],\n skipFirstH1: true,\n containerSelector: '',\n};\n\nexport function useTocItems(options: UseTocItemsOptions = {}): TocItem[] {\n const [tocItems, setTocItems] = useState<TocItem[]>([]);\n\n const { headingLevels, skipFirstH1, containerSelector } = {\n ...defaultOptions,\n ...options,\n };\n\n // Stable reference for headingLevels to avoid unnecessary re-renders\n const headingLevelsKey = useMemo(() => headingLevels.join(','), [headingLevels]);\n\n useEffect(() => {\n const container = containerSelector\n ? document.querySelector(containerSelector)\n : document;\n\n if (!container) {\n setTocItems([]);\n return;\n }\n\n const selector = headingLevels.map((level) => `h${level}`).join(', ');\n const headings = container.querySelectorAll(selector);\n const items: TocItem[] = [];\n const usedIds = new Set<string>();\n let skippedFirstH1 = false;\n\n headings.forEach((heading) => {\n const level = parseInt(heading.tagName.charAt(1));\n const title = heading.textContent || '';\n\n // Skip the first h1 if configured\n if (skipFirstH1 && level === 1 && !skippedFirstH1) {\n skippedFirstH1 = true;\n return;\n }\n\n // Generate unique ID if heading doesn't have one\n let id = heading.id;\n if (!id) {\n const baseId = title\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .trim();\n\n id = baseId;\n let counter = 1;\n while (usedIds.has(id)) {\n id = `${baseId}-${counter}`;\n counter++;\n }\n\n heading.id = id;\n }\n\n usedIds.add(id);\n\n items.push({\n id,\n title,\n level,\n });\n });\n\n setTocItems(items);\n }, [headingLevelsKey, skipFirstH1, containerSelector]);\n\n return tocItems;\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
headingLevels: [2, 3, 4],
|
|
5
|
+
skipFirstH1: true,
|
|
6
|
+
containerSelector: ""
|
|
7
|
+
};
|
|
8
|
+
function useTocItems(options = {}) {
|
|
9
|
+
const [tocItems, setTocItems] = useState([]);
|
|
10
|
+
const { headingLevels, skipFirstH1, containerSelector } = {
|
|
11
|
+
...defaultOptions,
|
|
12
|
+
...options
|
|
13
|
+
};
|
|
14
|
+
const headingLevelsKey = useMemo(() => headingLevels.join(","), [headingLevels]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const container = containerSelector ? document.querySelector(containerSelector) : document;
|
|
17
|
+
if (!container) {
|
|
18
|
+
setTocItems([]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const selector = headingLevels.map((level) => `h${level}`).join(", ");
|
|
22
|
+
const headings = container.querySelectorAll(selector);
|
|
23
|
+
const items = [];
|
|
24
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
25
|
+
let skippedFirstH1 = false;
|
|
26
|
+
headings.forEach((heading) => {
|
|
27
|
+
const level = parseInt(heading.tagName.charAt(1));
|
|
28
|
+
const title = heading.textContent || "";
|
|
29
|
+
if (skipFirstH1 && level === 1 && !skippedFirstH1) {
|
|
30
|
+
skippedFirstH1 = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let id = heading.id;
|
|
34
|
+
if (!id) {
|
|
35
|
+
const baseId = title.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").trim();
|
|
36
|
+
id = baseId;
|
|
37
|
+
let counter = 1;
|
|
38
|
+
while (usedIds.has(id)) {
|
|
39
|
+
id = `${baseId}-${counter}`;
|
|
40
|
+
counter++;
|
|
41
|
+
}
|
|
42
|
+
heading.id = id;
|
|
43
|
+
}
|
|
44
|
+
usedIds.add(id);
|
|
45
|
+
items.push({
|
|
46
|
+
id,
|
|
47
|
+
title,
|
|
48
|
+
level
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
setTocItems(items);
|
|
52
|
+
}, [headingLevelsKey, skipFirstH1, containerSelector]);
|
|
53
|
+
return tocItems;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { useTocItems };
|
|
57
|
+
//# sourceMappingURL=useTocItems.mjs.map
|
|
58
|
+
//# sourceMappingURL=useTocItems.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useTocItems.ts"],"names":[],"mappings":";;AAGA,MAAM,cAAA,GAA+C;AAAA,EACnD,aAAA,EAAe,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EACvB,WAAA,EAAa,IAAA;AAAA,EACb,iBAAA,EAAmB;AACrB,CAAA;AAEO,SAAS,WAAA,CAAY,OAAA,GAA8B,EAAC,EAAc;AACvE,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,QAAA,CAAoB,EAAE,CAAA;AAEtD,EAAA,MAAM,EAAE,aAAA,EAAe,WAAA,EAAa,iBAAA,EAAkB,GAAI;AAAA,IACxD,GAAG,cAAA;AAAA,IACH,GAAG;AAAA,GACL;AAGA,EAAA,MAAM,gBAAA,GAAmB,QAAQ,MAAM,aAAA,CAAc,KAAK,GAAG,CAAA,EAAG,CAAC,aAAa,CAAC,CAAA;AAE/E,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAA,GAAY,iBAAA,GACd,QAAA,CAAS,aAAA,CAAc,iBAAiB,CAAA,GACxC,QAAA;AAEJ,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,WAAA,CAAY,EAAE,CAAA;AACd,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,aAAA,CAAc,GAAA,CAAI,CAAC,KAAA,KAAU,IAAI,KAAK,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AACpE,IAAA,MAAM,QAAA,GAAW,SAAA,CAAU,gBAAA,CAAiB,QAAQ,CAAA;AACpD,IAAA,MAAM,QAAmB,EAAC;AAC1B,IAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,IAAA,IAAI,cAAA,GAAiB,KAAA;AAErB,IAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC5B,MAAA,MAAM,QAAQ,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAC,CAAA;AAChD,MAAA,MAAM,KAAA,GAAQ,QAAQ,WAAA,IAAe,EAAA;AAGrC,MAAA,IAAI,WAAA,IAAe,KAAA,KAAU,CAAA,IAAK,CAAC,cAAA,EAAgB;AACjD,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,KAAK,OAAA,CAAQ,EAAA;AACjB,MAAA,IAAI,CAAC,EAAA,EAAI;AACP,QAAA,MAAM,MAAA,GAAS,KAAA,CACZ,WAAA,EAAY,CACZ,OAAA,CAAQ,WAAA,EAAa,EAAE,CAAA,CACvB,OAAA,CAAQ,MAAA,EAAQ,GAAG,CAAA,CACnB,IAAA,EAAK;AAER,QAAA,EAAA,GAAK,MAAA;AACL,QAAA,IAAI,OAAA,GAAU,CAAA;AACd,QAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACtB,UAAA,EAAA,GAAK,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA;AACzB,UAAA,OAAA,EAAA;AAAA,QACF;AAEA,QAAA,OAAA,CAAQ,EAAA,GAAK,EAAA;AAAA,MACf;AAEA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAEd,MAAA,KAAA,CAAM,IAAA,CAAK;AAAA,QACT,EAAA;AAAA,QACA,KAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AAED,IAAA,WAAA,CAAY,KAAK,CAAA;AAAA,EACnB,CAAA,EAAG,CAAC,gBAAA,EAAkB,WAAA,EAAa,iBAAiB,CAAC,CAAA;AAErD,EAAA,OAAO,QAAA;AACT","file":"useTocItems.mjs","sourcesContent":["import { useState, useEffect, useMemo } from 'react';\nimport type { TocItem, UseTocItemsOptions } from './types';\n\nconst defaultOptions: Required<UseTocItemsOptions> = {\n headingLevels: [2, 3, 4],\n skipFirstH1: true,\n containerSelector: '',\n};\n\nexport function useTocItems(options: UseTocItemsOptions = {}): TocItem[] {\n const [tocItems, setTocItems] = useState<TocItem[]>([]);\n\n const { headingLevels, skipFirstH1, containerSelector } = {\n ...defaultOptions,\n ...options,\n };\n\n // Stable reference for headingLevels to avoid unnecessary re-renders\n const headingLevelsKey = useMemo(() => headingLevels.join(','), [headingLevels]);\n\n useEffect(() => {\n const container = containerSelector\n ? document.querySelector(containerSelector)\n : document;\n\n if (!container) {\n setTocItems([]);\n return;\n }\n\n const selector = headingLevels.map((level) => `h${level}`).join(', ');\n const headings = container.querySelectorAll(selector);\n const items: TocItem[] = [];\n const usedIds = new Set<string>();\n let skippedFirstH1 = false;\n\n headings.forEach((heading) => {\n const level = parseInt(heading.tagName.charAt(1));\n const title = heading.textContent || '';\n\n // Skip the first h1 if configured\n if (skipFirstH1 && level === 1 && !skippedFirstH1) {\n skippedFirstH1 = true;\n return;\n }\n\n // Generate unique ID if heading doesn't have one\n let id = heading.id;\n if (!id) {\n const baseId = title\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .trim();\n\n id = baseId;\n let counter = 1;\n while (usedIds.has(id)) {\n id = `${baseId}-${counter}`;\n counter++;\n }\n\n heading.id = id;\n }\n\n usedIds.add(id);\n\n items.push({\n id,\n title,\n level,\n });\n });\n\n setTocItems(items);\n }, [headingLevelsKey, skipFirstH1, containerSelector]);\n\n return tocItems;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "morphing-toc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A React table of contents component that shows minimal lines and morphs into a full menu on hover",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup && node scripts/ensure-use-client.js",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=18.0.0",
|
|
26
|
+
"react-dom": ">=18.0.0",
|
|
27
|
+
"motion": ">=11.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"react": "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"react",
|
|
39
|
+
"table-of-contents",
|
|
40
|
+
"toc",
|
|
41
|
+
"navigation",
|
|
42
|
+
"animation",
|
|
43
|
+
"framer-motion",
|
|
44
|
+
"motion",
|
|
45
|
+
"morphing"
|
|
46
|
+
],
|
|
47
|
+
"author": "Antoine Pirard",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/antoinepirard/morphing-toc"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://antoinepirard.com/open-source/morphing-toc"
|
|
54
|
+
}
|