@todovue/tv-toc 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Cristhian Daza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ <p align="center"><img width="150" src="https://res.cloudinary.com/dcdfhi8qz/image/upload/v1763663056/uqqtkgp1lg3xdplutpga.png" alt="TODOvue logo">
2
+ </p>
3
+
4
+ # TODOvue TOC Component (TvToc)
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
+
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
+ [![npm downloads](https://img.shields.io/npm/dm/@todovue/tv-toc.svg)](https://www.npmjs.com/package/@todovue/tv-toc)
10
+ [![npm total downloads](https://img.shields.io/npm/dt/@todovue/tv-toc.svg)](https://www.npmjs.com/package/@todovue/tv-toc)
11
+ ![License](https://img.shields.io/github/license/TODOvue/tv-toc)
12
+ ![Release Date](https://img.shields.io/github/release-date/TODOvue/tv-toc)
13
+ ![Bundle Size](https://img.shields.io/bundlephobia/minzip/@todovue/tv-toc)
14
+ ![Node Version](https://img.shields.io/node/v/@todovue/tv-toc)
15
+ ![Last Commit](https://img.shields.io/github/last-commit/TODOvue/tv-toc)
16
+ ![Stars](https://img.shields.io/github/stars/TODOvue/tv-toc?style=social)
17
+
18
+ > Demo: https://tv-toc.netlify.app/
19
+
20
+ ---
21
+ ## Table of Contents
22
+ - [Features](#features)
23
+ - [Installation](#installation)
24
+ - [Usage of Styles](#usage-of-styles)
25
+ - [Quick Start (SPA)](#quick-start-spa)
26
+ - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
27
+ - [Component Registration Options](#component-registration-options)
28
+ - [Props](#props)
29
+ - [Composable: useToc](#composable-usetoc)
30
+ - [Customization (Styles)](#customization-styles)
31
+ - [SSR Notes](#ssr-notes)
32
+ - [Examples](#examples)
33
+ - [Development](#development)
34
+ - [Contributing](#contributing)
35
+ - [License](#license)
36
+
37
+ ---
38
+ ## Features
39
+ - Simple and focused Table of Contents (TOC) component for Vue 3.
40
+ - Supports nested sections via children links.
41
+ - Smooth scrolling to headings using `scrollIntoView`.
42
+ - URL hash is updated using `history.pushState` for better navigation and shareable links.
43
+ - Works in SPA (Vite, Vue CLI) and Nuxt 3 (with client-side rendering constraints).
44
+ - Ships with minimal, customizable styles.
45
+
46
+ ---
47
+ ## Installation
48
+ Using npm:
49
+ ```bash
50
+ npm install @todovue/tv-toc
51
+ ```
52
+ Using yarn:
53
+ ```bash
54
+ yarn add @todovue/tv-toc
55
+ ```
56
+ Using pnpm:
57
+ ```bash
58
+ pnpm add @todovue/tv-toc
59
+ ```
60
+
61
+ ---
62
+ ## Usage of Styles
63
+
64
+ ### Vue/Vite (SPA)
65
+ Import the CSS generated by the library in your main entry file:
66
+ ```ts
67
+ // main.ts
68
+ import { createApp } from 'vue'
69
+ import App from './App.vue'
70
+
71
+ import '@todovue/tv-toc/style.css'
72
+ import { TvToc } from '@todovue/tv-toc'
73
+
74
+ const app = createApp(App)
75
+ app.component('TvToc', TvToc)
76
+ app.mount('#app')
77
+ ```
78
+
79
+ ### Nuxt 3/4
80
+ Add the library's CSS to your Nuxt configuration:
81
+ ```ts
82
+ // nuxt.config.ts
83
+ export default defineNuxtConfig({
84
+ modules: [
85
+ '@todovue/tv-toc/nuxt'
86
+ ]
87
+ })
88
+ ```
89
+
90
+ ---
91
+ ## Quick Start (SPA)
92
+ Global registration (main.js / main.ts):
93
+ ```js
94
+ import { createApp } from 'vue'
95
+ import App from './App.vue'
96
+ import TvToc from '@todovue/tv-toc'
97
+
98
+ createApp(App)
99
+ .component('TvToc', TvToc)
100
+ .mount('#app')
101
+ ```
102
+ Local import inside a component:
103
+ ```vue
104
+ <script setup>
105
+ import { TvToc } from '@todovue/tv-toc'
106
+
107
+ const toc = {
108
+ title: 'On this page',
109
+ links: [
110
+ { id: 'introduction', text: 'Introduction' },
111
+ {
112
+ id: 'getting-started',
113
+ text: 'Getting started',
114
+ children: [
115
+ { id: 'installation', text: 'Installation' },
116
+ { id: 'basic-usage', text: 'Basic usage' },
117
+ ],
118
+ },
119
+ { id: 'api', text: 'API Reference' },
120
+ ],
121
+ }
122
+ </script>
123
+
124
+ <template>
125
+ <div class="page-layout">
126
+ <main class="page-content">
127
+ <h2 id="introduction">Introduction</h2>
128
+ <!-- ... -->
129
+ <h2 id="getting-started">Getting started</h2>
130
+ <h3 id="installation">Installation</h3>
131
+ <h3 id="basic-usage">Basic usage</h3>
132
+ <!-- ... -->
133
+ <h2 id="api">API Reference</h2>
134
+ </main>
135
+
136
+ <aside class="page-toc">
137
+ <TvToc :toc="toc" />
138
+ </aside>
139
+ </div>
140
+ </template>
141
+ ```
142
+
143
+ ---
144
+ ## Nuxt 3 / SSR Usage
145
+ Create a plugin file: `plugins/tv-toc.client.ts` (client-only because it uses `document` and `history` under the hood when scrolling):
146
+ ```ts
147
+ import { defineNuxtPlugin } from '#app'
148
+ import TvToc from '@todovue/tv-toc'
149
+
150
+ export default defineNuxtPlugin(nuxtApp => {
151
+ nuxtApp.vueApp.component('TvToc', TvToc)
152
+ })
153
+ ```
154
+ Use anywhere (recommended inside `<client-only>` when using Nuxt 3):
155
+ ```vue
156
+ <template>
157
+ <client-only>
158
+ <TvToc :toc="toc" />
159
+ </client-only>
160
+ </template>
161
+ ```
162
+ Direct import (no plugin):
163
+ ```vue
164
+ <script setup>
165
+ import { TvToc } from '@todovue/tv-toc'
166
+ </script>
167
+
168
+ <template>
169
+ <TvToc :toc="toc" />
170
+ </template>
171
+ ```
172
+
173
+ ---
174
+ ## Component Registration Options
175
+ | Approach | When to use |
176
+ |-----------------------------------------------|-----------------------------------|
177
+ | Global via `app.component('TvToc', TvToc)` | Frequent use across the app |
178
+ | Local named import `{ TvToc }` | Isolated/code-split contexts |
179
+ | Direct default import `import TvToc from ...` | Single use or manual registration |
180
+
181
+ ---
182
+ ## Props
183
+ | Name | Type | Default | Description | Required |
184
+ |------|--------|---------|----------------------------------------------------------------------------|----------|
185
+ | toc | Object | - | TOC configuration: title and list of links (with optional nested children) | `true` |
186
+
187
+ ### `toc` shape
188
+ ```ts
189
+ type TocLink = {
190
+ id: string
191
+ text: string
192
+ children?: TocLink[]
193
+ }
194
+
195
+ type Toc = {
196
+ title?: string
197
+ links: TocLink[]
198
+ }
199
+ ```
200
+ - `title`: Optional title shown at the top of the TOC (`h3`).
201
+ - `links`: Array of top-level sections.
202
+ - `id`: Must match the `id` attribute of the target heading in your content.
203
+ - `text`: Label shown in the TOC.
204
+ - `children`: Optional array of sub-sections, rendered as nested list.
205
+
206
+ ---
207
+ ## Composable: `useToc`
208
+ This composable is used internally by `TvToc` but can also be imported directly if needed.
209
+
210
+ ```ts
211
+ import { useToc } from '@todovue/tv-toc'
212
+
213
+ const { formatId, scrollToId } = useToc()
214
+ ```
215
+
216
+ ### API
217
+ | Name | Type | Description |
218
+ |------------|--------------------------|----------------------------------------------------------------|
219
+ | formatId | `(id: string) => string` | Returns a hash-based id string (e.g. `"#section-id"`). |
220
+ | scrollToId | `(id: string) => void` | Smooth scrolls to the element with that `id` and updates hash. |
221
+
222
+ > Note: `scrollToId` accesses `document` and `history`, so it should run only in the browser (e.g. in event handlers or inside `onMounted`).
223
+
224
+ ---
225
+ ## Customization (Styles)
226
+ The component ships with minimal default styles, exposed through the built CSS file and scoped CSS classes.
227
+
228
+ Main CSS classes:
229
+ - `.tv-toc` — Root `<nav>` container.
230
+ - `.tv-toc-title` — Title of the TOC.
231
+ - `.tv-toc-list` — Top-level list container.
232
+ - `.tv-toc-item` — Top-level list item.
233
+ - `.tv-toc-link` — Anchor for top-level items.
234
+ - `.tv-toc-sublist` — Nested list container for children.
235
+ - `.tv-toc-subitem` — Nested list item.
236
+ - `.tv-toc-sublink` — Anchor for nested items.
237
+
238
+ You can override these styles in your own global stylesheet:
239
+ ```css
240
+ /* example overrides */
241
+ .tv-toc {
242
+ font-size: 0.9rem;
243
+ }
244
+
245
+ .tv-toc-link,
246
+ .tv-toc-sublink {
247
+ color: #4b5563;
248
+ }
249
+
250
+ .tv-toc-link:hover,
251
+ .tv-toc-sublink:hover {
252
+ color: #111827;
253
+ }
254
+ ```
255
+
256
+ 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
+
258
+ ---
259
+ ## SSR Notes
260
+ - The component can be rendered on the server (template is static), but scrolling behavior uses browser APIs.
261
+ - `scrollToId` uses `document.getElementById` and `history.pushState`; these are only invoked in event handlers on the client.
262
+ - 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
+
264
+ ---
265
+ ## Examples
266
+ This repository includes a small demo application built with Vite.
267
+
268
+ - Basic TOC example.
269
+ - Blog-like layout with nested headings.
270
+
271
+ To run the demo locally, see the [Development](#development) section.
272
+
273
+ ---
274
+ ## Development
275
+ ```bash
276
+ git clone https://github.com/TODOvue/tv-toc.git
277
+ cd tv-toc
278
+ npm install
279
+ npm run dev # run local demo
280
+ npm run build # build library
281
+ ```
282
+ The local demo is served with Vite using `index.html` and examples in `src/demo`.
283
+
284
+ To build the standalone demo used for documentation:
285
+ ```bash
286
+ npm run build:demo
287
+ ```
288
+
289
+ ---
290
+ ## Contributing
291
+ PRs and issues are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
292
+
293
+ ---
294
+ ## License
295
+ MIT © TODOvue
296
+
297
+ ---
298
+ ### Attributions
299
+ Crafted for the TODOvue component ecosystem
300
+
@@ -0,0 +1,11 @@
1
+ import { Plugin } from 'vue';
2
+ import { default as _TvToc } from './components/TvToc.vue';
3
+ declare const TvToc: typeof _TvToc & Plugin;
4
+ export { TvToc };
5
+ export declare const TvTocPlugin: Plugin;
6
+ export default TvToc;
7
+ declare module 'vue' {
8
+ interface GlobalComponents {
9
+ TvToc: typeof TvToc;
10
+ }
11
+ }
Binary file
@@ -0,0 +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;
@@ -0,0 +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}
@@ -0,0 +1,6 @@
1
+ export * from './entry'
2
+ export {}
3
+ import TvToc from './entry'
4
+ export default TvToc
5
+ export * from './entry'
6
+ export {}
@@ -0,0 +1,65 @@
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 = {
9
+ key: 0,
10
+ 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 = {
15
+ __name: "TvToc",
16
+ props: {
17
+ toc: {
18
+ type: Object,
19
+ required: !0
20
+ }
21
+ },
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,
31
+ class: "tv-toc-item"
32
+ }, [
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)
50
+ ]))), 128))
51
+ ])
52
+ ]));
53
+ }
54
+ }, a = C;
55
+ a.install = (c) => {
56
+ c.component("TvToc", a);
57
+ };
58
+ const B = {
59
+ install: a.install
60
+ };
61
+ export {
62
+ a as TvToc,
63
+ B as TvTocPlugin,
64
+ a as default
65
+ };
package/nuxt.js ADDED
@@ -0,0 +1,15 @@
1
+
2
+ import { defineNuxtModule } from '@nuxt/kit'
3
+
4
+ export default defineNuxtModule({
5
+ meta: {
6
+ name: '@todovue/tv-toc',
7
+ configKey: 'tvToc'
8
+ },
9
+ setup(_options, nuxt) {
10
+ const cssPath = '@todovue/tv-toc/style.scss';
11
+ if (!nuxt.options.css.includes(cssPath)) {
12
+ nuxt.options.css.push(cssPath);
13
+ }
14
+ }
15
+ })
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@todovue/tv-toc",
3
+ "private": false,
4
+ "author": "Cristhian Daza",
5
+ "description": "A Vue 3 component to generate a table of contents (TOC) for your articles or documentation, enhancing navigation and user experience.",
6
+ "license": "MIT",
7
+ "version": "1.0.0",
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/TODOvue/tv-toc.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/TODOvue/tv-toc/issues"
15
+ },
16
+ "keywords": [
17
+ "todovue",
18
+ "front-end",
19
+ "web",
20
+ "vue",
21
+ "vuejs",
22
+ "vue-js",
23
+ "toc",
24
+ "show-toc",
25
+ "vue-toc",
26
+ "vue-toc-component"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/tv-toc.es.js",
31
+ "require": "./dist/tv-toc.cjs.js"
32
+ },
33
+ "./style.css": "./dist/tv-toc.css",
34
+ "./nuxt": "./nuxt.js"
35
+ },
36
+ "main": "dist/tv-toc.cjs.js",
37
+ "module": "dist/tv-toc.es.js",
38
+ "types": "dist/tv-toc.d.ts",
39
+ "files": [
40
+ "dist",
41
+ "LICENSE",
42
+ "README.md",
43
+ "nuxt.js"
44
+ ],
45
+ "engines": {
46
+ "node": ">=20.19.0"
47
+ },
48
+ "sideEffects": [
49
+ "*.css",
50
+ "*.scss",
51
+ "dist/*.css"
52
+ ],
53
+ "scripts": {
54
+ "dev": "vite",
55
+ "build": "vite build",
56
+ "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
57
+ },
58
+ "peerDependencies": {
59
+ "vue": "^3.0.0"
60
+ },
61
+ "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"
67
+ }
68
+ }