@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 +21 -0
- package/README.md +364 -0
- package/dist/entry.d.ts +6 -0
- package/dist/favicon.ico +0 -0
- package/dist/tv-breadcrumbs.cjs.js +2 -0
- package/dist/tv-breadcrumbs.d.ts +6 -0
- package/dist/tv-breadcrumbs.es.js +191 -0
- package/package.json +61 -0
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
|
+
[](https://www.npmjs.com/package/@todovue/tv-breadcrumbs)
|
|
8
|
+
[](https://www.npmjs.com/package/@todovue/tv-breadcrumbs)
|
|
9
|
+

|
|
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
|
package/dist/entry.d.ts
ADDED
package/dist/favicon.ico
ADDED
|
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,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
|
+
}
|