@mundogamernetwork/shared-ui 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ <script setup lang="ts">
2
+ type MediaKitLink = { label: string; url: string };
3
+
4
+ const props = withDefaults(defineProps<{ links?: MediaKitLink[] }>(), {
5
+ links: () => [],
6
+ });
7
+ const emit = defineEmits<{ update: [key: 'custom_links', value: MediaKitLink[]] }>();
8
+
9
+ const draft = ref<MediaKitLink[]>([]);
10
+ const saved = ref(false);
11
+
12
+ watch(
13
+ () => props.links,
14
+ (value) => {
15
+ draft.value = (value || []).map(link => ({ label: link.label || '', url: link.url || '' }));
16
+ },
17
+ { immediate: true, deep: true },
18
+ );
19
+
20
+ const addLink = () => {
21
+ if (draft.value.length < 12) draft.value.push({ label: '', url: '' });
22
+ };
23
+
24
+ const removeLink = (index: number) => draft.value.splice(index, 1);
25
+
26
+ const move = (index: number, direction: -1 | 1) => {
27
+ const target = index + direction;
28
+ if (target < 0 || target >= draft.value.length) return;
29
+ const [link] = draft.value.splice(index, 1);
30
+ draft.value.splice(target, 0, link);
31
+ };
32
+
33
+ const canSave = computed(() => draft.value.every(link => {
34
+ if (!link.label.trim() || !link.url.trim()) return false;
35
+ try {
36
+ const parsed = new URL(link.url);
37
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
38
+ } catch {
39
+ return false;
40
+ }
41
+ }));
42
+
43
+ const save = () => {
44
+ if (!canSave.value) return;
45
+ emit('update', 'custom_links', draft.value.map(link => ({
46
+ label: link.label.trim(),
47
+ url: link.url.trim(),
48
+ })));
49
+ saved.value = true;
50
+ setTimeout(() => { saved.value = false; }, 1800);
51
+ };
52
+ </script>
53
+
54
+ <template>
55
+ <div class="mk-links-editor">
56
+ <div class="mk-links-head">
57
+ <div>
58
+ <h3>{{ $t('media_kit.links.title') }}</h3>
59
+ <p>{{ $t('media_kit.links.description') }}</p>
60
+ </div>
61
+ <button type="button" class="mk-link-add" :disabled="draft.length >= 12" @click="addLink">
62
+ + {{ $t('media_kit.links.add') }}
63
+ </button>
64
+ </div>
65
+
66
+ <div v-if="draft.length" class="mk-link-list">
67
+ <div v-for="(link, index) in draft" :key="index" class="mk-link-row">
68
+ <span class="mk-link-number">{{ index + 1 }}</span>
69
+ <label>
70
+ <span>{{ $t('media_kit.links.label') }}</span>
71
+ <input v-model="link.label" type="text" maxlength="80" :placeholder="$t('media_kit.links.label_placeholder')" />
72
+ </label>
73
+ <label class="mk-link-url">
74
+ <span>{{ $t('media_kit.links.url') }}</span>
75
+ <input v-model="link.url" type="url" maxlength="2048" placeholder="https://" />
76
+ </label>
77
+ <div class="mk-link-actions">
78
+ <button type="button" :disabled="index === 0" :aria-label="$t('media_kit.links.move_up')" @click="move(index, -1)">↑</button>
79
+ <button type="button" :disabled="index === draft.length - 1" :aria-label="$t('media_kit.links.move_down')" @click="move(index, 1)">↓</button>
80
+ <button type="button" class="danger" :aria-label="$t('media_kit.links.remove')" @click="removeLink(index)">×</button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <p v-else class="mk-links-empty">{{ $t('media_kit.links.empty') }}</p>
85
+
86
+ <div class="mk-links-footer">
87
+ <span v-if="draft.length && !canSave" class="mk-links-error">{{ $t('media_kit.links.invalid') }}</span>
88
+ <button type="button" class="mk-link-save" :disabled="!canSave" @click="save">
89
+ {{ saved ? $t('media_kit.links.saved') : $t('media_kit.links.save') }}
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </template>
94
+
95
+ <style scoped lang="scss">
96
+ .mk-links-editor { border: 1px solid rgba(140, 140, 160, .3); padding: 20px; background: rgba(255, 255, 255, .02); }
97
+ .mk-links-head { display: flex; justify-content: space-between; gap: 20px; align-items: start; }
98
+ .mk-links-head h3 { margin: 0 0 5px; font-size: 17px; }
99
+ .mk-links-head p, .mk-links-empty { margin: 0; color: #9292a4; font-size: 13px; line-height: 1.5; }
100
+ .mk-link-add, .mk-link-save { border: 1px solid #ffb000; background: #ffb000; color: #090909; min-height: 38px; padding: 0 16px; font-weight: 750; cursor: pointer; white-space: nowrap; }
101
+ button:disabled { opacity: .42; cursor: not-allowed; }
102
+ .mk-link-list { display: grid; gap: 10px; margin-top: 18px; }
103
+ .mk-link-row { display: grid; grid-template-columns: 30px minmax(150px, .75fr) minmax(220px, 1.4fr) auto; gap: 10px; align-items: end; padding: 12px; border: 1px solid rgba(140, 140, 160, .25); }
104
+ .mk-link-number { align-self: center; color: #ffb000; font-weight: 800; text-align: center; }
105
+ .mk-link-row label { display: grid; gap: 6px; color: #9292a4; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
106
+ .mk-link-row input { min-width: 0; height: 40px; border: 1px solid rgba(140, 140, 160, .35); background: rgba(0, 0, 0, .2); color: inherit; padding: 0 11px; outline: none; }
107
+ .mk-link-row input:focus { border-color: #ffb000; }
108
+ .mk-link-actions { display: flex; gap: 5px; }
109
+ .mk-link-actions button { width: 36px; height: 40px; border: 1px solid rgba(140, 140, 160, .35); background: transparent; color: inherit; cursor: pointer; }
110
+ .mk-link-actions .danger { color: #ff7b7b; }
111
+ .mk-links-empty { padding: 22px 0 6px; }
112
+ .mk-links-footer { display: flex; align-items: center; justify-content: flex-end; gap: 14px; margin-top: 16px; }
113
+ .mk-links-error { color: #ff9898; font-size: 12px; }
114
+
115
+ @media (max-width: 760px) {
116
+ .mk-links-head { flex-direction: column; }
117
+ .mk-link-row { grid-template-columns: 24px 1fr; }
118
+ .mk-link-row label, .mk-link-actions { grid-column: 2; }
119
+ .mk-link-actions { justify-content: flex-end; }
120
+ }
121
+ </style>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ store: Record<string, any>;
4
+ contactEmail?: string | null;
5
+ }>();
6
+
7
+ const emit = defineEmits<{
8
+ click: [type: string];
9
+ }>();
10
+
11
+ const products = computed(() => props.store?.products ?? []);
12
+
13
+ function formatPrice(product: Record<string, any>) {
14
+ try {
15
+ return new Intl.NumberFormat(undefined, {
16
+ style: 'currency',
17
+ currency: product.currency || props.store.currency || 'BRL',
18
+ }).format(Number(product.price || 0));
19
+ } catch {
20
+ return `${product.currency || props.store.currency || 'BRL'} ${Number(product.price || 0).toFixed(2)}`;
21
+ }
22
+ }
23
+
24
+ function interestUrl(product: Record<string, any>) {
25
+ if (!props.contactEmail) return null;
26
+ const subject = encodeURIComponent(`${props.store.name} — ${product.name}`);
27
+ return `mailto:${props.contactEmail}?subject=${subject}`;
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div class="mks">
33
+ <div class="mks__head">
34
+ <div class="mks__identity">
35
+ <img v-if="store.logo_url" :src="store.logo_url" :alt="store.name" />
36
+ <div>
37
+ <p class="mks__eyebrow">{{ $t('kit.media.store_eyebrow') }}</p>
38
+ <h2>{{ store.name }}</h2>
39
+ <p v-if="store.description">{{ store.description }}</p>
40
+ </div>
41
+ </div>
42
+ <span class="mks__badge">Mundo Gamer</span>
43
+ </div>
44
+
45
+ <div v-if="products.length" class="mks__grid">
46
+ <article v-for="product in products" :key="product.id" class="mks__product">
47
+ <div class="mks__image">
48
+ <img v-if="product.image" :src="product.image" :alt="product.name" />
49
+ <span v-else>{{ product.name?.charAt(0) }}</span>
50
+ <em v-if="product.is_featured">{{ $t('kit.media.store_featured') }}</em>
51
+ </div>
52
+ <div class="mks__body">
53
+ <div>
54
+ <h3>{{ product.name }}</h3>
55
+ <p v-if="product.description">{{ product.description }}</p>
56
+ </div>
57
+ <div class="mks__footer">
58
+ <strong>{{ formatPrice(product) }}</strong>
59
+ <a
60
+ v-if="interestUrl(product)"
61
+ :href="interestUrl(product) || undefined"
62
+ @click="emit('click', 'store_product')"
63
+ >{{ $t('kit.media.store_interest') }}</a>
64
+ </div>
65
+ </div>
66
+ </article>
67
+ </div>
68
+
69
+ <p v-else class="mks__empty">{{ $t('kit.media.store_empty') }}</p>
70
+ </div>
71
+ </template>
72
+
73
+ <style scoped lang="scss">
74
+ .mks { display: grid; gap: 22px; }
75
+ .mks__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
76
+ .mks__identity { display: flex; align-items: center; gap: 16px; min-width: 0; }
77
+ .mks__identity img { width: 64px; height: 64px; object-fit: cover; border: 1px solid var(--kit-line); }
78
+ .mks__identity h2 { margin: 2px 0 4px; font-size: 24px; color: var(--kit-text); }
79
+ .mks__identity p { margin: 0; color: var(--kit-muted); line-height: 1.5; }
80
+ .mks__eyebrow { color: var(--kit-accent) !important; text-transform: uppercase; letter-spacing: .12em; font-size: 10px; font-weight: 800; }
81
+ .mks__badge { flex: none; padding: 6px 9px; border: 1px solid var(--kit-line); color: var(--kit-muted); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
82
+ .mks__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; }
83
+ .mks__product { display: flex; flex-direction: column; min-height: 290px; overflow: hidden; border: 1px solid var(--kit-line); background: var(--kit-surface-2); }
84
+ .mks__image { position: relative; display: grid; place-items: center; aspect-ratio: 4 / 3; overflow: hidden; background: color-mix(in srgb, var(--kit-accent) 10%, var(--kit-bg)); color: var(--kit-accent); font-size: 38px; font-weight: 800; }
85
+ .mks__image img { width: 100%; height: 100%; object-fit: cover; transition: transform .25s ease; }
86
+ .mks__product:hover .mks__image img { transform: scale(1.035); }
87
+ .mks__image em { position: absolute; top: 10px; left: 10px; padding: 5px 7px; background: var(--kit-accent); color: #0c0e12; font-size: 9px; font-style: normal; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
88
+ .mks__body { display: flex; flex: 1; flex-direction: column; justify-content: space-between; gap: 18px; padding: 14px; }
89
+ .mks__body h3 { margin: 0 0 6px; color: var(--kit-text); font-size: 15px; }
90
+ .mks__body p { display: -webkit-box; margin: 0; overflow: hidden; color: var(--kit-muted); font-size: 12px; line-height: 1.45; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
91
+ .mks__footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
92
+ .mks__footer strong { color: var(--kit-text); font-size: 15px; }
93
+ .mks__footer a { padding: 7px 9px; border: 1px solid var(--kit-accent); color: var(--kit-accent); font-size: 11px; font-weight: 700; text-decoration: none; }
94
+ .mks__empty { margin: 0; color: var(--kit-muted); }
95
+
96
+ @media (max-width: 560px) {
97
+ .mks__head { flex-direction: column; }
98
+ .mks__badge { display: none; }
99
+ .mks__grid { grid-template-columns: 1fr 1fr; }
100
+ .mks__product { min-height: 250px; }
101
+ .mks__footer { align-items: flex-start; flex-direction: column; }
102
+ }
103
+
104
+ @media (max-width: 390px) {
105
+ .mks__grid { grid-template-columns: 1fr; }
106
+ }
107
+ </style>
@@ -13,8 +13,15 @@ const emit = defineEmits(["update:model"]);
13
13
  const props = defineProps<SortInputProps>();
14
14
  const sortModel = ref(props.model);
15
15
 
16
- watch(sortModel, () => {
17
- emit("update:model", sortModel);
16
+ watch(
17
+ () => props.model,
18
+ (value) => {
19
+ sortModel.value = value;
20
+ },
21
+ );
22
+
23
+ watch(sortModel, (value) => {
24
+ emit("update:model", value);
18
25
  });
19
26
 
20
27
  const toggleSort = () => {
package/nuxt.config.ts CHANGED
@@ -51,7 +51,14 @@ import { dirname } from "node:path";
51
51
  const SHARED_UI_COMPONENT_PRIORITY = 1000;
52
52
 
53
53
  const require = createRequire(`${process.cwd()}/`);
54
- const packageRoot = dirname(require.resolve("@mundogamernetwork/shared-ui/package.json"));
54
+ let packageRoot: string;
55
+ try {
56
+ packageRoot = dirname(require.resolve("@mundogamernetwork/shared-ui/package.json"));
57
+ } catch {
58
+ // The package does not resolve by its own published name while running the
59
+ // local playground directly from this repository.
60
+ packageRoot = process.cwd();
61
+ }
55
62
 
56
63
  export default defineNuxtConfig({
57
64
  components: [
@@ -92,8 +99,8 @@ export default defineNuxtConfig({
92
99
  ],
93
100
  },
94
101
  css: [
95
- "@mundogamernetwork/shared-ui/assets/kit/kit-base.css",
96
- "@mundogamernetwork/shared-ui/assets/kit/kit-theme.css",
102
+ `${packageRoot}/assets/kit/kit-base.css`,
103
+ `${packageRoot}/assets/kit/kit-theme.css`,
97
104
  ],
98
105
  runtimeConfig: {
99
106
  public: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -123,6 +123,7 @@
123
123
  :src="card.cover"
124
124
  :alt="card.text"
125
125
  :class="{ 'gray-out': card.isExpired || card.isUpcoming || !card.user_can_access }"
126
+ :style="{ objectPosition: `${card.coverFocalX * 100}% ${card.coverFocalY * 100}%` }"
126
127
  @error="(e) => { (e.target as HTMLImageElement).src = '/imgs/no-cover-img.jpg' }"
127
128
  />
128
129
 
@@ -419,7 +420,7 @@ const sortMode = ref<"recent" | "popular">("recent")
419
420
  // ------------------------------------------------------------------
420
421
  // Data
421
422
  // ------------------------------------------------------------------
422
- const sliderImages = ref<Array<{ slug: string; cover: string; name: string; id: number }>>([])
423
+ const sliderImages = ref<Array<{ slug: string; cover: string; name: string; id: number; coverFocalX: number; coverFocalY: number }>>([])
423
424
  const cards = ref<Array<any>>([])
424
425
  const redeemedCards = ref<Array<any>>([])
425
426
 
@@ -529,11 +530,9 @@ async function loadFeaturedCampaigns() {
529
530
  .filter((item: any) => (truthyFlag(item.featured) || truthyFlag(item.highlight)) && truthyFlag(item.is_open))
530
531
  .map((item: any) => ({
531
532
  id: item.id,
532
- cover: item.game?.url_banner_src || item.cover_src,
533
- // Only applies to the cover_src fallback — the game's own
534
- // url_banner_src is already banner-shaped and needs no repositioning.
535
- coverFocalX: item.game?.url_banner_src ? 0.5 : (item.cover_focal_x ?? 0.5),
536
- coverFocalY: item.game?.url_banner_src ? 0.5 : (item.cover_focal_y ?? 0.5),
533
+ cover: item.cover_src || item.game?.url_banner_src || item.game?.url_cover_src || "/imgs/no-cover-img.jpg",
534
+ coverFocalX: item.cover_src ? (item.cover_focal_x ?? 0.5) : 0.5,
535
+ coverFocalY: item.cover_src ? (item.cover_focal_y ?? 0.5) : 0.5,
537
536
  name: item.name,
538
537
  slug: item.slug,
539
538
  }))
@@ -592,6 +591,8 @@ async function loadCampaigns(page = 1) {
592
591
  type: item.type?.name,
593
592
  typeId: item.type?.id,
594
593
  cover: item.cover_src || item.game?.url_cover_src || item.game?.url_banner_src || "/imgs/no-cover-img.jpg",
594
+ coverFocalX: item.cover_src ? (item.cover_focal_x ?? 0.5) : 0.5,
595
+ coverFocalY: item.cover_src ? (item.cover_focal_y ?? 0.5) : 0.5,
595
596
  text: item.name,
596
597
  time: item.time_to_expire ? item.time_to_expire.split(":")[0] : "0",
597
598
  slug: item.slug,
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { fetchPublicMediaKit, trackMediaKitEvent } from '../../services/mediaKitService';
3
3
  import MediaKitWallBlock from '../../components/indie-wall/MediaKitWallBlock.vue';
4
+ import MgMediaKitStore from '../../components/MediaKit/MgMediaKitStore.vue';
4
5
 
5
6
  const route = useRoute();
6
7
  const slug = computed(() => route.params.slug as string);
@@ -25,6 +26,15 @@ const template = computed(() => kit.value?.template || 'creator');
25
26
  const platforms = computed(() => kit.value?.platforms ?? []);
26
27
  const totalFollowers = computed(() => kit.value?.total_followers ?? 0);
27
28
  const isPremium = computed(() => kit.value?.is_premium ?? false);
29
+ const coverUrl = computed(() => kit.value?.cover_url || streamer.value?.cover || null);
30
+ const portfolioImages = computed(() => kit.value?.portfolio_images ?? streamer.value?.portfolio_images ?? []);
31
+ const brandLogos = computed(() => kit.value?.brand_logos ?? streamer.value?.brand_logos ?? []);
32
+ const creatorStore = computed(() => kit.value?.creator_store ?? null);
33
+ const customLinks = computed(() => kit.value?.custom_links ?? streamer.value?.custom_links ?? []);
34
+
35
+ const coverStyle = computed(() => coverUrl.value
36
+ ? { backgroundImage: `linear-gradient(180deg, rgba(5,6,10,.14), rgba(5,6,10,.92)), url("${String(coverUrl.value).replace(/"/g, '\\"')}")` }
37
+ : undefined);
28
38
 
29
39
  const PLATFORM_LABEL: Record<string, string> = {
30
40
  instagram: 'Instagram', tiktok: 'TikTok', youtube: 'YouTube', twitch: 'Twitch', x: 'X',
@@ -109,7 +119,7 @@ function onDownload() {
109
119
  <div v-else-if="error || !kit" class="kit-state"><p>{{ $t('kit.media.not_available') }}</p></div>
110
120
 
111
121
  <template v-else>
112
- <div class="mk-cover" />
122
+ <div class="mk-cover" :class="{ 'has-image': coverUrl }" :style="coverStyle" />
113
123
 
114
124
  <div class="kit-wrap mk-sections">
115
125
  <header class="mk-header">
@@ -118,6 +128,7 @@ function onDownload() {
118
128
 
119
129
  <div class="mk-hid">
120
130
  <h1>{{ streamer.name }}</h1>
131
+ <p v-if="streamer.custom_headline" class="mk-headline">{{ streamer.custom_headline }}</p>
121
132
  <div class="mk-badges">
122
133
  <span v-if="streamer.platform" class="mk-badge plat">{{ streamer.platform }}</span>
123
134
  <span v-if="streamer.tier && streamer.tier !== 'free'" class="mk-badge tier">{{ String(streamer.tier).toUpperCase() }}</span>
@@ -132,6 +143,25 @@ function onDownload() {
132
143
  </div>
133
144
  </header>
134
145
 
146
+ <!-- Personal link hub: replaces a separate link-in-bio service. -->
147
+ <nav class="mk-link-hub no-print" data-sec="links" :aria-label="$t('kit.media.quick_links')">
148
+ <a v-for="(link, index) in customLinks" :key="`${link.url}-${index}`" class="mk-link" :class="{ 'mk-link--primary': index === 0 }" :href="link.url" target="_blank" rel="noopener" @click="track('click')">
149
+ <span>{{ link.label }}</span><b>↗</b>
150
+ </a>
151
+ <a v-if="streamer.contact_email" class="mk-link mk-link--primary" :href="`mailto:${streamer.contact_email}`" @click="track('contact')">
152
+ <span>{{ $t('kit.media.work_with_me') }}</span><b>↗</b>
153
+ </a>
154
+ <a v-if="creatorStore" class="mk-link" href="#store" @click="track('click')">
155
+ <span>{{ $t('kit.media.visit_store') }}</span><b>↓</b>
156
+ </a>
157
+ <a v-if="userId" class="mk-link" href="#support" @click="track('click')">
158
+ <span>{{ $t('kit.media.visit_mural') }}</span><b>↓</b>
159
+ </a>
160
+ <a v-for="s in socials" :key="s.platform" class="mk-link mk-link--social" :href="s.url" target="_blank" rel="noopener" @click="track('click')">
161
+ <span>{{ PLATFORM_LABEL[s.platform] || s.platform }}</span><b>↗</b>
162
+ </a>
163
+ </nav>
164
+
135
165
  <!-- Reach / multi-platform followers -->
136
166
  <section v-if="platforms.length" class="kit-block" data-sec="reach">
137
167
  <h2>{{ $t('kit.media.reach') }}</h2>
@@ -151,6 +181,24 @@ function onDownload() {
151
181
  </div>
152
182
  </section>
153
183
 
184
+ <!-- Mundo Gamer creator store -->
185
+ <section v-if="creatorStore" id="store" class="kit-block kit-card mk-store-block" data-sec="store">
186
+ <MgMediaKitStore :store="creatorStore" :contact-email="streamer.contact_email" @click="track('click')" />
187
+ </section>
188
+
189
+ <!-- Creator-selected portfolio and brand imagery -->
190
+ <section v-if="portfolioImages.length || brandLogos.length" class="kit-block" data-sec="portfolio">
191
+ <h2>{{ $t('kit.media.portfolio') }}</h2>
192
+ <div v-if="portfolioImages.length" class="mk-portfolio">
193
+ <a v-for="(image, index) in portfolioImages" :key="image" :href="image" target="_blank" rel="noopener" @click="track('click')">
194
+ <img :src="image" :alt="`${streamer.name} — ${index + 1}`" />
195
+ </a>
196
+ </div>
197
+ <div v-if="brandLogos.length" class="mk-brand-logos">
198
+ <img v-for="logo in brandLogos" :key="logo" :src="logo" :alt="$t('kit.media.partner_brand')" />
199
+ </div>
200
+ </section>
201
+
154
202
  <!-- TV streaming metrics -->
155
203
  <section v-if="stats" class="kit-block" data-sec="metrics">
156
204
  <h2>{{ $t('kit.media.key_metrics') }}</h2>
@@ -262,18 +310,8 @@ function onDownload() {
262
310
  </div>
263
311
  </section>
264
312
 
265
- <!-- Social links -->
266
- <section v-if="socials.length" class="kit-block no-print" data-sec="connect">
267
- <h2>{{ $t('kit.media.connect') }}</h2>
268
- <div class="mk-socials">
269
- <a v-for="s in socials" :key="s.platform" class="mk-soc" :href="s.url" target="_blank" rel="noopener" @click="track('click')">
270
- {{ s.platform }}
271
- </a>
272
- </div>
273
- </section>
274
-
275
313
  <!-- IndieWall / mural — self-contained widget (own fetch, realtime, stepper) -->
276
- <section v-if="userId" class="kit-block" data-sec="support">
314
+ <section v-if="userId" id="support" class="kit-block" data-sec="support">
277
315
  <MediaKitWallBlock :user-id="userId" />
278
316
  </section>
279
317
 
@@ -308,8 +346,10 @@ function onDownload() {
308
346
 
309
347
  /* Cover */
310
348
  .mk-cover {
311
- height: 200px;
349
+ height: 280px;
312
350
  background: linear-gradient(120deg, var(--kit-accent-2, #6b15ac) 0%, #2a0e4e 50%, var(--kit-bg) 100%);
351
+ background-position: center;
352
+ background-size: cover;
313
353
  position: relative;
314
354
  border-bottom: 1px solid var(--kit-line);
315
355
 
@@ -319,44 +359,59 @@ function onDownload() {
319
359
  inset: 0;
320
360
  background: radial-gradient(circle at 80% 20%, rgba(168, 85, 247, .35), transparent 60%);
321
361
  }
362
+
363
+ &.has-image::after {
364
+ background: linear-gradient(180deg, rgba(5, 6, 10, .04), rgba(5, 6, 10, .52));
365
+ }
322
366
  }
323
367
 
324
368
  /* Header */
325
369
  .mk-header {
326
- margin-top: -70px;
370
+ margin: -82px auto 0;
327
371
  position: relative;
328
372
  z-index: 2;
329
373
  display: flex;
330
- gap: 24px;
331
- align-items: flex-end;
332
- flex-wrap: wrap;
374
+ flex-direction: column;
375
+ align-items: center;
376
+ max-width: 760px;
377
+ text-align: center;
333
378
  }
334
379
  .mk-avatar {
335
- width: 140px;
336
- height: 140px;
380
+ width: 156px;
381
+ height: 156px;
337
382
  border: 3px solid var(--kit-accent);
338
383
  object-fit: cover;
339
384
  flex-shrink: 0;
340
385
  background: var(--kit-surface-2);
386
+ box-shadow: 0 20px 52px rgba(0, 0, 0, .42);
341
387
  }
342
388
  .mk-avatar-ph {
343
389
  background: linear-gradient(135deg, var(--kit-accent-2, #6b15ac), var(--kit-accent));
344
390
  }
345
391
  .mk-hid {
346
- flex: 1;
347
- min-width: 260px;
348
- padding-bottom: 6px;
392
+ width: 100%;
393
+ padding-top: 18px;
349
394
 
350
395
  h1 {
351
396
  font-family: 'Rajdhani', sans-serif;
352
- font-size: 40px;
397
+ font-size: clamp(36px, 7vw, 54px);
353
398
  line-height: 1.05;
399
+ margin: 0;
354
400
  }
355
401
  }
402
+ .mk-headline {
403
+ margin: 8px auto 0;
404
+ max-width: 620px;
405
+ color: var(--kit-accent);
406
+ font-family: 'Rajdhani', sans-serif;
407
+ font-size: 20px;
408
+ font-weight: 600;
409
+ }
356
410
  .mk-badges {
357
411
  display: flex;
412
+ justify-content: center;
358
413
  gap: 8px;
359
- margin: 8px 0;
414
+ margin: 12px 0;
360
415
  flex-wrap: wrap;
361
416
  }
362
417
  .mk-badge {
@@ -374,13 +429,60 @@ function onDownload() {
374
429
  .mk-bio {
375
430
  color: #c4c4d2;
376
431
  font-size: 15px;
377
- max-width: 60ch;
378
- margin-top: 6px;
432
+ line-height: 1.65;
433
+ max-width: 66ch;
434
+ margin: 6px auto 0;
379
435
  }
380
436
  .mk-actions {
381
437
  display: flex;
438
+ justify-content: center;
439
+ flex-wrap: wrap;
382
440
  gap: 10px;
383
- padding-bottom: 8px;
441
+ padding-top: 18px;
442
+ }
443
+
444
+ /* Link-in-bio hub */
445
+ .mk-link-hub {
446
+ display: grid;
447
+ grid-template-columns: 1fr 1fr;
448
+ gap: 10px;
449
+ width: min(100%, 720px);
450
+ margin: 30px auto 2px;
451
+ }
452
+ .mk-link {
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: space-between;
456
+ gap: 16px;
457
+ min-height: 54px;
458
+ padding: 0 18px;
459
+ border: 1px solid var(--kit-line);
460
+ background: color-mix(in srgb, var(--kit-surface) 92%, transparent);
461
+ color: var(--kit-text);
462
+ font-size: 14px;
463
+ font-weight: 700;
464
+ text-decoration: none;
465
+ transition: transform .16s ease, border-color .16s ease, background .16s ease;
466
+
467
+ &:hover {
468
+ transform: translateY(-2px);
469
+ border-color: var(--kit-accent);
470
+ background: var(--kit-surface-2);
471
+ }
472
+
473
+ b { color: var(--kit-accent); font-size: 16px; }
474
+
475
+ &--primary {
476
+ grid-column: 1 / -1;
477
+ background: var(--kit-accent);
478
+ border-color: var(--kit-accent);
479
+ color: var(--kit-accent-ink);
480
+
481
+ b { color: var(--kit-accent-ink); }
482
+ &:hover { background: var(--kit-accent); filter: brightness(1.06); }
483
+ }
484
+
485
+ &--social { min-height: 46px; color: var(--kit-muted); }
384
486
  }
385
487
 
386
488
  /* Buttons */
@@ -434,6 +536,29 @@ function onDownload() {
434
536
  }
435
537
  }
436
538
  }
539
+ .mk-store-block { padding: 24px; scroll-margin-top: 24px; }
540
+ .mk-portfolio {
541
+ display: grid;
542
+ grid-template-columns: repeat(3, minmax(0, 1fr));
543
+ gap: 10px;
544
+
545
+ a { display: block; overflow: hidden; aspect-ratio: 4 / 3; border: 1px solid var(--kit-line); background: var(--kit-surface); }
546
+ img { width: 100%; height: 100%; object-fit: cover; transition: transform .2s ease; }
547
+ a:hover img { transform: scale(1.03); }
548
+ }
549
+ .mk-brand-logos {
550
+ display: flex;
551
+ align-items: center;
552
+ justify-content: center;
553
+ flex-wrap: wrap;
554
+ gap: 16px;
555
+ margin-top: 16px;
556
+ padding: 18px;
557
+ border: 1px solid var(--kit-line);
558
+ background: var(--kit-surface);
559
+
560
+ img { width: 112px; height: 54px; object-fit: contain; filter: grayscale(1); opacity: .8; }
561
+ }
437
562
  .mk-period {
438
563
  color: var(--kit-muted);
439
564
  font-size: 13px;
@@ -768,16 +893,19 @@ function onDownload() {
768
893
  .mk-sections { display: flex; flex-direction: column; }
769
894
  .mk-sections > .mk-header { order: 0; }
770
895
  .mk-sections > .mk-footer { order: 100; }
771
- .mk-sections > [data-sec="reach"] { order: 1; }
772
- .mk-sections > [data-sec="metrics"] { order: 2; }
773
- .mk-sections > [data-sec="audience"] { order: 3; }
774
- .mk-sections > [data-sec="styles"] { order: 4; }
775
- .mk-sections > [data-sec="games"] { order: 5; }
776
- .mk-sections > [data-sec="rates"] { order: 6; }
777
- .mk-sections > [data-sec="brands"] { order: 7; }
778
- .mk-sections > [data-sec="schedule"] { order: 8; }
779
- .mk-sections > [data-sec="connect"] { order: 9; }
780
- .mk-sections > [data-sec="support"] { order: 10; }
896
+ .mk-sections > [data-sec="links"] { order: 1; }
897
+ .mk-sections > [data-sec="reach"] { order: 2; }
898
+ .mk-sections > [data-sec="store"] { order: 3; }
899
+ .mk-sections > [data-sec="portfolio"] { order: 4; }
900
+ .mk-sections > [data-sec="metrics"] { order: 5; }
901
+ .mk-sections > [data-sec="audience"] { order: 6; }
902
+ .mk-sections > [data-sec="styles"] { order: 7; }
903
+ .mk-sections > [data-sec="games"] { order: 8; }
904
+ .mk-sections > [data-sec="rates"] { order: 9; }
905
+ .mk-sections > [data-sec="brands"] { order: 10; }
906
+ .mk-sections > [data-sec="schedule"] { order: 11; }
907
+ .mk-sections > [data-sec="connect"] { order: 12; }
908
+ .mk-sections > [data-sec="support"] { order: 13; scroll-margin-top: 24px; }
781
909
 
782
910
  [data-template="performance"] {
783
911
  .mk-sections > [data-sec="metrics"] { order: 1; }
@@ -803,7 +931,20 @@ function onDownload() {
803
931
  .mk-cols2, .mk-rates, .mk-bundles { grid-template-columns: 1fr; }
804
932
  .mk-brands { grid-template-columns: repeat(3, 1fr); }
805
933
  .mk-schedule { grid-template-columns: repeat(4, 1fr); }
806
- .mk-header { flex-direction: column; align-items: flex-start; }
934
+ .mk-portfolio { grid-template-columns: 1fr 1fr; }
935
+ }
936
+
937
+ @media (max-width: 560px) {
938
+ .kit-wrap { padding-inline: 14px; }
939
+ .mk-cover { height: 230px; }
940
+ .mk-avatar { width: 132px; height: 132px; }
941
+ .mk-header { margin-top: -68px; }
942
+ .mk-link-hub { grid-template-columns: 1fr; }
943
+ .mk-link--primary { grid-column: auto; }
944
+ .mk-actions { width: 100%; }
945
+ .kit-btn { flex: 1; justify-content: center; }
946
+ .mk-portfolio { grid-template-columns: 1fr; }
947
+ .mk-store-block { padding: 16px; }
807
948
  }
808
949
 
809
950
  @media print {
@@ -811,7 +952,7 @@ function onDownload() {
811
952
  }
812
953
 
813
954
  // ── Scroll unlock (body has overflow-y: hidden globally in some frontends) ───
814
- :global(body):has(.pk-page) {
955
+ :global(body):has(.kit-media) {
815
956
  overflow-y: auto !important;
816
957
  max-height: none !important;
817
958
  }