@todovue/tv-footer 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@todovue/tv-footer` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.1] - 2026-01-27
9
+
10
+ ### Changed
11
+ - Simplified the file list in `package.json` to include only essential assets.
12
+ - Simplified the build configuration by removing demo-specific logic.
13
+ - Enhanced GitHub Actions workflows to automate npm package publishing and GitHub release creation.
14
+ - Moved the `@todovue/tv-demo` component import from main.js to `Demo.vue` to localize its usage.
15
+ - Updated build commands to include `README.md` and `CHANGELOG.md` files in the public directory during the build process.
16
+
17
+ ### Added
18
+ - Included the `src` directory in the `package.json` files list to ensure component source files are bundled in the package distribution.
19
+
20
+ ### Removed
21
+ - Eliminated the global import of the `@todovue/tv-demo` component from `main.js`.
22
+
23
+ ### Dependencies
24
+ - Updated `@todovue/tv-demo` to `^1.4.11`.
25
+ - Updated `vue` to `^3.5.27`.
26
+ - Updated `sass` to `^1.97.3`.
27
+
28
+ ## [1.1.0] - 2026-01-21
29
+
30
+ ### Added
31
+ - Added `newsletter` properties to `useFooter` composable return.
32
+ - Added new styles for newsletter form and back-to-top button.
33
+
34
+ ### Dependencies
35
+ - Updated `@todovue/tv-demo` to `^1.4.4`.
36
+ - Updated `sass` to `^1.97.2`.
37
+ - Updated `vite` to `^7.3.1`.
38
+
39
+ ### Changed
40
+ - Enhanced footer styles to improve visual consistency across the application.
41
+ - Improved responsiveness of the footer component for better mobile and tablet compatibility.
42
+
43
+ ## [1.0.0] - 2026-01-07
44
+
45
+ ### Added
46
+ - Initial release of TvFooter component
47
+ - Responsive grid layout (1 column mobile, 2 tablet, 4 desktop)
48
+ - Brand section with logo and name display support
49
+ - Version display capability
50
+ - Multiple navigation sections with titles and links
51
+ - Social media links with icon support (iconUrl and icon class)
52
+ - Legal links section (Privacy, Terms, etc.)
53
+ - Dynamic copyright with automatic year replacement (`{year}` placeholder)
54
+ - Light/Dark mode support built-in
55
+ - SSR-safe implementation (Nuxt 3 compatible)
56
+ - `useFooter` composable for custom implementations
57
+ - TypeScript support with type definitions
58
+ - Semantic HTML structure for accessibility
59
+ - ARIA labels and proper link attributes
60
+ - External link safety (`rel="noopener noreferrer"`)
61
+ - Smooth hover effects and transitions
62
+ - Backdrop blur effect on social icons
63
+ - Comprehensive documentation and examples
64
+
65
+ [1.1.1]: https://github.com/TODOvue/tv-footer/pull/3/files
66
+ [1.1.0]: https://github.com/TODOvue/tv-footer/pull/2/files
67
+ [1.0.0]: https://github.com/TODOvue/tv-footer/pull/1/files
package/README.md CHANGED
@@ -16,12 +16,11 @@ A simple, customizable, and responsive Vue 3 footer component with support for b
16
16
 
17
17
  > Demo: https://ui.todovue.blog/footer
18
18
 
19
- ---
20
19
  ## Table of Contents
21
20
  - [Features](#features)
22
21
  - [Installation](#installation)
23
22
  - [Quick Start (SPA)](#quick-start-spa)
24
- - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
23
+ - [Nuxt 4 / SSR Usage](#nuxt-4--ssr-usage)
25
24
  - [Component Registration Options](#component-registration-options)
26
25
  - [Props](#props)
27
26
  - [Configuration Object](#configuration-object)
@@ -34,7 +33,6 @@ A simple, customizable, and responsive Vue 3 footer component with support for b
34
33
  - [Contributing](#contributing)
35
34
  - [License](#license)
36
35
 
37
- ---
38
36
  ## Features
39
37
  - Fully responsive grid layout (1 column on mobile, 2 on tablet, 4 on desktop)
40
38
  - Customizable brand section with logo and version display
@@ -49,7 +47,6 @@ A simple, customizable, and responsive Vue 3 footer component with support for b
49
47
  - Lightweight and tree-shakeable
50
48
  - TypeScript support
51
49
 
52
- ---
53
50
  ## Installation
54
51
  Using npm:
55
52
  ```bash
@@ -93,7 +90,6 @@ export default defineNuxtConfig({
93
90
 
94
91
  Then register the component in a plugin as shown in the [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage) section.
95
92
 
96
- ---
97
93
  ## Quick Start (SPA)
98
94
  Global registration (main.js / main.ts):
99
95
  ```js
@@ -142,8 +138,7 @@ const footerConfig = {
142
138
  ```
143
139
  **Note:** Don't forget to import the CSS in your main entry file as shown above.
144
140
 
145
- ---
146
- ## Nuxt 3 / SSR Usage
141
+ ## Nuxt 4 / SSR Usage
147
142
  First, add the module to your `nuxt.config.ts`:
148
143
  ```ts
149
144
  // nuxt.config.ts
@@ -205,7 +200,6 @@ import { TvFooter } from '@todovue/tv-footer'
205
200
  </script>
206
201
  ```
207
202
 
208
- ---
209
203
  ## Component Registration Options
210
204
  | Approach | When to use |
211
205
  |--------------------------------------------------------------------------|------------------------------------------------|
@@ -213,7 +207,6 @@ import { TvFooter } from '@todovue/tv-footer'
213
207
  | Local named import `{ TvFooter }` | Isolated / code-split contexts |
214
208
  | Direct default import `import TvFooter from '@todovue/tv-footer'` | Single usage or manual registration |
215
209
 
216
- ---
217
210
  ## Props
218
211
  | Prop | Type | Default | Description |
219
212
  |--------|--------|---------|----------------------------------------------------------------|
@@ -229,7 +222,32 @@ Example:
229
222
  <TvFooter :config="myFooterConfig" />
230
223
  ```
231
224
 
232
- ---
225
+ ## Events
226
+
227
+ | Event Name | Payload | Description |
228
+ |:------------|:--------|:--------------------------------------------------------------------------------|
229
+ | `subscribe` | `email` | Emitted when the user submits the newsletter form. Payload is the email string. |
230
+
231
+ ## Slots
232
+ TvFooter provides slots to allow custom content injection for specific sections.
233
+
234
+ | Slot Name | Props (Scope) | Description |
235
+ |:-------------|:-------------------------------|:-------------------------------------------------|
236
+ | `brand` | `{ brand, version }` | Replace the brand/logo section |
237
+ | `newsletter` | `{ newsletter }` | Replace the newsletter subscription section |
238
+ | `bottom` | `{ copyright, legal }` | Replace the bottom section (copyright + legal) |
239
+
240
+ Example Usage:
241
+ ```vue
242
+ <TvFooter :config="config">
243
+ <template #brand="{ brand }">
244
+ <div class="custom-brand">
245
+ <h1>{{ brand.name }}</h1>
246
+ </div>
247
+ </template>
248
+ </TvFooter>
249
+ ```
250
+
233
251
  ## Configuration Object
234
252
  The `config` prop accepts an object with the following structure:
235
253
 
@@ -301,6 +319,25 @@ The `config` prop accepts an object with the following structure:
301
319
  }
302
320
  ```
303
321
 
322
+ ### Newsletter Section
323
+ ```ts
324
+ {
325
+ newsletter: {
326
+ title: string, // Newsletter Title (optional)
327
+ description: string, // Newsletter Description (optional)
328
+ placeholder: string, // Input placeholder (optional)
329
+ buttonText: string // Button text (optional)
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### Back To Top
335
+ ```ts
336
+ {
337
+ backToTop: boolean // Show back to top button (default: false)
338
+ }
339
+ ```
340
+
304
341
  ### Complete Configuration Example
305
342
  ```js
306
343
  const footerConfig = {
@@ -357,7 +394,6 @@ const footerConfig = {
357
394
  }
358
395
  ```
359
396
 
360
- ---
361
397
  ## Composable API
362
398
  TvFooter includes a composable `useFooter` that you can use to process footer configuration and build custom footer implementations.
363
399
 
@@ -408,7 +444,6 @@ const { brand, copyright } = useFooter(config)
408
444
  - Reactive computed properties
409
445
  - Type-safe array validation
410
446
 
411
- ---
412
447
  ## Usage Examples
413
448
 
414
449
  ### Minimal Footer
@@ -626,7 +661,6 @@ const { brand, navigation, copyright } = useFooter(config)
626
661
  </style>
627
662
  ```
628
663
 
629
- ---
630
664
  ## Styling
631
665
  TvFooter comes with built-in responsive styles and light/dark mode support.
632
666
 
@@ -690,7 +724,6 @@ You can override the default styles by targeting the CSS classes:
690
724
  - `.tv-footer__bottom` - Bottom section with copyright and legal
691
725
  - `.tv-footer__legal` - Legal links container
692
726
 
693
- ---
694
727
  ## Accessibility
695
728
  - **Semantic HTML**: Uses proper `<footer>`, `<nav>`, `<ul>`, and `<a>` elements
696
729
  - **ARIA Labels**: Social links include accessible labels
@@ -700,7 +733,6 @@ You can override the default styles by targeting the CSS classes:
700
733
  - **Screen Reader Friendly**: Proper heading hierarchy and semantic structure
701
734
  - **Alternative Text**: Images include alt attributes
702
735
 
703
- ---
704
736
  ## SSR Notes
705
737
  - **SSR-Safe**: No direct `window`/`document` access during module evaluation
706
738
  - **Nuxt 3 Compatible**: Works seamlessly with Nuxt 3 out of the box
@@ -708,7 +740,6 @@ You can override the default styles by targeting the CSS classes:
708
740
  - **Universal Rendering**: Works in both client and server contexts
709
741
  - **Vue 3 Composition API**: Uses modern Vue 3 composables
710
742
 
711
- ---
712
743
  ## Development
713
744
  ```bash
714
745
  git clone https://github.com/TODOvue/tv-footer.git
@@ -719,14 +750,11 @@ npm run build # build library
719
750
  ```
720
751
  Local demo served from Vite using `index.html` and demo examples in `src/demo`.
721
752
 
722
- ---
723
753
  ## Contributing
724
754
  PRs and issues welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
725
755
 
726
- ---
727
756
  ## License
728
757
  MIT © TODOvue
729
758
 
730
- ---
731
759
  ### Attributions
732
760
  Crafted for the TODOvue component ecosystem
@@ -1 +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;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");function E(t){const d=e.computed(()=>t?.brand||null),m=e.computed(()=>!t?.navigation||!Array.isArray(t.navigation)?[]:t.navigation),r=e.computed(()=>!t?.social||!Array.isArray(t.social)?[]:t.social),f=e.computed(()=>!t?.legal||!Array.isArray(t.legal)?[]:t.legal),c=e.computed(()=>t?.version||""),l=e.computed(()=>t?.copyright||""),a=e.computed(()=>t?.newsletter||null),i=new Date().getFullYear(),n=e.computed(()=>l.value.replace("{year}",i));return{brand:d,navigation:m,social:r,legal:f,version:c,copyright:n,newsletter:a}}const g={class:"tv-footer"},b={class:"tv-footer__container"},V={class:"tv-footer__left-column"},N={key:0,class:"tv-footer__brand"},S=["href"],w=["src","alt"],C={key:1},D={key:0,class:"tv-footer__version"},F={key:0,class:"tv-footer__newsletter"},T={key:0,class:"tv-footer__newsletter-title"},A={key:1,class:"tv-footer__newsletter-text"},x=["placeholder"],L={type:"submit",class:"tv-footer__newsletter-button"},M={class:"tv-footer__nav-definitions"},P={key:0,class:"tv-footer__section-title"},U={class:"tv-footer__links"},$=["href"],j={key:0,class:"tv-footer__social-section"},q={class:"tv-footer__social"},O=["href"],Y=["src","alt"],z={key:2},G={class:"tv-footer__bottom"},H={key:0},I={key:1,class:"tv-footer__legal"},J={class:"tv-footer__links",style:{"flex-direction":"row",gap:"1.5rem"}},K=["href"],Q={__name:"TvFooter",props:{config:{type:Object,default:()=>({})}},emits:["subscribe"],setup(t,{emit:d}){const m=t,{brand:r,navigation:f,social:c,legal:l,version:a,copyright:i,newsletter:n}=E(m.config),s=e.ref(""),h=d,y=()=>{s.value&&(h("subscribe",s.value),s.value="")};return(p,k)=>(e.openBlock(),e.createElementBlock("footer",g,[e.createElementVNode("div",b,[e.createElementVNode("div",V,[e.renderSlot(p.$slots,"brand",{brand:e.unref(r),version:e.unref(a)},()=>[e.unref(r)?(e.openBlock(),e.createElementBlock("div",N,[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,w)):e.createCommentVNode("",!0),e.unref(r).name?(e.openBlock(),e.createElementBlock("span",C,e.toDisplayString(e.unref(r).name),1)):e.createCommentVNode("",!0)],8,S),e.unref(a)?(e.openBlock(),e.createElementBlock("span",D,e.toDisplayString(e.unref(a)),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0)]),e.renderSlot(p.$slots,"newsletter",{newsletter:e.unref(n)},()=>[e.unref(n)?(e.openBlock(),e.createElementBlock("div",F,[e.unref(n).title?(e.openBlock(),e.createElementBlock("h3",T,e.toDisplayString(e.unref(n).title),1)):e.createCommentVNode("",!0),e.unref(n).description?(e.openBlock(),e.createElementBlock("p",A,e.toDisplayString(e.unref(n).description),1)):e.createCommentVNode("",!0),e.createElementVNode("form",{class:"tv-footer__newsletter-form",onSubmit:e.withModifiers(y,["prevent"])},[e.withDirectives(e.createElementVNode("input",{"onUpdate:modelValue":k[0]||(k[0]=o=>s.value=o),type:"email",placeholder:e.unref(n).placeholder||"Enter your email",class:"tv-footer__newsletter-input",required:""},null,8,x),[[e.vModelText,s.value]]),e.createElementVNode("button",L,e.toDisplayString(e.unref(n).buttonText||"Subscribe"),1)],32)])):e.createCommentVNode("",!0)])]),e.createElementVNode("div",M,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(f),(o,u)=>(e.openBlock(),e.createElementBlock("div",{key:u,class:"tv-footer__section"},[o.title?(e.openBlock(),e.createElementBlock("h3",P,e.toDisplayString(o.title),1)):e.createCommentVNode("",!0),e.createElementVNode("ul",U,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.items,(v,B)=>(e.openBlock(),e.createElementBlock("li",{key:B},[e.createElementVNode("a",{href:v.url,class:"tv-footer__link"},e.toDisplayString(v.label),9,$)]))),128))])]))),128))]),e.unref(c)&&e.unref(c).length?(e.openBlock(),e.createElementBlock("div",j,[e.createElementVNode("div",q,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(c),(o,u)=>(e.openBlock(),e.createElementBlock("a",{key:u,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,Y)):o.icon?(e.openBlock(),e.createElementBlock("i",{key:1,class:e.normalizeClass(o.icon)},null,2)):(e.openBlock(),e.createElementBlock("span",z,e.toDisplayString(o.label),1))],8,O))),128))])])):e.createCommentVNode("",!0)]),e.renderSlot(p.$slots,"bottom",{copyright:e.unref(i),legal:e.unref(l)},()=>[e.createElementVNode("div",G,[e.unref(i)?(e.openBlock(),e.createElementBlock("div",H,e.toDisplayString(e.unref(i)),1)):e.createCommentVNode("",!0),e.unref(l)&&e.unref(l).length?(e.openBlock(),e.createElementBlock("div",I,[e.createElementVNode("ul",J,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(l),(o,u)=>(e.openBlock(),e.createElementBlock("li",{key:u},[e.createElementVNode("a",{href:o.url,class:"tv-footer__link"},e.toDisplayString(o.label),9,K)]))),128))])])):e.createCommentVNode("",!0)])])]))}},_=Q;_.install=t=>{t.component("TvFooter",_)};const R={install:_.install};exports.TvFooter=_;exports.TvFooterPlugin=R;exports.default=_;
@@ -1 +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}}
1
+ @charset "UTF-8";.tv-footer{width:100%;padding:3rem 1.5rem;box-sizing:border-box;transition:background-color .4s ease,color .4s ease;position:relative;background-color:#0e131f;color:#cbd5e1;border-top:1px solid rgba(203,213,225,.05)}.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:#ffffff08;color:inherit;border:1px solid rgba(255,255,255,.05)}.tv-footer .tv-footer__social-link:hover,.dark-mode .tv-footer .tv-footer__social-link:hover{background-color:#ef233c;border-color:#ef233c;color:#fff;box-shadow:0 0 15px #ef233c66}.light-mode .tv-footer{background-color:#b9c4df;color:#1e293b;border-top-color:#1e293b0d}.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:#ffffff80;color:inherit;border:1px solid rgba(0,0,0,.05);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.light-mode .tv-footer .tv-footer__social-link:hover{background-color:#ef233c;border-color:#ef233c;color:#f1f9f9;box-shadow:0 4px 12px #ef233c4d}.light-mode .tv-footer .tv-footer__link:hover{color:#ef233c}.light-mode .tv-footer .tv-footer__newsletter-input{background-color:#fff9;border-color:#0000000d;color:#1e293b;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.light-mode .tv-footer .tv-footer__newsletter-input:focus{background-color:#ffffffe6;border-color:#ef233c;box-shadow:0 0 0 4px #ef233c1a}.tv-footer__container{display:flex;flex-wrap:wrap;gap:4rem;max-width:1200px;margin:0 auto}.tv-footer__left-column{flex:1 1 250px;max-width:400px;display:flex;flex-direction:column;gap:2rem}.tv-footer__brand{display:flex;flex-direction:column;gap:1rem}.tv-footer__logo{font-weight:800;font-size:1.5rem;text-decoration:none;display:inline-flex;align-items:center;gap:.75rem;letter-spacing:-.02em}.tv-footer__logo img{height:100px;width:auto}.tv-footer__version{font-size:.75rem;opacity:.6;font-weight:500;background:#7f7f7f1a;padding:.125rem .5rem;border-radius:12px;align-self:flex-start}.tv-footer__nav-definitions{display:flex;flex:999 1 400px;flex-wrap:wrap;gap:3rem}@media(max-width:768px){.tv-footer__nav-definitions{gap:2rem;flex-basis:100%}}.tv-footer__section{min-width:140px}.tv-footer__section-title{font-weight:700;margin-bottom:1.25rem;font-size:.9rem;letter-spacing:.05em;text-transform:uppercase;opacity:1;position:relative}.tv-footer__section-title:after{content:"";display:block;width:20px;height:2px;background-color:#ef233c;margin-top:.5rem;border-radius:2px}.tv-footer__links{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.875rem}.tv-footer__link{color:inherit;text-decoration:none;font-size:.95rem;transition:all .2s ease;cursor:pointer;display:flex;align-items:center;gap:.5rem;opacity:.75;width:fit-content}.tv-footer__link:hover{color:#ef233c;opacity:1;transform:translate(4px)}.tv-footer__link:before{content:"";display:block;width:4px;height:4px;background-color:currentColor;border-radius:50%;opacity:0;transition:opacity .2s}.tv-footer__link:hover:before{opacity:1}.tv-footer__social-section{display:flex;flex-direction:column;justify-content:flex-start;padding-top:.5rem;flex:0 0 auto}.tv-footer__social{display:flex;gap:.75rem;flex-wrap:wrap}.tv-footer__social-link{font-size:1.1rem;text-decoration:none;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border-radius:50%;transition:all .3s cubic-bezier(.175,.885,.32,1.275)}.tv-footer__social-link:hover{transform:translateY(-4px) scale(1.1)}.tv-footer__social-link img{width:1.25rem;height:1.25rem;object-fit:contain;display:block}.tv-footer__bottom{border-top:1px solid rgba(127,127,127,.1);margin-top:3rem;padding-top:1.5rem;text-align:center;font-size:.85rem;display:flex;flex-direction:column;align-items:center;gap:1rem;max-width:1200px;margin-left:auto;margin-right:auto;opacity:.7}@media(min-width:640px){.tv-footer__bottom{flex-direction:row;justify-content:space-between}}.tv-footer__bottom .tv-footer__links{flex-direction:row;gap:1.5rem;flex-wrap:wrap;justify-content:center}.tv-footer__bottom .tv-footer__links li{display:inline-block}.tv-footer__bottom .tv-footer__links .tv-footer__link{font-size:.8rem}.tv-footer__bottom .tv-footer__links .tv-footer__link:hover{transform:none;text-decoration:underline}.tv-footer__bottom .tv-footer__links .tv-footer__link:before{display:none}.tv-footer__newsletter{display:flex;flex-direction:column;gap:1rem;margin-top:1rem;width:100%}.tv-footer__newsletter-title{font-weight:700;font-size:1rem;margin:0}.tv-footer__newsletter-text{font-size:.9rem;line-height:1.5;opacity:.8;margin:0}.tv-footer__newsletter-form{display:flex;gap:.5rem;flex-direction:column}@media(min-width:400px){.tv-footer__newsletter-form{flex-direction:row}}.tv-footer__newsletter-input{flex:1;min-width:0;padding:.75rem 1.25rem;border-radius:9999px;border:1px solid transparent;background-color:#ffffff0d;color:inherit;font-size:.9rem;outline:none;transition:all .3s ease}.tv-footer__newsletter-input:focus{background-color:#ffffff1a;border-color:#ef233c;box-shadow:0 0 0 4px #ef233c33}.tv-footer__newsletter-input::placeholder{opacity:.5}.tv-footer__newsletter-button{padding:.75rem 1.5rem;border-radius:9999px;border:none;background-color:#ef233c;color:#fff;font-weight:600;font-size:.9rem;cursor:pointer;transition:all .3s cubic-bezier(.175,.885,.32,1.275);display:inline-flex;align-items:center;justify-content:center;white-space:nowrap}.tv-footer__newsletter-button:hover{background-color:#e8112b;transform:translateY(-2px);box-shadow:0 4px 12px #ef233c66}.tv-footer__newsletter-button:active{transform:translateY(0)}.tv-footer__back-to-top{position:fixed;bottom:2.5rem;right:2.5rem;width:3.5rem;height:3.5rem;border-radius:50%;background-color:#ef233c;color:#fff;border:none;font-size:1.5rem;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 8px 16px #0003;transition:all .3s cubic-bezier(.175,.885,.32,1.275);z-index:1000}.tv-footer__back-to-top:hover{transform:translateY(-4px) scale(1.05);background-color:#e8112b;box-shadow:0 12px 20px #ef233c4d}.tv-footer__back-to-top:active{transform:translateY(-1px)}
@@ -1,34 +1,47 @@
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));
1
+ import { computed as _, ref as E, createElementBlock as e, openBlock as o, createElementVNode as l, renderSlot as F, createCommentVNode as n, unref as t, toDisplayString as c, withModifiers as U, withDirectives as V, vModelText as $, Fragment as p, renderList as m, normalizeClass as B } from "vue";
2
+ function M(s) {
3
+ const b = _(() => s?.brand || null), k = _(() => !s?.navigation || !Array.isArray(s.navigation) ? [] : s.navigation), i = _(() => !s?.social || !Array.isArray(s.social) ? [] : s.social), g = _(() => !s?.legal || !Array.isArray(s.legal) ? [] : s.legal), v = _(() => s?.version || ""), u = _(() => s?.copyright || ""), h = _(() => s?.newsletter || null), f = (/* @__PURE__ */ new Date()).getFullYear(), a = _(() => u.value.replace("{year}", f));
4
4
  return {
5
- brand: k,
6
- navigation: n,
7
- social: p,
8
- legal: _,
9
- version: u,
10
- copyright: m
5
+ brand: b,
6
+ navigation: k,
7
+ social: i,
8
+ legal: g,
9
+ version: v,
10
+ copyright: a,
11
+ newsletter: h
11
12
  };
12
13
  }
13
- const C = { class: "tv-footer" }, w = { class: "tv-footer__container" }, B = {
14
+ const N = { class: "tv-footer" }, Y = { class: "tv-footer__container" }, j = { class: "tv-footer__left-column" }, q = {
14
15
  key: 0,
15
16
  class: "tv-footer__brand"
16
- }, D = ["href"], E = ["src", "alt"], N = { key: 1 }, U = {
17
+ }, z = ["href"], L = ["src", "alt"], O = { key: 1 }, P = {
17
18
  key: 0,
18
19
  class: "tv-footer__version"
19
- }, V = {
20
+ }, G = {
20
21
  key: 0,
21
- class: "tv-footer__section-title"
22
- }, Y = { class: "tv-footer__links" }, j = ["href"], z = {
22
+ class: "tv-footer__newsletter"
23
+ }, H = {
24
+ key: 0,
25
+ class: "tv-footer__newsletter-title"
26
+ }, I = {
23
27
  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 = {
28
+ class: "tv-footer__newsletter-text"
29
+ }, J = ["placeholder"], K = {
30
+ type: "submit",
31
+ class: "tv-footer__newsletter-button"
32
+ }, Q = { class: "tv-footer__nav-definitions" }, R = {
33
+ key: 0,
34
+ class: "tv-footer__section-title"
35
+ }, W = { class: "tv-footer__links" }, X = ["href"], Z = {
36
+ key: 0,
37
+ class: "tv-footer__social-section"
38
+ }, tt = { class: "tv-footer__social" }, et = ["href"], ot = ["src", "alt"], st = { key: 2 }, rt = { class: "tv-footer__bottom" }, lt = { key: 0 }, nt = {
26
39
  key: 1,
27
40
  class: "tv-footer__legal"
28
- }, I = {
41
+ }, it = {
29
42
  class: "tv-footer__links",
30
43
  style: { "flex-direction": "row", gap: "1.5rem" }
31
- }, J = ["href"], K = {
44
+ }, at = ["href"], ct = {
32
45
  __name: "TvFooter",
33
46
  props: {
34
47
  config: {
@@ -36,84 +49,122 @@ const C = { class: "tv-footer" }, w = { class: "tv-footer__container" }, B = {
36
49
  default: () => ({})
37
50
  }
38
51
  },
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 }, [
52
+ emits: ["subscribe"],
53
+ setup(s, { emit: b }) {
54
+ const k = s, { brand: i, navigation: g, social: v, legal: u, version: h, copyright: f, newsletter: a } = M(k.config), d = E(""), S = b, C = () => {
55
+ d.value && (S("subscribe", d.value), d.value = "");
56
+ };
57
+ return (w, T) => (o(), e("footer", N, [
58
+ l("div", Y, [
59
+ l("div", j, [
60
+ F(w.$slots, "brand", {
61
+ brand: t(i),
62
+ version: t(h)
63
+ }, () => [
64
+ t(i) ? (o(), e("div", q, [
64
65
  l("a", {
65
- href: b.url,
66
- class: "tv-footer__link"
67
- }, i(b.label), 9, j)
68
- ]))), 128))
66
+ href: t(i).url || "/",
67
+ class: "tv-footer__logo"
68
+ }, [
69
+ t(i).logo ? (o(), e("img", {
70
+ key: 0,
71
+ src: t(i).logo,
72
+ alt: t(i).name
73
+ }, null, 8, L)) : n("", !0),
74
+ t(i).name ? (o(), e("span", O, c(t(i).name), 1)) : n("", !0)
75
+ ], 8, z),
76
+ t(h) ? (o(), e("span", P, c(t(h)), 1)) : n("", !0)
77
+ ])) : n("", !0)
78
+ ]),
79
+ F(w.$slots, "newsletter", { newsletter: t(a) }, () => [
80
+ t(a) ? (o(), e("div", G, [
81
+ t(a).title ? (o(), e("h3", H, c(t(a).title), 1)) : n("", !0),
82
+ t(a).description ? (o(), e("p", I, c(t(a).description), 1)) : n("", !0),
83
+ l("form", {
84
+ class: "tv-footer__newsletter-form",
85
+ onSubmit: U(C, ["prevent"])
86
+ }, [
87
+ V(l("input", {
88
+ "onUpdate:modelValue": T[0] || (T[0] = (r) => d.value = r),
89
+ type: "email",
90
+ placeholder: t(a).placeholder || "Enter your email",
91
+ class: "tv-footer__newsletter-input",
92
+ required: ""
93
+ }, null, 8, J), [
94
+ [$, d.value]
95
+ ]),
96
+ l("button", K, c(t(a).buttonText || "Subscribe"), 1)
97
+ ], 32)
98
+ ])) : n("", !0)
69
99
  ])
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,
100
+ ]),
101
+ l("div", Q, [
102
+ (o(!0), e(p, null, m(t(g), (r, y) => (o(), e("div", {
103
+ key: y,
104
+ class: "tv-footer__section"
105
+ }, [
106
+ r.title ? (o(), e("h3", R, c(r.title), 1)) : n("", !0),
107
+ l("ul", W, [
108
+ (o(!0), e(p, null, m(r.items, (x, D) => (o(), e("li", { key: D }, [
109
+ l("a", {
110
+ href: x.url,
111
+ class: "tv-footer__link"
112
+ }, c(x.label), 9, X)
113
+ ]))), 128))
114
+ ])
115
+ ]))), 128))
116
+ ]),
117
+ t(v) && t(v).length ? (o(), e("div", Z, [
118
+ l("div", tt, [
119
+ (o(!0), e(p, null, m(t(v), (r, y) => (o(), e("a", {
120
+ key: y,
121
+ href: r.url,
76
122
  class: "tv-footer__social-link",
77
123
  target: "_blank",
78
124
  rel: "noopener noreferer"
79
125
  }, [
80
- s.iconUrl ? (e(), t("img", {
126
+ r.iconUrl ? (o(), e("img", {
81
127
  key: 0,
82
- src: s.iconUrl,
83
- alt: s.label,
128
+ src: r.iconUrl,
129
+ alt: r.label,
84
130
  class: "tv-footer__social-icon-img"
85
- }, null, 8, P)) : s.icon ? (e(), t("i", {
131
+ }, null, 8, ot)) : r.icon ? (o(), e("i", {
86
132
  key: 1,
87
- class: x(s.icon)
88
- }, null, 2)) : (e(), t("span", S, i(s.label), 1))
89
- ], 8, O))), 128))
133
+ class: B(r.icon)
134
+ }, null, 2)) : (o(), e("span", st, c(r.label), 1))
135
+ ], 8, et))), 128))
90
136
  ])
91
- ])) : a("", !0)
137
+ ])) : n("", !0)
92
138
  ]),
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)
139
+ F(w.$slots, "bottom", {
140
+ copyright: t(f),
141
+ legal: t(u)
142
+ }, () => [
143
+ l("div", rt, [
144
+ t(f) ? (o(), e("div", lt, c(t(f)), 1)) : n("", !0),
145
+ t(u) && t(u).length ? (o(), e("div", nt, [
146
+ l("ul", it, [
147
+ (o(!0), e(p, null, m(t(u), (r, y) => (o(), e("li", { key: y }, [
148
+ l("a", {
149
+ href: r.url,
150
+ class: "tv-footer__link"
151
+ }, c(r.label), 9, at)
152
+ ]))), 128))
153
+ ])
154
+ ])) : n("", !0)
155
+ ])
105
156
  ])
106
157
  ]));
107
158
  }
108
- }, g = K;
109
- g.install = (r) => {
110
- r.component("TvFooter", g);
159
+ }, A = ct;
160
+ A.install = (s) => {
161
+ s.component("TvFooter", A);
111
162
  };
112
- const Q = {
113
- install: g.install
163
+ const ut = {
164
+ install: A.install
114
165
  };
115
166
  export {
116
- g as TvFooter,
117
- Q as TvFooterPlugin,
118
- g as default
167
+ A as TvFooter,
168
+ ut as TvFooterPlugin,
169
+ A as default
119
170
  };
package/package.json CHANGED
@@ -4,10 +4,11 @@
4
4
  "author": "Cristhian Daza",
5
5
  "description": "A simple and customizable footer component for Vue.js applications, perfect for enhancing your web projects with ease.",
6
6
  "license": "MIT",
7
- "version": "1.0.0",
7
+ "version": "1.1.1",
8
8
  "type": "module",
9
9
  "homepage": "https://ui.todovue.blog/footer",
10
10
  "repository": {
11
+ "name": "@todovue/tv-footer",
11
12
  "type": "git",
12
13
  "url": "git+https://github.com/TODOvue/tv-footer.git"
13
14
  },
@@ -32,16 +33,21 @@
32
33
  "require": "./dist/tv-footer.cjs.js"
33
34
  },
34
35
  "./style.css": "./dist/tv-footer.css",
35
- "./nuxt": "./nuxt.js"
36
+ "./nuxt": "./nuxt.js",
37
+ "./demo": {
38
+ "import": "./src/demo/Demo.vue"
39
+ }
36
40
  },
37
41
  "main": "dist/tv-footer.cjs.js",
38
42
  "module": "dist/tv-footer.es.js",
39
43
  "types": "dist/tv-footer.d.ts",
40
44
  "files": [
41
- "dist",
45
+ "CHANGELOG.md",
42
46
  "LICENSE",
43
47
  "README.md",
44
- "nuxt.js"
48
+ "dist",
49
+ "nuxt.js",
50
+ "src"
45
51
  ],
46
52
  "engines": {
47
53
  "node": ">=20.19.0"
@@ -53,17 +59,16 @@
53
59
  ],
54
60
  "scripts": {
55
61
  "dev": "vite",
56
- "build": "vite build",
57
- "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
62
+ "build": "vite build"
58
63
  },
59
64
  "peerDependencies": {
60
- "vue": "^3.5.26"
65
+ "vue": "^3.5.27"
61
66
  },
62
67
  "devDependencies": {
63
- "@todovue/tv-demo": "^1.2.7",
68
+ "@todovue/tv-demo": "^1.4.11",
64
69
  "@vitejs/plugin-vue": "^6.0.3",
65
- "sass": "^1.97.1",
66
- "vite": "^7.3.0",
70
+ "sass": "^1.97.3",
71
+ "vite": "^7.3.1",
67
72
  "vite-plugin-dts": "^4.5.4"
68
73
  }
69
74
  }
@@ -0,0 +1,12 @@
1
+ /* 🌙 Dark Theme */
2
+ $dark-body-bg: #161E31;
3
+ $dark-card-bg: #0E131F;
4
+ $dark-text: #CBD5E1;
5
+
6
+ /* ☀️ Light Theme */
7
+ $light-body-bg: #f8FAFC;
8
+ $light-card-bg: #B9C4DF;
9
+ $light-button-text: #F1F9F9;
10
+ $light-text: #1E293B;
11
+
12
+ $primary-color: #ef233c;