@xosen/site-sdk 0.0.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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@xosen/site-sdk",
3
+ "version": "0.0.1",
4
+ "description": "Shared Vue components and composables for Xosen site templates",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "development": "./src/index.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./style.css": "./dist/index.css",
15
+ "./worker": {
16
+ "types": "./src/worker/index.ts",
17
+ "default": "./src/worker/index.ts"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "scripts": {
28
+ "build": "vue-tsc --declaration --emitDeclarationOnly --outDir dist && vite build",
29
+ "dev": "vite build --watch",
30
+ "clean": "rm -rf dist"
31
+ },
32
+ "peerDependencies": {
33
+ "vue": "catalog:",
34
+ "vue-i18n": "catalog:",
35
+ "vue-router": "catalog:"
36
+ },
37
+ "devDependencies": {
38
+ "vue": "catalog:",
39
+ "vue-i18n": "catalog:",
40
+ "vue-router": "catalog:",
41
+ "vue-tsc": "catalog:",
42
+ "typescript": "catalog:",
43
+ "vite": "catalog:",
44
+ "@vitejs/plugin-vue": "catalog:"
45
+ }
46
+ }
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="x-block-renderer">
3
+ <template v-for="(block, i) in visibleBlocks" :key="i">
4
+ <component
5
+ :is="getBlockComponent(block.type)"
6
+ v-if="getBlockComponent(block.type)"
7
+ :data="block.data || block.props"
8
+ v-bind="block.settings"
9
+ :class="block.settings?.className"
10
+ />
11
+ </template>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed, type Component } from 'vue';
17
+
18
+ import type { Block } from '../../types/blocks.js';
19
+ import XHeroBlock from './XHeroBlock.vue';
20
+ import XHtmlBlock from './XHtmlBlock.vue';
21
+ import XCardsBlock from './XCardsBlock.vue';
22
+ import XImageTextBlock from './XImageTextBlock.vue';
23
+ import XPricingBlock from './XPricingBlock.vue';
24
+ import XContactsBlock from './XContactsBlock.vue';
25
+
26
+ const props = defineProps<{
27
+ blocks: Block[];
28
+ }>();
29
+
30
+ const blockRegistry: Record<string, Component> = {
31
+ hero: XHeroBlock,
32
+ html: XHtmlBlock,
33
+ cards: XCardsBlock,
34
+ 'image-text': XImageTextBlock,
35
+ pricing: XPricingBlock,
36
+ contacts: XContactsBlock,
37
+ };
38
+
39
+ function getBlockComponent(type: string): Component | undefined {
40
+ return blockRegistry[type];
41
+ }
42
+
43
+ const visibleBlocks = computed(() => props.blocks.filter((b) => b.settings?.visible !== false));
44
+ </script>
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <section class="x-cards-block" :class="`x-cards-block--${variant}`">
3
+ <div class="x-cards-block__container">
4
+ <h2 v-if="data.title" class="x-cards-block__title">{{ data.title }}</h2>
5
+ <p v-if="data.subtitle" class="x-cards-block__subtitle">{{ data.subtitle }}</p>
6
+
7
+ <div class="x-cards-block__grid" :style="gridStyle">
8
+ <component
9
+ :is="card.link ? 'a' : 'div'"
10
+ v-for="(card, i) in data.cards"
11
+ :key="i"
12
+ class="x-card"
13
+ :href="card.link"
14
+ >
15
+ <img
16
+ v-if="card.image && variant === 'image-cards'"
17
+ :src="card.image"
18
+ :alt="card.title"
19
+ class="x-card__image"
20
+ />
21
+ <div v-if="card.icon && variant !== 'image-cards'" class="x-card__icon" :style="iconStyle">{{ iconDisplay(card.icon) }}</div>
22
+ <h3 class="x-card__title">{{ card.title }}</h3>
23
+ <p class="x-card__desc">{{ card.desc }}</p>
24
+ </component>
25
+ </div>
26
+ </div>
27
+ </section>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed } from 'vue';
32
+
33
+ import type { CardsBlockData } from '../../types/blocks.js';
34
+
35
+ const props = withDefaults(
36
+ defineProps<{
37
+ data: CardsBlockData;
38
+ variant?: 'default' | 'icon-cards' | 'image-cards';
39
+ columns?: 2 | 3 | 4;
40
+ }>(),
41
+ { variant: 'default', columns: undefined },
42
+ );
43
+
44
+ const gridStyle = computed(() => {
45
+ const cols = props.columns || Math.min(props.data.cards.length, 4);
46
+ return { '--columns': cols };
47
+ });
48
+
49
+ const iconStyle = { color: 'var(--x-color-primary, #1e73be)' };
50
+
51
+ // Map mdi-* icon names to emoji, or pass through if already an emoji/character
52
+ const iconMap: Record<string, string> = {
53
+ 'mdi-lightbulb': '💡',
54
+ 'mdi-code-braces': '⚙️',
55
+ 'mdi-headset': '🎧',
56
+ 'mdi-check-circle': '✅',
57
+ 'mdi-shield': '🛡️',
58
+ 'mdi-chart-line': '📈',
59
+ 'mdi-rocket': '🚀',
60
+ 'mdi-heart': '❤️',
61
+ 'mdi-star': '⭐',
62
+ 'mdi-cog': '⚙️',
63
+ 'mdi-email': '📧',
64
+ 'mdi-phone': '📞',
65
+ 'mdi-map-marker': '📍',
66
+ 'mdi-clock': '🕐',
67
+ 'mdi-account': '👤',
68
+ 'mdi-team': '👥',
69
+ };
70
+
71
+ function iconDisplay(icon: string): string {
72
+ return iconMap[icon] || icon;
73
+ }
74
+ </script>
75
+
76
+ <style scoped>
77
+ .x-cards-block {
78
+ padding: 80px 24px;
79
+ }
80
+
81
+ .x-cards-block__container {
82
+ max-width: var(--x-max-width, 1200px);
83
+ margin: 0 auto;
84
+ }
85
+
86
+ .x-cards-block__title {
87
+ text-align: center;
88
+ font-size: 2rem;
89
+ font-weight: 700;
90
+ color: var(--x-color-text, #2c3e50);
91
+ margin: 0 0 8px;
92
+ }
93
+
94
+ .x-cards-block__subtitle {
95
+ text-align: center;
96
+ color: var(--x-color-text-light, #6b7c93);
97
+ margin: 0 0 48px;
98
+ }
99
+
100
+ .x-cards-block__grid {
101
+ display: grid;
102
+ grid-template-columns: repeat(var(--columns, 4), 1fr);
103
+ gap: 24px;
104
+ }
105
+
106
+ @media (max-width: 768px) {
107
+ .x-cards-block__grid {
108
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
109
+ }
110
+ }
111
+
112
+ .x-card {
113
+ background: var(--x-color-surface, #fff);
114
+ border: 1px solid var(--x-color-border, #e2e8f0);
115
+ border-radius: var(--x-border-radius, 12px);
116
+ padding: 32px 24px;
117
+ text-align: center;
118
+ text-decoration: none;
119
+ color: inherit;
120
+ transition:
121
+ transform 0.2s,
122
+ box-shadow 0.2s;
123
+ }
124
+
125
+ .x-card:hover {
126
+ transform: translateY(-4px);
127
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
128
+ }
129
+
130
+ .x-card__icon {
131
+ font-size: 2.5rem;
132
+ margin-bottom: 16px;
133
+ width: 64px;
134
+ height: 64px;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ margin-left: auto;
139
+ margin-right: auto;
140
+ background: var(--x-color-background-alt, #f7f9fc);
141
+ border-radius: 50%;
142
+ }
143
+
144
+ .x-card__image {
145
+ width: 100%;
146
+ aspect-ratio: 16/9;
147
+ object-fit: cover;
148
+ border-radius: calc(var(--x-border-radius, 12px) - 4px);
149
+ margin-bottom: 16px;
150
+ }
151
+
152
+ .x-card__title {
153
+ font-size: 1.125rem;
154
+ font-weight: 600;
155
+ color: var(--x-color-text, #2c3e50);
156
+ margin: 0 0 8px;
157
+ }
158
+
159
+ .x-card__desc {
160
+ font-size: 0.9rem;
161
+ color: var(--x-color-text-light, #6b7c93);
162
+ line-height: 1.6;
163
+ margin: 0;
164
+ }
165
+ </style>
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <section class="x-contacts">
3
+ <div class="x-contacts__container">
4
+ <h2 v-if="data?.title" class="x-contacts__title">{{ data.title }}</h2>
5
+
6
+ <div v-if="offices" class="x-contacts__grid">
7
+ <div
8
+ v-for="(office, i) in offices"
9
+ :key="i"
10
+ class="x-contacts__card"
11
+ :class="{ 'x-contacts__card--main': office.isMain }"
12
+ >
13
+ <h3 class="x-contacts__name">{{ office.name }}</h3>
14
+ <p v-if="office.address" class="x-contacts__info">{{ office.address }}</p>
15
+ <p v-if="office.phone" class="x-contacts__info">
16
+ <a :href="`tel:${office.phone}`">{{ office.phone }}</a>
17
+ </p>
18
+ <p v-if="office.email" class="x-contacts__info">
19
+ <a :href="`mailto:${office.email}`">{{ office.email }}</a>
20
+ </p>
21
+ <p v-if="office.hours" class="x-contacts__hours">{{ office.hours }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </section>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed } from 'vue';
30
+
31
+ import type { ContactsBlockData } from '../../types/blocks.js';
32
+ import { useSiteData } from '../../composables/useSiteData.js';
33
+
34
+ interface Office {
35
+ name: string;
36
+ address?: string;
37
+ phone?: string;
38
+ email?: string;
39
+ hours?: string;
40
+ isMain?: boolean;
41
+ }
42
+
43
+ defineProps<{
44
+ data?: ContactsBlockData;
45
+ }>();
46
+
47
+ const kvOffices = useSiteData<Office[]>('contacts:offices', 'contacts:offices');
48
+ const offices = computed(() => kvOffices.value || []);
49
+ </script>
50
+
51
+ <style scoped>
52
+ .x-contacts {
53
+ padding: 80px 24px;
54
+ }
55
+
56
+ .x-contacts__container {
57
+ max-width: var(--x-max-width, 1200px);
58
+ margin: 0 auto;
59
+ }
60
+
61
+ .x-contacts__title {
62
+ text-align: center;
63
+ font-size: 2rem;
64
+ font-weight: 700;
65
+ color: var(--x-color-text, #2c3e50);
66
+ margin: 0 0 48px;
67
+ }
68
+
69
+ .x-contacts__grid {
70
+ display: grid;
71
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
72
+ gap: 24px;
73
+ }
74
+
75
+ .x-contacts__card {
76
+ background: var(--x-color-surface, #fff);
77
+ border: 1px solid var(--x-color-border, #e2e8f0);
78
+ border-radius: var(--x-border-radius, 12px);
79
+ padding: 24px;
80
+ }
81
+
82
+ .x-contacts__card--main {
83
+ border-color: var(--x-color-primary, #1e73be);
84
+ background: var(--x-color-primary-light, #e8f2fc);
85
+ }
86
+
87
+ .x-contacts__name {
88
+ font-size: 1.125rem;
89
+ font-weight: 600;
90
+ margin: 0 0 12px;
91
+ color: var(--x-color-text, #2c3e50);
92
+ }
93
+
94
+ .x-contacts__info {
95
+ margin: 0 0 8px;
96
+ font-size: 0.9rem;
97
+ color: var(--x-color-text, #2c3e50);
98
+ }
99
+
100
+ .x-contacts__info a {
101
+ color: var(--x-color-primary, #1e73be);
102
+ text-decoration: none;
103
+ }
104
+
105
+ .x-contacts__hours {
106
+ margin: 8px 0 0;
107
+ font-size: 0.85rem;
108
+ color: var(--x-color-text-light, #6b7c93);
109
+ }
110
+ </style>
@@ -0,0 +1,152 @@
1
+ <template>
2
+ <section class="x-hero" :class="{ 'x-hero--fullscreen': variant === 'fullscreen' }" :style="bgStyle">
3
+ <div v-if="data.image && data.overlay !== false" class="x-hero__overlay" />
4
+ <div class="x-hero__content">
5
+ <h1 v-if="data.title" class="x-hero__title">{{ data.title }}</h1>
6
+ <p v-if="data.subtitle" class="x-hero__subtitle">{{ data.subtitle }}</p>
7
+
8
+ <div v-if="data.buttons?.length" class="x-hero__buttons">
9
+ <a
10
+ v-for="(btn, i) in data.buttons"
11
+ :key="i"
12
+ :href="btn.url"
13
+ class="x-btn"
14
+ :class="`x-btn--${btn.variant || 'primary'}`"
15
+ >
16
+ {{ btn.text }}
17
+ </a>
18
+ </div>
19
+
20
+ <div v-if="data.stats?.length" class="x-hero__stats">
21
+ <div v-for="(stat, i) in data.stats" :key="i" class="x-hero__stat">
22
+ <span class="x-hero__stat-value">{{ stat.value }}</span>
23
+ <span class="x-hero__stat-label">{{ stat.label }}</span>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </section>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed } from 'vue';
32
+
33
+ import type { HeroBlockData } from '../../types/blocks.js';
34
+
35
+ const props = withDefaults(
36
+ defineProps<{
37
+ data: HeroBlockData;
38
+ variant?: 'fullscreen' | 'compact' | 'centered';
39
+ }>(),
40
+ { variant: 'fullscreen' },
41
+ );
42
+
43
+ const bgStyle = computed(() => {
44
+ if (!props.data.image) return {};
45
+ return { backgroundImage: `url(${props.data.image})` };
46
+ });
47
+ </script>
48
+
49
+ <style scoped>
50
+ .x-hero {
51
+ position: relative;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ min-height: 400px;
56
+ padding: 120px 24px;
57
+ background: linear-gradient(135deg, var(--x-color-primary, #1e73be) 0%, var(--x-color-secondary, #2c3e50) 100%);
58
+ background-size: cover;
59
+ background-position: center;
60
+ color: white;
61
+ text-align: center;
62
+ }
63
+
64
+ .x-hero--fullscreen {
65
+ min-height: 100vh;
66
+ }
67
+
68
+ .x-hero__overlay {
69
+ position: absolute;
70
+ inset: 0;
71
+ background: rgba(0, 0, 0, 0.5);
72
+ }
73
+
74
+ .x-hero__content {
75
+ position: relative;
76
+ max-width: var(--x-max-width, 1200px);
77
+ z-index: 1;
78
+ }
79
+
80
+ .x-hero__title {
81
+ font-family: var(--x-font-heading, var(--x-font-family, inherit));
82
+ font-size: clamp(2rem, 5vw, 3.5rem);
83
+ font-weight: 800;
84
+ margin: 0 0 16px;
85
+ }
86
+
87
+ .x-hero__subtitle {
88
+ font-size: clamp(1rem, 2vw, 1.25rem);
89
+ opacity: 0.9;
90
+ max-width: 600px;
91
+ margin: 0 auto 32px;
92
+ }
93
+
94
+ .x-hero__buttons {
95
+ display: flex;
96
+ gap: 16px;
97
+ justify-content: center;
98
+ flex-wrap: wrap;
99
+ margin-bottom: 48px;
100
+ }
101
+
102
+ .x-hero__stats {
103
+ display: flex;
104
+ gap: 48px;
105
+ justify-content: center;
106
+ flex-wrap: wrap;
107
+ }
108
+
109
+ .x-hero__stat {
110
+ display: flex;
111
+ flex-direction: column;
112
+ }
113
+
114
+ .x-hero__stat-value {
115
+ font-size: 2rem;
116
+ font-weight: 700;
117
+ }
118
+
119
+ .x-hero__stat-label {
120
+ font-size: 0.875rem;
121
+ opacity: 0.8;
122
+ }
123
+
124
+ .x-btn {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ padding: 12px 32px;
128
+ border-radius: var(--x-border-radius, 8px);
129
+ font-weight: 600;
130
+ text-decoration: none;
131
+ transition: all 0.2s;
132
+ cursor: pointer;
133
+ border: 2px solid transparent;
134
+ }
135
+
136
+ .x-btn--primary {
137
+ background: white;
138
+ color: var(--x-color-primary, #1e73be);
139
+ }
140
+
141
+ .x-btn--outline {
142
+ border-color: white;
143
+ color: white;
144
+ background: transparent;
145
+ }
146
+
147
+ .x-btn--white {
148
+ background: rgba(255, 255, 255, 0.15);
149
+ color: white;
150
+ border-color: rgba(255, 255, 255, 0.3);
151
+ }
152
+ </style>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <section class="x-html-block">
3
+ <div class="x-html-block__content" v-html="data.content" />
4
+ </section>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { HtmlBlockData } from '../../types/blocks.js';
9
+
10
+ defineProps<{
11
+ data: HtmlBlockData;
12
+ }>();
13
+ </script>
14
+
15
+ <style scoped>
16
+ .x-html-block {
17
+ padding: 48px 24px;
18
+ }
19
+
20
+ .x-html-block__content {
21
+ max-width: 800px;
22
+ margin: 0 auto;
23
+ font-family: var(--x-font-family, inherit);
24
+ color: var(--x-color-text, #2c3e50);
25
+ line-height: 1.8;
26
+ }
27
+
28
+ .x-html-block__content :deep(h2) {
29
+ font-size: 1.75rem;
30
+ font-weight: 700;
31
+ margin: 32px 0 16px;
32
+ color: var(--x-color-text, #2c3e50);
33
+ }
34
+
35
+ .x-html-block__content :deep(h3) {
36
+ font-size: 1.25rem;
37
+ font-weight: 600;
38
+ margin: 24px 0 12px;
39
+ }
40
+
41
+ .x-html-block__content :deep(p) {
42
+ margin: 0 0 16px;
43
+ }
44
+
45
+ .x-html-block__content :deep(ul),
46
+ .x-html-block__content :deep(ol) {
47
+ padding-left: 24px;
48
+ margin: 0 0 16px;
49
+ }
50
+
51
+ .x-html-block__content :deep(img) {
52
+ max-width: 100%;
53
+ border-radius: var(--x-border-radius, 8px);
54
+ margin: 16px 0;
55
+ }
56
+
57
+ .x-html-block__content :deep(blockquote) {
58
+ border-left: 4px solid var(--x-color-primary, #1e73be);
59
+ margin: 16px 0;
60
+ padding: 12px 24px;
61
+ background: var(--x-color-background-alt, #f7f9fc);
62
+ border-radius: 0 var(--x-border-radius, 8px) var(--x-border-radius, 8px) 0;
63
+ }
64
+ </style>
@@ -0,0 +1,101 @@
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>