@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.
@@ -0,0 +1,396 @@
1
+ @use "variables" as *;
2
+ @use "sass:color";
3
+
4
+ .tv-footer {
5
+ width: 100%;
6
+ padding: 3rem 1.5rem;
7
+ box-sizing: border-box;
8
+ transition: background-color 0.4s ease, color 0.4s ease;
9
+ position: relative;
10
+
11
+ background-color: $dark-card-bg;
12
+ color: $dark-text;
13
+ border-top: 1px solid rgba($dark-text, 0.05);
14
+
15
+ &,
16
+ .dark-mode & {
17
+
18
+ .tv-footer__logo,
19
+ .tv-footer__section-title {
20
+ color: #fff;
21
+ }
22
+
23
+ .tv-footer__social-link {
24
+ background-color: rgba(255, 255, 255, 0.03);
25
+ color: inherit;
26
+ border: 1px solid rgba(255, 255, 255, 0.05);
27
+
28
+ &:hover {
29
+ background-color: $primary-color;
30
+ border-color: $primary-color;
31
+ color: white;
32
+ box-shadow: 0 0 15px rgba($primary-color, 0.4);
33
+ }
34
+ }
35
+ }
36
+
37
+ .light-mode & {
38
+ background-color: $light-card-bg;
39
+ color: $light-text;
40
+ border-top-color: rgba($light-text, 0.05);
41
+
42
+ .tv-footer__logo,
43
+ .tv-footer__section-title {
44
+ color: $light-text;
45
+ }
46
+
47
+ .tv-footer__social-link {
48
+ background-color: rgba(255, 255, 255, 0.5);
49
+ color: inherit;
50
+ border: 1px solid rgba(0, 0, 0, 0.05);
51
+ backdrop-filter: blur(4px);
52
+
53
+ &:hover {
54
+ background-color: $primary-color;
55
+ border-color: $primary-color;
56
+ color: $light-button-text;
57
+ box-shadow: 0 4px 12px rgba($primary-color, 0.3);
58
+ }
59
+ }
60
+
61
+ .tv-footer__link:hover {
62
+ color: $primary-color;
63
+ }
64
+
65
+ .tv-footer__newsletter-input {
66
+ background-color: rgba(255, 255, 255, 0.6);
67
+ border-color: rgba(0, 0, 0, 0.05);
68
+ color: $light-text;
69
+ backdrop-filter: blur(8px);
70
+
71
+ &:focus {
72
+ background-color: rgba(255, 255, 255, 0.9);
73
+ border-color: $primary-color;
74
+ box-shadow: 0 0 0 4px rgba($primary-color, 0.1);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ .tv-footer__container {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ gap: 4rem;
84
+ max-width: 1200px;
85
+ margin: 0 auto;
86
+ }
87
+
88
+ .tv-footer__left-column {
89
+ flex: 1 1 250px;
90
+ max-width: 400px;
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 2rem;
94
+ }
95
+
96
+ .tv-footer__brand {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 1rem;
100
+ }
101
+
102
+ .tv-footer__logo {
103
+ font-weight: 800;
104
+ font-size: 1.5rem;
105
+ text-decoration: none;
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 0.75rem;
109
+ letter-spacing: -0.02em;
110
+
111
+ img {
112
+ height: 100px;
113
+ width: auto;
114
+ }
115
+ }
116
+
117
+ .tv-footer__version {
118
+ font-size: 0.75rem;
119
+ opacity: 0.6;
120
+ font-weight: 500;
121
+ background: rgba(127, 127, 127, 0.1);
122
+ padding: 0.125rem 0.5rem;
123
+ border-radius: 12px;
124
+ align-self: flex-start;
125
+ }
126
+
127
+ .tv-footer__nav-definitions {
128
+ display: flex;
129
+ flex: 999 1 400px;
130
+ flex-wrap: wrap;
131
+ gap: 3rem;
132
+
133
+ @media (max-width: 768px) {
134
+ gap: 2rem;
135
+ flex-basis: 100%;
136
+ }
137
+ }
138
+
139
+ .tv-footer__section {
140
+ min-width: 140px;
141
+ }
142
+
143
+ .tv-footer__section-title {
144
+ font-weight: 700;
145
+ margin-bottom: 1.25rem;
146
+ font-size: 0.9rem;
147
+ letter-spacing: 0.05em;
148
+ text-transform: uppercase;
149
+ opacity: 1;
150
+ position: relative;
151
+
152
+ &::after {
153
+ content: '';
154
+ display: block;
155
+ width: 20px;
156
+ height: 2px;
157
+ background-color: $primary-color;
158
+ margin-top: 0.5rem;
159
+ border-radius: 2px;
160
+ }
161
+ }
162
+
163
+ .tv-footer__links {
164
+ list-style: none;
165
+ padding: 0;
166
+ margin: 0;
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 0.875rem;
170
+ }
171
+
172
+ .tv-footer__link {
173
+ color: inherit;
174
+ text-decoration: none;
175
+ font-size: 0.95rem;
176
+ transition: all 0.2s ease;
177
+ cursor: pointer;
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 0.5rem;
181
+ opacity: 0.75;
182
+ width: fit-content;
183
+
184
+ &:hover {
185
+ color: $primary-color;
186
+ opacity: 1;
187
+ transform: translateX(4px);
188
+ }
189
+
190
+ &::before {
191
+ content: '';
192
+ display: block;
193
+ width: 4px;
194
+ height: 4px;
195
+ background-color: currentColor;
196
+ border-radius: 50%;
197
+ opacity: 0;
198
+ transition: opacity 0.2s;
199
+ }
200
+
201
+ &:hover::before {
202
+ opacity: 1;
203
+ }
204
+ }
205
+
206
+ .tv-footer__social-section {
207
+ display: flex;
208
+ flex-direction: column;
209
+ justify-content: flex-start;
210
+ padding-top: 0.5rem;
211
+ flex: 0 0 auto;
212
+ }
213
+
214
+ .tv-footer__social {
215
+ display: flex;
216
+ gap: 0.75rem;
217
+ flex-wrap: wrap;
218
+ }
219
+
220
+ .tv-footer__social-link {
221
+ font-size: 1.1rem;
222
+ text-decoration: none;
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ width: 2.5rem;
227
+ height: 2.5rem;
228
+ border-radius: 50%;
229
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
230
+
231
+ &:hover {
232
+ transform: translateY(-4px) scale(1.1);
233
+ }
234
+
235
+ img {
236
+ width: 1.25rem;
237
+ height: 1.25rem;
238
+ object-fit: contain;
239
+ display: block;
240
+ }
241
+ }
242
+
243
+ .tv-footer__bottom {
244
+ border-top: 1px solid rgba(127, 127, 127, 0.1);
245
+ margin-top: 3rem;
246
+ padding-top: 1.5rem;
247
+ text-align: center;
248
+ font-size: 0.85rem;
249
+ display: flex;
250
+ flex-direction: column;
251
+ align-items: center;
252
+ gap: 1rem;
253
+ max-width: 1200px;
254
+ margin-left: auto;
255
+ margin-right: auto;
256
+ opacity: 0.7;
257
+
258
+ @media (min-width: 640px) {
259
+ flex-direction: row;
260
+ justify-content: space-between;
261
+ }
262
+
263
+ .tv-footer__links {
264
+ flex-direction: row;
265
+ gap: 1.5rem;
266
+ flex-wrap: wrap;
267
+ justify-content: center;
268
+
269
+ li {
270
+ display: inline-block;
271
+ }
272
+
273
+ .tv-footer__link {
274
+ font-size: 0.8rem;
275
+
276
+ &:hover {
277
+ transform: none;
278
+ text-decoration: underline;
279
+ }
280
+
281
+ &::before {
282
+ display: none;
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ .tv-footer__newsletter {
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 1rem;
292
+ margin-top: 1rem;
293
+ width: 100%;
294
+ }
295
+
296
+ .tv-footer__newsletter-title {
297
+ font-weight: 700;
298
+ font-size: 1rem;
299
+ margin: 0;
300
+ }
301
+
302
+ .tv-footer__newsletter-text {
303
+ font-size: 0.9rem;
304
+ line-height: 1.5;
305
+ opacity: 0.8;
306
+ margin: 0;
307
+ }
308
+
309
+ .tv-footer__newsletter-form {
310
+ display: flex;
311
+ gap: 0.5rem;
312
+ flex-direction: column;
313
+
314
+ @media(min-width: 400px) {
315
+ flex-direction: row;
316
+ }
317
+ }
318
+
319
+ .tv-footer__newsletter-input {
320
+ flex: 1;
321
+ min-width: 0;
322
+ padding: 0.75rem 1.25rem;
323
+ border-radius: 9999px;
324
+ border: 1px solid transparent;
325
+ background-color: rgba(255, 255, 255, 0.05);
326
+ color: inherit;
327
+ font-size: 0.9rem;
328
+ outline: none;
329
+ transition: all 0.3s ease;
330
+
331
+ &:focus {
332
+ background-color: rgba(255, 255, 255, 0.1);
333
+ border-color: $primary-color;
334
+ box-shadow: 0 0 0 4px rgba($primary-color, 0.2);
335
+ }
336
+
337
+ &::placeholder {
338
+ opacity: 0.5;
339
+ }
340
+ }
341
+
342
+ .tv-footer__newsletter-button {
343
+ padding: 0.75rem 1.5rem;
344
+ border-radius: 9999px;
345
+ border: none;
346
+ background-color: $primary-color;
347
+ color: #fff;
348
+ font-weight: 600;
349
+ font-size: 0.9rem;
350
+ cursor: pointer;
351
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
352
+ display: inline-flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ white-space: nowrap;
356
+
357
+ &:hover {
358
+ background-color: color.adjust($primary-color, $lightness: -5%);
359
+ transform: translateY(-2px);
360
+ box-shadow: 0 4px 12px rgba($primary-color, 0.4);
361
+ }
362
+
363
+ &:active {
364
+ transform: translateY(0);
365
+ }
366
+ }
367
+
368
+ .tv-footer__back-to-top {
369
+ position: fixed;
370
+ bottom: 2.5rem;
371
+ right: 2.5rem;
372
+ width: 3.5rem;
373
+ height: 3.5rem;
374
+ border-radius: 50%;
375
+ background-color: $primary-color;
376
+ color: #fff;
377
+ border: none;
378
+ font-size: 1.5rem;
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ cursor: pointer;
383
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
384
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
385
+ z-index: 1000;
386
+
387
+ &:hover {
388
+ transform: translateY(-4px) scale(1.05);
389
+ background-color: color.adjust($primary-color, $lightness: -5%);
390
+ box-shadow: 0 12px 20px rgba($primary-color, 0.3);
391
+ }
392
+
393
+ &:active {
394
+ transform: translateY(-1px);
395
+ }
396
+ }
@@ -0,0 +1,101 @@
1
+ <script setup>
2
+ import { useFooter } from '../composables/useFooter.js'
3
+ import { ref } from 'vue'
4
+
5
+ const props = defineProps({
6
+ config: {
7
+ type: Object,
8
+ default: () => ({})
9
+ }
10
+ })
11
+
12
+ const { brand, navigation, social, legal, version, copyright, newsletter } = useFooter(props.config)
13
+ const email = ref('')
14
+ const emit = defineEmits(['subscribe'])
15
+
16
+ const handleSubscribe = () => {
17
+ if (email.value) {
18
+ emit('subscribe', email.value)
19
+ email.value = ''
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <footer class="tv-footer">
26
+ <div class="tv-footer__container">
27
+ <div class="tv-footer__left-column">
28
+ <slot name="brand" :brand="brand" :version="version">
29
+ <div v-if="brand" class="tv-footer__brand">
30
+ <a :href="brand.url || '/'" class="tv-footer__logo">
31
+ <img v-if="brand.logo" :src="brand.logo" :alt="brand.name" />
32
+ <span v-if="brand.name">{{ brand.name }}</span>
33
+ </a>
34
+ <span v-if="version" class="tv-footer__version">{{ version }}</span>
35
+ </div>
36
+ </slot>
37
+
38
+ <slot name="newsletter" :newsletter="newsletter">
39
+ <div v-if="newsletter" class="tv-footer__newsletter">
40
+ <h3 v-if="newsletter.title" class="tv-footer__newsletter-title">{{ newsletter.title }}</h3>
41
+ <p v-if="newsletter.description" class="tv-footer__newsletter-text">{{ newsletter.description }}</p>
42
+ <form class="tv-footer__newsletter-form" @submit.prevent="handleSubscribe">
43
+ <input
44
+ v-model="email"
45
+ type="email"
46
+ :placeholder="newsletter.placeholder || 'Enter your email'"
47
+ class="tv-footer__newsletter-input"
48
+ required
49
+ />
50
+ <button type="submit" class="tv-footer__newsletter-button">
51
+ {{ newsletter.buttonText || 'Subscribe' }}
52
+ </button>
53
+ </form>
54
+ </div>
55
+ </slot>
56
+ </div>
57
+
58
+ <div class="tv-footer__nav-definitions">
59
+ <div v-for="(group, index) in navigation" :key="index" class="tv-footer__section">
60
+ <h3 v-if="group.title" class="tv-footer__section-title">{{ group.title }}</h3>
61
+ <ul class="tv-footer__links">
62
+ <li v-for="(link, i) in group.items" :key="i">
63
+ <a :href="link.url" class="tv-footer__link">
64
+ {{ link.label }}
65
+ </a>
66
+ </li>
67
+ </ul>
68
+ </div>
69
+ </div>
70
+
71
+ <div v-if="social && social.length" class="tv-footer__social-section">
72
+ <div class="tv-footer__social">
73
+ <a v-for="(item, index) in social" :key="index" :href="item.url" class="tv-footer__social-link" target="_blank" rel="noopener noreferer">
74
+ <img v-if="item.iconUrl" :src="item.iconUrl" :alt="item.label" class="tv-footer__social-icon-img" />
75
+ <i v-else-if="item.icon" :class="item.icon"></i>
76
+ <span v-else>{{ item.label }}</span>
77
+ </a>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <slot name="bottom" :copyright="copyright" :legal="legal">
83
+ <div class="tv-footer__bottom">
84
+ <div v-if="copyright">{{ copyright }}</div>
85
+ <div v-if="legal && legal.length" class="tv-footer__legal">
86
+ <ul class="tv-footer__links" style="flex-direction: row; gap: 1.5rem;">
87
+ <li v-for="(link, index) in legal" :key="index">
88
+ <a :href="link.url" class="tv-footer__link">
89
+ {{ link.label }}
90
+ </a>
91
+ </li>
92
+ </ul>
93
+ </div>
94
+ </div>
95
+ </slot>
96
+ </footer>
97
+ </template>
98
+
99
+ <style scoped>
100
+
101
+ </style>
@@ -0,0 +1,45 @@
1
+ import { computed } from 'vue'
2
+
3
+ export function useFooter(config) {
4
+ const brand = computed(() => config?.brand || null)
5
+
6
+ const navigation = computed(() => {
7
+ if (!config?.navigation || !Array.isArray(config.navigation)) return []
8
+ return config.navigation
9
+ })
10
+
11
+ const social = computed(() => {
12
+ if (!config?.social || !Array.isArray(config.social)) return []
13
+ return config.social
14
+ })
15
+
16
+ const legal = computed(() => {
17
+ if (!config?.legal || !Array.isArray(config.legal)) return []
18
+ return config.legal
19
+ })
20
+
21
+ const version = computed(() => config?.version || '')
22
+
23
+ const copyright = computed(() => config?.copyright || '')
24
+
25
+ const newsletter = computed(() => {
26
+ return config?.newsletter || null
27
+ })
28
+
29
+ const currentYear = new Date().getFullYear()
30
+
31
+ const formattedCopyright = computed(() => {
32
+ const text = copyright.value
33
+ return text.replace('{year}', currentYear)
34
+ })
35
+
36
+ return {
37
+ brand,
38
+ navigation,
39
+ social,
40
+ legal,
41
+ version,
42
+ copyright: formattedCopyright,
43
+ newsletter
44
+ }
45
+ }
@@ -0,0 +1,21 @@
1
+ <script setup>
2
+ import { defineAsyncComponent } from 'vue'
3
+ import { demos } from './utils/mocks.js'
4
+ import { TvDemo } from '@todovue/tv-demo'
5
+
6
+ const TvFooter = defineAsyncComponent(/* webpackChunkName: "tvFooter" */() => import('../components/TvFooter.vue'))
7
+ </script>
8
+
9
+ <template>
10
+ <TvDemo
11
+ hide-background
12
+ :component="TvFooter"
13
+ :variants="demos"
14
+ :manual-emits="['subscribe']"
15
+ component-name="TvFooter"
16
+ npm-install="@todovue/tv-footer"
17
+ source-link="https://github.com/TODOvue/tv-footer"
18
+ url-clone="https://github.com/TODOvue/tv-footer.git"
19
+ version="1.1.1"
20
+ />
21
+ </template>
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <TvFooter :config="conf" />
3
+ </template>
4
+
5
+ <script setup>
6
+ import { TvFooter } from '@todovue/tv-footer'
7
+ import '@todovue/tv-footer/style.css'
8
+ import FacebookIcon from './utils/icons/facebook.png'
9
+ import GitHubWithIcon from "../icons/github-white.svg";
10
+ import TODOvue from "../icons/todovue.png";
11
+
12
+ const conf = {
13
+ brand: {
14
+ logo: 'https://res.cloudinary.com/denj4fg7f/image/upload/v1766199952/logo_ohpadg.png', // Optional
15
+ url: '/'
16
+ },
17
+ version: 'v2.4.0',
18
+ navigation: [
19
+ {
20
+ title: 'Product',
21
+ items: [
22
+ { label: 'Features', url: '/#' },
23
+ { label: 'Pricing', url: '/#' },
24
+ { label: 'Showcase', url: '/#' }
25
+ ]
26
+ },
27
+ {
28
+ title: 'Resources',
29
+ items: [
30
+ { label: 'Documentation', url: '/#' },
31
+ { label: 'API Reference', url: '/#' },
32
+ { label: 'Community', url: '/#' }
33
+ ]
34
+ },
35
+ {
36
+ title: 'Company',
37
+ items: [
38
+ { label: 'About Us', url: '/#' },
39
+ { label: 'Blog', url: '/#' },
40
+ { label: 'Careers', url: '/#' }
41
+ ]
42
+ }
43
+ ],
44
+ social: [
45
+ {
46
+ label: 'GitHub',
47
+ url: 'https://github.com/TODOvue',
48
+ iconUrl: GitHubWithIcon // Icon library class (e.g. FontAwesome, UnoCSS) use /icon
49
+ },
50
+ {
51
+ label: 'Facebook',
52
+ url: 'https://facebook.com',
53
+ iconUrl: FacebookIcon
54
+ },
55
+ {
56
+ label: 'TODOvue',
57
+ url: 'https://todovue.blog',
58
+ iconUrl: TODOvue
59
+ }
60
+ ],
61
+ legal: [
62
+ { label: 'Privacy', url: '/#' },
63
+ { label: 'Terms', url: '/#' },
64
+ { label: 'Cookies', url: '/#' }
65
+ ],
66
+ copyright: '© {year} TvFooter. All rights reserved.'
67
+ }
68
+ </script>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <TvFooter :config="conf" @subscribe="handleSubscribe" />
3
+ </template>
4
+
5
+ <script setup>
6
+ import { TvFooter } from '@todovue/tv-footer'
7
+ import '@todovue/tv-footer/style.css'
8
+ import FacebookIcon from './utils/icons/facebook.png'
9
+ import GitHubWithIcon from "../icons/github-white.svg";
10
+ import TODOvue from "../icons/todovue.png";
11
+
12
+ const handleSubscribe = (email) => {
13
+ console.log('Subscribe event received:', email);
14
+ alert(`Subscribed with: ${email}`);
15
+ }
16
+
17
+ const conf = {
18
+ newsletter: {
19
+ title: 'Subscribe to our newsletter',
20
+ description: 'Get the latest news and updates right to your inbox.',
21
+ placeholder: 'Your email address',
22
+ buttonText: 'Subscribe'
23
+ },
24
+ brand: {
25
+ logo: 'https://res.cloudinary.com/denj4fg7f/image/upload/v1766199952/logo_ohpadg.png', // Optional
26
+ url: '/'
27
+ },
28
+ version: 'v2.4.0',
29
+ navigation: [
30
+ {
31
+ title: 'Product',
32
+ items: [
33
+ { label: 'Features', url: '/#' },
34
+ { label: 'Pricing', url: '/#' },
35
+ { label: 'Showcase', url: '/#' }
36
+ ]
37
+ },
38
+ {
39
+ title: 'Resources',
40
+ items: [
41
+ { label: 'Documentation', url: '/#' },
42
+ { label: 'API Reference', url: '/#' },
43
+ { label: 'Community', url: '/#' }
44
+ ]
45
+ },
46
+ {
47
+ title: 'Company',
48
+ items: [
49
+ { label: 'About Us', url: '/#' },
50
+ { label: 'Blog', url: '/#' },
51
+ { label: 'Careers', url: '/#' }
52
+ ]
53
+ }
54
+ ],
55
+ social: [
56
+ {
57
+ label: 'GitHub',
58
+ url: 'https://github.com/TODOvue',
59
+ iconUrl: GitHubWithIcon // Icon library class (e.g. FontAwesome, UnoCSS) use /icon
60
+ },
61
+ {
62
+ label: 'Facebook',
63
+ url: 'https://facebook.com',
64
+ iconUrl: FacebookIcon
65
+ },
66
+ {
67
+ label: 'TODOvue',
68
+ url: 'https://todovue.blog',
69
+ iconUrl: TODOvue
70
+ }
71
+ ],
72
+ legal: [
73
+ { label: 'Privacy', url: '/#' },
74
+ { label: 'Terms', url: '/#' },
75
+ { label: 'Cookies', url: '/#' }
76
+ ],
77
+ copyright: '© {year} TvFooter. All rights reserved.'
78
+ }
79
+ </script>