@xosen/site-sdk 0.0.6 → 0.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xosen/site-sdk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Shared Vue components and composables for Xosen site templates",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,9 @@ export interface PreviewMessage {
9
9
  title?: string;
10
10
  blocks?: any[];
11
11
  meta?: Record<string, any>;
12
+ layout?: string;
13
+ /** Page-level context fields (author, featuredImage, excerpt, etc.) */
14
+ [key: string]: any;
12
15
  };
13
16
  }
14
17
 
package/src/index.ts CHANGED
@@ -1,22 +1,3 @@
1
- // Layout
2
- export { default as XSiteLayout } from './components/layout/XSiteLayout.vue';
3
- export { default as XSiteNav } from './components/layout/XSiteNav.vue';
4
- export { default as XSiteFooter } from './components/layout/XSiteFooter.vue';
5
-
6
- // Blocks
7
- export { default as XBlockRenderer } from './components/blocks/XBlockRenderer.vue';
8
- export { default as XHeroBlock } from './components/blocks/XHeroBlock.vue';
9
- export { default as XHtmlBlock } from './components/blocks/XHtmlBlock.vue';
10
- export { default as XCardsBlock } from './components/blocks/XCardsBlock.vue';
11
- export { default as XImageTextBlock } from './components/blocks/XImageTextBlock.vue';
12
- export { default as XPricingBlock } from './components/blocks/XPricingBlock.vue';
13
- export { default as XContactsBlock } from './components/blocks/XContactsBlock.vue';
14
- export { default as XGalleryBlock } from './components/blocks/XGalleryBlock.vue';
15
- export { default as XMapBlock } from './components/blocks/XMapBlock.vue';
16
-
17
- // Common
18
- export { default as XLocaleSwitcher } from './components/common/XLocaleSwitcher.vue';
19
-
20
1
  // Composables
21
2
  export { useSiteData } from './composables/useSiteData.js';
22
3
  export { useSiteConfig } from './composables/useSiteConfig.js';
@@ -44,5 +25,6 @@ export type {
44
25
  GalleryBlockData,
45
26
  MapBlockData,
46
27
  } from './types/blocks.js';
47
- export type { SiteConfig, NavItem, PageConfig, SiteData } from './types/config.js';
28
+ export type { SiteConfig, NavItem, PageConfig, SiteData, PageContext } from './types/config.js';
29
+ export { PAGE_CONTEXT_KEY } from './types/config.js';
48
30
  export type { Skin } from './types/skin.js';
@@ -82,7 +82,7 @@ export interface ContactsBlockData {
82
82
 
83
83
  export interface GalleryBlockData {
84
84
  title?: string;
85
- images: Array<{ src: string; alt?: string; caption?: string }>;
85
+ images: Array<{ url: string; alt?: string; caption?: string }>;
86
86
  }
87
87
 
88
88
  export interface MapBlockData {
@@ -1,5 +1,20 @@
1
+ import type { InjectionKey, Ref } from 'vue';
1
2
  import type { Block } from './blocks.js';
2
3
 
4
+ export interface PageContext {
5
+ title?: string;
6
+ type?: string;
7
+ author?: string;
8
+ featuredImage?: string;
9
+ excerpt?: string;
10
+ isGated?: boolean;
11
+ termIds?: string[];
12
+ layout?: string;
13
+ publishedAt?: string;
14
+ }
15
+
16
+ export const PAGE_CONTEXT_KEY: InjectionKey<Ref<PageContext>> = Symbol('pageContext');
17
+
3
18
  export interface SiteConfig {
4
19
  template: string;
5
20
  version: string;
@@ -36,4 +36,10 @@ export interface PageData {
36
36
  ogTitle?: string;
37
37
  ogImage?: string;
38
38
  };
39
+ type?: string;
40
+ author?: string;
41
+ featuredImage?: string;
42
+ excerpt?: string;
43
+ termIds?: string[];
44
+ layout?: string;
39
45
  }
package/dist/index.css DELETED
@@ -1 +0,0 @@
1
- .x-locale-switcher[data-v-675f0bbd]{display:flex;gap:4px}.x-locale-switcher__btn[data-v-675f0bbd]{padding:4px 8px;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer;font-size:.75rem;font-weight:600;opacity:.6;transition:all .2s}.x-locale-switcher__btn--active[data-v-675f0bbd]{opacity:1;border-color:currentColor}.x-locale-switcher__btn[data-v-675f0bbd]:hover{opacity:1}.x-nav[data-v-07c4b748]{position:fixed;top:0;left:0;right:0;z-index:100;transition:background .3s,box-shadow .3s}.x-nav--scrolled[data-v-07c4b748]{background:#fffffff2;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:0 2px 8px #00000014}.x-nav__container[data-v-07c4b748]{max-width:var(--x-max-width, 1200px);margin:0 auto;padding:0 24px;height:64px;display:flex;align-items:center;justify-content:space-between}.x-nav__logo[data-v-07c4b748]{text-decoration:none;display:flex;align-items:center}.x-nav__logo-img[data-v-07c4b748]{height:36px}.x-nav__logo-text[data-v-07c4b748]{font-size:1.25rem;font-weight:700;color:var(--x-color-text, #2c3e50)}.x-nav--scrolled .x-nav__logo-text[data-v-07c4b748]{color:var(--x-color-text, #2c3e50)}.x-nav:not(.x-nav--scrolled) .x-nav__logo-text[data-v-07c4b748]{color:#fff}.x-nav__links[data-v-07c4b748]{display:flex;align-items:center;gap:24px}.x-nav__link[data-v-07c4b748]{text-decoration:none;font-weight:500;font-size:.9rem;transition:color .2s}.x-nav--scrolled .x-nav__link[data-v-07c4b748]{color:var(--x-color-text, #2c3e50)}.x-nav:not(.x-nav--scrolled) .x-nav__link[data-v-07c4b748]{color:#ffffffe6}.x-nav__link[data-v-07c4b748]:hover{color:var(--x-color-primary, #1e73be)}.x-nav__hamburger[data-v-07c4b748]{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:4px}.x-nav__hamburger span[data-v-07c4b748]{width:24px;height:2px;background:var(--x-color-text, #2c3e50);transition:all .2s}.x-nav:not(.x-nav--scrolled) .x-nav__hamburger span[data-v-07c4b748]{background:#fff}@media(max-width:768px){.x-nav__hamburger[data-v-07c4b748]{display:flex}.x-nav__links[data-v-07c4b748]{display:none;position:fixed;top:64px;right:0;width:280px;height:calc(100vh - 64px);background:var(--x-color-surface, #fff);flex-direction:column;padding:24px;box-shadow:-4px 0 16px #0000001a;align-items:flex-start}.x-nav__links--open[data-v-07c4b748]{display:flex}.x-nav__links .x-nav__link[data-v-07c4b748]{color:var(--x-color-text, #2c3e50)!important;padding:8px 0}}.x-footer[data-v-f9b38381]{background:var(--x-color-text, #2c3e50);color:#ffffffb3;padding:32px 24px;text-align:center}.x-footer__container[data-v-f9b38381]{max-width:var(--x-max-width, 1200px);margin:0 auto}.x-footer__links[data-v-f9b38381]{display:flex;gap:24px;justify-content:center;margin-bottom:16px;flex-wrap:wrap}.x-footer__link[data-v-f9b38381]{color:#ffffffb3;text-decoration:none;font-size:.9rem}.x-footer__link[data-v-f9b38381]:hover{color:#fff}.x-footer__copyright[data-v-f9b38381]{font-size:.85rem;margin:0;opacity:.6}.x-site-layout[data-v-41b0f2a9]{min-height:100vh;display:flex;flex-direction:column;font-family:var(--x-font-family, "Inter", sans-serif);color:var(--x-color-text, #2c3e50);background:var(--x-color-background, #ffffff)}.x-site-layout__main[data-v-41b0f2a9]{flex:1}.x-hero[data-v-d8cfe56f]{position:relative;display:flex;align-items:center;justify-content:center;min-height:400px;padding:120px 24px;background:linear-gradient(135deg,var(--x-color-primary, #1e73be) 0%,var(--x-color-secondary, #2c3e50) 100%);background-size:cover;background-position:center;color:#fff;text-align:center}.x-hero--fullscreen[data-v-d8cfe56f]{min-height:100vh}.x-hero__overlay[data-v-d8cfe56f]{position:absolute;inset:0;background:#00000080}.x-hero__content[data-v-d8cfe56f]{position:relative;max-width:var(--x-max-width, 1200px);z-index:1}.x-hero__title[data-v-d8cfe56f]{font-family:var(--x-font-heading, var(--x-font-family, inherit));font-size:clamp(2rem,5vw,3.5rem);font-weight:800;margin:0 0 16px}.x-hero__subtitle[data-v-d8cfe56f]{font-size:clamp(1rem,2vw,1.25rem);opacity:.9;max-width:600px;margin:0 auto 32px}.x-hero__buttons[data-v-d8cfe56f]{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-bottom:48px}.x-hero__stats[data-v-d8cfe56f]{display:flex;gap:48px;justify-content:center;flex-wrap:wrap}.x-hero__stat[data-v-d8cfe56f]{display:flex;flex-direction:column}.x-hero__stat-value[data-v-d8cfe56f]{font-size:2rem;font-weight:700}.x-hero__stat-label[data-v-d8cfe56f]{font-size:.875rem;opacity:.8}.x-btn[data-v-d8cfe56f]{display:inline-flex;align-items:center;padding:12px 32px;border-radius:var(--x-border-radius, 8px);font-weight:600;text-decoration:none;transition:all .2s;cursor:pointer;border:2px solid transparent}.x-btn--primary[data-v-d8cfe56f]{background:#fff;color:var(--x-color-primary, #1e73be)}.x-btn--outline[data-v-d8cfe56f]{border-color:#fff;color:#fff;background:transparent}.x-btn--white[data-v-d8cfe56f]{background:#ffffff26;color:#fff;border-color:#ffffff4d}.x-html-block[data-v-abfd69e8]{padding:48px 24px}.x-html-block__content[data-v-abfd69e8]{max-width:800px;margin:0 auto;font-family:var(--x-font-family, inherit);color:var(--x-color-text, #2c3e50);line-height:1.8}.x-html-block__content[data-v-abfd69e8] h2{font-size:1.75rem;font-weight:700;margin:32px 0 16px;color:var(--x-color-text, #2c3e50)}.x-html-block__content[data-v-abfd69e8] h3{font-size:1.25rem;font-weight:600;margin:24px 0 12px}.x-html-block__content[data-v-abfd69e8] p{margin:0 0 16px}.x-html-block__content[data-v-abfd69e8] ul,.x-html-block__content[data-v-abfd69e8] ol{padding-left:24px;margin:0 0 16px}.x-html-block__content[data-v-abfd69e8] img{max-width:100%;border-radius:var(--x-border-radius, 8px);margin:16px 0}.x-html-block__content[data-v-abfd69e8] blockquote{border-left:4px solid var(--x-color-primary, #1e73be);margin:16px 0;padding:12px 24px;background:var(--x-color-background-alt, #f7f9fc);border-radius:0 var(--x-border-radius, 8px) var(--x-border-radius, 8px) 0}.x-cards-block[data-v-a1505674]{padding:80px 24px}.x-cards-block__container[data-v-a1505674]{max-width:var(--x-max-width, 1200px);margin:0 auto}.x-cards-block__title[data-v-a1505674]{text-align:center;font-size:2rem;font-weight:700;color:var(--x-color-text, #2c3e50);margin:0 0 8px}.x-cards-block__subtitle[data-v-a1505674]{text-align:center;color:var(--x-color-text-light, #6b7c93);margin:0 0 48px}.x-cards-block__grid[data-v-a1505674]{display:grid;grid-template-columns:repeat(var(--columns, 4),1fr);gap:24px}@media(max-width:768px){.x-cards-block__grid[data-v-a1505674]{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}}.x-card[data-v-a1505674]{background:var(--x-color-surface, #fff);border:1px solid var(--x-color-border, #e2e8f0);border-radius:var(--x-border-radius, 12px);padding:32px 24px;text-align:center;text-decoration:none;color:inherit;transition:transform .2s,box-shadow .2s}.x-card[data-v-a1505674]:hover{transform:translateY(-4px);box-shadow:0 8px 24px #00000014}.x-card__icon[data-v-a1505674]{font-size:2.5rem;margin-bottom:16px;width:64px;height:64px;display:flex;align-items:center;justify-content:center;margin-left:auto;margin-right:auto;background:var(--x-color-background-alt, #f7f9fc);border-radius:50%}.x-card__image[data-v-a1505674]{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:calc(var(--x-border-radius, 12px) - 4px);margin-bottom:16px}.x-card__title[data-v-a1505674]{font-size:1.125rem;font-weight:600;color:var(--x-color-text, #2c3e50);margin:0 0 8px}.x-card__desc[data-v-a1505674]{font-size:.9rem;color:var(--x-color-text-light, #6b7c93);line-height:1.6;margin:0}.x-image-text[data-v-5ca1e218]{padding:80px 24px}.x-image-text__container[data-v-5ca1e218]{max-width:var(--x-max-width, 1200px);margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:center}.x-image-text--reverse .x-image-text__container[data-v-5ca1e218]{direction:rtl}.x-image-text--reverse .x-image-text__text[data-v-5ca1e218]{direction:ltr}.x-image-text__title[data-v-5ca1e218]{font-size:2rem;font-weight:700;color:var(--x-color-text, #2c3e50);margin:0 0 16px}.x-image-text__content[data-v-5ca1e218]{color:var(--x-color-text, #2c3e50);line-height:1.8}.x-image-text__checklist[data-v-5ca1e218]{list-style:none;padding:0;margin:0}.x-image-text__checklist li[data-v-5ca1e218]{display:flex;align-items:center;gap:12px;padding:8px 0;font-size:1rem;color:var(--x-color-text, #2c3e50)}.x-image-text__check-icon[data-v-5ca1e218]{color:var(--x-color-success, #00d084);font-weight:700}.x-image-text__image[data-v-5ca1e218]{width:100%;border-radius:var(--x-border-radius, 12px)}@media(max-width:768px){.x-image-text__container[data-v-5ca1e218]{grid-template-columns:1fr}.x-image-text--reverse .x-image-text__container[data-v-5ca1e218]{direction:ltr}}.x-pricing[data-v-bd1e5d79]{padding:80px 24px;background:var(--x-color-background-alt, #f7f9fc)}.x-pricing__container[data-v-bd1e5d79]{max-width:var(--x-max-width, 1200px);margin:0 auto}.x-pricing__title[data-v-bd1e5d79]{text-align:center;font-size:2rem;font-weight:700;color:var(--x-color-text, #2c3e50);margin:0 0 8px}.x-pricing__subtitle[data-v-bd1e5d79]{text-align:center;color:var(--x-color-text-light, #6b7c93);margin:0 0 32px}.x-pricing__tabs[data-v-bd1e5d79]{display:flex;justify-content:center;gap:8px;margin-bottom:32px}.x-pricing__tab[data-v-bd1e5d79]{padding:8px 24px;border:2px solid var(--x-color-border, #e2e8f0);border-radius:var(--x-border-radius, 8px);background:transparent;cursor:pointer;font-weight:500;transition:all .2s}.x-pricing__tab--active[data-v-bd1e5d79]{background:var(--x-color-primary, #1e73be);border-color:var(--x-color-primary, #1e73be);color:#fff}.x-pricing__grid[data-v-bd1e5d79]{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:24px;align-items:start}.x-pricing-card[data-v-bd1e5d79]{background:var(--x-color-surface, #fff);border:1px solid var(--x-color-border, #e2e8f0);border-radius:var(--x-border-radius, 12px);padding:32px;text-align:center;transition:transform .2s}.x-pricing-card--featured[data-v-bd1e5d79]{transform:scale(1.04);border-color:var(--x-color-primary, #1e73be);box-shadow:0 8px 32px #0000001a}.x-pricing-card__name[data-v-bd1e5d79]{font-size:1.25rem;font-weight:600;margin:0 0 8px}.x-pricing-card__desc[data-v-bd1e5d79]{color:var(--x-color-text-light, #6b7c93);font-size:.9rem;margin:0 0 16px}.x-pricing-card__price[data-v-bd1e5d79]{margin:16px 0}.x-pricing-card__amount[data-v-bd1e5d79]{font-size:2.5rem;font-weight:800;color:var(--x-color-primary, #1e73be)}.x-pricing-card__period[data-v-bd1e5d79]{color:var(--x-color-text-light, #6b7c93)}.x-pricing-card__features[data-v-bd1e5d79]{list-style:none;padding:0;margin:0 0 24px;text-align:left}.x-pricing-card__features li[data-v-bd1e5d79]{padding:6px 0;border-bottom:1px solid var(--x-color-border, #e2e8f0);font-size:.9rem}.x-pricing-card__features li[data-v-bd1e5d79]:before{content:"✓ ";color:var(--x-color-success, #00d084);font-weight:700}.x-pricing-card__action[data-v-bd1e5d79]{width:100%;justify-content:center}.x-btn[data-v-bd1e5d79]{display:inline-flex;align-items:center;padding:12px 32px;border-radius:var(--x-border-radius, 8px);font-weight:600;text-decoration:none;transition:all .2s;cursor:pointer}.x-btn--primary[data-v-bd1e5d79]{background:var(--x-color-primary, #1e73be);color:#fff}.x-contacts[data-v-e873ba26]{padding:80px 24px}.x-contacts__container[data-v-e873ba26]{max-width:var(--x-max-width, 1200px);margin:0 auto}.x-contacts__title[data-v-e873ba26]{text-align:center;font-size:2rem;font-weight:700;color:var(--x-color-text, #2c3e50);margin:0 0 48px}.x-contacts__grid[data-v-e873ba26]{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:24px}.x-contacts__card[data-v-e873ba26]{background:var(--x-color-surface, #fff);border:1px solid var(--x-color-border, #e2e8f0);border-radius:var(--x-border-radius, 12px);padding:24px}.x-contacts__card--main[data-v-e873ba26]{border-color:var(--x-color-primary, #1e73be);background:var(--x-color-primary-light, #e8f2fc)}.x-contacts__name[data-v-e873ba26]{font-size:1.125rem;font-weight:600;margin:0 0 12px;color:var(--x-color-text, #2c3e50)}.x-contacts__info[data-v-e873ba26]{margin:0 0 8px;font-size:.9rem;color:var(--x-color-text, #2c3e50)}.x-contacts__info a[data-v-e873ba26]{color:var(--x-color-primary, #1e73be);text-decoration:none}.x-contacts__hours[data-v-e873ba26]{margin:8px 0 0;font-size:.85rem;color:var(--x-color-text-light, #6b7c93)}.x-gallery-block[data-v-b99bb987]{padding:48px 24px}.x-gallery-block__title[data-v-b99bb987]{text-align:center;font-size:1.75rem;font-weight:700;margin-bottom:32px;color:var(--x-color-text, #2c3e50)}.x-gallery-block__grid[data-v-b99bb987]{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:16px;max-width:1200px;margin:0 auto}.x-gallery-block__item img[data-v-b99bb987]{width:100%;height:200px;object-fit:cover;border-radius:var(--x-border-radius, 8px);display:block}.x-gallery-block__item figcaption[data-v-b99bb987]{text-align:center;font-size:.875rem;color:var(--x-color-text-secondary, #6c757d);margin-top:8px}.x-map-block[data-v-583f4762]{padding:48px 24px}.x-map-block__title[data-v-583f4762]{text-align:center;font-size:1.75rem;font-weight:700;margin-bottom:32px;color:var(--x-color-text, #2c3e50)}.x-map-block__container[data-v-583f4762]{max-width:1200px;margin:0 auto;border-radius:var(--x-border-radius, 8px);overflow:hidden}
@@ -1,48 +0,0 @@
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
- import XGalleryBlock from './XGalleryBlock.vue';
26
- import XMapBlock from './XMapBlock.vue';
27
-
28
- const props = defineProps<{
29
- blocks: Block[];
30
- }>();
31
-
32
- const blockRegistry: Record<string, Component> = {
33
- hero: XHeroBlock,
34
- html: XHtmlBlock,
35
- cards: XCardsBlock,
36
- 'image-text': XImageTextBlock,
37
- pricing: XPricingBlock,
38
- contacts: XContactsBlock,
39
- gallery: XGalleryBlock,
40
- map: XMapBlock,
41
- };
42
-
43
- function getBlockComponent(type: string): Component | undefined {
44
- return blockRegistry[type];
45
- }
46
-
47
- const visibleBlocks = computed(() => props.blocks.filter((b) => b.settings?.visible !== false));
48
- </script>
@@ -1,167 +0,0 @@
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">
22
- {{ iconDisplay(card.icon) }}
23
- </div>
24
- <h3 class="x-card__title">{{ card.title }}</h3>
25
- <p class="x-card__desc">{{ card.desc }}</p>
26
- </component>
27
- </div>
28
- </div>
29
- </section>
30
- </template>
31
-
32
- <script setup lang="ts">
33
- import { computed } from 'vue';
34
-
35
- import type { CardsBlockData } from '../../types/blocks.js';
36
-
37
- const props = withDefaults(
38
- defineProps<{
39
- data: CardsBlockData;
40
- variant?: 'default' | 'icon-cards' | 'image-cards';
41
- columns?: 2 | 3 | 4;
42
- }>(),
43
- { variant: 'default', columns: undefined },
44
- );
45
-
46
- const gridStyle = computed(() => {
47
- const cols = props.columns || Math.min(props.data.cards.length, 4);
48
- return { '--columns': cols };
49
- });
50
-
51
- const iconStyle = { color: 'var(--x-color-primary, #1e73be)' };
52
-
53
- // Map mdi-* icon names to emoji, or pass through if already an emoji/character
54
- const iconMap: Record<string, string> = {
55
- 'mdi-lightbulb': '💡',
56
- 'mdi-code-braces': '⚙️',
57
- 'mdi-headset': '🎧',
58
- 'mdi-check-circle': '✅',
59
- 'mdi-shield': '🛡️',
60
- 'mdi-chart-line': '📈',
61
- 'mdi-rocket': '🚀',
62
- 'mdi-heart': '❤️',
63
- 'mdi-star': '⭐',
64
- 'mdi-cog': '⚙️',
65
- 'mdi-email': '📧',
66
- 'mdi-phone': '📞',
67
- 'mdi-map-marker': '📍',
68
- 'mdi-clock': '🕐',
69
- 'mdi-account': '👤',
70
- 'mdi-team': '👥',
71
- };
72
-
73
- function iconDisplay(icon: string): string {
74
- return iconMap[icon] || icon;
75
- }
76
- </script>
77
-
78
- <style scoped>
79
- .x-cards-block {
80
- padding: 80px 24px;
81
- }
82
-
83
- .x-cards-block__container {
84
- max-width: var(--x-max-width, 1200px);
85
- margin: 0 auto;
86
- }
87
-
88
- .x-cards-block__title {
89
- text-align: center;
90
- font-size: 2rem;
91
- font-weight: 700;
92
- color: var(--x-color-text, #2c3e50);
93
- margin: 0 0 8px;
94
- }
95
-
96
- .x-cards-block__subtitle {
97
- text-align: center;
98
- color: var(--x-color-text-light, #6b7c93);
99
- margin: 0 0 48px;
100
- }
101
-
102
- .x-cards-block__grid {
103
- display: grid;
104
- grid-template-columns: repeat(var(--columns, 4), 1fr);
105
- gap: 24px;
106
- }
107
-
108
- @media (max-width: 768px) {
109
- .x-cards-block__grid {
110
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
111
- }
112
- }
113
-
114
- .x-card {
115
- background: var(--x-color-surface, #fff);
116
- border: 1px solid var(--x-color-border, #e2e8f0);
117
- border-radius: var(--x-border-radius, 12px);
118
- padding: 32px 24px;
119
- text-align: center;
120
- text-decoration: none;
121
- color: inherit;
122
- transition:
123
- transform 0.2s,
124
- box-shadow 0.2s;
125
- }
126
-
127
- .x-card:hover {
128
- transform: translateY(-4px);
129
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
130
- }
131
-
132
- .x-card__icon {
133
- font-size: 2.5rem;
134
- margin-bottom: 16px;
135
- width: 64px;
136
- height: 64px;
137
- display: flex;
138
- align-items: center;
139
- justify-content: center;
140
- margin-left: auto;
141
- margin-right: auto;
142
- background: var(--x-color-background-alt, #f7f9fc);
143
- border-radius: 50%;
144
- }
145
-
146
- .x-card__image {
147
- width: 100%;
148
- aspect-ratio: 16/9;
149
- object-fit: cover;
150
- border-radius: calc(var(--x-border-radius, 12px) - 4px);
151
- margin-bottom: 16px;
152
- }
153
-
154
- .x-card__title {
155
- font-size: 1.125rem;
156
- font-weight: 600;
157
- color: var(--x-color-text, #2c3e50);
158
- margin: 0 0 8px;
159
- }
160
-
161
- .x-card__desc {
162
- font-size: 0.9rem;
163
- color: var(--x-color-text-light, #6b7c93);
164
- line-height: 1.6;
165
- margin: 0;
166
- }
167
- </style>
@@ -1,110 +0,0 @@
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>
@@ -1,56 +0,0 @@
1
- <template>
2
- <section class="x-gallery-block">
3
- <h2 v-if="data.title" class="x-gallery-block__title">{{ data.title }}</h2>
4
- <div class="x-gallery-block__grid">
5
- <figure v-for="(img, i) in data.images" :key="i" class="x-gallery-block__item">
6
- <img :src="img.src" :alt="img.alt || ''" loading="lazy" />
7
- <figcaption v-if="img.caption">{{ img.caption }}</figcaption>
8
- </figure>
9
- </div>
10
- </section>
11
- </template>
12
-
13
- <script setup lang="ts">
14
- import type { GalleryBlockData } from '../../types/blocks.js';
15
-
16
- defineProps<{
17
- data: GalleryBlockData;
18
- }>();
19
- </script>
20
-
21
- <style scoped>
22
- .x-gallery-block {
23
- padding: 48px 24px;
24
- }
25
-
26
- .x-gallery-block__title {
27
- text-align: center;
28
- font-size: 1.75rem;
29
- font-weight: 700;
30
- margin-bottom: 32px;
31
- color: var(--x-color-text, #2c3e50);
32
- }
33
-
34
- .x-gallery-block__grid {
35
- display: grid;
36
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
37
- gap: 16px;
38
- max-width: 1200px;
39
- margin: 0 auto;
40
- }
41
-
42
- .x-gallery-block__item img {
43
- width: 100%;
44
- height: 200px;
45
- object-fit: cover;
46
- border-radius: var(--x-border-radius, 8px);
47
- display: block;
48
- }
49
-
50
- .x-gallery-block__item figcaption {
51
- text-align: center;
52
- font-size: 0.875rem;
53
- color: var(--x-color-text-secondary, #6c757d);
54
- margin-top: 8px;
55
- }
56
- </style>
@@ -1,152 +0,0 @@
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>