@todovue/tv-breadcrumbs 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,364 @@
1
+ <p align="center"><img width="150" src="https://firebasestorage.googleapis.com/v0/b/todovue-blog.appspot.com/o/logo.png?alt=media&token=d8eb592f-e4a9-4b02-8aff-62d337745f41" alt="TODOvue logo">
2
+ </p>
3
+
4
+ # TODOvue Breadcrumbs (TvBreadcrumbs)
5
+ A flexible, framework‑agnostic Vue 3 breadcrumb navigation component with auto-generation from routes, custom separators, max items control, and full customization. Works seamlessly in Single Page Apps or Server-Side Rendered (SSR) environments (e.g. Nuxt 3).
6
+
7
+ [![npm](https://img.shields.io/npm/v/@todovue/tv-breadcrumbs.svg)](https://www.npmjs.com/package/@todovue/tv-breadcrumbs)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@todovue/tv-breadcrumbs.svg)](https://www.npmjs.com/package/@todovue/tv-breadcrumbs)
9
+ ![License](https://img.shields.io/github/license/TODOvue/tv-breadcrumbs)
10
+
11
+ > Demo: https://tv-breadcrumbs.netlify.app/
12
+
13
+ ---
14
+ ## Table of Contents
15
+ - [Features](#features)
16
+ - [Installation](#installation)
17
+ - [Quick Start (SPA)](#quick-start-spa)
18
+ - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
19
+ - [Component Registration Options](#component-registration-options)
20
+ - [Props](#props)
21
+ - [Events](#events)
22
+ - [Slots](#slots)
23
+ - [Auto-Generate Mode](#auto-generate-mode)
24
+ - [Max Items & Ellipsis](#max-items--ellipsis)
25
+ - [Router Integration](#router-integration)
26
+ - [Customization (Styles / Theming)](#customization-styles--theming)
27
+ - [Accessibility](#accessibility)
28
+ - [SSR Notes](#ssr-notes)
29
+ - [Development](#development)
30
+ - [Contributing](#contributing)
31
+ - [License](#license)
32
+
33
+ ---
34
+ ## Features
35
+ - **Manual items**: Provide a static array of breadcrumb items
36
+ - **Auto-generation**: Automatically generate breadcrumbs from Vue Router routes
37
+ - **Max items control**: Collapse long breadcrumb trails with ellipsis (`…`)
38
+ - **Custom separators**: Choose your own separator character or component
39
+ - **Customizable slots**: Override item, current item, and separator rendering
40
+ - **Router integration**: Automatically integrates with Vue Router for navigation
41
+ - **Accessibility**: Full ARIA support with semantic HTML and structured data
42
+ - **SSR-ready**: Works in both SPA and SSR (Nuxt 3) contexts
43
+ - **Lightweight**: Tree-shakeable with Vue marked external in library build
44
+
45
+ ---
46
+ ## Installation
47
+ Using npm:
48
+ ```bash
49
+ npm install @todovue/tv-breadcrumbs
50
+ ```
51
+ Using yarn:
52
+ ```bash
53
+ yarn add @todovue/tv-breadcrumbs
54
+ ```
55
+ Using pnpm:
56
+ ```bash
57
+ pnpm add @todovue/tv-breadcrumbs
58
+ ```
59
+
60
+ ---
61
+ ## Quick Start (SPA)
62
+ Global registration (main.js / main.ts):
63
+ ```js
64
+ import { createApp } from 'vue'
65
+ import App from './App.vue'
66
+ import { TvBreadcrumbs } from '@todovue/tv-breadcrumbs'
67
+
68
+ createApp(App)
69
+ .use(TvBreadcrumbs) // enables <TvBreadcrumbs /> globally
70
+ .mount('#app')
71
+ ```
72
+ Local import inside a component:
73
+ ```vue
74
+ <script setup>
75
+ import { TvBreadcrumbs } from '@todovue/tv-breadcrumbs'
76
+
77
+ const items = [
78
+ { label: 'Home', href: '/' },
79
+ { label: 'Products', href: '/products' },
80
+ { label: 'Category', href: '/products/category' },
81
+ { label: 'Item Details' }
82
+ ]
83
+
84
+ function onItemClick({ item, index, event }) {
85
+ console.log('Clicked:', item.label)
86
+ }
87
+ </script>
88
+
89
+ <template>
90
+ <TvBreadcrumbs :items="items" @item-click="onItemClick" />
91
+ </template>
92
+ ```
93
+
94
+ ---
95
+ ## Nuxt 3 / SSR Usage
96
+ Create a plugin file: `plugins/tv-breadcrumbs.client.ts` (or without `.client` suffix as it's SSR-safe):
97
+ ```ts
98
+ import { defineNuxtPlugin } from '#app'
99
+ import { TvBreadcrumbs } from '@todovue/tv-breadcrumbs'
100
+
101
+ export default defineNuxtPlugin(nuxtApp => {
102
+ nuxtApp.vueApp.use(TvBreadcrumbs)
103
+ })
104
+ ```
105
+ Use anywhere:
106
+ ```vue
107
+ <TvBreadcrumbs auto-generate />
108
+ ```
109
+ Optional direct import (no plugin):
110
+ ```vue
111
+ <script setup>
112
+ import { TvBreadcrumbs } from '@todovue/tv-breadcrumbs'
113
+ </script>
114
+ ```
115
+
116
+ ---
117
+ ## Component Registration Options
118
+ | Approach | When to use |
119
+ |---------------------------------------------------------------------------|------------------------------------------------|
120
+ | Global via `app.use(TvBreadcrumbs)` | Many usages across app / design system install |
121
+ | Local named import `{ TvBreadcrumbs }` | Isolated / code-split contexts |
122
+ | Direct default import `import { TvBreadcrumbs } from '@todovue/tv-breadcrumbs'` | Single usage or manual registration |
123
+
124
+ ---
125
+ ## Props
126
+ | Prop | Type | Default | Description |
127
+ |---------------|---------|--------------|--------------------------------------------------------------------------------------------------|
128
+ | items | Array | `[]` | Array of breadcrumb items. Each item: `{ label, href?, disabled?, key? }`. |
129
+ | separator | String | `'›'` | Character or string to display between breadcrumb items. |
130
+ | maxItems | Number | `0` | Maximum items to display. If exceeded, shows first item + ellipsis + last N items. 0 = no limit.|
131
+ | autoGenerate | Boolean | `false` | Auto-generate breadcrumbs from `$route.path` or route meta (`breadcrumb`). |
132
+ | homeLabel | String | `'Home'` | Label for the home item when auto-generating breadcrumbs. |
133
+ | ariaLabel | String | `'Breadcrumb'` | ARIA label for the `<nav>` element. |
134
+
135
+ ### Item Object Structure
136
+ Each item in the `items` array can have:
137
+ ```typescript
138
+ {
139
+ label: string // Display text (required)
140
+ href?: string // Link URL (optional, null for current page)
141
+ disabled?: boolean // Disable interaction (optional)
142
+ key?: string // Unique key for rendering (optional, auto-generated if not provided)
143
+ }
144
+ ```
145
+
146
+ ---
147
+ ## Events
148
+ | Event name (kebab) | Payload | Description |
149
+ |--------------------|--------------------------------------------|----------------------------------------------------------------------|
150
+ | `item-click` | `{ item, index, event }` | Emitted when any breadcrumb item is clicked. |
151
+ | `navigate` | `{ to, item, index }` | Emitted when navigation occurs via Vue Router (if router is present).|
152
+
153
+ Usage:
154
+ ```vue
155
+ <TvBreadcrumbs
156
+ :items="items"
157
+ @item-click="handleClick"
158
+ @navigate="handleNavigate"
159
+ />
160
+ ```
161
+
162
+ ```js
163
+ function handleClick({ item, index, event }) {
164
+ console.log('Clicked item:', item.label, 'at index:', index)
165
+ }
166
+
167
+ function handleNavigate({ to, item, index }) {
168
+ console.log('Navigating to:', to)
169
+ }
170
+ ```
171
+
172
+ ---
173
+ ## Slots
174
+ The component provides three slots for full customization:
175
+
176
+ ### `item` slot
177
+ Customize the rendering of each breadcrumb link (except the last one).
178
+ ```vue
179
+ <TvBreadcrumbs :items="items">
180
+ <template #item="{ item, index }">
181
+ <strong>{{ item.label }}</strong>
182
+ </template>
183
+ </TvBreadcrumbs>
184
+ ```
185
+
186
+ ### `current` slot
187
+ Customize the rendering of the current (last) breadcrumb item.
188
+ ```vue
189
+ <TvBreadcrumbs :items="items">
190
+ <template #current="{ item, index }">
191
+ <em>{{ item.label }}</em>
192
+ </template>
193
+ </TvBreadcrumbs>
194
+ ```
195
+
196
+ ### `separator` slot
197
+ Customize the separator between breadcrumb items.
198
+ ```vue
199
+ <TvBreadcrumbs :items="items">
200
+ <template #separator>
201
+ <span> / </span>
202
+ </template>
203
+ </TvBreadcrumbs>
204
+ ```
205
+
206
+ ---
207
+ ## Auto-Generate Mode
208
+ When `autoGenerate` is enabled, the component automatically creates breadcrumbs from your current route.
209
+
210
+ ### Basic Auto-Generation
211
+ ```vue
212
+ <TvBreadcrumbs auto-generate />
213
+ ```
214
+ This reads `$route.path` and creates breadcrumb items. For example:
215
+ - Path: `/docs/guides/installation`
216
+ - Result: `Home › Docs › Guides › Installation`
217
+
218
+ ### Custom Home Label
219
+ ```vue
220
+ <TvBreadcrumbs auto-generate home-label="Dashboard" />
221
+ ```
222
+
223
+ ### Route Meta Integration
224
+ You can define breadcrumbs in your route configuration:
225
+ ```js
226
+ const routes = [
227
+ {
228
+ path: '/products',
229
+ meta: {
230
+ breadcrumb: 'Products'
231
+ }
232
+ },
233
+ {
234
+ path: '/products/:id',
235
+ meta: {
236
+ breadcrumb: (route) => [
237
+ { label: 'Products', href: '/products' },
238
+ { label: route.params.id }
239
+ ]
240
+ }
241
+ }
242
+ ]
243
+ ```
244
+
245
+ The component will use:
246
+ 1. Route meta `breadcrumb` if defined (string, array, or function)
247
+ 2. Fallback to auto-generated path segments if no meta found
248
+
249
+ ---
250
+ ## Max Items & Ellipsis
251
+ Control long breadcrumb trails with the `maxItems` prop:
252
+
253
+ ```vue
254
+ <TvBreadcrumbs :items="longItemsList" :max-items="4" />
255
+ ```
256
+
257
+ **Example:**
258
+ - Original: `Home › Docs › API › v1 › Auth › Scopes`
259
+ - With `max-items="4"`: `Home › … › Auth › Scopes`
260
+
261
+ The algorithm keeps:
262
+ - First item (always visible)
263
+ - Ellipsis (`…`) as a disabled item
264
+ - Last N-1 items (where N = maxItems)
265
+
266
+ ---
267
+ ## Router Integration
268
+ TvBreadcrumbs automatically detects and integrates with Vue Router:
269
+
270
+ 1. **Automatic navigation**: Clicks on breadcrumb links call `router.push()` instead of native navigation
271
+ 2. **Route reading**: Accesses `$route` for auto-generation
272
+ 3. **Navigate event**: Emits when programmatic navigation occurs
273
+
274
+ **Without router**: Links work as standard `<a>` tags with `href`.
275
+
276
+ **With router**: Navigation is handled programmatically, and the `navigate` event fires.
277
+
278
+ ---
279
+ ## Customization (Styles / Theming)
280
+ The component uses scoped styles with BEM-like class names for easy customization:
281
+
282
+ ### CSS Classes
283
+ - `.tv-breadcrumb-root`: Main `<nav>` container
284
+ - `.tv-breadcrumb-list`: `<ol>` list wrapper
285
+ - `.tv-breadcrumb-item`: Each `<li>` item
286
+ - `.tv-breadcrumb-item--link`: Non-current items
287
+ - `.tv-breadcrumb-item--current`: Current (last) item
288
+ - `.tv-breadcrumb-item--disabled`: Disabled items
289
+ - `.tv-breadcrumb-link`: `<a>` link element
290
+ - `.tv-breadcrumb-current`: Current item `<span>`
291
+ - `.tv-breadcrumb-separator`: Separator `<span>`
292
+
293
+ ### Custom Styling Example
294
+ ```css
295
+ /* Override default styles */
296
+ .tv-breadcrumb-list {
297
+ font-size: 14px;
298
+ color: #333;
299
+ }
300
+
301
+ .tv-breadcrumb-link {
302
+ color: #0066cc;
303
+ text-decoration: none;
304
+ }
305
+
306
+ .tv-breadcrumb-link:hover {
307
+ text-decoration: underline;
308
+ }
309
+
310
+ .tv-breadcrumb-separator {
311
+ margin: 0 8px;
312
+ color: #999;
313
+ }
314
+
315
+ .tv-breadcrumb-current {
316
+ font-weight: 600;
317
+ color: #000;
318
+ }
319
+ ```
320
+
321
+ ---
322
+ ## Accessibility
323
+ TvBreadcrumbs follows WAI-ARIA best practices:
324
+
325
+ - **Semantic HTML**: Uses `<nav>`, `<ol>`, `<li>` for proper structure
326
+ - **ARIA attributes**:
327
+ - `aria-label` on `<nav>` (customizable via prop)
328
+ - `aria-current="page"` on the current item
329
+ - `aria-disabled` on disabled items
330
+ - `aria-hidden` on separator
331
+ - **Structured data**: Implements Schema.org `ListItem` markup for search engines
332
+ - **Keyboard navigation**: All interactive elements are focusable and keyboard-accessible
333
+ - **Screen reader friendly**: Proper semantic structure and ARIA labels
334
+
335
+ ---
336
+ ## SSR Notes
337
+ - No direct DOM (`window` / `document`) access → safe for SSR
338
+ - Router access is wrapped with safe guards (checks for `$route` / `$router` existence)
339
+ - Works with Nuxt 3 out of the box
340
+ - Styles are automatically injected when importing the component
341
+ - Auto-generation mode gracefully handles missing router in SSR context
342
+
343
+ ---
344
+ ## Development
345
+ ```bash
346
+ git clone https://github.com/TODOvue/tv-breadcrumbs.git
347
+ cd tv-breadcrumbs
348
+ npm install
349
+ npm run dev # run demo playground
350
+ npm run build # build library
351
+ ```
352
+ Local demo served from Vite using `index.html` + `src/demo` examples.
353
+
354
+ ---
355
+ ## Contributing
356
+ PRs and issues welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
357
+
358
+ ---
359
+ ## License
360
+ MIT © TODOvue
361
+
362
+ ---
363
+ ### Attributions
364
+ Crafted for the TODOvue component ecosystem
@@ -0,0 +1,6 @@
1
+ import { default as TvBreadcrumbs } from './components/TvBreadcrumbs.vue';
2
+ export declare const TvBreadcrumbsPlugin: {
3
+ install(app: any): void;
4
+ };
5
+ export { TvBreadcrumbs };
6
+ export default TvBreadcrumbs;
Binary file
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('@charset "UTF-8";[data-v-d1c54a52]{box-sizing:border-box;margin:0;padding:0}.tv-breadcrumb.tv-breadcrumb-root.tv-breadcrumb-container[data-v-d1c54a52]{width:100%;overflow-x:auto}.tv-breadcrumb .tv-breadcrumb-list[data-v-d1c54a52]{display:flex;align-items:center;gap:.5rem;list-style:none;padding:0;margin:0;white-space:nowrap}.tv-breadcrumb .tv-breadcrumb-item[data-v-d1c54a52]{display:inline-flex;align-items:center;gap:.5rem}.tv-breadcrumb .tv-breadcrumb-item--link .tv-breadcrumb-link[data-v-d1c54a52]{text-decoration:none;transition:opacity .16s ease,text-decoration-color .16s ease}.tv-breadcrumb .tv-breadcrumb-item--current .tv-breadcrumb-current[data-v-d1c54a52]{font-weight:600;cursor:default}.tv-breadcrumb .tv-breadcrumb-item--disabled .tv-breadcrumb-link[data-v-d1c54a52]{cursor:not-allowed;opacity:.5;pointer-events:none}.tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{-webkit-user-select:none;user-select:none;opacity:.7}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]{color:#000b14;text-decoration:underline;text-underline-offset:2px;text-decoration-color:#000b144d}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:hover{opacity:.85;text-decoration-color:#000b1499}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:focus-visible{outline:2px solid rgba(0,11,20,.4);outline-offset:2px;border-radius:6px}.light-mode .tv-breadcrumb .tv-breadcrumb-current[data-v-d1c54a52],.light-mode .tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{color:#000b14}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]{color:#f4faff;text-decoration:underline;text-underline-offset:2px;text-decoration-color:#f4faff40}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:hover{opacity:.9;text-decoration-color:#f4faff8c}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:focus-visible{outline:2px solid rgba(244,250,255,.35);outline-offset:2px;border-radius:6px}.dark-mode .tv-breadcrumb .tv-breadcrumb-current[data-v-d1c54a52]{color:#f4faff}.dark-mode .tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{color:#f4faff;opacity:.75}')),document.head.appendChild(e)}}catch(r){console.error("vite-plugin-css-injected-by-js",r)}})();
2
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function B(t,r){const u=e.getCurrentInstance()?.appContext?.config?.globalProperties||{},l=e.computed(()=>u.$route||null),d=u.$router||null,f=e.computed(()=>{if(!t.autoGenerate)return[];const n=e.unref(l);if(!n)return[];const i=(n.matched||[]).map(b=>{const m=b.meta?.breadcrumb;return m?typeof m=="function"?m(n):Array.isArray(m)?m:typeof m=="string"?[{label:m,href:b.path}]:null:null}).filter(Boolean).flat();if(i.length>0)return v(i);const g=String(n.path||"").split("/").filter(Boolean);if(g.length===0)return v([{label:t.homeLabel,href:"/"}]);const k=[];let _="";for(const b of g)_+=`/${b}`,k.push({label:S(b),href:_});return v([{label:t.homeLabel,href:"/"},...k])}),y=e.computed(()=>Array.isArray(t.items)&&t.items.length>0?v(t.items):f.value),a=e.computed(()=>{const n=y.value,s=Number(t.maxItems||0);if(!s||n.length<=s)return n;const i=Math.max(1,s-2),h=n[0],g=n.slice(-i);return[h,{label:"…",href:null,key:"__ellipsis__",disabled:!0},...g]});function o(n,s,i){if(!s||s.disabled){n.preventDefault();return}r("item-click",{item:s,index:i,event:n});const h=i===a.value.length-1;if(!s.href||h){n.preventDefault();return}d&&typeof d.push=="function"&&(n.preventDefault(),d.push(s.href),r("navigate",{to:s.href,item:s,index:i}))}return{itemsToRender:a,handleClick:o}}function S(t){return t.replace(/[-_]+/g," ").replace(/\b\w/g,r=>r.toUpperCase())}function v(t){return t.filter(Boolean).map((r,c)=>{if(typeof r=="string")return{label:r,href:null,key:`i-${c}`};const u=r.label!=null?String(r.label):"",l=r.href!=null&&r.href!==""?r.href:null,d=r.key!=null?r.key:`i-${c}`;return{...r,label:u,href:l,key:d}})}const T=(t,r)=>{const c=t.__vccOpts||t;for(const[u,l]of r)c[u]=l;return c},E=["aria-label"],N={class:"tv-breadcrumb-list",role:"list"},C=["itemscope","itemtype"],I=["href","aria-disabled","tabindex","onClick"],L={itemprop:"name"},V=["content"],$={class:"tv-breadcrumb-separator","aria-hidden":"true"},D={key:1,class:"tv-breadcrumb-current","aria-current":"page"},A={__name:"TvBreadcrumbs",props:{items:{type:Array,default:()=>[]},separator:{type:String,default:"›"},maxItems:{type:Number,default:0},autoGenerate:{type:Boolean,default:!1},homeLabel:{type:String,default:"Home"},ariaLabel:{type:String,default:"Breadcrumb"}},emits:["item-click","navigate"],setup(t,{emit:r}){const c=t,u=r,{itemsToRender:l,handleClick:d}=B(c,u);return(f,y)=>(e.openBlock(),e.createElementBlock("nav",{class:"tv-breadcrumb tv-breadcrumb-root tv-breadcrumb-container","aria-label":t.ariaLabel},[e.createElementVNode("ol",N,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(l),(a,o)=>(e.openBlock(),e.createElementBlock("li",{key:a.key||o,class:e.normalizeClass(["tv-breadcrumb-item",{"tv-breadcrumb-item--current":o===e.unref(l).length-1,"tv-breadcrumb-item--link":o!==e.unref(l).length-1,"tv-breadcrumb-item--disabled":a.disabled}]),itemscope:o===e.unref(l).length-1?void 0:!0,itemtype:o===e.unref(l).length-1?void 0:"https://schema.org/ListItem"},[o!==e.unref(l).length-1?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createElementVNode("a",{class:"tv-breadcrumb-link",href:a.disabled?void 0:a.href||"#","aria-disabled":a.disabled?"true":void 0,tabindex:a.disabled?-1:void 0,itemprop:"item",onClick:n=>e.unref(d)(n,a,o)},[e.createElementVNode("span",L,[e.renderSlot(f.$slots,"item",{item:a,index:o},()=>[e.createTextVNode(e.toDisplayString(a.label),1)],!0)])],8,I),e.createElementVNode("meta",{itemprop:"position",content:String(o+1)},null,8,V),e.createElementVNode("span",$,[e.renderSlot(f.$slots,"separator",{},()=>[e.createTextVNode(e.toDisplayString(t.separator),1)],!0)])],64)):(e.openBlock(),e.createElementBlock("span",D,[e.renderSlot(f.$slots,"current",{item:a,index:o},()=>[e.createTextVNode(e.toDisplayString(a.label),1)],!0)]))],10,C))),128))])],8,E))}},p=T(A,[["__scopeId","data-v-d1c54a52"]]);p.install=t=>{t.component("TvBreadcrumbs",p)};const M={install(t){t.component("TvBreadcrumbs",p)}};exports.TvBreadcrumbs=p;exports.TvBreadcrumbsPlugin=M;exports.default=p;
@@ -0,0 +1,6 @@
1
+ export * from './entry'
2
+ export {}
3
+ import TvBreadcrumbs from './entry'
4
+ export default TvBreadcrumbs
5
+ export * from './entry'
6
+ export {}
@@ -0,0 +1,191 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('@charset "UTF-8";[data-v-d1c54a52]{box-sizing:border-box;margin:0;padding:0}.tv-breadcrumb.tv-breadcrumb-root.tv-breadcrumb-container[data-v-d1c54a52]{width:100%;overflow-x:auto}.tv-breadcrumb .tv-breadcrumb-list[data-v-d1c54a52]{display:flex;align-items:center;gap:.5rem;list-style:none;padding:0;margin:0;white-space:nowrap}.tv-breadcrumb .tv-breadcrumb-item[data-v-d1c54a52]{display:inline-flex;align-items:center;gap:.5rem}.tv-breadcrumb .tv-breadcrumb-item--link .tv-breadcrumb-link[data-v-d1c54a52]{text-decoration:none;transition:opacity .16s ease,text-decoration-color .16s ease}.tv-breadcrumb .tv-breadcrumb-item--current .tv-breadcrumb-current[data-v-d1c54a52]{font-weight:600;cursor:default}.tv-breadcrumb .tv-breadcrumb-item--disabled .tv-breadcrumb-link[data-v-d1c54a52]{cursor:not-allowed;opacity:.5;pointer-events:none}.tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{-webkit-user-select:none;user-select:none;opacity:.7}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]{color:#000b14;text-decoration:underline;text-underline-offset:2px;text-decoration-color:#000b144d}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:hover{opacity:.85;text-decoration-color:#000b1499}.light-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:focus-visible{outline:2px solid rgba(0,11,20,.4);outline-offset:2px;border-radius:6px}.light-mode .tv-breadcrumb .tv-breadcrumb-current[data-v-d1c54a52],.light-mode .tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{color:#000b14}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]{color:#f4faff;text-decoration:underline;text-underline-offset:2px;text-decoration-color:#f4faff40}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:hover{opacity:.9;text-decoration-color:#f4faff8c}.dark-mode .tv-breadcrumb .tv-breadcrumb-link[data-v-d1c54a52]:focus-visible{outline:2px solid rgba(244,250,255,.35);outline-offset:2px;border-radius:6px}.dark-mode .tv-breadcrumb .tv-breadcrumb-current[data-v-d1c54a52]{color:#f4faff}.dark-mode .tv-breadcrumb .tv-breadcrumb-separator[data-v-d1c54a52]{color:#f4faff;opacity:.75}')),document.head.appendChild(e)}}catch(r){console.error("vite-plugin-css-injected-by-js",r)}})();
2
+ import { getCurrentInstance as D, computed as _, unref as i, createElementBlock as h, openBlock as p, createElementVNode as g, Fragment as A, renderList as N, normalizeClass as z, renderSlot as B, createTextVNode as S, toDisplayString as C } from "vue";
3
+ function R(e, t) {
4
+ const c = D()?.appContext?.config?.globalProperties || {}, a = _(() => c.$route || null), d = c.$router || null, f = _(() => {
5
+ if (!e.autoGenerate) return [];
6
+ const r = i(a);
7
+ if (!r) return [];
8
+ const u = (r.matched || []).map((b) => {
9
+ const m = b.meta?.breadcrumb;
10
+ return m ? typeof m == "function" ? m(r) : Array.isArray(m) ? m : typeof m == "string" ? [{ label: m, href: b.path }] : null : null;
11
+ }).filter(Boolean).flat();
12
+ if (u.length > 0)
13
+ return k(u);
14
+ const v = String(r.path || "").split("/").filter(Boolean);
15
+ if (v.length === 0)
16
+ return k([{ label: e.homeLabel, href: "/" }]);
17
+ const T = [];
18
+ let $ = "";
19
+ for (const b of v)
20
+ $ += `/${b}`, T.push({
21
+ label: E(b),
22
+ href: $
23
+ });
24
+ return k([{ label: e.homeLabel, href: "/" }, ...T]);
25
+ }), L = _(() => Array.isArray(e.items) && e.items.length > 0 ? k(e.items) : f.value), n = _(() => {
26
+ const r = L.value, s = Number(e.maxItems || 0);
27
+ if (!s || r.length <= s)
28
+ return r;
29
+ const u = Math.max(1, s - 2), y = r[0], v = r.slice(-u);
30
+ return [
31
+ y,
32
+ { label: "…", href: null, key: "__ellipsis__", disabled: !0 },
33
+ ...v
34
+ ];
35
+ });
36
+ function l(r, s, u) {
37
+ if (!s || s.disabled) {
38
+ r.preventDefault();
39
+ return;
40
+ }
41
+ t("item-click", {
42
+ item: s,
43
+ index: u,
44
+ event: r
45
+ });
46
+ const y = u === n.value.length - 1;
47
+ if (!s.href || y) {
48
+ r.preventDefault();
49
+ return;
50
+ }
51
+ d && typeof d.push == "function" && (r.preventDefault(), d.push(s.href), t("navigate", {
52
+ to: s.href,
53
+ item: s,
54
+ index: u
55
+ }));
56
+ }
57
+ return {
58
+ itemsToRender: n,
59
+ handleClick: l
60
+ };
61
+ }
62
+ function E(e) {
63
+ return e.replace(/[-_]+/g, " ").replace(/\b\w/g, (t) => t.toUpperCase());
64
+ }
65
+ function k(e) {
66
+ return e.filter(Boolean).map((t, o) => {
67
+ if (typeof t == "string")
68
+ return { label: t, href: null, key: `i-${o}` };
69
+ const c = t.label != null ? String(t.label) : "", a = t.href != null && t.href !== "" ? t.href : null, d = t.key != null ? t.key : `i-${o}`;
70
+ return {
71
+ ...t,
72
+ label: c,
73
+ href: a,
74
+ key: d
75
+ };
76
+ });
77
+ }
78
+ const G = (e, t) => {
79
+ const o = e.__vccOpts || e;
80
+ for (const [c, a] of t)
81
+ o[c] = a;
82
+ return o;
83
+ }, M = ["aria-label"], P = {
84
+ class: "tv-breadcrumb-list",
85
+ role: "list"
86
+ }, V = ["itemscope", "itemtype"], w = ["href", "aria-disabled", "tabindex", "onClick"], F = { itemprop: "name" }, H = ["content"], O = {
87
+ class: "tv-breadcrumb-separator",
88
+ "aria-hidden": "true"
89
+ }, U = {
90
+ key: 1,
91
+ class: "tv-breadcrumb-current",
92
+ "aria-current": "page"
93
+ }, j = {
94
+ __name: "TvBreadcrumbs",
95
+ props: {
96
+ items: {
97
+ type: Array,
98
+ default: () => []
99
+ },
100
+ separator: {
101
+ type: String,
102
+ default: "›"
103
+ },
104
+ maxItems: {
105
+ type: Number,
106
+ default: 0
107
+ },
108
+ autoGenerate: {
109
+ type: Boolean,
110
+ default: !1
111
+ },
112
+ homeLabel: {
113
+ type: String,
114
+ default: "Home"
115
+ },
116
+ ariaLabel: {
117
+ type: String,
118
+ default: "Breadcrumb"
119
+ }
120
+ },
121
+ emits: ["item-click", "navigate"],
122
+ setup(e, { emit: t }) {
123
+ const o = e, c = t, { itemsToRender: a, handleClick: d } = R(o, c);
124
+ return (f, L) => (p(), h("nav", {
125
+ class: "tv-breadcrumb tv-breadcrumb-root tv-breadcrumb-container",
126
+ "aria-label": e.ariaLabel
127
+ }, [
128
+ g("ol", P, [
129
+ (p(!0), h(A, null, N(i(a), (n, l) => (p(), h("li", {
130
+ key: n.key || l,
131
+ class: z(["tv-breadcrumb-item", {
132
+ "tv-breadcrumb-item--current": l === i(a).length - 1,
133
+ "tv-breadcrumb-item--link": l !== i(a).length - 1,
134
+ "tv-breadcrumb-item--disabled": n.disabled
135
+ }]),
136
+ itemscope: l === i(a).length - 1 ? void 0 : !0,
137
+ itemtype: l === i(a).length - 1 ? void 0 : "https://schema.org/ListItem"
138
+ }, [
139
+ l !== i(a).length - 1 ? (p(), h(A, { key: 0 }, [
140
+ g("a", {
141
+ class: "tv-breadcrumb-link",
142
+ href: n.disabled ? void 0 : n.href || "#",
143
+ "aria-disabled": n.disabled ? "true" : void 0,
144
+ tabindex: n.disabled ? -1 : void 0,
145
+ itemprop: "item",
146
+ onClick: (r) => i(d)(r, n, l)
147
+ }, [
148
+ g("span", F, [
149
+ B(f.$slots, "item", {
150
+ item: n,
151
+ index: l
152
+ }, () => [
153
+ S(C(n.label), 1)
154
+ ], !0)
155
+ ])
156
+ ], 8, w),
157
+ g("meta", {
158
+ itemprop: "position",
159
+ content: String(l + 1)
160
+ }, null, 8, H),
161
+ g("span", O, [
162
+ B(f.$slots, "separator", {}, () => [
163
+ S(C(e.separator), 1)
164
+ ], !0)
165
+ ])
166
+ ], 64)) : (p(), h("span", U, [
167
+ B(f.$slots, "current", {
168
+ item: n,
169
+ index: l
170
+ }, () => [
171
+ S(C(n.label), 1)
172
+ ], !0)
173
+ ]))
174
+ ], 10, V))), 128))
175
+ ])
176
+ ], 8, M));
177
+ }
178
+ }, I = /* @__PURE__ */ G(j, [["__scopeId", "data-v-d1c54a52"]]);
179
+ I.install = (e) => {
180
+ e.component("TvBreadcrumbs", I);
181
+ };
182
+ const J = {
183
+ install(e) {
184
+ e.component("TvBreadcrumbs", I);
185
+ }
186
+ };
187
+ export {
188
+ I as TvBreadcrumbs,
189
+ J as TvBreadcrumbsPlugin,
190
+ I as default
191
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@todovue/tv-breadcrumbs",
3
+ "private": false,
4
+ "author": "Cristhian Daza",
5
+ "description": "A simple and customizable Vue 3 breadcrumbs component for your applications.",
6
+ "license": "MIT",
7
+ "version": "1.0.0",
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/TODOvue/tv-breadcrumbs.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/TODOvue/tv-breadcrumbs/issues"
15
+ },
16
+ "keywords": [
17
+ "todovue",
18
+ "front-end",
19
+ "web",
20
+ "vue",
21
+ "vuejs",
22
+ "vue-js",
23
+ "breadcrumbs",
24
+ "show-breadcrumbs",
25
+ "vue-breadcrumbs",
26
+ "vue-breadcrumbs-component"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/tv-breadcrumbs.es.js",
31
+ "require": "./dist/tv-breadcrumbs.cjs.js"
32
+ }
33
+ },
34
+ "main": "dist/tv-breadcrumbs.cjs.js",
35
+ "module": "dist/tv-breadcrumbs.es.js",
36
+ "types": "dist/tv-breadcrumbs.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20.19.0"
44
+ },
45
+ "scripts": {
46
+ "dev": "vite",
47
+ "build": "vite build",
48
+ "build:demo": "cp README.md public/ && VITE_BUILD_TARGET=demo vite build"
49
+ },
50
+ "peerDependencies": {
51
+ "vue": "^3.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@todovue/tv-demo": "^1.0.0",
55
+ "@vitejs/plugin-vue": "^6.0.0",
56
+ "sass": "^1.0.0",
57
+ "vite": "^7.0.0",
58
+ "vite-plugin-css-injected-by-js": "^3.0.0",
59
+ "vite-plugin-dts": "^4.0.0"
60
+ }
61
+ }