@todovue/tv-toc 1.0.2 → 1.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,80 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@todovue/tv-toc` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.1] - 2026-01-27
9
+
10
+ ### Changed
11
+ - Simplified the file list in `package.json` to include only essential assets.
12
+ - Simplified the build configuration by removing demo-specific logic.
13
+ - Enhanced GitHub Actions workflows to automate npm package publishing and GitHub release creation.
14
+ - Moved the `@todovue/tv-demo` component import from main.js to `Demo.vue` to localize its usage.
15
+ - Updated build commands to include `README.md` and `CHANGELOG.md` files in the public directory during the build process.
16
+
17
+ ### Added
18
+ - Included the `src` directory in the `package.json` files list to ensure component source files are bundled in the package distribution.
19
+
20
+ ### Removed
21
+ - Eliminated the global import of the `@todovue/tv-demo` component from `main.js`.
22
+
23
+ ### Dependencies
24
+ - Updated `@todovue/tv-demo` to `^1.4.11`.
25
+ - Updated `vue` to `^3.5.27`.
26
+ - Updated `sass` to `^1.97.3`.
27
+
28
+ ## [1.1.0] - 2026-01-20
29
+
30
+ ### Added
31
+ - Introduced a customizable marker for Table of Contents (ToC) items to allow for personalized bullet styles or icons.
32
+ - Added an active class property to ToC items, enabling specific styling for the currently viewed section to improve navigation clarity.
33
+ - Added `observerOptions` prop to customize IntersectionObserver behavior (rootMargin, threshold).
34
+ - Introduced a scroll progress indicator to the Table of Contents, providing a visual cue of the user's current position within the document.
35
+ - Implemented collapsible sections in the Table of Contents, allowing users to toggle the visibility of subsections for a cleaner, more focused navigation experience.
36
+ - Introduced animated active indicators to the Table of Contents, providing smooth transitions as the user scrolls through different sections.
37
+ - Added customizable color properties for ToC elements, allowing for deeper integration with specific brand palettes.
38
+
39
+ ### Dependencies
40
+ - Updated `@todovue/tv-demo` to `^1.4.4`.
41
+ - Updated `sass` to `^1.97.2`.
42
+ - Updated `vite` to `^7.3.1`.
43
+
44
+ ## [1.0.2] - 2025-12-27
45
+
46
+ ### Added
47
+ - Added automatic publishing to the TODOvue cPanel in `release.yml` for each release, simplifying package distribution and updates.
48
+ - Added `package-lock.json` to the repository to ensure dependency consistency and facilitate version management across development and production environments.
49
+
50
+ ### Changed
51
+ - Changed the `base` option in `vite.config.js` for website deployment in cpanel.
52
+
53
+ ### Fixed
54
+ - Fixed repository URL in `package.json` to point to the correct GitHub repository.
55
+ - Fixed the token configuration used to generate the package in the GitHub Actions workflow `release.yml`.
56
+
57
+ ### Dependencies
58
+ - Updated dependency versions in `package.json` to maintain compatibility and benefit from improvements and bug fixes in the used libraries.
59
+
60
+ ## [1.0.1] - 2025-11-28
61
+
62
+ ### Fixed
63
+ - Correct CSS file extension for Table of Contents styles.
64
+
65
+ ## [1.0.0] - 2025-11-28
66
+
67
+ ### Added
68
+ - Initial stable release of `@todovue/tv-toc`.
69
+ - `TvToc` Vue 3 component to render a table of contents (TOC) from a `toc` object.
70
+ - Support for nested sections via `children` links.
71
+ - Smooth scrolling to headings using `scrollIntoView` and URL hash update with `history.pushState`.
72
+ - `useToc` composable with `scrollToId` and `formatId` helpers.
73
+ - Minimal default styles and BEM-like CSS classes for easy customization.
74
+ - Vite demo with basic and blog-like TOC examples.
75
+
76
+ [1.1.1]: https://github.com/TODOvue/tv-toc/pull/5/files
77
+ [1.1.0]: https://github.com/TODOvue/tv-toc/pull/4/files
78
+ [1.0.2]: https://github.com/TODOvue/tv-toc/pull/3/files
79
+ [1.0.1]: https://github.com/TODOvue/tv-toc/pull/2/files
80
+ [1.0.0]: https://github.com/TODOvue/tv-toc/pull/1/files
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,10 +4,11 @@
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.1",
8
8
  "type": "module",
9
9
  "homepage": "https://ui.todovue.blog/toc",
10
10
  "repository": {
11
+ "name": "@todovue/tv-toc",
11
12
  "type": "git",
12
13
  "url": "git+https://github.com/TODOvue/tv-toc.git"
13
14
  },
@@ -32,16 +33,21 @@
32
33
  "require": "./dist/tv-toc.cjs.js"
33
34
  },
34
35
  "./style.css": "./dist/tv-toc.css",
35
- "./nuxt": "./nuxt.js"
36
+ "./nuxt": "./nuxt.js",
37
+ "./demo": {
38
+ "import": "./src/demo/Demo.vue"
39
+ }
36
40
  },
37
41
  "main": "dist/tv-toc.cjs.js",
38
42
  "module": "dist/tv-toc.es.js",
39
43
  "types": "dist/tv-toc.d.ts",
40
44
  "files": [
41
- "dist",
45
+ "CHANGELOG.md",
42
46
  "LICENSE",
43
47
  "README.md",
44
- "nuxt.js"
48
+ "dist",
49
+ "nuxt.js",
50
+ "src"
45
51
  ],
46
52
  "engines": {
47
53
  "node": ">=20.19.0"
@@ -53,17 +59,16 @@
53
59
  ],
54
60
  "scripts": {
55
61
  "dev": "vite",
56
- "build": "vite build",
57
- "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
62
+ "build": "vite build"
58
63
  },
59
64
  "peerDependencies": {
60
- "vue": "^3.5.26"
65
+ "vue": "^3.5.27"
61
66
  },
62
67
  "devDependencies": {
63
- "@todovue/tv-demo": "^1.2.7",
68
+ "@todovue/tv-demo": "^1.4.11",
64
69
  "@vitejs/plugin-vue": "^6.0.3",
65
- "sass": "^1.97.1",
66
- "vite": "^7.3.0",
70
+ "sass": "^1.97.3",
71
+ "vite": "^7.3.1",
67
72
  "vite-plugin-dts": "^4.5.4"
68
73
  }
69
74
  }
@@ -0,0 +1,15 @@
1
+ $dark-body-bg: #7A7D7D;
2
+ $dark-button-bg: #EF233C;
3
+ $dark-card-bg: #0E131F;
4
+ $dark-button-text: #AFDEDC;
5
+ $dark-text: #F4FAFF;
6
+
7
+ $light-body-bg: #E0E1E1;
8
+ $light-button-bg: #EF233C;
9
+ $light-card-bg: #B9C4DF;
10
+ $light-button-text: #F1F9F9;
11
+ $light-text: #000B14;
12
+
13
+ $active-color: $light-button-bg;
14
+ $active-color-dark: $dark-button-bg;
15
+