@todovue/tv-toc 1.0.2 → 1.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/README.md CHANGED
@@ -16,13 +16,12 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
16
16
 
17
17
  > Demo: https://ui.todovue.blog/toc
18
18
 
19
- ---
20
19
  ## Table of Contents
21
20
  - [Features](#features)
22
21
  - [Installation](#installation)
23
22
  - [Usage of Styles](#usage-of-styles)
24
23
  - [Quick Start (SPA)](#quick-start-spa)
25
- - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
24
+ - [Nuxt 4 / SSR Usage](#nuxt-4--ssr-usage)
26
25
  - [Component Registration Options](#component-registration-options)
27
26
  - [Props](#props)
28
27
  - [Composable: useToc](#composable-usetoc)
@@ -33,7 +32,6 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
33
32
  - [Contributing](#contributing)
34
33
  - [License](#license)
35
34
 
36
- ---
37
35
  ## Features
38
36
  - Simple and focused Table of Contents (TOC) component for Vue 3.
39
37
  - Supports nested sections via children links.
@@ -42,7 +40,6 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
42
40
  - Works in SPA (Vite, Vue CLI) and Nuxt 3 (with client-side rendering constraints).
43
41
  - Ships with minimal, customizable styles.
44
42
 
45
- ---
46
43
  ## Installation
47
44
  Using npm:
48
45
  ```bash
@@ -57,7 +54,6 @@ Using pnpm:
57
54
  pnpm add @todovue/tv-toc
58
55
  ```
59
56
 
60
- ---
61
57
  ## Usage of Styles
62
58
 
63
59
  ### Vue/Vite (SPA)
@@ -86,7 +82,6 @@ export default defineNuxtConfig({
86
82
  })
87
83
  ```
88
84
 
89
- ---
90
85
  ## Quick Start (SPA)
91
86
  Global registration (main.js / main.ts):
92
87
  ```js
@@ -139,8 +134,7 @@ const toc = {
139
134
  </template>
140
135
  ```
141
136
 
142
- ---
143
- ## Nuxt 3 / SSR Usage
137
+ ## Nuxt 4 / SSR Usage
144
138
  Create a plugin file: `plugins/tv-toc.client.ts` (client-only because it uses `document` and `history` under the hood when scrolling):
145
139
  ```ts
146
140
  import { defineNuxtPlugin } from '#app'
@@ -169,7 +163,6 @@ import { TvToc } from '@todovue/tv-toc'
169
163
  </template>
170
164
  ```
171
165
 
172
- ---
173
166
  ## Component Registration Options
174
167
  | Approach | When to use |
175
168
  |-----------------------------------------------|-----------------------------------|
@@ -177,11 +170,14 @@ import { TvToc } from '@todovue/tv-toc'
177
170
  | Local named import `{ TvToc }` | Isolated/code-split contexts |
178
171
  | Direct default import `import TvToc from ...` | Single use or manual registration |
179
172
 
180
- ---
181
173
  ## Props
182
- | Name | Type | Default | Description | Required |
183
- |------|--------|---------|----------------------------------------------------------------------------|----------|
184
- | toc | Object | - | TOC configuration: title and list of links (with optional nested children) | `true` |
174
+ | Name | Type | Default | Description | Required |
175
+ |-----------------|---------|------------|----------------------------------------------------------------------------|----------|
176
+ | toc | Object | - | TOC configuration: title and list of links (with optional nested children) | `true` |
177
+ | marker | Boolean | `false` | Whether to display a visual marker for the active item. | `false` |
178
+ | collapsible | Boolean | `false` | Whether sublists can be collapsed/expanded. | `false` |
179
+ | activeClass | String | `'active'` | Custom CSS class for the active item. | `false` |
180
+ | observerOptions | Object | `{}` | options to pass to the IntersectionObserver (rootMargin, threshold, etc). | `false` |
185
181
 
186
182
  ### `toc` shape
187
183
  ```ts
@@ -200,16 +196,17 @@ type Toc = {
200
196
  - `links`: Array of top-level sections.
201
197
  - `id`: Must match the `id` attribute of the target heading in your content.
202
198
  - `text`: Label shown in the TOC.
203
- - `children`: Optional array of sub-sections, rendered as nested list.
199
+ - `children`: Optional array of subsections, rendered as nested list.
204
200
 
205
- ---
206
201
  ## Composable: `useToc`
207
202
  This composable is used internally by `TvToc` but can also be imported directly if needed.
208
203
 
209
204
  ```ts
210
205
  import { useToc } from '@todovue/tv-toc'
211
206
 
212
- const { formatId, scrollToId } = useToc()
207
+ const { formatId, scrollToId } = useToc(links, {
208
+ rootMargin: '0px 0px -50% 0px'
209
+ })
213
210
  ```
214
211
 
215
212
  ### API
@@ -220,7 +217,6 @@ const { formatId, scrollToId } = useToc()
220
217
 
221
218
  > Note: `scrollToId` accesses `document` and `history`, so it should run only in the browser (e.g. in event handlers or inside `onMounted`).
222
219
 
223
- ---
224
220
  ## Customization (Styles)
225
221
  The component ships with minimal default styles, exposed through the built CSS file and scoped CSS classes.
226
222
 
@@ -254,13 +250,11 @@ You can override these styles in your own global stylesheet:
254
250
 
255
251
  If you are using SCSS, you can also rely on your own design tokens and overrides. The package itself internally uses SCSS (see `src/assets/scss/_variables.scss` and `src/assets/scss/style.scss`).
256
252
 
257
- ---
258
253
  ## SSR Notes
259
254
  - The component can be rendered on the server (template is static), but scrolling behavior uses browser APIs.
260
255
  - `scrollToId` uses `document.getElementById` and `history.pushState`; these are only invoked in event handlers on the client.
261
256
  - When using Nuxt 3, prefer registering `TvToc` in a `*.client.ts` plugin or wrap usages in `<client-only>` to avoid hydration edge cases in environments with stricter SSR.
262
257
 
263
- ---
264
258
  ## Examples
265
259
  This repository includes a small demo application built with Vite.
266
260
 
@@ -269,7 +263,6 @@ This repository includes a small demo application built with Vite.
269
263
 
270
264
  To run the demo locally, see the [Development](#development) section.
271
265
 
272
- ---
273
266
  ## Development
274
267
  ```bash
275
268
  git clone https://github.com/TODOvue/tv-toc.git
@@ -285,15 +278,11 @@ To build the standalone demo used for documentation:
285
278
  npm run build:demo
286
279
  ```
287
280
 
288
- ---
289
281
  ## Contributing
290
282
  PRs and issues are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
291
283
 
292
- ---
293
284
  ## License
294
285
  MIT © TODOvue
295
286
 
296
- ---
297
287
  ### Attributions
298
288
  Crafted for the TODOvue component ecosystem
299
-
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),g=(r=[])=>{const a=e.ref(null),d=e.ref(null);let s=null;const u=c=>`#${c}`,v=c=>{const t=document.getElementById(c);t&&(t.scrollIntoView({behavior:"smooth"}),history.pushState(null,null,`#${c}`))},h=c=>{const t=[];return c.forEach(n=>{t.push({id:n.id,parentId:null}),n.children&&n.children.forEach(o=>{t.push({id:o.id,parentId:n.id})})}),t};return{formatId:u,scrollToId:v,activeId:a,activeParentId:d,setupObserver:()=>{if(typeof window>"u"||!r.length)return;const c=h(r),t=c.map(({id:n})=>document.getElementById(n)).filter(n=>n!==null);t.length&&(s=new IntersectionObserver(n=>{n.forEach(o=>{if(o.isIntersecting){const l=o.target.id,f=c.find(I=>I.id===l);f&&(a.value=l,d.value=f.parentId,history.replaceState(null,null,`#${l}`))}})},{rootMargin:"-20% 0px -70% 0px",threshold:0}),t.forEach(n=>s.observe(n)))},cleanup:()=>{s&&(s.disconnect(),s=null)}}},B={class:"tv-toc"},E={key:0,class:"tv-toc-title"},T={class:"tv-toc-list"},_=["href","onClick"],k={key:0,class:"tv-toc-sublist"},y=["href","onClick"],b={__name:"TvToc",props:{toc:{type:Object,required:!0}},setup(r){const a=r,{scrollToId:d,activeId:s,activeParentId:u,setupObserver:v,cleanup:h}=g(a.toc?.links||[]),m=t=>{d(t)},p=t=>s.value===t,c=t=>u.value===t;return e.onMounted(()=>{v()}),e.onUnmounted(()=>{h()}),(t,n)=>(e.openBlock(),e.createElementBlock("nav",B,[r.toc?.title?(e.openBlock(),e.createElementBlock("h3",E,e.toDisplayString(r.toc.title),1)):e.createCommentVNode("",!0),e.createElementVNode("ul",T,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(r.toc?.links,o=>(e.openBlock(),e.createElementBlock("li",{key:o.id,class:"tv-toc-item"},[e.createElementVNode("a",{href:`#${o.id}`,class:e.normalizeClass(["tv-toc-link",{active:p(o.id),"parent-active":c(o.id)}]),onClick:e.withModifiers(l=>m(o.id),["prevent"])},e.toDisplayString(o.text),11,_),o.children?(e.openBlock(),e.createElementBlock("ul",k,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.children,l=>(e.openBlock(),e.createElementBlock("li",{key:l.id,class:"tv-toc-subitem"},[e.createElementVNode("a",{href:`#${l.id}`,class:e.normalizeClass(["tv-toc-sublink",{active:p(l.id)}]),onClick:e.withModifiers(f=>m(l.id),["prevent"])},e.toDisplayString(l.text),11,y)]))),128))])):e.createCommentVNode("",!0)]))),128))])]))}},i=b;i.install=r=>{r.component("TvToc",i)};const C={install:i.install};exports.TvToc=i;exports.TvTocPlugin=C;exports.default=i;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),B=(n=[],a={})=>{const p=e.ref(null),h=e.ref(null);let d=null;const k=r=>`#${r}`,y=r=>{const c=document.getElementById(r);c&&(c.scrollIntoView({behavior:"smooth"}),history.pushState(null,null,`#${r}`))},u=r=>{const c=[];return r.forEach(s=>{c.push({id:s.id,parentId:null}),s.children&&s.children.forEach(v=>{c.push({id:v.id,parentId:s.id})})}),c};return{formatId:k,scrollToId:y,activeId:p,activeParentId:h,setupObserver:()=>{if(typeof window>"u"||!n.length)return;const r=u(n),c=r.map(({id:s})=>document.getElementById(s)).filter(s=>s!==null);c.length&&(d=new IntersectionObserver(s=>{s.forEach(v=>{if(v.isIntersecting){const m=v.target.id,t=r.find(l=>l.id===m);t&&(p.value=m,h.value=t.parentId,history.replaceState(null,null,`#${m}`))}})},{rootMargin:"-20% 0px -70% 0px",threshold:0,...a}),c.forEach(s=>d.observe(s)))},cleanup:()=>{d&&(d.disconnect(),d=null)}}},b={class:"tv-toc"},w={key:0,class:"tv-toc-progress-container"},C={key:1,class:"tv-toc-title"},I={class:"tv-toc-list"},T={class:"tv-toc-item-content"},S=["href","onClick"],x=["onClick"],V={class:"tv-toc-sublist"},N=["href","onClick"],O={__name:"TvToc",props:{toc:{type:Object,required:!0},marker:{type:Boolean,default:!1},activeClass:{type:String,default:"active"},observerOptions:{type:Object,default:()=>({})},collapsible:{type:Boolean,default:!1}},setup(n){const a=n,{scrollToId:p,activeId:h,activeParentId:d,setupObserver:k,cleanup:y}=B(a.toc?.links||[],a.observerOptions),u=e.ref(new Set),E=t=>{const l=new Set(u.value);l.has(t)?l.delete(t):l.add(t),u.value=l},g=t=>!a.collapsible||u.value.has(t),r=t=>{p(t)},c=t=>h.value===t,s=t=>d.value===t;e.watch(d,t=>{if(a.collapsible&&t&&!u.value.has(t)){const l=new Set(u.value);l.add(t),u.value=l}});const v=e.ref(0),m=e.computed(()=>{const t=[],l=o=>{if(o)for(const i of o)t.push(i.id),i.children&&l(i.children)};return l(a.toc?.links),t});return e.watch(h,t=>{if(!t)return;const l=m.value.indexOf(t);if(l!==-1){const o=m.value.length;o>0&&(v.value=(l+1)/o*100)}},{immediate:!0}),e.onMounted(()=>{a.collapsible&&d.value&&E(d.value),k()}),e.onUnmounted(()=>{y()}),(t,l)=>(e.openBlock(),e.createElementBlock("nav",b,[n.toc?.links?.length?(e.openBlock(),e.createElementBlock("div",w,[e.createElementVNode("div",{class:"tv-toc-progress-bar",style:e.normalizeStyle({height:`${v.value}%`})},null,4)])):e.createCommentVNode("",!0),n.toc?.title?(e.openBlock(),e.createElementBlock("h3",C,e.toDisplayString(n.toc.title),1)):e.createCommentVNode("",!0),e.createElementVNode("ul",I,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(n.toc?.links,o=>(e.openBlock(),e.createElementBlock("li",{key:o.id,class:"tv-toc-item"},[e.createElementVNode("div",T,[e.createElementVNode("a",{href:`#${o.id}`,class:e.normalizeClass(["tv-toc-link",{[a.activeClass]:c(o.id),"parent-active":s(o.id),"tv-toc-marker":n.marker&&c(o.id)}]),onClick:e.withModifiers(i=>r(o.id),["prevent"])},e.toDisplayString(o.text),11,S),n.collapsible&&o.children&&o.children.length?(e.openBlock(),e.createElementBlock("button",{key:0,class:e.normalizeClass(["tv-toc-toggle",{"is-expanded":g(o.id)}]),onClick:e.withModifiers(i=>E(o.id),["stop"]),"aria-label":"Toggle section"},[...l[0]||(l[0]=[e.createElementVNode("svg",{xmlns:"http://www.w3.org/2000/svg",width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e.createElementVNode("polyline",{points:"6 9 12 15 18 9"})],-1)])],10,x)):e.createCommentVNode("",!0)]),o.children?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["tv-toc-sublist-wrapper",{"is-collapsed":n.collapsible&&!g(o.id)}]),style:e.normalizeStyle(n.collapsible?{"--content-height":g(o.id)?"1000px":"0px"}:{})},[e.createElementVNode("ul",V,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.children,i=>(e.openBlock(),e.createElementBlock("li",{key:i.id,class:"tv-toc-subitem"},[e.createElementVNode("a",{href:`#${i.id}`,class:e.normalizeClass(["tv-toc-sublink",{[a.activeClass]:c(i.id),"tv-toc-marker":n.marker&&c(i.id)}]),onClick:e.withModifiers(M=>r(i.id),["prevent"])},e.toDisplayString(i.text),11,N)]))),128))])],6)):e.createCommentVNode("",!0)]))),128))])]))}},f=O;f.install=n=>{n.component("TvToc",f)};const $={install:f.install};exports.TvToc=f;exports.TvTocPlugin=$;exports.default=f;
package/dist/tv-toc.css CHANGED
@@ -1 +1 @@
1
- .tv-toc{padding:1rem;background-color:#b9c4df;border-radius:8px;color:#000b14;min-width:200px}@media(prefers-color-scheme:dark){.tv-toc{background-color:#0e131f;color:#f4faff}}.tv-toc .tv-toc-title{font-size:1.2rem;font-weight:700;margin-bottom:.5rem}.tv-toc .tv-toc-list{list-style:none;padding:0;margin:0}.tv-toc .tv-toc-item{margin-bottom:.5rem;position:relative}.tv-toc .tv-toc-item:hover .tv-toc-link,.tv-toc .tv-toc-item:hover .tv-toc-sublink{background-color:#ef233c0d}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-item:hover .tv-toc-link,.tv-toc .tv-toc-item:hover .tv-toc-sublink{background-color:#ef233c0d}}.tv-toc .tv-toc-link{display:block;text-decoration:none;color:inherit;font-weight:500;padding:.5rem .75rem;border-radius:4px;transition:all .3s ease;position:relative}.tv-toc .tv-toc-link:hover{color:#ef233c}.tv-toc .tv-toc-link.active{color:#ef233c;font-weight:700;background-color:#ef233c1a;border-left:3px solid #EF233C;padding-left:calc(1rem - 3px)}.tv-toc .tv-toc-link.parent-active{color:#ef233c;font-weight:500;border-left:2px solid rgba(239,35,60,.5);padding-left:calc(1rem - 2px)}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-link:hover{color:#ef233c}.tv-toc .tv-toc-link.active{color:#ef233c;background-color:#ef233c1a;border-left-color:#ef233c}.tv-toc .tv-toc-link.parent-active{color:#ef233c;border-left-color:#ef233c80}}.tv-toc .tv-toc-sublist{list-style:none;padding-left:1rem;margin-top:.25rem;margin-bottom:.25rem;border-left:2px solid rgba(0,11,20,.1)}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-sublist{border-left-color:#f4faff1a}}.tv-toc .tv-toc-subitem{margin-bottom:.25rem;position:relative}.tv-toc .tv-toc-sublink{display:block;text-decoration:none;color:inherit;font-size:.9rem;opacity:.8;padding:.4rem .75rem;border-radius:4px;transition:all .3s ease;position:relative}.tv-toc .tv-toc-sublink:hover{opacity:1;color:#ef233c}.tv-toc .tv-toc-sublink.active{opacity:1;color:#ef233c;font-weight:700;background-color:#ef233c1a;border-left:3px solid #EF233C;padding-left:calc(1rem - 3px)}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-sublink:hover{color:#ef233c}.tv-toc .tv-toc-sublink.active{color:#ef233c;background-color:#ef233c1a;border-left-color:#ef233c}}.light-mode .tv-toc{background-color:#b9c4df;color:#000b14}.light-mode .tv-toc .tv-toc-link:hover{color:#ef233c}.light-mode .tv-toc .tv-toc-link.active,.light-mode .tv-toc .tv-toc-link.parent-active{color:#ef233c;border-left-color:#ef233c}.light-mode .tv-toc .tv-toc-link.parent-active{border-left-color:#ef233c80}.light-mode .tv-toc .tv-toc-sublist{border-left-color:#000b141a}.light-mode .tv-toc .tv-toc-sublink:hover,.light-mode .tv-toc .tv-toc-sublink.active{color:#ef233c}.light-mode .tv-toc .tv-toc-sublink.active{border-left-color:#ef233c}
1
+ .tv-toc{position:relative;padding:1rem;background-color:#b9c4df;border-radius:8px;color:#000b14;min-width:200px;overflow:hidden;--toc-active-rgb: 239, 35, 60;--toc-active-color: #EF233C}@media(prefers-color-scheme:dark){.tv-toc{background-color:#0e131f;color:#f4faff;--toc-active-rgb: 239, 35, 60;--toc-active-color: #EF233C}}.tv-toc .tv-toc-progress-container{position:absolute;left:0;top:0;bottom:0;width:4px;background-color:#ef233c1a;z-index:1}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-progress-container{background-color:#ef233c1a}}.tv-toc .tv-toc-progress-bar{width:100%;background:linear-gradient(to bottom,#ef233c,#f68290);transition:height .1s linear;border-radius:0 0 4px 4px}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-progress-bar{background:linear-gradient(to bottom,#ef233c,#f68290)}}.tv-toc .tv-toc-title{font-size:1.2rem;font-weight:700;margin-bottom:.5rem}.tv-toc .tv-toc-list{list-style:none;padding:0;margin:0}.tv-toc .tv-toc-item{margin-bottom:.5rem;position:relative;opacity:0;animation:fadeSlideIn .5s ease forwards}.tv-toc .tv-toc-item:nth-child(1){animation-delay:.05s}.tv-toc .tv-toc-item:nth-child(2){animation-delay:.1s}.tv-toc .tv-toc-item:nth-child(3){animation-delay:.15s}.tv-toc .tv-toc-item:nth-child(4){animation-delay:.2s}.tv-toc .tv-toc-item:nth-child(5){animation-delay:.25s}.tv-toc .tv-toc-item:nth-child(6){animation-delay:.3s}.tv-toc .tv-toc-item:nth-child(7){animation-delay:.35s}.tv-toc .tv-toc-item:nth-child(8){animation-delay:.4s}.tv-toc .tv-toc-item:nth-child(9){animation-delay:.45s}.tv-toc .tv-toc-item:nth-child(10){animation-delay:.5s}.tv-toc .tv-toc-item:nth-child(11){animation-delay:.55s}.tv-toc .tv-toc-item:nth-child(12){animation-delay:.6s}.tv-toc .tv-toc-item:nth-child(13){animation-delay:.65s}.tv-toc .tv-toc-item:nth-child(14){animation-delay:.7s}.tv-toc .tv-toc-item:nth-child(15){animation-delay:.75s}.tv-toc .tv-toc-item:nth-child(16){animation-delay:.8s}.tv-toc .tv-toc-item:nth-child(17){animation-delay:.85s}.tv-toc .tv-toc-item:nth-child(18){animation-delay:.9s}.tv-toc .tv-toc-item:nth-child(19){animation-delay:.95s}.tv-toc .tv-toc-item:nth-child(20){animation-delay:1s}.tv-toc .tv-toc-item .tv-toc-item-content{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.tv-toc .tv-toc-item .tv-toc-item-content .tv-toc-link{flex:1}.tv-toc .tv-toc-item .tv-toc-toggle{background:transparent;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:.6;transition:all .2s ease;border-radius:4px;margin-right:4px}.tv-toc .tv-toc-item .tv-toc-toggle:hover{background-color:#ef233c1a;opacity:1}.tv-toc .tv-toc-item .tv-toc-toggle svg{transition:transform .3s ease}.tv-toc .tv-toc-item .tv-toc-toggle.is-expanded svg{transform:rotate(180deg)}.tv-toc .tv-toc-item:hover .tv-toc-link,.tv-toc .tv-toc-item:hover .tv-toc-sublink{background-color:#ef233c0d}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-item:hover .tv-toc-link,.tv-toc .tv-toc-item:hover .tv-toc-sublink{background-color:#ef233c0d}.tv-toc .tv-toc-item .tv-toc-toggle:hover{background-color:#ef233c1a}}.tv-toc .tv-toc-link{display:block;text-decoration:none;color:inherit;font-weight:500;padding:.5rem .75rem;border-radius:4px;transition:all .3s ease;position:relative}.tv-toc .tv-toc-link:hover{color:#ef233c}.tv-toc .tv-toc-link.active{color:#ef233c;font-weight:700;background-color:#ef233c1a;padding-left:1rem;position:relative;animation:toc-pulse 2s infinite}.tv-toc .tv-toc-link.active:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,var(--toc-active-color),rgba(var(--toc-active-rgb),.2),var(--toc-active-color));background-size:100% 200%;animation:border-flow 2s linear infinite;border-radius:4px 0 0 4px}.tv-toc .tv-toc-link.parent-active{color:#ef233c;font-weight:500;border-left:2px solid rgba(239,35,60,.5);padding-left:calc(1rem - 2px)}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-link:hover{color:#ef233c}.tv-toc .tv-toc-link.active{color:#ef233c;background-color:#ef233c1a}.tv-toc .tv-toc-link.parent-active{color:#ef233c;border-left-color:#ef233c80}}.tv-toc .tv-toc-sublist-wrapper{overflow:hidden;transition:opacity .3s ease,max-height .3s ease;max-height:1000px;opacity:1}.tv-toc .tv-toc-sublist-wrapper.is-collapsed{max-height:0;opacity:0;margin-bottom:0}.tv-toc .tv-toc-sublist{list-style:none;padding-left:1rem;margin-top:.25rem;margin-bottom:.25rem;border-left:2px solid rgba(0,11,20,.1)}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-sublist{border-left-color:#f4faff1a}}.tv-toc .tv-toc-subitem{margin-bottom:.25rem;position:relative;opacity:0;animation:fadeSlideIn .5s ease forwards}.tv-toc .tv-toc-subitem:nth-child(1){animation-delay:.35s}.tv-toc .tv-toc-subitem:nth-child(2){animation-delay:.4s}.tv-toc .tv-toc-subitem:nth-child(3){animation-delay:.45s}.tv-toc .tv-toc-subitem:nth-child(4){animation-delay:.5s}.tv-toc .tv-toc-subitem:nth-child(5){animation-delay:.55s}.tv-toc .tv-toc-subitem:nth-child(6){animation-delay:.6s}.tv-toc .tv-toc-subitem:nth-child(7){animation-delay:.65s}.tv-toc .tv-toc-subitem:nth-child(8){animation-delay:.7s}.tv-toc .tv-toc-subitem:nth-child(9){animation-delay:.75s}.tv-toc .tv-toc-subitem:nth-child(10){animation-delay:.8s}.tv-toc .tv-toc-sublink{display:block;text-decoration:none;color:inherit;font-size:.9rem;opacity:.8;padding:.4rem .75rem;border-radius:4px;transition:all .3s ease;position:relative}.tv-toc .tv-toc-sublink:hover{opacity:1;color:#ef233c}.tv-toc .tv-toc-sublink.active{opacity:1;color:#ef233c;font-weight:700;background-color:#ef233c1a;padding-left:1rem;position:relative;animation:toc-pulse 2s infinite}.tv-toc .tv-toc-sublink.active:before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(180deg,var(--toc-active-color),rgba(var(--toc-active-rgb),.2),var(--toc-active-color));background-size:100% 200%;animation:border-flow 2s linear infinite;border-radius:4px 0 0 4px}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-sublink:hover{color:#ef233c}.tv-toc .tv-toc-sublink.active{color:#ef233c;background-color:#ef233c1a}}.light-mode .tv-toc{background-color:#b9c4df;color:#000b14;--toc-active-rgb: 239, 35, 60;--toc-active-color: #EF233C}.light-mode .tv-toc .tv-toc-link:hover,.light-mode .tv-toc .tv-toc-link.active,.light-mode .tv-toc .tv-toc-link.parent-active{color:#ef233c}.light-mode .tv-toc .tv-toc-link.parent-active{border-left-color:#ef233c80}.light-mode .tv-toc .tv-toc-sublist{border-left-color:#000b141a}.light-mode .tv-toc .tv-toc-sublink:hover,.light-mode .tv-toc .tv-toc-sublink.active{color:#ef233c}.tv-toc-marker{border-left:2px solid var(--tv-toc-active-color, #3b82f6);padding-left:8px;transition:all .2s ease}@keyframes fadeSlideIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes toc-pulse{0%{box-shadow:0 0 rgba(var(--toc-active-rgb),.4)}70%{box-shadow:0 0 0 6px rgba(var(--toc-active-rgb),0)}to{box-shadow:0 0 rgba(var(--toc-active-rgb),0)}}@keyframes border-flow{0%{background-position:0 0}to{background-position:0 200%}}
package/dist/tv-toc.es.js CHANGED
@@ -1,107 +1,186 @@
1
- import { ref as T, onMounted as O, onUnmounted as w, createElementBlock as r, openBlock as i, createCommentVNode as y, createElementVNode as I, toDisplayString as _, Fragment as E, renderList as b, withModifiers as C, normalizeClass as $ } from "vue";
2
- const B = (s = []) => {
3
- const a = T(null), d = T(null);
4
- let l = null;
5
- const u = (o) => `#${o}`, v = (o) => {
6
- const t = document.getElementById(o);
7
- t && (t.scrollIntoView({ behavior: "smooth" }), history.pushState(null, null, `#${o}`));
8
- }, h = (o) => {
9
- const t = [];
10
- return o.forEach((e) => {
11
- t.push({ id: e.id, parentId: null }), e.children && e.children.forEach((n) => {
12
- t.push({ id: n.id, parentId: e.id });
1
+ import { ref as w, watch as $, computed as _, onMounted as j, onUnmounted as A, createElementBlock as d, openBlock as u, createCommentVNode as y, createElementVNode as h, normalizeStyle as k, toDisplayString as O, Fragment as B, renderList as P, withModifiers as S, normalizeClass as I } from "vue";
2
+ const M = (l = [], i = {}) => {
3
+ const g = w(null), m = w(null);
4
+ let a = null;
5
+ const x = (c) => `#${c}`, C = (c) => {
6
+ const s = document.getElementById(c);
7
+ s && (s.scrollIntoView({ behavior: "smooth" }), history.pushState(null, null, `#${c}`));
8
+ }, v = (c) => {
9
+ const s = [];
10
+ return c.forEach((n) => {
11
+ s.push({ id: n.id, parentId: null }), n.children && n.children.forEach((f) => {
12
+ s.push({ id: f.id, parentId: n.id });
13
13
  });
14
- }), t;
14
+ }), s;
15
15
  };
16
16
  return {
17
- formatId: u,
18
- scrollToId: v,
19
- activeId: a,
20
- activeParentId: d,
17
+ formatId: x,
18
+ scrollToId: C,
19
+ activeId: g,
20
+ activeParentId: m,
21
21
  setupObserver: () => {
22
- if (typeof window > "u" || !s.length) return;
23
- const o = h(s), t = o.map(({ id: e }) => document.getElementById(e)).filter((e) => e !== null);
24
- t.length && (l = new IntersectionObserver(
25
- (e) => {
26
- e.forEach((n) => {
27
- if (n.isIntersecting) {
28
- const c = n.target.id, m = o.find((x) => x.id === c);
29
- m && (a.value = c, d.value = m.parentId, history.replaceState(null, null, `#${c}`));
22
+ if (typeof window > "u" || !l.length) return;
23
+ const c = v(l), s = c.map(({ id: n }) => document.getElementById(n)).filter((n) => n !== null);
24
+ s.length && (a = new IntersectionObserver(
25
+ (n) => {
26
+ n.forEach((f) => {
27
+ if (f.isIntersecting) {
28
+ const p = f.target.id, e = c.find((o) => o.id === p);
29
+ e && (g.value = p, m.value = e.parentId, history.replaceState(null, null, `#${p}`));
30
30
  }
31
31
  });
32
32
  },
33
33
  {
34
34
  rootMargin: "-20% 0px -70% 0px",
35
- threshold: 0
35
+ threshold: 0,
36
+ ...i
36
37
  }
37
- ), t.forEach((e) => l.observe(e)));
38
+ ), s.forEach((n) => a.observe(n)));
38
39
  },
39
40
  cleanup: () => {
40
- l && (l.disconnect(), l = null);
41
+ a && (a.disconnect(), a = null);
41
42
  }
42
43
  };
43
- }, P = { class: "tv-toc" }, A = {
44
+ }, V = { class: "tv-toc" }, z = {
44
45
  key: 0,
46
+ class: "tv-toc-progress-container"
47
+ }, N = {
48
+ key: 1,
45
49
  class: "tv-toc-title"
46
- }, M = { class: "tv-toc-list" }, S = ["href", "onClick"], V = {
47
- key: 0,
48
- class: "tv-toc-sublist"
49
- }, N = ["href", "onClick"], j = {
50
+ }, q = { class: "tv-toc-list" }, D = { class: "tv-toc-item-content" }, F = ["href", "onClick"], U = ["onClick"], G = { class: "tv-toc-sublist" }, H = ["href", "onClick"], J = {
50
51
  __name: "TvToc",
51
52
  props: {
52
53
  toc: {
53
54
  type: Object,
54
55
  required: !0
56
+ },
57
+ marker: {
58
+ type: Boolean,
59
+ default: !1
60
+ },
61
+ activeClass: {
62
+ type: String,
63
+ default: "active"
64
+ },
65
+ observerOptions: {
66
+ type: Object,
67
+ default: () => ({})
68
+ },
69
+ collapsible: {
70
+ type: Boolean,
71
+ default: !1
55
72
  }
56
73
  },
57
- setup(s) {
58
- const a = s, { scrollToId: d, activeId: l, activeParentId: u, setupObserver: v, cleanup: h } = B(a.toc?.links || []), f = (t) => {
59
- d(t);
60
- }, p = (t) => l.value === t, o = (t) => u.value === t;
61
- return O(() => {
62
- v();
63
- }), w(() => {
64
- h();
65
- }), (t, e) => (i(), r("nav", P, [
66
- s.toc?.title ? (i(), r("h3", A, _(s.toc.title), 1)) : y("", !0),
67
- I("ul", M, [
68
- (i(!0), r(E, null, b(s.toc?.links, (n) => (i(), r("li", {
69
- key: n.id,
74
+ setup(l) {
75
+ const i = l, { scrollToId: g, activeId: m, activeParentId: a, setupObserver: x, cleanup: C } = M(i.toc?.links || [], i.observerOptions), v = w(/* @__PURE__ */ new Set()), T = (e) => {
76
+ const o = new Set(v.value);
77
+ o.has(e) ? o.delete(e) : o.add(e), v.value = o;
78
+ }, b = (e) => !i.collapsible || v.value.has(e), c = (e) => {
79
+ g(e);
80
+ }, s = (e) => m.value === e, n = (e) => a.value === e;
81
+ $(a, (e) => {
82
+ if (i.collapsible && e && !v.value.has(e)) {
83
+ const o = new Set(v.value);
84
+ o.add(e), v.value = o;
85
+ }
86
+ });
87
+ const f = w(0), p = _(() => {
88
+ const e = [], o = (t) => {
89
+ if (t)
90
+ for (const r of t)
91
+ e.push(r.id), r.children && o(r.children);
92
+ };
93
+ return o(i.toc?.links), e;
94
+ });
95
+ return $(m, (e) => {
96
+ if (!e) return;
97
+ const o = p.value.indexOf(e);
98
+ if (o !== -1) {
99
+ const t = p.value.length;
100
+ t > 0 && (f.value = (o + 1) / t * 100);
101
+ }
102
+ }, { immediate: !0 }), j(() => {
103
+ i.collapsible && a.value && T(a.value), x();
104
+ }), A(() => {
105
+ C();
106
+ }), (e, o) => (u(), d("nav", V, [
107
+ l.toc?.links?.length ? (u(), d("div", z, [
108
+ h("div", {
109
+ class: "tv-toc-progress-bar",
110
+ style: k({ height: `${f.value}%` })
111
+ }, null, 4)
112
+ ])) : y("", !0),
113
+ l.toc?.title ? (u(), d("h3", N, O(l.toc.title), 1)) : y("", !0),
114
+ h("ul", q, [
115
+ (u(!0), d(B, null, P(l.toc?.links, (t) => (u(), d("li", {
116
+ key: t.id,
70
117
  class: "tv-toc-item"
71
118
  }, [
72
- I("a", {
73
- href: `#${n.id}`,
74
- class: $(["tv-toc-link", {
75
- active: p(n.id),
76
- "parent-active": o(n.id)
77
- }]),
78
- onClick: C((c) => f(n.id), ["prevent"])
79
- }, _(n.text), 11, S),
80
- n.children ? (i(), r("ul", V, [
81
- (i(!0), r(E, null, b(n.children, (c) => (i(), r("li", {
82
- key: c.id,
83
- class: "tv-toc-subitem"
84
- }, [
85
- I("a", {
86
- href: `#${c.id}`,
87
- class: $(["tv-toc-sublink", { active: p(c.id) }]),
88
- onClick: C((m) => f(c.id), ["prevent"])
89
- }, _(c.text), 11, N)
90
- ]))), 128))
91
- ])) : y("", !0)
119
+ h("div", D, [
120
+ h("a", {
121
+ href: `#${t.id}`,
122
+ class: I(["tv-toc-link", {
123
+ [i.activeClass]: s(t.id),
124
+ "parent-active": n(t.id),
125
+ "tv-toc-marker": l.marker && s(t.id)
126
+ }]),
127
+ onClick: S((r) => c(t.id), ["prevent"])
128
+ }, O(t.text), 11, F),
129
+ l.collapsible && t.children && t.children.length ? (u(), d("button", {
130
+ key: 0,
131
+ class: I(["tv-toc-toggle", { "is-expanded": b(t.id) }]),
132
+ onClick: S((r) => T(t.id), ["stop"]),
133
+ "aria-label": "Toggle section"
134
+ }, [...o[0] || (o[0] = [
135
+ h("svg", {
136
+ xmlns: "http://www.w3.org/2000/svg",
137
+ width: "16",
138
+ height: "16",
139
+ viewBox: "0 0 24 24",
140
+ fill: "none",
141
+ stroke: "currentColor",
142
+ "stroke-width": "2",
143
+ "stroke-linecap": "round",
144
+ "stroke-linejoin": "round"
145
+ }, [
146
+ h("polyline", { points: "6 9 12 15 18 9" })
147
+ ], -1)
148
+ ])], 10, U)) : y("", !0)
149
+ ]),
150
+ t.children ? (u(), d("div", {
151
+ key: 0,
152
+ class: I(["tv-toc-sublist-wrapper", { "is-collapsed": l.collapsible && !b(t.id) }]),
153
+ style: k(l.collapsible ? { "--content-height": b(t.id) ? "1000px" : "0px" } : {})
154
+ }, [
155
+ h("ul", G, [
156
+ (u(!0), d(B, null, P(t.children, (r) => (u(), d("li", {
157
+ key: r.id,
158
+ class: "tv-toc-subitem"
159
+ }, [
160
+ h("a", {
161
+ href: `#${r.id}`,
162
+ class: I(["tv-toc-sublink", {
163
+ [i.activeClass]: s(r.id),
164
+ "tv-toc-marker": l.marker && s(r.id)
165
+ }]),
166
+ onClick: S((K) => c(r.id), ["prevent"])
167
+ }, O(r.text), 11, H)
168
+ ]))), 128))
169
+ ])
170
+ ], 6)) : y("", !0)
92
171
  ]))), 128))
93
172
  ])
94
173
  ]));
95
174
  }
96
- }, g = j;
97
- g.install = (s) => {
98
- s.component("TvToc", g);
175
+ }, E = J;
176
+ E.install = (l) => {
177
+ l.component("TvToc", E);
99
178
  };
100
- const q = {
101
- install: g.install
179
+ const R = {
180
+ install: E.install
102
181
  };
103
182
  export {
104
- g as TvToc,
105
- q as TvTocPlugin,
106
- g as default
183
+ E as TvToc,
184
+ R as TvTocPlugin,
185
+ E as default
107
186
  };
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Cristhian Daza",
5
5
  "description": "A Vue 3 component to generate a table of contents (TOC) for your articles or documentation, enhancing navigation and user experience.",
6
6
  "license": "MIT",
7
- "version": "1.0.2",
7
+ "version": "1.1.0",
8
8
  "type": "module",
9
9
  "homepage": "https://ui.todovue.blog/toc",
10
10
  "repository": {
@@ -60,10 +60,10 @@
60
60
  "vue": "^3.5.26"
61
61
  },
62
62
  "devDependencies": {
63
- "@todovue/tv-demo": "^1.2.7",
63
+ "@todovue/tv-demo": "^1.4.4",
64
64
  "@vitejs/plugin-vue": "^6.0.3",
65
- "sass": "^1.97.1",
66
- "vite": "^7.3.0",
65
+ "sass": "^1.97.2",
66
+ "vite": "^7.3.1",
67
67
  "vite-plugin-dts": "^4.5.4"
68
68
  }
69
69
  }