feat(landing): refine hero timeline and ctas

This commit is contained in:
777genius 2026-05-21 11:57:18 +03:00
parent 99e8e2e017
commit 98405b9040
9 changed files with 525 additions and 215 deletions

View file

@ -516,51 +516,163 @@
.cyber-hero__actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
gap: 14px;
margin-bottom: 18px;
}
.cyber-hero__action.v-btn {
min-height: 52px !important;
min-width: 148px !important;
border-radius: var(--cyber-radius-sm) !important;
padding-inline: 18px !important;
font-weight: 800 !important;
font-size: 0.9rem !important;
letter-spacing: 0.01em !important;
.cyber-action-button.v-btn {
--action-accent-a: var(--cyber-cyan);
--action-accent-b: var(--cyber-magenta);
--action-bg:
linear-gradient(135deg, rgba(0, 210, 255, 0.74) 0%, rgba(43, 103, 255, 0.56) 48%, rgba(207, 34, 215, 0.76) 100%),
linear-gradient(180deg, rgba(7, 18, 42, 0.9), rgba(4, 10, 25, 0.92));
--action-border: rgba(0, 234, 255, 0.78);
--action-text: #f2fbff;
--action-subtext: rgba(231, 240, 255, 0.72);
--action-clip: polygon(18px 0, calc(100% - 12px) 0, 100% 12px, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%, 0 18px);
position: relative;
min-height: 76px !important;
min-width: 312px !important;
overflow: hidden !important;
border: 0 !important;
border-radius: 0 !important;
padding: 0 26px !important;
color: var(--action-text) !important;
background: var(--action-bg) !important;
clip-path: var(--action-clip);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.1) inset,
0 0 22px color-mix(in srgb, var(--action-accent-a) 24%, transparent),
0 0 34px color-mix(in srgb, var(--action-accent-b) 20%, transparent) !important;
font-weight: 900 !important;
letter-spacing: 0 !important;
text-transform: uppercase !important;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.18s ease,
background 0.18s ease !important;
filter 0.18s ease !important;
}
.cyber-hero__action.v-btn:hover {
transform: translateY(-1px);
.cyber-action-button.v-btn .v-btn__content {
position: static;
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
gap: 16px;
}
.cyber-hero__action--primary.v-btn {
color: var(--cyber-action-primary-color) !important;
background: linear-gradient(135deg, var(--cyber-cyan), var(--cyber-magenta)) !important;
.cyber-action-button.v-btn .v-btn__overlay,
.cyber-action-button.v-btn .v-btn__underlay {
display: none !important;
}
.cyber-action-button.v-btn:hover {
filter: brightness(1.08) saturate(1.08);
transform: translateY(-2px);
}
.cyber-action-button__glow {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.16), transparent 28%),
radial-gradient(ellipse at 16% 48%, color-mix(in srgb, var(--action-accent-a) 22%, transparent), transparent 38%),
linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent 52%);
opacity: 0.62;
}
.cyber-action-button__frame {
position: absolute;
inset: 0;
z-index: 3;
width: 100%;
height: 100%;
overflow: visible;
color: var(--action-border);
pointer-events: none;
filter:
drop-shadow(0 0 8px color-mix(in srgb, var(--action-accent-a) 22%, transparent))
drop-shadow(0 0 12px color-mix(in srgb, var(--action-accent-b) 12%, transparent));
}
.cyber-action-button__frame path {
fill: none;
stroke: currentColor;
stroke-linecap: square;
stroke-linejoin: miter;
stroke-width: 1.35;
shape-rendering: geometricPrecision;
}
.cyber-action-button__icon {
position: relative;
z-index: 4;
display: grid;
flex: 0 0 auto;
width: 34px;
height: 34px;
place-items: center;
color: var(--action-accent-a);
filter:
drop-shadow(0 0 10px color-mix(in srgb, var(--action-accent-a) 54%, transparent))
drop-shadow(0 0 18px color-mix(in srgb, var(--action-accent-b) 30%, transparent));
}
.cyber-action-button--primary .cyber-action-button__icon {
color: #00d8ff;
filter:
drop-shadow(0 0 10px rgba(0, 234, 255, 0.56))
drop-shadow(0 0 16px rgba(35, 91, 255, 0.22));
}
.cyber-action-button__copy {
position: relative;
z-index: 4;
display: grid;
min-width: 0;
gap: 6px;
text-align: left;
}
.cyber-action-button__label {
color: var(--action-text);
font-size: 1rem;
line-height: 1;
}
.cyber-action-button__subtitle {
color: var(--action-subtext);
font-size: 0.66rem;
font-weight: 700;
line-height: 1.1;
text-transform: none;
opacity: 0.92;
}
.cyber-action-button--primary.v-btn {
--action-text: #f8fbff;
--action-subtext: rgba(237, 247, 255, 0.74);
}
.cyber-action-button--secondary.v-btn {
--action-accent-a: #91b6ff;
--action-accent-b: var(--cyber-cyan);
--action-bg:
linear-gradient(135deg, rgba(10, 18, 38, 0.96), rgba(7, 14, 31, 0.78)),
rgba(1, 7, 18, 0.84);
--action-border: rgba(123, 160, 231, 0.64);
--action-text: #f3f7ff;
--action-subtext: rgba(182, 198, 226, 0.76);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.16) inset,
0 0 24px rgba(0, 234, 255, 0.34),
0 0 34px rgba(255, 43, 255, 0.22) !important;
}
.cyber-hero__action--watch.v-btn,
.cyber-hero__action--docs.v-btn {
color: var(--cyber-text) !important;
border-color: rgba(0, 234, 255, 0.46) !important;
background: var(--cyber-action-secondary-bg) !important;
}
.cyber-hero__action--watch.v-btn:hover,
.cyber-hero__action--docs.v-btn:hover {
color: var(--cyber-cyan) !important;
border-color: rgba(0, 234, 255, 0.74) !important;
background: var(--cyber-action-secondary-hover-bg) !important;
0 0 0 1px rgba(255, 255, 255, 0.05) inset,
0 0 24px rgba(80, 130, 255, 0.16),
0 0 34px rgba(0, 234, 255, 0.1) !important;
}
.cyber-hero__terminal-note {
@ -1210,31 +1322,67 @@
.cyber-feature-rail-shell {
position: relative;
z-index: 7;
margin: 28px auto 0;
width: min(1540px, 96%);
margin: clamp(42px, 5vw, 72px) auto 0;
width: min(1580px, 96%);
}
.cyber-feature-rail-shell::before {
content: "";
position: absolute;
z-index: -1;
inset: -118px -3vw -96px;
inset: -82px -4vw -76px;
pointer-events: none;
background: var(--cyber-feature-shell-bg);
opacity: 0.86;
mask-image: radial-gradient(ellipse at 50% 54%, black 0 48%, rgba(0, 0, 0, 0.5) 62%, transparent 78%);
background:
radial-gradient(ellipse at 50% 42%, rgba(0, 234, 255, 0.12), transparent 48%),
radial-gradient(ellipse at 88% 54%, rgba(255, 43, 255, 0.12), transparent 42%);
opacity: 0.72;
mask-image: radial-gradient(ellipse at 50% 52%, black 0 48%, rgba(0, 0, 0, 0.5) 62%, transparent 78%);
}
.cyber-feature-rail {
--feature-line-top: 118px;
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0;
width: 100%;
padding: 17px 18px;
background: var(--cyber-feature-rail-bg);
box-shadow: var(--cyber-feature-rail-shadow);
min-height: 300px;
padding: 0 22px;
background: transparent;
box-shadow: none;
}
.cyber-feature-rail::before {
position: absolute;
z-index: 0;
top: var(--feature-line-top);
left: 0;
right: 0;
height: 3px;
content: "";
background:
linear-gradient(90deg, transparent 0 2.5%, var(--cyber-cyan) 6%, rgba(0, 234, 255, 0.92) 78%, rgba(255, 43, 255, 0.95) 92%, transparent 98%),
linear-gradient(90deg, transparent 0 3%, rgba(255, 255, 255, 0.34) 6%, transparent 13%, transparent 86%, rgba(255, 255, 255, 0.24) 92%, transparent 98%);
box-shadow:
0 0 18px rgba(0, 234, 255, 0.42),
0 0 30px rgba(255, 43, 255, 0.18);
}
.cyber-feature-rail::after {
position: absolute;
z-index: 0;
top: calc(var(--feature-line-top) - 7px);
left: 0;
width: 86px;
height: 17px;
content: "";
background:
repeating-linear-gradient(115deg, var(--cyber-cyan) 0 4px, transparent 4px 10px),
linear-gradient(90deg, transparent, rgba(0, 234, 255, 0.6));
clip-path: polygon(0 42%, 44% 42%, 54% 0, 100% 0, 100% 100%, 54% 100%, 44% 58%, 0 58%);
filter: drop-shadow(0 0 10px rgba(0, 234, 255, 0.45));
}
.cyber-feature-rail__collaboration {
@ -1405,41 +1553,127 @@
}
.cyber-feature-rail__item {
--feature-accent: var(--cyber-cyan);
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 46px minmax(0, 1fr);
align-items: center;
gap: 12px;
grid-template-rows: 82px 54px minmax(0, 1fr);
justify-items: center;
align-items: start;
gap: 0;
min-width: 0;
padding: 0 18px;
border-right: 1px solid var(--cyber-feature-divider);
padding: 0 14px;
text-align: center;
}
.cyber-feature-rail__item:last-child {
border-right: 0;
.cyber-feature-rail__item:nth-child(5) {
--feature-accent: var(--cyber-magenta);
}
.cyber-feature-rail__icon {
position: relative;
display: grid;
place-items: center;
width: 46px;
height: 46px;
color: var(--cyber-cyan);
border: 1px solid rgba(0, 234, 255, 0.44);
border-radius: var(--cyber-radius-sm);
box-shadow: 0 0 22px rgba(0, 234, 255, 0.16);
width: 80px;
height: 70px;
color: var(--feature-accent);
filter:
drop-shadow(0 0 16px color-mix(in srgb, var(--feature-accent) 48%, transparent))
drop-shadow(0 0 28px color-mix(in srgb, var(--feature-accent) 18%, transparent));
}
.cyber-feature-rail__icon::before,
.cyber-feature-rail__icon::after {
position: absolute;
width: 18px;
height: 18px;
content: "";
opacity: 0.82;
}
.cyber-feature-rail__icon::before {
top: 0;
left: 0;
border-top: 1px solid var(--feature-accent);
border-left: 1px solid var(--feature-accent);
}
.cyber-feature-rail__icon::after {
right: 0;
bottom: 0;
border-right: 1px solid var(--feature-accent);
border-bottom: 1px solid var(--feature-accent);
}
.cyber-feature-rail__icon .v-icon {
font-size: 46px !important;
}
.cyber-feature-rail__node {
position: relative;
display: grid;
width: 58px;
height: 58px;
place-items: center;
color: var(--feature-accent);
background: rgba(2, 10, 24, 0.9);
border: 2px solid var(--feature-accent);
clip-path: polygon(28% 0, 72% 0, 100% 28%, 100% 72%, 72% 100%, 28% 100%, 0 72%, 0 28%);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
0 0 20px color-mix(in srgb, var(--feature-accent) 34%, transparent);
font-family: var(--at-font-mono);
font-size: 1.14rem;
font-weight: 900;
line-height: 1;
}
.cyber-feature-rail__node::before {
position: absolute;
left: 50%;
top: -26px;
width: 1px;
height: 25px;
content: "";
background: linear-gradient(180deg, var(--feature-accent), transparent);
box-shadow: 0 0 12px color-mix(in srgb, var(--feature-accent) 46%, transparent);
}
.cyber-feature-rail__copy {
position: relative;
isolation: isolate;
max-width: 235px;
padding: 18px 12px 12px;
}
.cyber-feature-rail__copy::before {
position: absolute;
z-index: -1;
inset: 2px -8px -4px;
content: "";
background:
linear-gradient(180deg, rgba(3, 8, 22, 0.46), rgba(3, 8, 22, 0.2)),
radial-gradient(ellipse at 50% 38%, rgba(0, 234, 255, 0.08), transparent 72%);
border: 1px solid rgba(0, 234, 255, 0.08);
border-radius: 18px;
opacity: 0.92;
backdrop-filter: blur(11px) saturate(1.08);
mask-image: radial-gradient(ellipse at 50% 50%, black 0 76%, rgba(0, 0, 0, 0.62) 88%, transparent 100%);
}
.cyber-feature-rail__title {
margin-bottom: 3px;
margin-bottom: 11px;
color: var(--cyber-feature-title);
font-weight: 800;
font-size: 0.92rem;
font-size: clamp(0.98rem, 1.12vw, 1.22rem);
line-height: 1.18;
}
.cyber-feature-rail__text {
color: var(--cyber-feature-text);
font-size: 0.8rem;
line-height: 1.45;
font-size: clamp(0.82rem, 0.88vw, 1rem);
line-height: 1.52;
}
@keyframes cyberRobotBob {
@ -1554,36 +1788,38 @@
.cyber-feature-rail {
grid-template-columns: repeat(3, minmax(0, 1fr));
min-height: auto;
row-gap: 34px;
padding: 0;
}
.cyber-feature-rail__collaboration {
left: 50%;
bottom: calc(100% - 14px);
width: 132px;
.cyber-feature-rail::before,
.cyber-feature-rail::after {
display: none;
}
.cyber-feature-rail__reviewer {
--reviewer-robot-width: 78px;
right: 56px;
gap: 10px;
.cyber-feature-rail__item {
grid-template-rows: 72px 48px minmax(0, 1fr);
padding-inline: 12px;
}
.cyber-feature-rail__reviewer-card {
width: 220px;
font-size: 0.62rem;
.cyber-feature-rail__icon {
width: 72px;
height: 62px;
}
.cyber-feature-rail__robot {
width: var(--reviewer-robot-width);
.cyber-feature-rail__icon .v-icon {
font-size: 38px !important;
}
.cyber-feature-rail__item:nth-child(3) {
border-right: 0;
.cyber-feature-rail__node {
width: 50px;
height: 50px;
font-size: 1rem;
}
.cyber-feature-rail__item:nth-child(n + 4) {
margin-top: 16px;
.cyber-feature-rail__copy {
padding-top: 12px;
}
}
@ -1678,9 +1914,11 @@
grid-template-columns: 1fr;
}
.cyber-hero__action.v-btn {
.cyber-action-button.v-btn {
width: 100%;
min-height: 72px !important;
min-width: 0 !important;
padding-inline: 20px !important;
}
.cyber-hero__terminal-note {
@ -1751,26 +1989,64 @@
.cyber-feature-rail {
grid-template-columns: 1fr;
padding: 16px;
}
.cyber-feature-rail__collaboration {
display: none;
}
.cyber-feature-rail__reviewer {
display: none;
gap: 20px;
padding: 0 4px;
}
.cyber-feature-rail__item {
grid-template-columns: 42px minmax(0, 1fr);
padding: 12px 0;
border-right: 0;
border-bottom: 1px solid rgba(0, 234, 255, 0.14);
grid-template-columns: 48px 44px minmax(0, 1fr);
grid-template-rows: auto;
align-items: center;
justify-items: start;
gap: 10px;
padding: 0;
text-align: left;
}
.cyber-feature-rail__item:last-child {
border-bottom: 0;
.cyber-feature-rail__icon {
width: 44px;
height: 44px;
}
.cyber-feature-rail__icon::before,
.cyber-feature-rail__icon::after {
width: 12px;
height: 12px;
}
.cyber-feature-rail__icon .v-icon {
font-size: 26px !important;
}
.cyber-feature-rail__node {
width: 40px;
height: 40px;
font-size: 0.8rem;
}
.cyber-feature-rail__node::before {
display: none;
}
.cyber-feature-rail__copy {
max-width: none;
padding: 8px 10px 9px;
}
.cyber-feature-rail__copy::before {
inset: 0 -6px;
border-radius: 14px;
backdrop-filter: blur(9px) saturate(1.06);
}
.cyber-feature-rail__title {
margin-bottom: 5px;
font-size: 0.92rem;
}
.cyber-feature-rail__text {
font-size: 0.78rem;
line-height: 1.42;
}
}

View file

@ -0,0 +1,49 @@
<script setup lang="ts">
withDefaults(defineProps<{
href: string;
icon: string;
tone?: "primary" | "secondary";
target?: string;
subtitle?: string;
}>(), {
tone: "primary",
target: undefined,
subtitle: undefined,
});
</script>
<template>
<v-btn
:href="href"
:target="target"
variant="flat"
size="large"
class="cyber-action-button"
:class="`cyber-action-button--${tone}`"
>
<span class="cyber-action-button__glow" aria-hidden="true" />
<svg
class="cyber-action-button__frame"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
focusable="false"
>
<path
d="M 5 0 H 96 L 100 16 V 76 L 94 100 H 0 V 24 Z"
vector-effect="non-scaling-stroke"
/>
</svg>
<span class="cyber-action-button__icon" aria-hidden="true">
<v-icon :icon="icon" size="28" />
</span>
<span class="cyber-action-button__copy">
<span class="cyber-action-button__label">
<slot />
</span>
<span v-if="subtitle" class="cyber-action-button__subtitle">
{{ subtitle }}
</span>
</span>
</v-btn>
</template>

View file

@ -6,24 +6,10 @@ import {
mdiShieldCheckOutline,
mdiMonitorDashboard,
} from "@mdi/js";
import {
heroCollaborationFeature,
getLocalizedHeroFeatureRail,
getLocalizedHeroReviewerFeatureCard,
type HeroMessage,
type HeroMessagePhase,
} from "~/data/heroScene";
const props = defineProps<{
activeMessage?: HeroMessage | null;
phase?: HeroMessagePhase;
reducedMotion?: boolean;
}>();
import { getLocalizedHeroFeatureRail } from "~/data/heroScene";
const { locale } = useI18n();
const localizedHeroFeatureRail = computed(() => getLocalizedHeroFeatureRail(locale.value));
const localizedHeroReviewerFeatureCard = computed(() => getLocalizedHeroReviewerFeatureCard(locale.value));
const statusLabel = computed(() => locale.value === "ru" ? "Статус:" : "Status:");
const icons = [
mdiRobotOutline,
@ -32,73 +18,11 @@ const icons = [
mdiShieldCheckOutline,
mdiMonitorDashboard,
] as const;
const reviewerIsSender = computed(() =>
props.activeMessage?.from === "reviewer" && props.phase !== "cooldown",
);
const reviewerIsReceiver = computed(() =>
props.activeMessage?.to === "reviewer" && props.phase === "receiver",
);
const reviewerIsActive = computed(() => reviewerIsSender.value || reviewerIsReceiver.value);
const reviewerBubbleText = computed(() => {
if (!props.activeMessage || props.reducedMotion) return null;
if (props.activeMessage.from === "reviewer" && (props.phase === "sender" || props.phase === "packet")) {
return props.activeMessage.text;
}
if (props.activeMessage.to === "reviewer" && props.phase === "receiver") {
return props.activeMessage.response;
}
return null;
});
</script>
<template>
<div class="cyber-feature-rail-shell">
<img
class="cyber-feature-rail__collaboration"
:src="heroCollaborationFeature.asset"
alt=""
loading="lazy"
decoding="async"
aria-hidden="true"
>
<div
class="cyber-feature-rail__reviewer"
:class="{
'cyber-feature-rail__reviewer--active': reviewerIsActive,
'cyber-feature-rail__reviewer--sending': reviewerIsSender,
'cyber-feature-rail__reviewer--receiving': reviewerIsReceiver,
}"
aria-hidden="true"
>
<Transition name="cyber-feature-bubble">
<RobotSpeechBubble
v-if="reviewerBubbleText"
class="cyber-feature-rail__reviewer-bubble"
tail="down"
>
{{ reviewerBubbleText }}
</RobotSpeechBubble>
</Transition>
<div class="cyber-feature-rail__reviewer-card cyber-panel">
<div class="cyber-feature-rail__reviewer-label">{{ localizedHeroReviewerFeatureCard.label }}</div>
<ul class="cyber-feature-rail__reviewer-tasks">
<li v-for="task in localizedHeroReviewerFeatureCard.tasks" :key="task">{{ task }}</li>
</ul>
<div class="cyber-feature-rail__reviewer-status">
<span>{{ statusLabel }}</span>
<strong>{{ localizedHeroReviewerFeatureCard.status }}</strong>
</div>
</div>
<img
class="cyber-feature-rail__robot"
:src="localizedHeroReviewerFeatureCard.asset"
alt=""
loading="lazy"
decoding="async"
>
</div>
<div class="cyber-feature-rail cyber-panel">
<div class="cyber-feature-rail">
<div
v-for="(feature, index) in localizedHeroFeatureRail"
:key="feature.id"
@ -107,6 +31,9 @@ const reviewerBubbleText = computed(() => {
<div class="cyber-feature-rail__icon">
<v-icon :icon="icons[index]" size="28" />
</div>
<div class="cyber-feature-rail__node">
{{ (index + 1).toString().padStart(2, "0") }}
</div>
<div class="cyber-feature-rail__copy">
<div class="cyber-feature-rail__title">{{ feature.title }}</div>
<div class="cyber-feature-rail__text">{{ feature.text }}</div>

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import { mdiApple, mdiMicrosoftWindows, mdiPenguin, mdiDownload, mdiCheckCircle } from '@mdi/js';
import robotAvatarSeatedMagenta from '~/assets/images/hero/robots/robot-avatar-seated-magenta-v1.webp';
import { downloadAssets } from '~/data/downloads';
import type { DownloadOs, DownloadArch } from '~/data/downloads';
const { content } = useLandingContent();
@ -10,6 +9,7 @@ const downloadStore = useDownloadStore();
const { data: releaseData, resolve } = useReleaseDownloads();
const { trackDownloadClick } = useAnalytics();
const { releaseDownloadUrl } = useGithubRepo();
const { getDownloadArch, visibleDownloadAssets: visibleAssets } = useDownloadAssetPresentation();
const isMounted = ref(false);
const showLinuxRobotMessage = ref(false);
const showFallingLinuxRobot = ref(false);
@ -236,34 +236,10 @@ const platformColors: Record<string, string> = {
linux: '#ffd700',
};
const visibleAssets = computed(() => {
const enriched = downloadAssets.map((asset) => {
if (asset.os !== 'macos') return { ...asset };
if (!downloadStore.isMacOs) return { ...asset };
return {
...asset,
archLabel: downloadStore.macArch === 'arm64' ? 'Apple Silicon' : 'Intel',
};
});
// Reorder so detected OS is always in the center (index 1)
const detectedIdx = enriched.findIndex((a) => a.id === downloadStore.selectedId);
if (detectedIdx === -1 || detectedIdx === 1) return enriched;
const result = [...enriched];
const [detected] = result.splice(detectedIdx, 1);
const [first, ...rest] = result;
return [first, detected, ...rest];
});
const getDownloadUrl = (asset: { os: string; arch: string; fileName: string }) => {
const getDownloadUrl = (asset: { os: DownloadOs; arch: DownloadArch; fileName: string }) => {
if (!isMounted.value) return releaseDownloadUrl(asset.fileName);
const arch = (asset.os === 'macos' ? downloadStore.macArch : asset.arch) as DownloadArch;
return resolve(asset.os as DownloadOs, arch)?.url || releaseDownloadUrl(asset.fileName);
};
const getDownloadArch = (asset: { os: string; arch: string }) => {
return asset.os === 'macos' ? downloadStore.macArch : asset.arch;
const arch = getDownloadArch(asset);
return resolve(asset.os, arch)?.url || releaseDownloadUrl(asset.fileName);
};
const releaseVersion = computed(() => releaseData.value?.version || null);

View file

@ -20,6 +20,7 @@ let heroMotionQuery: MediaQueryList | null = null;
const downloadStore = useDownloadStore();
const { resolve, data: releaseData } = useReleaseDownloads();
const { latestReleaseUrl, releaseDownloadUrl } = useGithubRepo();
const { selectedDownloadAsset } = useDownloadAssetPresentation();
const withBase = (path: string) => `${baseURL.replace(/\/?$/, "/")}${path.replace(/^\/+/, "")}`;
useCyberHeroParallax(heroRef);
@ -71,6 +72,20 @@ const heroDownloadUrl = computed(() => {
});
const docsHref = computed(() => withBase(locale.value === "ru" ? "docs/ru/" : "docs/"));
const downloadActionSubtitle = computed(() => {
if (!selectedDownloadAsset.value) {
return locale.value === "ru"
? "Для вашей платформы"
: "For your platform";
}
return selectedDownloadAsset.value.actionSubtitle;
});
const docsActionSubtitle = computed(() => (
locale.value === "ru"
? "Гайды и настройка"
: "Guides and setup"
));
function clearHeroMessageTimers() {
heroMessageTimers.forEach(window.clearTimeout);
@ -195,25 +210,23 @@ onUnmounted(() => {
</div>
<div class="cyber-hero__actions">
<v-btn
variant="flat"
size="large"
<CyberHeroActionButton
:href="heroDownloadUrl"
target="_blank"
class="cyber-hero__action cyber-hero__action--primary"
:prepend-icon="mdiDownload"
tone="primary"
:icon="mdiDownload"
:subtitle="downloadActionSubtitle"
>
{{ t("hero.downloadNow") }}
</v-btn>
<v-btn
variant="outlined"
size="large"
</CyberHeroActionButton>
<CyberHeroActionButton
:href="docsHref"
class="cyber-hero__action cyber-hero__action--docs"
:prepend-icon="mdiBookOpenPageVariantOutline"
tone="secondary"
:icon="mdiBookOpenPageVariantOutline"
:subtitle="docsActionSubtitle"
>
{{ t("hero.ctaDocs") }}
</v-btn>
</CyberHeroActionButton>
</div>
<p
@ -239,9 +252,6 @@ onUnmounted(() => {
<CyberHeroFeatureStrip
class="cyber-hero__feature-strip"
:active-message="activeHeroMessage"
:phase="heroMessagePhase"
:reduced-motion="heroReducedMotion"
/>
</v-container>
</section>

View file

@ -0,0 +1,72 @@
import { downloadAssets, type DownloadArch } from "~/data/downloads";
type DownloadAsset = (typeof downloadAssets)[number];
type DownloadAssetLike = Pick<DownloadAsset, "id" | "os" | "arch" | "archLabel">;
export type PresentedDownloadAsset = DownloadAsset & {
archLabel: string;
actionSubtitle: string;
resolvedArch: DownloadArch;
};
export function useDownloadAssetPresentation() {
const downloadStore = useDownloadStore();
const getDownloadArch = (asset: Pick<DownloadAsset, "os" | "arch">): DownloadArch => (
asset.os === "macos" ? downloadStore.macArch : asset.arch
);
const getDownloadArchLabel = (asset: DownloadAssetLike) => {
if (asset.os === "macos" && downloadStore.isMacOs) {
return downloadStore.macArch === "arm64" ? "Apple Silicon" : "Intel";
}
return asset.archLabel;
};
const getDownloadActionSubtitle = (asset: DownloadAssetLike) => {
const archLabel = getDownloadArchLabel(asset);
if (asset.os === "macos") {
const macArchLabel = archLabel === "Apple Silicon / Intel" ? "Apple Silicon & Intel" : archLabel;
return `macOS 11+ · ${macArchLabel}`;
}
if (asset.os === "windows") return `Windows 10+ · ${archLabel}`;
return `Linux · AppImage ${archLabel}`;
};
const presentDownloadAsset = (asset: DownloadAsset): PresentedDownloadAsset => ({
...asset,
archLabel: getDownloadArchLabel(asset),
actionSubtitle: getDownloadActionSubtitle(asset),
resolvedArch: getDownloadArch(asset),
});
const visibleDownloadAssets = computed(() => {
const enriched = downloadAssets.map(presentDownloadAsset);
const detectedIdx = enriched.findIndex((asset) => asset.id === downloadStore.selectedId);
if (detectedIdx === -1 || detectedIdx === 1) return enriched;
const result = [...enriched];
const [detected] = result.splice(detectedIdx, 1);
const [first, ...rest] = result;
return [first, detected, ...rest];
});
const selectedDownloadAsset = computed(() => {
const asset = downloadStore.selectedAsset;
return asset ? presentDownloadAsset(asset) : null;
});
return {
getDownloadActionSubtitle,
getDownloadArch,
getDownloadArchLabel,
presentDownloadAsset,
selectedDownloadAsset,
visibleDownloadAssets,
};
}

View file

@ -193,7 +193,7 @@ export const heroFeatureRail = [
{
id: "local",
title: "Your Machine, Your Code",
text: "Local-first workflow with task logs, process control, and Git visibility.",
text: "Track activity, logs, file changes, and what every agent is doing inside each task.",
},
] as const;
@ -255,7 +255,7 @@ const ruHeroFeatureRail: Record<string, { title: string; text: string }> = {
},
local: {
title: "Ваша машина, ваш код",
text: окальный рабочий процесс с логами задач, управлением процессами и видимостью Git.",
text: егко отслеживайте активность, логи, изменения файлов и работу каждого агента внутри каждой задачи.",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB