@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/dist/index.js +143 -1101
- package/package.json +1 -1
- package/src/composables/useLivePreview.ts +3 -0
- package/src/index.ts +2 -20
- package/src/types/blocks.ts +1 -1
- package/src/types/config.ts +15 -0
- package/src/worker/types.ts +6 -0
- package/dist/index.css +0 -1
- package/src/components/blocks/XBlockRenderer.vue +0 -48
- package/src/components/blocks/XCardsBlock.vue +0 -167
- package/src/components/blocks/XContactsBlock.vue +0 -110
- package/src/components/blocks/XGalleryBlock.vue +0 -56
- package/src/components/blocks/XHeroBlock.vue +0 -152
- package/src/components/blocks/XHtmlBlock.vue +0 -64
- package/src/components/blocks/XImageTextBlock.vue +0 -101
- package/src/components/blocks/XMapBlock.vue +0 -52
- package/src/components/blocks/XPricingBlock.vue +0 -261
- package/src/components/common/XLocaleSwitcher.vue +0 -56
- package/src/components/layout/XSiteFooter.vue +0 -64
- package/src/components/layout/XSiteLayout.vue +0 -35
- package/src/components/layout/XSiteNav.vue +0 -184
package/package.json
CHANGED
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';
|
package/src/types/blocks.ts
CHANGED
|
@@ -82,7 +82,7 @@ export interface ContactsBlockData {
|
|
|
82
82
|
|
|
83
83
|
export interface GalleryBlockData {
|
|
84
84
|
title?: string;
|
|
85
|
-
images: Array<{
|
|
85
|
+
images: Array<{ url: string; alt?: string; caption?: string }>;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export interface MapBlockData {
|
package/src/types/config.ts
CHANGED
|
@@ -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;
|
package/src/worker/types.ts
CHANGED
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>
|