105 lines
3.3 KiB
Vue
105 lines
3.3 KiB
Vue
<template>
|
|
<section id="top" class="relative flex min-h-svh flex-col overflow-hidden">
|
|
<ClientOnly>
|
|
<ParticleField />
|
|
</ClientOnly>
|
|
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(200,255,46,0.08),transparent_55%)]" />
|
|
<div class="pointer-events-none absolute inset-0 bg-gradient-to-b from-ink/40 via-transparent to-ink" />
|
|
|
|
<div class="relative flex flex-1 flex-col justify-end px-6 pb-12 pt-36 md:px-10 md:pb-16 md:pt-44">
|
|
<div
|
|
class="mb-10 flex items-center justify-between font-mono text-[12px] tracking-[0.14em] text-ash animate-fade-up md:mb-16 md:text-[13px] md:tracking-[0.22em]"
|
|
style="animation-delay: 0.2s"
|
|
>
|
|
<span class="flex items-center gap-3">
|
|
<span class="h-1.5 w-1.5 animate-blink rounded-full bg-volt" />
|
|
{{ data.eyebrow }}
|
|
</span>
|
|
<span class="hidden md:block">{{ data.location }}</span>
|
|
</div>
|
|
|
|
<h1 class="w-full font-display text-[clamp(2.2rem,8.5vw,7.5rem)] font-extrabold leading-[1.05] tracking-[-0.03em]">
|
|
<div
|
|
v-for="(line, i) in data.lines"
|
|
:key="i"
|
|
class="overflow-hidden pb-[0.06em] -mb-[0.06em] animate-fade-up"
|
|
:style="{ animationDelay: `${0.15 + i * 0.12}s` }"
|
|
>
|
|
<div class="flex flex-wrap items-baseline gap-x-[0.28em]">
|
|
<span
|
|
v-for="(token, ti) in tokenize(line)"
|
|
:key="ti"
|
|
:class="token.volt ? 'font-accent text-volt' : undefined"
|
|
>{{ token.text }}</span>
|
|
</div>
|
|
</div>
|
|
</h1>
|
|
|
|
<div
|
|
class="mt-14 flex items-end justify-between border-t border-white/[0.07] pt-6 font-mono text-[11px] tracking-[0.14em] text-ash animate-fade-up md:mt-20 md:text-xs md:tracking-[0.22em]"
|
|
style="animation-delay: 0.8s"
|
|
>
|
|
<span class="flex items-center gap-2">
|
|
<span class="inline-block h-8 w-px animate-scroll-line bg-volt/80" />
|
|
اسکرول
|
|
</span>
|
|
<span class="hidden sm:block">{{ data.coords }}</span>
|
|
<span class="hidden sm:block">{{ data.projectsLabel }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
data: {
|
|
eyebrow: string;
|
|
location: string;
|
|
lines: string[];
|
|
highlightWord?: string;
|
|
highlightWords?: string[];
|
|
coords: string;
|
|
projectsLabel: string;
|
|
};
|
|
}>();
|
|
|
|
type Token = { text: string; volt?: boolean };
|
|
|
|
function highlights() {
|
|
return [
|
|
...(props.data.highlightWords || []),
|
|
props.data.highlightWord || "",
|
|
]
|
|
.map((h) => h.trim())
|
|
.filter(Boolean)
|
|
.filter((h, i, arr) => arr.indexOf(h) === i)
|
|
.sort((a, b) => b.length - a.length);
|
|
}
|
|
|
|
function tokenize(line: string): Token[] {
|
|
const hs = highlights();
|
|
const tokens: Token[] = [];
|
|
let rest = line.trim();
|
|
|
|
while (rest.length) {
|
|
let matched = false;
|
|
for (const h of hs) {
|
|
if (rest === h || rest.startsWith(`${h} `) || rest.startsWith(h)) {
|
|
tokens.push({ text: h, volt: true });
|
|
rest = rest.slice(h.length).trimStart();
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (matched) continue;
|
|
|
|
const m = rest.match(/^(\S+)(\s*)/);
|
|
if (!m) break;
|
|
tokens.push({ text: m[1] });
|
|
rest = rest.slice(m[0].length);
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
</script>
|