@xosen/site-sdk 0.0.6 → 0.0.7

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.
@@ -1,101 +0,0 @@
1
- <template>
2
- <section class="x-image-text" :class="{ 'x-image-text--reverse': reverse }">
3
- <div class="x-image-text__container">
4
- <div class="x-image-text__text">
5
- <h2 v-if="data.title" class="x-image-text__title">{{ data.title }}</h2>
6
- <div v-if="data.content" class="x-image-text__content" v-html="data.content" />
7
- <ul v-if="data.items?.length" class="x-image-text__checklist">
8
- <li v-for="(item, i) in data.items" :key="i">
9
- <span class="x-image-text__check-icon">{{ item.icon || '✓' }}</span>
10
- {{ item.text }}
11
- </li>
12
- </ul>
13
- </div>
14
- <div v-if="data.image" class="x-image-text__image-wrap">
15
- <img :src="data.image" :alt="data.title || ''" class="x-image-text__image" />
16
- </div>
17
- </div>
18
- </section>
19
- </template>
20
-
21
- <script setup lang="ts">
22
- import type { ImageTextBlockData } from '../../types/blocks.js';
23
-
24
- withDefaults(
25
- defineProps<{
26
- data: ImageTextBlockData;
27
- reverse?: boolean;
28
- }>(),
29
- { reverse: false },
30
- );
31
- </script>
32
-
33
- <style scoped>
34
- .x-image-text {
35
- padding: 80px 24px;
36
- }
37
-
38
- .x-image-text__container {
39
- max-width: var(--x-max-width, 1200px);
40
- margin: 0 auto;
41
- display: grid;
42
- grid-template-columns: 1fr 1fr;
43
- gap: 48px;
44
- align-items: center;
45
- }
46
-
47
- .x-image-text--reverse .x-image-text__container {
48
- direction: rtl;
49
- }
50
-
51
- .x-image-text--reverse .x-image-text__text {
52
- direction: ltr;
53
- }
54
-
55
- .x-image-text__title {
56
- font-size: 2rem;
57
- font-weight: 700;
58
- color: var(--x-color-text, #2c3e50);
59
- margin: 0 0 16px;
60
- }
61
-
62
- .x-image-text__content {
63
- color: var(--x-color-text, #2c3e50);
64
- line-height: 1.8;
65
- }
66
-
67
- .x-image-text__checklist {
68
- list-style: none;
69
- padding: 0;
70
- margin: 0;
71
- }
72
-
73
- .x-image-text__checklist li {
74
- display: flex;
75
- align-items: center;
76
- gap: 12px;
77
- padding: 8px 0;
78
- font-size: 1rem;
79
- color: var(--x-color-text, #2c3e50);
80
- }
81
-
82
- .x-image-text__check-icon {
83
- color: var(--x-color-success, #00d084);
84
- font-weight: bold;
85
- }
86
-
87
- .x-image-text__image {
88
- width: 100%;
89
- border-radius: var(--x-border-radius, 12px);
90
- }
91
-
92
- @media (max-width: 768px) {
93
- .x-image-text__container {
94
- grid-template-columns: 1fr;
95
- }
96
-
97
- .x-image-text--reverse .x-image-text__container {
98
- direction: ltr;
99
- }
100
- }
101
- </style>
@@ -1,52 +0,0 @@
1
- <template>
2
- <section class="x-map-block">
3
- <h2 v-if="data.title" class="x-map-block__title">{{ data.title }}</h2>
4
- <div class="x-map-block__container">
5
- <iframe
6
- :src="mapUrl"
7
- width="100%"
8
- height="400"
9
- style="border: 0"
10
- allowfullscreen
11
- loading="lazy"
12
- referrerpolicy="no-referrer-when-downgrade"
13
- />
14
- </div>
15
- </section>
16
- </template>
17
-
18
- <script setup lang="ts">
19
- import { computed } from 'vue';
20
- import type { MapBlockData } from '../../types/blocks.js';
21
-
22
- const props = defineProps<{
23
- data: MapBlockData;
24
- }>();
25
-
26
- const mapUrl = computed(() => {
27
- const { lat, lng } = props.data.center || { lat: 0, lng: 0 };
28
- const zoom = props.data.zoom || 14;
29
- return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`;
30
- });
31
- </script>
32
-
33
- <style scoped>
34
- .x-map-block {
35
- padding: 48px 24px;
36
- }
37
-
38
- .x-map-block__title {
39
- text-align: center;
40
- font-size: 1.75rem;
41
- font-weight: 700;
42
- margin-bottom: 32px;
43
- color: var(--x-color-text, #2c3e50);
44
- }
45
-
46
- .x-map-block__container {
47
- max-width: 1200px;
48
- margin: 0 auto;
49
- border-radius: var(--x-border-radius, 8px);
50
- overflow: hidden;
51
- }
52
- </style>
@@ -1,261 +0,0 @@
1
- <template>
2
- <section class="x-pricing">
3
- <div class="x-pricing__container">
4
- <h2 v-if="title" class="x-pricing__title">{{ title }}</h2>
5
- <p v-if="subtitle" class="x-pricing__subtitle">{{ subtitle }}</p>
6
-
7
- <!-- Tabs for tariff groups -->
8
- <div v-if="groups.length > 1" class="x-pricing__tabs">
9
- <button
10
- v-for="group in groups"
11
- :key="group"
12
- class="x-pricing__tab"
13
- :class="{ 'x-pricing__tab--active': activeGroup === group }"
14
- @click="activeGroup = group"
15
- >
16
- {{ group }}
17
- </button>
18
- </div>
19
-
20
- <div class="x-pricing__grid">
21
- <div
22
- v-for="(plan, i) in activePlans"
23
- :key="plan.id || i"
24
- class="x-pricing-card"
25
- :class="{ 'x-pricing-card--featured': i === featuredIndex }"
26
- >
27
- <h3 class="x-pricing-card__name">{{ plan.name }}</h3>
28
- <p v-if="plan.description" class="x-pricing-card__desc">{{ plan.description }}</p>
29
- <div class="x-pricing-card__price">
30
- <span class="x-pricing-card__amount">{{ plan.price }}</span>
31
- <span v-if="plan.period" class="x-pricing-card__period">/{{ plan.period }}</span>
32
- </div>
33
- <ul v-if="plan.features?.length" class="x-pricing-card__features">
34
- <li v-for="(feature, fi) in plan.features" :key="fi">{{ feature }}</li>
35
- </ul>
36
- <a v-if="plan.actionUrl" :href="plan.actionUrl" class="x-btn x-btn--primary x-pricing-card__action">
37
- {{ plan.actionText || 'Choose' }}
38
- </a>
39
- </div>
40
- </div>
41
- </div>
42
- </section>
43
- </template>
44
-
45
- <script setup lang="ts">
46
- import { ref, computed, onMounted, watch } from 'vue';
47
-
48
- import type { PricingBlockData } from '../../types/blocks.js';
49
- import { useSiteData } from '../../composables/useSiteData.js';
50
- import { useSiteApi } from '../../composables/useSiteApi.js';
51
-
52
- interface Plan {
53
- id?: string;
54
- name: string;
55
- description?: string;
56
- price: string | number;
57
- period?: string;
58
- features?: string[];
59
- actionUrl?: string;
60
- actionText?: string;
61
- tariffGroupName?: string;
62
- }
63
-
64
- const props = withDefaults(
65
- defineProps<{
66
- data?: PricingBlockData;
67
- source?: 'api' | 'kv';
68
- featuredIndex?: number;
69
- }>(),
70
- { source: 'kv', featuredIndex: 1 },
71
- );
72
-
73
- const title = computed(() => props.data?.title);
74
- const subtitle = computed(() => props.data?.subtitle);
75
-
76
- const plans = ref<Plan[]>([]);
77
- const activeGroup = ref('');
78
-
79
- // Load from KV (preloaded)
80
- const kvTariffs = useSiteData<Plan[]>('tariffs', 'tariffs');
81
-
82
- // Load from API if source is 'api'
83
- const api = useSiteApi();
84
-
85
- onMounted(async () => {
86
- if (props.source === 'api') {
87
- try {
88
- plans.value = await api.getTariffs();
89
- } catch {
90
- // Fallback to KV
91
- }
92
- }
93
- });
94
-
95
- watch(
96
- kvTariffs,
97
- (val) => {
98
- if (val && props.source === 'kv') {
99
- plans.value = val;
100
- }
101
- },
102
- { immediate: true },
103
- );
104
-
105
- const groups = computed(() => {
106
- const names = new Set(plans.value.map((p) => p.tariffGroupName).filter(Boolean));
107
- return Array.from(names) as string[];
108
- });
109
-
110
- watch(
111
- groups,
112
- (g) => {
113
- if (g.length > 0 && !activeGroup.value) {
114
- activeGroup.value = g[0];
115
- }
116
- },
117
- { immediate: true },
118
- );
119
-
120
- const activePlans = computed(() => {
121
- if (!activeGroup.value || groups.value.length <= 1) return plans.value;
122
- return plans.value.filter((p) => p.tariffGroupName === activeGroup.value);
123
- });
124
- </script>
125
-
126
- <style scoped>
127
- .x-pricing {
128
- padding: 80px 24px;
129
- background: var(--x-color-background-alt, #f7f9fc);
130
- }
131
-
132
- .x-pricing__container {
133
- max-width: var(--x-max-width, 1200px);
134
- margin: 0 auto;
135
- }
136
-
137
- .x-pricing__title {
138
- text-align: center;
139
- font-size: 2rem;
140
- font-weight: 700;
141
- color: var(--x-color-text, #2c3e50);
142
- margin: 0 0 8px;
143
- }
144
-
145
- .x-pricing__subtitle {
146
- text-align: center;
147
- color: var(--x-color-text-light, #6b7c93);
148
- margin: 0 0 32px;
149
- }
150
-
151
- .x-pricing__tabs {
152
- display: flex;
153
- justify-content: center;
154
- gap: 8px;
155
- margin-bottom: 32px;
156
- }
157
-
158
- .x-pricing__tab {
159
- padding: 8px 24px;
160
- border: 2px solid var(--x-color-border, #e2e8f0);
161
- border-radius: var(--x-border-radius, 8px);
162
- background: transparent;
163
- cursor: pointer;
164
- font-weight: 500;
165
- transition: all 0.2s;
166
- }
167
-
168
- .x-pricing__tab--active {
169
- background: var(--x-color-primary, #1e73be);
170
- border-color: var(--x-color-primary, #1e73be);
171
- color: white;
172
- }
173
-
174
- .x-pricing__grid {
175
- display: grid;
176
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
177
- gap: 24px;
178
- align-items: start;
179
- }
180
-
181
- .x-pricing-card {
182
- background: var(--x-color-surface, #fff);
183
- border: 1px solid var(--x-color-border, #e2e8f0);
184
- border-radius: var(--x-border-radius, 12px);
185
- padding: 32px;
186
- text-align: center;
187
- transition: transform 0.2s;
188
- }
189
-
190
- .x-pricing-card--featured {
191
- transform: scale(1.04);
192
- border-color: var(--x-color-primary, #1e73be);
193
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
194
- }
195
-
196
- .x-pricing-card__name {
197
- font-size: 1.25rem;
198
- font-weight: 600;
199
- margin: 0 0 8px;
200
- }
201
-
202
- .x-pricing-card__desc {
203
- color: var(--x-color-text-light, #6b7c93);
204
- font-size: 0.9rem;
205
- margin: 0 0 16px;
206
- }
207
-
208
- .x-pricing-card__price {
209
- margin: 16px 0;
210
- }
211
-
212
- .x-pricing-card__amount {
213
- font-size: 2.5rem;
214
- font-weight: 800;
215
- color: var(--x-color-primary, #1e73be);
216
- }
217
-
218
- .x-pricing-card__period {
219
- color: var(--x-color-text-light, #6b7c93);
220
- }
221
-
222
- .x-pricing-card__features {
223
- list-style: none;
224
- padding: 0;
225
- margin: 0 0 24px;
226
- text-align: left;
227
- }
228
-
229
- .x-pricing-card__features li {
230
- padding: 6px 0;
231
- border-bottom: 1px solid var(--x-color-border, #e2e8f0);
232
- font-size: 0.9rem;
233
- }
234
-
235
- .x-pricing-card__features li::before {
236
- content: '✓ ';
237
- color: var(--x-color-success, #00d084);
238
- font-weight: bold;
239
- }
240
-
241
- .x-pricing-card__action {
242
- width: 100%;
243
- justify-content: center;
244
- }
245
-
246
- .x-btn {
247
- display: inline-flex;
248
- align-items: center;
249
- padding: 12px 32px;
250
- border-radius: var(--x-border-radius, 8px);
251
- font-weight: 600;
252
- text-decoration: none;
253
- transition: all 0.2s;
254
- cursor: pointer;
255
- }
256
-
257
- .x-btn--primary {
258
- background: var(--x-color-primary, #1e73be);
259
- color: white;
260
- }
261
- </style>
@@ -1,56 +0,0 @@
1
- <template>
2
- <div class="x-locale-switcher">
3
- <button
4
- v-for="loc in locales"
5
- :key="loc"
6
- class="x-locale-switcher__btn"
7
- :class="{ 'x-locale-switcher__btn--active': locale === loc }"
8
- @click="switchLocale(loc)"
9
- >
10
- {{ loc.toUpperCase() }}
11
- </button>
12
- </div>
13
- </template>
14
-
15
- <script setup lang="ts">
16
- import { useI18n } from 'vue-i18n';
17
-
18
- defineProps<{
19
- locales: string[];
20
- }>();
21
-
22
- const { locale } = useI18n();
23
-
24
- function switchLocale(loc: string) {
25
- locale.value = loc;
26
- localStorage.setItem('site-locale', loc);
27
- }
28
- </script>
29
-
30
- <style scoped>
31
- .x-locale-switcher {
32
- display: flex;
33
- gap: 4px;
34
- }
35
-
36
- .x-locale-switcher__btn {
37
- padding: 4px 8px;
38
- border: 1px solid transparent;
39
- border-radius: 4px;
40
- background: transparent;
41
- cursor: pointer;
42
- font-size: 0.75rem;
43
- font-weight: 600;
44
- opacity: 0.6;
45
- transition: all 0.2s;
46
- }
47
-
48
- .x-locale-switcher__btn--active {
49
- opacity: 1;
50
- border-color: currentColor;
51
- }
52
-
53
- .x-locale-switcher__btn:hover {
54
- opacity: 1;
55
- }
56
- </style>
@@ -1,64 +0,0 @@
1
- <template>
2
- <footer class="x-footer">
3
- <div class="x-footer__container">
4
- <div v-if="footerConfig.links?.length" class="x-footer__links">
5
- <router-link v-for="link in footerConfig.links" :key="link.url" :to="link.url" class="x-footer__link">
6
- {{ link.text }}
7
- </router-link>
8
- </div>
9
- <p v-if="copyright" class="x-footer__copyright">{{ copyright }}</p>
10
- </div>
11
- </footer>
12
- </template>
13
-
14
- <script setup lang="ts">
15
- import { computed } from 'vue';
16
-
17
- import { useSiteConfig } from '../../composables/useSiteConfig.js';
18
-
19
- const { footer: footerConfig, branding } = useSiteConfig();
20
-
21
- const copyright = computed(() => {
22
- if (footerConfig.value.copyright) return footerConfig.value.copyright;
23
- const year = new Date().getFullYear();
24
- return `© ${year} ${branding.value.siteName || ''}`;
25
- });
26
- </script>
27
-
28
- <style scoped>
29
- .x-footer {
30
- background: var(--x-color-text, #2c3e50);
31
- color: rgba(255, 255, 255, 0.7);
32
- padding: 32px 24px;
33
- text-align: center;
34
- }
35
-
36
- .x-footer__container {
37
- max-width: var(--x-max-width, 1200px);
38
- margin: 0 auto;
39
- }
40
-
41
- .x-footer__links {
42
- display: flex;
43
- gap: 24px;
44
- justify-content: center;
45
- margin-bottom: 16px;
46
- flex-wrap: wrap;
47
- }
48
-
49
- .x-footer__link {
50
- color: rgba(255, 255, 255, 0.7);
51
- text-decoration: none;
52
- font-size: 0.9rem;
53
- }
54
-
55
- .x-footer__link:hover {
56
- color: white;
57
- }
58
-
59
- .x-footer__copyright {
60
- font-size: 0.85rem;
61
- margin: 0;
62
- opacity: 0.6;
63
- }
64
- </style>
@@ -1,35 +0,0 @@
1
- <template>
2
- <div class="x-site-layout">
3
- <XSiteNav />
4
- <main class="x-site-layout__main">
5
- <slot />
6
- </main>
7
- <XSiteFooter />
8
- </div>
9
- </template>
10
-
11
- <script setup lang="ts">
12
- import { onMounted } from 'vue';
13
-
14
- import { useSkin } from '../../composables/useSkin.js';
15
- import XSiteNav from './XSiteNav.vue';
16
- import XSiteFooter from './XSiteFooter.vue';
17
-
18
- // Apply skin CSS variables on mount
19
- useSkin();
20
- </script>
21
-
22
- <style scoped>
23
- .x-site-layout {
24
- min-height: 100vh;
25
- display: flex;
26
- flex-direction: column;
27
- font-family: var(--x-font-family, 'Inter', sans-serif);
28
- color: var(--x-color-text, #2c3e50);
29
- background: var(--x-color-background, #ffffff);
30
- }
31
-
32
- .x-site-layout__main {
33
- flex: 1;
34
- }
35
- </style>