@todovue/tv-footer 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) 2026 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,732 @@
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 Footer (TvFooter)
5
+ A simple, customizable, and responsive Vue 3 footer component with support for branding, navigation sections, social links, legal links, and SSR compatibility. Perfect for building professional footers in Single Page Apps and Server-Side Rendered environments like Nuxt 3.
6
+
7
+ [![npm](https://img.shields.io/npm/v/@todovue/tv-footer.svg)](https://www.npmjs.com/package/@todovue/tv-footer)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@todovue/tv-footer.svg)](https://www.npmjs.com/package/@todovue/tv-footer)
9
+ [![npm total downloads](https://img.shields.io/npm/dt/@todovue/tv-footer.svg)](https://www.npmjs.com/package/@todovue/tv-footer)
10
+ ![License](https://img.shields.io/github/license/TODOvue/tv-footer)
11
+ ![Release Date](https://img.shields.io/github/release-date/TODOvue/tv-footer)
12
+ ![Bundle Size](https://img.shields.io/bundlephobia/minzip/@todovue/tv-footer)
13
+ ![Node Version](https://img.shields.io/node/v/@todovue/tv-footer)
14
+ ![Last Commit](https://img.shields.io/github/last-commit/TODOvue/tv-footer)
15
+ ![Stars](https://img.shields.io/github/stars/TODOvue/tv-footer?style=social)
16
+
17
+ > Demo: https://ui.todovue.blog/footer
18
+
19
+ ---
20
+ ## Table of Contents
21
+ - [Features](#features)
22
+ - [Installation](#installation)
23
+ - [Quick Start (SPA)](#quick-start-spa)
24
+ - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
25
+ - [Component Registration Options](#component-registration-options)
26
+ - [Props](#props)
27
+ - [Configuration Object](#configuration-object)
28
+ - [Composable API](#composable-api)
29
+ - [Usage Examples](#usage-examples)
30
+ - [Styling](#styling)
31
+ - [Accessibility](#accessibility)
32
+ - [SSR Notes](#ssr-notes)
33
+ - [Development](#development)
34
+ - [Contributing](#contributing)
35
+ - [License](#license)
36
+
37
+ ---
38
+ ## Features
39
+ - Fully responsive grid layout (1 column on mobile, 2 on tablet, 4 on desktop)
40
+ - Customizable brand section with logo and version display
41
+ - Multiple navigation sections with titles and links
42
+ - Social media links with icon support
43
+ - Legal links section (Privacy, Terms, etc.)
44
+ - Dynamic copyright with automatic year replacement
45
+ - Light/Dark mode support built-in
46
+ - SSR-safe (works with Nuxt 3 and other SSR frameworks)
47
+ - Composable API (`useFooter`) for custom implementations
48
+ - Accessible and semantic HTML
49
+ - Lightweight and tree-shakeable
50
+ - TypeScript support
51
+
52
+ ---
53
+ ## Installation
54
+ Using npm:
55
+ ```bash
56
+ npm install @todovue/tv-footer
57
+ ```
58
+ Using yarn:
59
+ ```bash
60
+ yarn add @todovue/tv-footer
61
+ ```
62
+ Using pnpm:
63
+ ```bash
64
+ pnpm add @todovue/tv-footer
65
+ ```
66
+
67
+ ### Importing Styles
68
+ **Important:** You must explicitly import the stylesheet in your application.
69
+
70
+ #### For Vue/Vite SPA:
71
+ ```ts
72
+ // main.ts
73
+ import { createApp } from 'vue'
74
+ import App from './App.vue'
75
+
76
+ import '@todovue/tv-footer/style.css'
77
+ import { TvFooter } from '@todovue/tv-footer'
78
+
79
+ const app = createApp(App)
80
+ app.component('TvFooter', TvFooter)
81
+ app.mount('#app')
82
+ ```
83
+
84
+ #### For Nuxt 3/4:
85
+ ```ts
86
+ // nuxt.config.ts
87
+ export default defineNuxtConfig({
88
+ modules: [
89
+ '@todovue/tv-footer/nuxt'
90
+ ]
91
+ })
92
+ ```
93
+
94
+ Then register the component in a plugin as shown in the [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage) section.
95
+
96
+ ---
97
+ ## Quick Start (SPA)
98
+ Global registration (main.js / main.ts):
99
+ ```js
100
+ import { createApp } from 'vue'
101
+ import App from './App.vue'
102
+ import '@todovue/tv-footer/style.css'
103
+ import TvFooter from '@todovue/tv-footer'
104
+
105
+ createApp(App)
106
+ .use(TvFooter) // enables <TvFooter /> globally
107
+ .mount('#app')
108
+ ```
109
+ Local import inside a component:
110
+ ```vue
111
+ <script setup>
112
+ import { TvFooter } from '@todovue/tv-footer'
113
+ import '@todovue/tv-footer/style.css'
114
+
115
+ const footerConfig = {
116
+ brand: {
117
+ logo: 'https://example.com/logo.png',
118
+ name: 'My Company',
119
+ url: '/'
120
+ },
121
+ navigation: [
122
+ {
123
+ title: 'Product',
124
+ items: [
125
+ { label: 'Features', url: '/features' },
126
+ { label: 'Pricing', url: '/pricing' }
127
+ ]
128
+ }
129
+ ],
130
+ copyright: '© {year} My Company. All rights reserved.'
131
+ }
132
+ </script>
133
+
134
+ <template>
135
+ <div>
136
+ <!-- Your page content -->
137
+
138
+ <!-- Footer at the bottom -->
139
+ <TvFooter :config="footerConfig" />
140
+ </div>
141
+ </template>
142
+ ```
143
+ **Note:** Don't forget to import the CSS in your main entry file as shown above.
144
+
145
+ ---
146
+ ## Nuxt 3 / SSR Usage
147
+ First, add the module to your `nuxt.config.ts`:
148
+ ```ts
149
+ // nuxt.config.ts
150
+ export default defineNuxtConfig({
151
+ modules: ['@todovue/tv-footer/nuxt']
152
+ })
153
+ ```
154
+
155
+ Alternatively, you can manually add the CSS:
156
+ ```ts
157
+ // nuxt.config.ts
158
+ export default defineNuxtConfig({
159
+ css: ['@todovue/tv-footer/style.css'],
160
+ })
161
+ ```
162
+
163
+ Then create a plugin file: `plugins/tv-footer.client.ts`:
164
+ ```ts
165
+ import { defineNuxtPlugin } from '#app'
166
+ import TvFooter from '@todovue/tv-footer'
167
+
168
+ export default defineNuxtPlugin(nuxtApp => {
169
+ nuxtApp.vueApp.use(TvFooter)
170
+ })
171
+ ```
172
+ Use anywhere in your Nuxt app:
173
+ ```vue
174
+ <template>
175
+ <div>
176
+ <NuxtPage />
177
+
178
+ <!-- Footer -->
179
+ <TvFooter :config="footerConfig" />
180
+ </div>
181
+ </template>
182
+
183
+ <script setup>
184
+ const footerConfig = {
185
+ brand: {
186
+ logo: 'https://example.com/logo.png',
187
+ url: '/'
188
+ },
189
+ navigation: [
190
+ {
191
+ title: 'Product',
192
+ items: [
193
+ { label: 'Features', url: '/features' }
194
+ ]
195
+ }
196
+ ],
197
+ copyright: '© {year} My Company. All rights reserved.'
198
+ }
199
+ </script>
200
+ ```
201
+ Optional direct import (no plugin):
202
+ ```vue
203
+ <script setup>
204
+ import { TvFooter } from '@todovue/tv-footer'
205
+ </script>
206
+ ```
207
+
208
+ ---
209
+ ## Component Registration Options
210
+ | Approach | When to use |
211
+ |--------------------------------------------------------------------------|------------------------------------------------|
212
+ | Global via `app.use(TvFooter)` | Many usages across app / design system install |
213
+ | Local named import `{ TvFooter }` | Isolated / code-split contexts |
214
+ | Direct default import `import TvFooter from '@todovue/tv-footer'` | Single usage or manual registration |
215
+
216
+ ---
217
+ ## Props
218
+ | Prop | Type | Default | Description |
219
+ |--------|--------|---------|----------------------------------------------------------------|
220
+ | config | Object | `{}` | Configuration object containing all footer data and settings. |
221
+
222
+ ### Prop Details
223
+
224
+ #### `config`
225
+ The main configuration object that defines all footer content including brand, navigation, social links, legal links, and copyright information. See [Configuration Object](#configuration-object) for detailed structure.
226
+
227
+ Example:
228
+ ```vue
229
+ <TvFooter :config="myFooterConfig" />
230
+ ```
231
+
232
+ ---
233
+ ## Configuration Object
234
+ The `config` prop accepts an object with the following structure:
235
+
236
+ ### Brand Section
237
+ ```ts
238
+ {
239
+ brand: {
240
+ logo: string, // URL to logo image (optional)
241
+ name: string, // Brand name (optional, shown if no logo or alongside logo)
242
+ url: string // URL for brand link (default: '/')
243
+ }
244
+ }
245
+ ```
246
+
247
+ ### Version Display
248
+ ```ts
249
+ {
250
+ version: string // Version string to display (e.g., 'v2.4.0')
251
+ }
252
+ ```
253
+
254
+ ### Navigation Sections
255
+ ```ts
256
+ {
257
+ navigation: [
258
+ {
259
+ title: string, // Section title
260
+ items: [
261
+ {
262
+ label: string, // Link text
263
+ url: string // Link URL
264
+ }
265
+ ]
266
+ }
267
+ ]
268
+ }
269
+ ```
270
+
271
+ ### Social Links
272
+ ```ts
273
+ {
274
+ social: [
275
+ {
276
+ label: string, // Accessible label for the link
277
+ url: string, // Social media URL
278
+ iconUrl: string, // URL to icon image (optional)
279
+ icon: string // CSS class for icon (e.g., 'fab fa-github') (optional)
280
+ }
281
+ ]
282
+ }
283
+ ```
284
+
285
+ ### Legal Links
286
+ ```ts
287
+ {
288
+ legal: [
289
+ {
290
+ label: string, // Link text (e.g., 'Privacy Policy')
291
+ url: string // Link URL
292
+ }
293
+ ]
294
+ }
295
+ ```
296
+
297
+ ### Copyright
298
+ ```ts
299
+ {
300
+ copyright: string // Copyright text. Use {year} for automatic year replacement
301
+ }
302
+ ```
303
+
304
+ ### Complete Configuration Example
305
+ ```js
306
+ const footerConfig = {
307
+ brand: {
308
+ logo: 'https://example.com/logo.png',
309
+ name: 'MyApp',
310
+ url: '/'
311
+ },
312
+ version: 'v2.4.0',
313
+ navigation: [
314
+ {
315
+ title: 'Product',
316
+ items: [
317
+ { label: 'Features', url: '/features' },
318
+ { label: 'Pricing', url: '/pricing' },
319
+ { label: 'Showcase', url: '/showcase' }
320
+ ]
321
+ },
322
+ {
323
+ title: 'Resources',
324
+ items: [
325
+ { label: 'Documentation', url: '/docs' },
326
+ { label: 'API Reference', url: '/api' },
327
+ { label: 'Community', url: '/community' }
328
+ ]
329
+ },
330
+ {
331
+ title: 'Company',
332
+ items: [
333
+ { label: 'About Us', url: '/about' },
334
+ { label: 'Blog', url: '/blog' },
335
+ { label: 'Careers', url: '/careers' }
336
+ ]
337
+ }
338
+ ],
339
+ social: [
340
+ {
341
+ label: 'GitHub',
342
+ url: 'https://github.com/mycompany',
343
+ iconUrl: '/icons/github.svg'
344
+ },
345
+ {
346
+ label: 'Twitter',
347
+ url: 'https://twitter.com/mycompany',
348
+ icon: 'fab fa-twitter'
349
+ }
350
+ ],
351
+ legal: [
352
+ { label: 'Privacy Policy', url: '/privacy' },
353
+ { label: 'Terms of Service', url: '/terms' },
354
+ { label: 'Cookie Policy', url: '/cookies' }
355
+ ],
356
+ copyright: '© {year} MyApp. All rights reserved.'
357
+ }
358
+ ```
359
+
360
+ ---
361
+ ## Composable API
362
+ TvFooter includes a composable `useFooter` that you can use to process footer configuration and build custom footer implementations.
363
+
364
+ ### `useFooter(config)`
365
+ ```js
366
+ import { useFooter } from '@todovue/tv-footer'
367
+
368
+ const { brand, navigation, social, legal, version, copyright } = useFooter(config)
369
+ ```
370
+
371
+ **Parameters:**
372
+ - `config` (Object): Footer configuration object
373
+
374
+ **Returns:**
375
+ - `brand` (ComputedRef): Processed brand configuration
376
+ - `navigation` (ComputedRef): Array of navigation sections
377
+ - `social` (ComputedRef): Array of social links
378
+ - `legal` (ComputedRef): Array of legal links
379
+ - `version` (ComputedRef): Version string
380
+ - `copyright` (ComputedRef): Copyright text with {year} replaced by current year
381
+
382
+ **Example:**
383
+ ```vue
384
+ <script setup>
385
+ import { useFooter } from '@todovue/tv-footer'
386
+
387
+ const config = {
388
+ brand: { name: 'MyApp', url: '/' },
389
+ copyright: '© {year} MyApp. All rights reserved.'
390
+ }
391
+
392
+ const { brand, copyright } = useFooter(config)
393
+ </script>
394
+
395
+ <template>
396
+ <footer>
397
+ <div>
398
+ <a :href="brand.url">{{ brand.name }}</a>
399
+ </div>
400
+ <div>{{ copyright }}</div>
401
+ </footer>
402
+ </template>
403
+ ```
404
+
405
+ **Features:**
406
+ - Automatic current year replacement in copyright text
407
+ - Safe defaults for missing configuration
408
+ - Reactive computed properties
409
+ - Type-safe array validation
410
+
411
+ ---
412
+ ## Usage Examples
413
+
414
+ ### Minimal Footer
415
+ ```vue
416
+ <script setup>
417
+ import { TvFooter } from '@todovue/tv-footer'
418
+ import '@todovue/tv-footer/style.css'
419
+
420
+ const config = {
421
+ copyright: '© {year} My Company. All rights reserved.'
422
+ }
423
+ </script>
424
+
425
+ <template>
426
+ <TvFooter :config="config" />
427
+ </template>
428
+ ```
429
+
430
+ ### Footer with Brand and Navigation
431
+ ```vue
432
+ <script setup>
433
+ import { TvFooter } from '@todovue/tv-footer'
434
+ import '@todovue/tv-footer/style.css'
435
+
436
+ const config = {
437
+ brand: {
438
+ logo: 'https://example.com/logo.png',
439
+ name: 'MyApp',
440
+ url: '/'
441
+ },
442
+ version: 'v1.0.0',
443
+ navigation: [
444
+ {
445
+ title: 'Product',
446
+ items: [
447
+ { label: 'Features', url: '/features' },
448
+ { label: 'Pricing', url: '/pricing' }
449
+ ]
450
+ },
451
+ {
452
+ title: 'Support',
453
+ items: [
454
+ { label: 'Help Center', url: '/help' },
455
+ { label: 'Contact', url: '/contact' }
456
+ ]
457
+ }
458
+ ],
459
+ copyright: '© {year} MyApp. All rights reserved.'
460
+ }
461
+ </script>
462
+
463
+ <template>
464
+ <TvFooter :config="config" />
465
+ </template>
466
+ ```
467
+
468
+ ### Complete Footer with Social & Legal Links
469
+ ```vue
470
+ <script setup>
471
+ import { TvFooter } from '@todovue/tv-footer'
472
+ import '@todovue/tv-footer/style.css'
473
+ import GitHubIcon from './assets/github.svg'
474
+ import TwitterIcon from './assets/twitter.svg'
475
+
476
+ const config = {
477
+ brand: {
478
+ logo: 'https://example.com/logo.png',
479
+ name: 'MyApp',
480
+ url: '/'
481
+ },
482
+ version: 'v2.4.0',
483
+ navigation: [
484
+ {
485
+ title: 'Product',
486
+ items: [
487
+ { label: 'Features', url: '/features' },
488
+ { label: 'Pricing', url: '/pricing' },
489
+ { label: 'Showcase', url: '/showcase' }
490
+ ]
491
+ },
492
+ {
493
+ title: 'Resources',
494
+ items: [
495
+ { label: 'Documentation', url: '/docs' },
496
+ { label: 'API Reference', url: '/api' },
497
+ { label: 'Community', url: '/community' }
498
+ ]
499
+ },
500
+ {
501
+ title: 'Company',
502
+ items: [
503
+ { label: 'About Us', url: '/about' },
504
+ { label: 'Blog', url: '/blog' },
505
+ { label: 'Careers', url: '/careers' }
506
+ ]
507
+ }
508
+ ],
509
+ social: [
510
+ {
511
+ label: 'GitHub',
512
+ url: 'https://github.com/mycompany',
513
+ iconUrl: GitHubIcon
514
+ },
515
+ {
516
+ label: 'Twitter',
517
+ url: 'https://twitter.com/mycompany',
518
+ iconUrl: TwitterIcon
519
+ }
520
+ ],
521
+ legal: [
522
+ { label: 'Privacy Policy', url: '/privacy' },
523
+ { label: 'Terms of Service', url: '/terms' },
524
+ { label: 'Cookie Policy', url: '/cookies' }
525
+ ],
526
+ copyright: '© {year} MyApp. All rights reserved.'
527
+ }
528
+ </script>
529
+
530
+ <template>
531
+ <div>
532
+ <!-- Your page content -->
533
+ <main>
534
+ <!-- ... -->
535
+ </main>
536
+
537
+ <!-- Footer -->
538
+ <TvFooter :config="config" />
539
+ </div>
540
+ </template>
541
+ ```
542
+
543
+ ### Using Icon Libraries (Font Awesome, etc.)
544
+ ```vue
545
+ <script setup>
546
+ import { TvFooter } from '@todovue/tv-footer'
547
+ import '@todovue/tv-footer/style.css'
548
+
549
+ const config = {
550
+ social: [
551
+ {
552
+ label: 'GitHub',
553
+ url: 'https://github.com/mycompany',
554
+ icon: 'fab fa-github' // Font Awesome class
555
+ },
556
+ {
557
+ label: 'Twitter',
558
+ url: 'https://twitter.com/mycompany',
559
+ icon: 'fab fa-twitter' // Font Awesome class
560
+ },
561
+ {
562
+ label: 'LinkedIn',
563
+ url: 'https://linkedin.com/company/mycompany',
564
+ icon: 'fab fa-linkedin' // Font Awesome class
565
+ }
566
+ ],
567
+ copyright: '© {year} MyApp. All rights reserved.'
568
+ }
569
+ </script>
570
+
571
+ <template>
572
+ <TvFooter :config="config" />
573
+ </template>
574
+ ```
575
+
576
+ ### Custom Implementation with Composable
577
+ ```vue
578
+ <script setup>
579
+ import { useFooter } from '@todovue/tv-footer'
580
+
581
+ const config = {
582
+ brand: {
583
+ name: 'MyApp',
584
+ url: '/'
585
+ },
586
+ navigation: [
587
+ {
588
+ title: 'Links',
589
+ items: [
590
+ { label: 'Home', url: '/' },
591
+ { label: 'About', url: '/about' }
592
+ ]
593
+ }
594
+ ],
595
+ copyright: '© {year} MyApp. All rights reserved.'
596
+ }
597
+
598
+ const { brand, navigation, copyright } = useFooter(config)
599
+ </script>
600
+
601
+ <template>
602
+ <footer class="custom-footer">
603
+ <div class="custom-footer__brand">
604
+ <a :href="brand.url">{{ brand.name }}</a>
605
+ </div>
606
+
607
+ <nav v-for="(section, index) in navigation" :key="index">
608
+ <h4>{{ section.title }}</h4>
609
+ <ul>
610
+ <li v-for="(link, i) in section.items" :key="i">
611
+ <a :href="link.url">{{ link.label }}</a>
612
+ </li>
613
+ </ul>
614
+ </nav>
615
+
616
+ <div class="custom-footer__copyright">
617
+ {{ copyright }}
618
+ </div>
619
+ </footer>
620
+ </template>
621
+
622
+ <style scoped>
623
+ .custom-footer {
624
+ /* Your custom styles */
625
+ }
626
+ </style>
627
+ ```
628
+
629
+ ---
630
+ ## Styling
631
+ TvFooter comes with built-in responsive styles and light/dark mode support.
632
+
633
+ ### Built-in Features
634
+ - **Responsive Grid Layout**:
635
+ - Mobile (< 640px): 1 column
636
+ - Tablet (≥ 640px): 2 columns
637
+ - Desktop (≥ 1024px): 4 columns
638
+ - **Dark Mode Support**: Automatically adapts to `.dark-mode` class on parent
639
+ - **Light Mode Support**: Adapts to `.light-mode` class on parent
640
+ - **Hover Effects**: Smooth transitions on links and social icons
641
+ - **Backdrop Blur**: Modern glassmorphism effect on social icons
642
+
643
+ ### Customization
644
+ You can override the default styles by targeting the CSS classes:
645
+
646
+ ```css
647
+ /* Override brand logo size */
648
+ .tv-footer__logo img {
649
+ height: 80px;
650
+ }
651
+
652
+ /* Change link colors */
653
+ .tv-footer__link {
654
+ color: #your-color;
655
+ }
656
+
657
+ .tv-footer__link:hover {
658
+ color: #your-hover-color;
659
+ }
660
+
661
+ /* Customize social icons */
662
+ .tv-footer__social-link {
663
+ background-color: #your-background;
664
+ }
665
+
666
+ /* Adjust spacing */
667
+ .tv-footer {
668
+ padding: 3rem 1rem;
669
+ }
670
+
671
+ /* Customize container max-width */
672
+ .tv-footer__container {
673
+ max-width: 1400px;
674
+ }
675
+ ```
676
+
677
+ ### Available CSS Classes
678
+ - `.tv-footer` - Main footer container
679
+ - `.tv-footer__container` - Inner grid container
680
+ - `.tv-footer__brand` - Brand section
681
+ - `.tv-footer__logo` - Brand logo/name link
682
+ - `.tv-footer__version` - Version display
683
+ - `.tv-footer__section` - Navigation or social section
684
+ - `.tv-footer__section-title` - Section title
685
+ - `.tv-footer__links` - List of links
686
+ - `.tv-footer__link` - Individual link
687
+ - `.tv-footer__social` - Social links container
688
+ - `.tv-footer__social-link` - Individual social link
689
+ - `.tv-footer__social-icon-img` - Social icon image
690
+ - `.tv-footer__bottom` - Bottom section with copyright and legal
691
+ - `.tv-footer__legal` - Legal links container
692
+
693
+ ---
694
+ ## Accessibility
695
+ - **Semantic HTML**: Uses proper `<footer>`, `<nav>`, `<ul>`, and `<a>` elements
696
+ - **ARIA Labels**: Social links include accessible labels
697
+ - **External Link Safety**: Social links include `rel="noopener noreferrer"` for security
698
+ - **Keyboard Navigation**: All links are keyboard accessible
699
+ - **Focus States**: Clear focus indicators for keyboard navigation
700
+ - **Screen Reader Friendly**: Proper heading hierarchy and semantic structure
701
+ - **Alternative Text**: Images include alt attributes
702
+
703
+ ---
704
+ ## SSR Notes
705
+ - **SSR-Safe**: No direct `window`/`document` access during module evaluation
706
+ - **Nuxt 3 Compatible**: Works seamlessly with Nuxt 3 out of the box
707
+ - **Hydration Safe**: No hydration mismatches
708
+ - **Universal Rendering**: Works in both client and server contexts
709
+ - **Vue 3 Composition API**: Uses modern Vue 3 composables
710
+
711
+ ---
712
+ ## Development
713
+ ```bash
714
+ git clone https://github.com/TODOvue/tv-footer.git
715
+ cd tv-footer
716
+ npm install
717
+ npm run dev # run demo playground
718
+ npm run build # build library
719
+ ```
720
+ Local demo served from Vite using `index.html` and demo examples in `src/demo`.
721
+
722
+ ---
723
+ ## Contributing
724
+ PRs and issues welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
725
+
726
+ ---
727
+ ## License
728
+ MIT © TODOvue
729
+
730
+ ---
731
+ ### Attributions
732
+ Crafted for the TODOvue component ecosystem
@@ -0,0 +1,11 @@
1
+ import { Plugin } from 'vue';
2
+ import { default as _TvFooter } from './components/TvFooter.vue';
3
+ declare const TvFooter: typeof _TvFooter & Plugin;
4
+ export { TvFooter };
5
+ export declare const TvFooterPlugin: Plugin;
6
+ export default TvFooter;
7
+ declare module 'vue' {
8
+ interface GlobalComponents {
9
+ TvFooter: typeof TvFooter;
10
+ }
11
+ }
Binary file
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function f(t){const u=e.computed(()=>t?.brand||null),r=e.computed(()=>!t?.navigation||!Array.isArray(t.navigation)?[]:t.navigation),_=e.computed(()=>!t?.social||!Array.isArray(t.social)?[]:t.social),n=e.computed(()=>!t?.legal||!Array.isArray(t.legal)?[]:t.legal),l=e.computed(()=>t?.version||""),a=e.computed(()=>t?.copyright||""),i=new Date().getFullYear(),d=e.computed(()=>a.value.replace("{year}",i));return{brand:u,navigation:r,social:_,legal:n,version:l,copyright:d}}const v={class:"tv-footer"},B={class:"tv-footer__container"},h={key:0,class:"tv-footer__brand"},y=["href"],g=["src","alt"],E={key:1},N={key:0,class:"tv-footer__version"},V={key:0,class:"tv-footer__section-title"},b={class:"tv-footer__links"},F=["href"],C={key:1,class:"tv-footer__section"},S={class:"tv-footer__social"},D=["href"],T=["src","alt"],A={key:2},x={class:"tv-footer__bottom"},L={key:0},P={key:1,class:"tv-footer__legal"},j={class:"tv-footer__links",style:{"flex-direction":"row",gap:"1.5rem"}},w=["href"],M={__name:"TvFooter",props:{config:{type:Object,default:()=>({})}},setup(t){const u=t,{brand:r,navigation:_,social:n,legal:l,version:a,copyright:i}=f(u.config);return(d,k)=>(e.openBlock(),e.createElementBlock("footer",v,[e.createElementVNode("div",B,[e.unref(r)?(e.openBlock(),e.createElementBlock("div",h,[e.createElementVNode("a",{href:e.unref(r).url||"/",class:"tv-footer__logo"},[e.unref(r).logo?(e.openBlock(),e.createElementBlock("img",{key:0,src:e.unref(r).logo,alt:e.unref(r).name},null,8,g)):e.createCommentVNode("",!0),e.unref(r).name?(e.openBlock(),e.createElementBlock("span",E,e.toDisplayString(e.unref(r).name),1)):e.createCommentVNode("",!0)],8,y),e.unref(a)?(e.openBlock(),e.createElementBlock("span",N,e.toDisplayString(e.unref(a)),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(_),(o,c)=>(e.openBlock(),e.createElementBlock("div",{key:c,class:"tv-footer__section"},[o.title?(e.openBlock(),e.createElementBlock("h3",V,e.toDisplayString(o.title),1)):e.createCommentVNode("",!0),e.createElementVNode("ul",b,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.items,(m,p)=>(e.openBlock(),e.createElementBlock("li",{key:p},[e.createElementVNode("a",{href:m.url,class:"tv-footer__link"},e.toDisplayString(m.label),9,F)]))),128))])]))),128)),e.unref(n)&&e.unref(n).length?(e.openBlock(),e.createElementBlock("div",C,[e.createElementVNode("div",S,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(n),(o,c)=>(e.openBlock(),e.createElementBlock("a",{key:c,href:o.url,class:"tv-footer__social-link",target:"_blank",rel:"noopener noreferer"},[o.iconUrl?(e.openBlock(),e.createElementBlock("img",{key:0,src:o.iconUrl,alt:o.label,class:"tv-footer__social-icon-img"},null,8,T)):o.icon?(e.openBlock(),e.createElementBlock("i",{key:1,class:e.normalizeClass(o.icon)},null,2)):(e.openBlock(),e.createElementBlock("span",A,e.toDisplayString(o.label),1))],8,D))),128))])])):e.createCommentVNode("",!0)]),e.createElementVNode("div",x,[e.unref(i)?(e.openBlock(),e.createElementBlock("div",L,e.toDisplayString(e.unref(i)),1)):e.createCommentVNode("",!0),e.unref(l)&&e.unref(l).length?(e.openBlock(),e.createElementBlock("div",P,[e.createElementVNode("ul",j,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(l),(o,c)=>(e.openBlock(),e.createElementBlock("li",{key:c},[e.createElementVNode("a",{href:o.url,class:"tv-footer__link"},e.toDisplayString(o.label),9,w)]))),128))])])):e.createCommentVNode("",!0)])]))}},s=M;s.install=t=>{t.component("TvFooter",s)};const O={install:s.install};exports.TvFooter=s;exports.TvFooterPlugin=O;exports.default=s;
@@ -0,0 +1 @@
1
+ @charset "UTF-8";.tv-footer{width:100%;padding:4rem 1.5rem;font-family:system-ui,-apple-system,sans-serif;box-sizing:border-box;transition:background-color .3s ease,color .3s ease;background-color:#0e131f;color:#cbd5e1;border-top:1px solid rgba(203,213,225,.1)}.tv-footer .tv-footer__logo,.tv-footer .tv-footer__section-title,.dark-mode .tv-footer .tv-footer__logo,.dark-mode .tv-footer .tv-footer__section-title{color:#fff}.tv-footer .tv-footer__social-link,.dark-mode .tv-footer .tv-footer__social-link{background-color:#ffffff0d;color:inherit}.tv-footer .tv-footer__social-link:hover,.dark-mode .tv-footer .tv-footer__social-link:hover{background-color:#ef233c;color:#fff}.light-mode .tv-footer{background-color:#b9c4df;color:#1e293b;border-top-color:#1e293b1a}.light-mode .tv-footer .tv-footer__logo,.light-mode .tv-footer .tv-footer__section-title{color:#1e293b}.light-mode .tv-footer .tv-footer__social-link{background-color:#0000000d;color:inherit}.light-mode .tv-footer .tv-footer__social-link:hover{background-color:#ef233c;color:#f1f9f9}.light-mode .tv-footer .tv-footer__link:hover{color:#d00f27}.tv-footer__container{display:grid;grid-template-columns:1fr;gap:2.5rem;max-width:1200px;margin:0 auto}@media(min-width:640px){.tv-footer__container{grid-template-columns:repeat(2,1fr)}}@media(min-width:1024px){.tv-footer__container{grid-template-columns:repeat(4,1fr)}}.tv-footer__brand{display:flex;flex-direction:column;gap:1rem}.tv-footer__logo{font-weight:700;font-size:1.5rem;text-decoration:none;display:inline-flex;align-items:center;gap:.5rem}.tv-footer__logo img{height:100px;width:auto}.tv-footer__version{font-size:.875rem;opacity:.7}.tv-footer__section-title{font-weight:600;margin-bottom:1.25rem;font-size:1rem}.tv-footer__links{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.75rem}.tv-footer__link{color:inherit;text-decoration:none;font-size:.95rem;transition:color .2s;cursor:pointer}.tv-footer__link:hover{color:#ef233c}.tv-footer__social{display:flex;gap:1rem;flex-wrap:wrap}.tv-footer__social-link{font-size:1.25rem;text-decoration:none;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border-radius:50%;transition:all .2s;transform:translateY(0)}.tv-footer__social-link:hover{transform:translateY(-2px)}.tv-footer__social-link img{width:1.25rem;height:1.25rem;object-fit:contain;display:block}.tv-footer__bottom{border-top-style:solid;border-top-width:1px;margin-top:4rem;padding-top:2rem;text-align:center;font-size:.875rem;display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:1200px;margin-left:auto;margin-right:auto}@media(min-width:640px){.tv-footer__bottom{flex-direction:row;justify-content:space-between}}
@@ -0,0 +1,6 @@
1
+ export * from './entry'
2
+ export {}
3
+ import TvFooter from './entry'
4
+ export default TvFooter
5
+ export * from './entry'
6
+ export {}
@@ -0,0 +1,119 @@
1
+ import { computed as c, createElementBlock as t, openBlock as e, createElementVNode as l, createCommentVNode as a, unref as o, toDisplayString as i, Fragment as y, renderList as f, normalizeClass as x } from "vue";
2
+ function T(r) {
3
+ const k = c(() => r?.brand || null), n = c(() => !r?.navigation || !Array.isArray(r.navigation) ? [] : r.navigation), p = c(() => !r?.social || !Array.isArray(r.social) ? [] : r.social), _ = c(() => !r?.legal || !Array.isArray(r.legal) ? [] : r.legal), u = c(() => r?.version || ""), v = c(() => r?.copyright || ""), h = (/* @__PURE__ */ new Date()).getFullYear(), m = c(() => v.value.replace("{year}", h));
4
+ return {
5
+ brand: k,
6
+ navigation: n,
7
+ social: p,
8
+ legal: _,
9
+ version: u,
10
+ copyright: m
11
+ };
12
+ }
13
+ const C = { class: "tv-footer" }, w = { class: "tv-footer__container" }, B = {
14
+ key: 0,
15
+ class: "tv-footer__brand"
16
+ }, D = ["href"], E = ["src", "alt"], N = { key: 1 }, U = {
17
+ key: 0,
18
+ class: "tv-footer__version"
19
+ }, V = {
20
+ key: 0,
21
+ class: "tv-footer__section-title"
22
+ }, Y = { class: "tv-footer__links" }, j = ["href"], z = {
23
+ key: 1,
24
+ class: "tv-footer__section"
25
+ }, L = { class: "tv-footer__social" }, O = ["href"], P = ["src", "alt"], S = { key: 2 }, q = { class: "tv-footer__bottom" }, G = { key: 0 }, H = {
26
+ key: 1,
27
+ class: "tv-footer__legal"
28
+ }, I = {
29
+ class: "tv-footer__links",
30
+ style: { "flex-direction": "row", gap: "1.5rem" }
31
+ }, J = ["href"], K = {
32
+ __name: "TvFooter",
33
+ props: {
34
+ config: {
35
+ type: Object,
36
+ default: () => ({})
37
+ }
38
+ },
39
+ setup(r) {
40
+ const k = r, { brand: n, navigation: p, social: _, legal: u, version: v, copyright: h } = T(k.config);
41
+ return (m, F) => (e(), t("footer", C, [
42
+ l("div", w, [
43
+ o(n) ? (e(), t("div", B, [
44
+ l("a", {
45
+ href: o(n).url || "/",
46
+ class: "tv-footer__logo"
47
+ }, [
48
+ o(n).logo ? (e(), t("img", {
49
+ key: 0,
50
+ src: o(n).logo,
51
+ alt: o(n).name
52
+ }, null, 8, E)) : a("", !0),
53
+ o(n).name ? (e(), t("span", N, i(o(n).name), 1)) : a("", !0)
54
+ ], 8, D),
55
+ o(v) ? (e(), t("span", U, i(o(v)), 1)) : a("", !0)
56
+ ])) : a("", !0),
57
+ (e(!0), t(y, null, f(o(p), (s, d) => (e(), t("div", {
58
+ key: d,
59
+ class: "tv-footer__section"
60
+ }, [
61
+ s.title ? (e(), t("h3", V, i(s.title), 1)) : a("", !0),
62
+ l("ul", Y, [
63
+ (e(!0), t(y, null, f(s.items, (b, A) => (e(), t("li", { key: A }, [
64
+ l("a", {
65
+ href: b.url,
66
+ class: "tv-footer__link"
67
+ }, i(b.label), 9, j)
68
+ ]))), 128))
69
+ ])
70
+ ]))), 128)),
71
+ o(_) && o(_).length ? (e(), t("div", z, [
72
+ l("div", L, [
73
+ (e(!0), t(y, null, f(o(_), (s, d) => (e(), t("a", {
74
+ key: d,
75
+ href: s.url,
76
+ class: "tv-footer__social-link",
77
+ target: "_blank",
78
+ rel: "noopener noreferer"
79
+ }, [
80
+ s.iconUrl ? (e(), t("img", {
81
+ key: 0,
82
+ src: s.iconUrl,
83
+ alt: s.label,
84
+ class: "tv-footer__social-icon-img"
85
+ }, null, 8, P)) : s.icon ? (e(), t("i", {
86
+ key: 1,
87
+ class: x(s.icon)
88
+ }, null, 2)) : (e(), t("span", S, i(s.label), 1))
89
+ ], 8, O))), 128))
90
+ ])
91
+ ])) : a("", !0)
92
+ ]),
93
+ l("div", q, [
94
+ o(h) ? (e(), t("div", G, i(o(h)), 1)) : a("", !0),
95
+ o(u) && o(u).length ? (e(), t("div", H, [
96
+ l("ul", I, [
97
+ (e(!0), t(y, null, f(o(u), (s, d) => (e(), t("li", { key: d }, [
98
+ l("a", {
99
+ href: s.url,
100
+ class: "tv-footer__link"
101
+ }, i(s.label), 9, J)
102
+ ]))), 128))
103
+ ])
104
+ ])) : a("", !0)
105
+ ])
106
+ ]));
107
+ }
108
+ }, g = K;
109
+ g.install = (r) => {
110
+ r.component("TvFooter", g);
111
+ };
112
+ const Q = {
113
+ install: g.install
114
+ };
115
+ export {
116
+ g as TvFooter,
117
+ Q as TvFooterPlugin,
118
+ g as default
119
+ };
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-footer',
7
+ configKey: 'tvFooter'
8
+ },
9
+ setup(_options, nuxt) {
10
+ const cssPath = '@todovue/tv-footer/style.css';
11
+ if (!nuxt.options.css.includes(cssPath)) {
12
+ nuxt.options.css.push(cssPath);
13
+ }
14
+ }
15
+ })
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@todovue/tv-footer",
3
+ "private": false,
4
+ "author": "Cristhian Daza",
5
+ "description": "A simple and customizable footer component for Vue.js applications, perfect for enhancing your web projects with ease.",
6
+ "license": "MIT",
7
+ "version": "1.0.0",
8
+ "type": "module",
9
+ "homepage": "https://ui.todovue.blog/footer",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/TODOvue/tv-footer.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/TODOvue/tv-footer/issues"
16
+ },
17
+ "keywords": [
18
+ "todovue",
19
+ "front-end",
20
+ "web",
21
+ "vue",
22
+ "vuejs",
23
+ "vue-js",
24
+ "footer",
25
+ "show-footer",
26
+ "vue-footer",
27
+ "vue-footer-component"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/tv-footer.es.js",
32
+ "require": "./dist/tv-footer.cjs.js"
33
+ },
34
+ "./style.css": "./dist/tv-footer.css",
35
+ "./nuxt": "./nuxt.js"
36
+ },
37
+ "main": "dist/tv-footer.cjs.js",
38
+ "module": "dist/tv-footer.es.js",
39
+ "types": "dist/tv-footer.d.ts",
40
+ "files": [
41
+ "dist",
42
+ "LICENSE",
43
+ "README.md",
44
+ "nuxt.js"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20.19.0"
48
+ },
49
+ "sideEffects": [
50
+ "*.css",
51
+ "*.scss",
52
+ "dist/*.css"
53
+ ],
54
+ "scripts": {
55
+ "dev": "vite",
56
+ "build": "vite build",
57
+ "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
58
+ },
59
+ "peerDependencies": {
60
+ "vue": "^3.5.26"
61
+ },
62
+ "devDependencies": {
63
+ "@todovue/tv-demo": "^1.2.7",
64
+ "@vitejs/plugin-vue": "^6.0.3",
65
+ "sass": "^1.97.1",
66
+ "vite": "^7.3.0",
67
+ "vite-plugin-dts": "^4.5.4"
68
+ }
69
+ }