@todovue/tv-toc 1.0.1 → 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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Cristhian Daza
3
+ Copyright (c) 2026 Cristhian Daza
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  A lightweight Vue 3 component to render a Table of Contents (TOC) for your articles or documentation, with smooth scrolling and nested sections support.
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@todovue/tv-toc.svg)](https://www.npmjs.com/package/@todovue/tv-toc)
8
- [![Netlify Status](https://api.netlify.com/api/v1/badges/2a6f2c38-c236-44bb-8d8e-66a86f4295ee/deploy-status)](https://app.netlify.com/projects/tv-toc/deploys)
9
8
  [![npm downloads](https://img.shields.io/npm/dm/@todovue/tv-toc.svg)](https://www.npmjs.com/package/@todovue/tv-toc)
10
9
  [![npm total downloads](https://img.shields.io/npm/dt/@todovue/tv-toc.svg)](https://www.npmjs.com/package/@todovue/tv-toc)
11
10
  ![License](https://img.shields.io/github/license/TODOvue/tv-toc)
@@ -15,15 +14,14 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
15
14
  ![Last Commit](https://img.shields.io/github/last-commit/TODOvue/tv-toc)
16
15
  ![Stars](https://img.shields.io/github/stars/TODOvue/tv-toc?style=social)
17
16
 
18
- > Demo: https://tv-toc.netlify.app/
17
+ > Demo: https://ui.todovue.blog/toc
19
18
 
20
- ---
21
19
  ## Table of Contents
22
20
  - [Features](#features)
23
21
  - [Installation](#installation)
24
22
  - [Usage of Styles](#usage-of-styles)
25
23
  - [Quick Start (SPA)](#quick-start-spa)
26
- - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
24
+ - [Nuxt 4 / SSR Usage](#nuxt-4--ssr-usage)
27
25
  - [Component Registration Options](#component-registration-options)
28
26
  - [Props](#props)
29
27
  - [Composable: useToc](#composable-usetoc)
@@ -34,7 +32,6 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
34
32
  - [Contributing](#contributing)
35
33
  - [License](#license)
36
34
 
37
- ---
38
35
  ## Features
39
36
  - Simple and focused Table of Contents (TOC) component for Vue 3.
40
37
  - Supports nested sections via children links.
@@ -43,7 +40,6 @@ A lightweight Vue 3 component to render a Table of Contents (TOC) for your artic
43
40
  - Works in SPA (Vite, Vue CLI) and Nuxt 3 (with client-side rendering constraints).
44
41
  - Ships with minimal, customizable styles.
45
42
 
46
- ---
47
43
  ## Installation
48
44
  Using npm:
49
45
  ```bash
@@ -58,7 +54,6 @@ Using pnpm:
58
54
  pnpm add @todovue/tv-toc
59
55
  ```
60
56
 
61
- ---
62
57
  ## Usage of Styles
63
58
 
64
59
  ### Vue/Vite (SPA)
@@ -87,7 +82,6 @@ export default defineNuxtConfig({
87
82
  })
88
83
  ```
89
84
 
90
- ---
91
85
  ## Quick Start (SPA)
92
86
  Global registration (main.js / main.ts):
93
87
  ```js
@@ -140,8 +134,7 @@ const toc = {
140
134
  </template>
141
135
  ```
142
136
 
143
- ---
144
- ## Nuxt 3 / SSR Usage
137
+ ## Nuxt 4 / SSR Usage
145
138
  Create a plugin file: `plugins/tv-toc.client.ts` (client-only because it uses `document` and `history` under the hood when scrolling):
146
139
  ```ts
147
140
  import { defineNuxtPlugin } from '#app'
@@ -170,7 +163,6 @@ import { TvToc } from '@todovue/tv-toc'
170
163
  </template>
171
164
  ```
172
165
 
173
- ---
174
166
  ## Component Registration Options
175
167
  | Approach | When to use |
176
168
  |-----------------------------------------------|-----------------------------------|
@@ -178,11 +170,14 @@ import { TvToc } from '@todovue/tv-toc'
178
170
  | Local named import `{ TvToc }` | Isolated/code-split contexts |
179
171
  | Direct default import `import TvToc from ...` | Single use or manual registration |
180
172
 
181
- ---
182
173
  ## Props
183
- | Name | Type | Default | Description | Required |
184
- |------|--------|---------|----------------------------------------------------------------------------|----------|
185
- | 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` |
186
181
 
187
182
  ### `toc` shape
188
183
  ```ts
@@ -201,16 +196,17 @@ type Toc = {
201
196
  - `links`: Array of top-level sections.
202
197
  - `id`: Must match the `id` attribute of the target heading in your content.
203
198
  - `text`: Label shown in the TOC.
204
- - `children`: Optional array of sub-sections, rendered as nested list.
199
+ - `children`: Optional array of subsections, rendered as nested list.
205
200
 
206
- ---
207
201
  ## Composable: `useToc`
208
202
  This composable is used internally by `TvToc` but can also be imported directly if needed.
209
203
 
210
204
  ```ts
211
205
  import { useToc } from '@todovue/tv-toc'
212
206
 
213
- const { formatId, scrollToId } = useToc()
207
+ const { formatId, scrollToId } = useToc(links, {
208
+ rootMargin: '0px 0px -50% 0px'
209
+ })
214
210
  ```
215
211
 
216
212
  ### API
@@ -221,7 +217,6 @@ const { formatId, scrollToId } = useToc()
221
217
 
222
218
  > Note: `scrollToId` accesses `document` and `history`, so it should run only in the browser (e.g. in event handlers or inside `onMounted`).
223
219
 
224
- ---
225
220
  ## Customization (Styles)
226
221
  The component ships with minimal default styles, exposed through the built CSS file and scoped CSS classes.
227
222
 
@@ -255,13 +250,11 @@ You can override these styles in your own global stylesheet:
255
250
 
256
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`).
257
252
 
258
- ---
259
253
  ## SSR Notes
260
254
  - The component can be rendered on the server (template is static), but scrolling behavior uses browser APIs.
261
255
  - `scrollToId` uses `document.getElementById` and `history.pushState`; these are only invoked in event handlers on the client.
262
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.
263
257
 
264
- ---
265
258
  ## Examples
266
259
  This repository includes a small demo application built with Vite.
267
260
 
@@ -270,7 +263,6 @@ This repository includes a small demo application built with Vite.
270
263
 
271
264
  To run the demo locally, see the [Development](#development) section.
272
265
 
273
- ---
274
266
  ## Development
275
267
  ```bash
276
268
  git clone https://github.com/TODOvue/tv-toc.git
@@ -286,15 +278,11 @@ To build the standalone demo used for documentation:
286
278
  npm run build:demo
287
279
  ```
288
280
 
289
- ---
290
281
  ## Contributing
291
282
  PRs and issues are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
292
283
 
293
- ---
294
284
  ## License
295
285
  MIT © TODOvue
296
286
 
297
- ---
298
287
  ### Attributions
299
288
  Crafted for the TODOvue component ecosystem
300
-
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),i=()=>({formatId:t=>`#${t}`,scrollToId:t=>{const l=document.getElementById(t);l&&(l.scrollIntoView({behavior:"smooth"}),history.pushState(null,null,`#${t}`))}}),a={class:"tv-toc"},d={key:0,class:"tv-toc-title"},u={class:"tv-toc-list"},m=["href","onClick"],v={key:0,class:"tv-toc-sublist"},h=["href","onClick"],k={__name:"TvToc",props:{toc:{type:Object,required:!0}},setup(o){const{scrollToId:s}=i(),t=l=>{s(l)};return(l,_)=>(e.openBlock(),e.createElementBlock("nav",a,[o.toc?.title?(e.openBlock(),e.createElementBlock("h3",d,e.toDisplayString(o.toc.title),1)):e.createCommentVNode("",!0),e.createElementVNode("ul",u,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.toc?.links,c=>(e.openBlock(),e.createElementBlock("li",{key:c.id,class:"tv-toc-item"},[e.createElementVNode("a",{href:`#${c.id}`,class:"tv-toc-link",onClick:e.withModifiers(n=>t(c.id),["prevent"])},e.toDisplayString(c.text),9,m),c.children?(e.openBlock(),e.createElementBlock("ul",v,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(c.children,n=>(e.openBlock(),e.createElementBlock("li",{key:n.id,class:"tv-toc-subitem"},[e.createElementVNode("a",{href:`#${n.id}`,class:"tv-toc-sublink",onClick:e.withModifiers(p=>t(n.id),["prevent"])},e.toDisplayString(n.text),9,h)]))),128))])):e.createCommentVNode("",!0)]))),128))])]))}},r=k;r.install=o=>{o.component("TvToc",r)};const T={install:r.install};exports.TvToc=r;exports.TvTocPlugin=T;exports.default=r;
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}.tv-toc .tv-toc-link{text-decoration:none;color:inherit;font-weight:500;transition:color .2s}.tv-toc .tv-toc-link:hover{color:#ef233c}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-link:hover{color:#ef233c}}.tv-toc .tv-toc-sublist{list-style:none;padding-left:1rem;margin-top:.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}.tv-toc .tv-toc-sublink{text-decoration:none;color:inherit;font-size:.9rem;opacity:.8;transition:opacity .2s,color .2s}.tv-toc .tv-toc-sublink:hover{opacity:1;color:#ef233c}@media(prefers-color-scheme:dark){.tv-toc .tv-toc-sublink:hover{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-sublist{border-left-color:#000b141a}.light-mode .tv-toc .tv-toc-sublink:hover{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,65 +1,186 @@
1
- import { createElementBlock as e, openBlock as o, createCommentVNode as u, createElementVNode as r, toDisplayString as i, Fragment as h, renderList as m, withModifiers as v } from "vue";
2
- const _ = () => ({
3
- formatId: (t) => `#${t}`,
4
- scrollToId: (t) => {
5
- const l = document.getElementById(t);
6
- l && (l.scrollIntoView({ behavior: "smooth" }), history.pushState(null, null, `#${t}`));
7
- }
8
- }), T = { class: "tv-toc" }, f = {
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
+ });
14
+ }), s;
15
+ };
16
+ return {
17
+ formatId: x,
18
+ scrollToId: C,
19
+ activeId: g,
20
+ activeParentId: m,
21
+ setupObserver: () => {
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
+ }
31
+ });
32
+ },
33
+ {
34
+ rootMargin: "-20% 0px -70% 0px",
35
+ threshold: 0,
36
+ ...i
37
+ }
38
+ ), s.forEach((n) => a.observe(n)));
39
+ },
40
+ cleanup: () => {
41
+ a && (a.disconnect(), a = null);
42
+ }
43
+ };
44
+ }, V = { class: "tv-toc" }, z = {
9
45
  key: 0,
46
+ class: "tv-toc-progress-container"
47
+ }, N = {
48
+ key: 1,
10
49
  class: "tv-toc-title"
11
- }, p = { class: "tv-toc-list" }, y = ["href", "onClick"], I = {
12
- key: 0,
13
- class: "tv-toc-sublist"
14
- }, k = ["href", "onClick"], C = {
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 = {
15
51
  __name: "TvToc",
16
52
  props: {
17
53
  toc: {
18
54
  type: Object,
19
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
20
72
  }
21
73
  },
22
- setup(c) {
23
- const { scrollToId: d } = _(), t = (l) => {
24
- d(l);
25
- };
26
- return (l, $) => (o(), e("nav", T, [
27
- c.toc?.title ? (o(), e("h3", f, i(c.toc.title), 1)) : u("", !0),
28
- r("ul", p, [
29
- (o(!0), e(h, null, m(c.toc?.links, (s) => (o(), e("li", {
30
- key: s.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,
31
117
  class: "tv-toc-item"
32
118
  }, [
33
- r("a", {
34
- href: `#${s.id}`,
35
- class: "tv-toc-link",
36
- onClick: v((n) => t(s.id), ["prevent"])
37
- }, i(s.text), 9, y),
38
- s.children ? (o(), e("ul", I, [
39
- (o(!0), e(h, null, m(s.children, (n) => (o(), e("li", {
40
- key: n.id,
41
- class: "tv-toc-subitem"
42
- }, [
43
- r("a", {
44
- href: `#${n.id}`,
45
- class: "tv-toc-sublink",
46
- onClick: v((g) => t(n.id), ["prevent"])
47
- }, i(n.text), 9, k)
48
- ]))), 128))
49
- ])) : u("", !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)
50
171
  ]))), 128))
51
172
  ])
52
173
  ]));
53
174
  }
54
- }, a = C;
55
- a.install = (c) => {
56
- c.component("TvToc", a);
175
+ }, E = J;
176
+ E.install = (l) => {
177
+ l.component("TvToc", E);
57
178
  };
58
- const B = {
59
- install: a.install
179
+ const R = {
180
+ install: E.install
60
181
  };
61
182
  export {
62
- a as TvToc,
63
- B as TvTocPlugin,
64
- a as default
183
+ E as TvToc,
184
+ R as TvTocPlugin,
185
+ E as default
65
186
  };
package/package.json CHANGED
@@ -4,11 +4,12 @@
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.1",
7
+ "version": "1.1.0",
8
8
  "type": "module",
9
+ "homepage": "https://ui.todovue.blog/toc",
9
10
  "repository": {
10
11
  "type": "git",
11
- "url": "https://github.com/TODOvue/tv-toc.git"
12
+ "url": "git+https://github.com/TODOvue/tv-toc.git"
12
13
  },
13
14
  "bugs": {
14
15
  "url": "https://github.com/TODOvue/tv-toc/issues"
@@ -56,13 +57,13 @@
56
57
  "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
57
58
  },
58
59
  "peerDependencies": {
59
- "vue": "^3.0.0"
60
+ "vue": "^3.5.26"
60
61
  },
61
62
  "devDependencies": {
62
- "@todovue/tv-demo": "^1.2.2",
63
- "@vitejs/plugin-vue": "^6.0.0",
64
- "sass": "^1.0.0",
65
- "vite": "^7.0.0",
66
- "vite-plugin-dts": "^4.0.0"
63
+ "@todovue/tv-demo": "^1.4.4",
64
+ "@vitejs/plugin-vue": "^6.0.3",
65
+ "sass": "^1.97.2",
66
+ "vite": "^7.3.1",
67
+ "vite-plugin-dts": "^4.5.4"
67
68
  }
68
69
  }