mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 01:29:31 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6454b40a76 |
@@ -46,6 +46,9 @@
|
||||
{
|
||||
"path": "../../libs/components/upload-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/pagedraw/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/slider-v2/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
{
|
||||
"path": "../../libs/components/upload-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/pagedraw"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/slider-v2"
|
||||
},
|
||||
|
||||
@@ -1,141 +1,105 @@
|
||||
import { ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
getCreatorIconPath,
|
||||
IMAGE_MODELS,
|
||||
VIDEO_MODELS,
|
||||
} from "@storyteller/model-list";
|
||||
|
||||
interface BadgeInfo {
|
||||
id: string;
|
||||
label?: string | ReactNode;
|
||||
svg?: ReactNode | null;
|
||||
label: string;
|
||||
iconSrc: string;
|
||||
}
|
||||
|
||||
interface ModelBadgeGridProps {
|
||||
className?: string;
|
||||
highlight?: string;
|
||||
rowOffsets?: number[];
|
||||
}
|
||||
|
||||
const baseBadgeClasses =
|
||||
"rounded-2xl px-6 py-3 text-2xl font-normal flex-shrink-0 bg-[#121212] text-white/90 text-center h-[58px] flex items-center justify-center gap-1.5";
|
||||
const highlightClasses =
|
||||
"bg-primary/30 text-white font-semibold shadow-lg border-2 border-primary/60";
|
||||
const ROW_COUNT = 3;
|
||||
// Per-row marquee durations (seconds). Deliberately mismatched so the three
|
||||
// rows never move in lockstep.
|
||||
const ROW_DURATIONS = [56, 68, 62];
|
||||
|
||||
const rows: BadgeInfo[][] = [
|
||||
[
|
||||
{ id: "empty1", svg: null },
|
||||
{ id: "empty2", svg: null },
|
||||
{ id: "empty3", svg: null },
|
||||
{
|
||||
id: "seedance-2",
|
||||
label: "Seedance 2.0",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/bytedance.svg"
|
||||
alt="Seedance logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty4", svg: null },
|
||||
// Transparent border in the base keeps the box size constant when the hover
|
||||
// highlight border appears, so the marquee doesn't jitter. The chip lights up
|
||||
// only while the pointer is over it.
|
||||
const badgeClasses =
|
||||
"rounded-2xl px-6 py-3 text-2xl font-normal flex-shrink-0 bg-[#121212] text-white/90 text-center h-[58px] flex items-center justify-center gap-1.5 border-2 border-transparent transition-all duration-300 hover:bg-primary/30 hover:text-white hover:font-semibold hover:shadow-lg hover:border-primary/60";
|
||||
|
||||
{ id: "empty6", svg: null },
|
||||
{ id: "empty7", svg: null },
|
||||
{ id: "empty8", svg: null },
|
||||
],
|
||||
[
|
||||
{ id: "empty9", svg: null },
|
||||
{
|
||||
id: "midjourney",
|
||||
label: "Midjourney",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/midjourney.svg"
|
||||
alt="Midjourney logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "nano-banana-2",
|
||||
label: "Nano Banana 2",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/google.svg"
|
||||
alt="Google gemini icon"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty10", svg: null },
|
||||
// Build one badge per unique model (deduped by display name so e.g. an
|
||||
// image+video pair like "Grok" only shows once), pulling the correct brand
|
||||
// icon from the canonical model mapping.
|
||||
const BADGES: BadgeInfo[] = (() => {
|
||||
const seen = new Set<string>();
|
||||
const badges: BadgeInfo[] = [];
|
||||
for (const model of [...IMAGE_MODELS, ...VIDEO_MODELS]) {
|
||||
if (seen.has(model.fullName)) continue;
|
||||
seen.add(model.fullName);
|
||||
badges.push({
|
||||
id: model.id,
|
||||
label: model.fullName,
|
||||
iconSrc: getCreatorIconPath(model.creator),
|
||||
});
|
||||
}
|
||||
return badges;
|
||||
})();
|
||||
|
||||
{ id: "empty11", svg: null },
|
||||
{ id: "empty12", svg: null },
|
||||
{ id: "empty13", svg: null },
|
||||
{ id: "empty14", svg: null },
|
||||
],
|
||||
[
|
||||
{ id: "empty15", svg: null },
|
||||
{ id: "empty16", svg: null },
|
||||
{ id: "empty17", svg: null },
|
||||
{
|
||||
id: "kling",
|
||||
label: "Kling 3.0 Pro",
|
||||
svg: (
|
||||
<img
|
||||
src="/model-logos/kling.svg"
|
||||
alt="Kling AI logo"
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: "empty18", svg: null },
|
||||
{ id: "empty19", svg: null },
|
||||
{ id: "empty20", svg: null },
|
||||
{ id: "empty21", svg: null },
|
||||
],
|
||||
];
|
||||
// Distribute badges round-robin across the rows so each row carries a mix of
|
||||
// brands rather than all of one creator clumping together.
|
||||
const ROWS: BadgeInfo[][] = Array.from({ length: ROW_COUNT }, () => []);
|
||||
BADGES.forEach((badge, i) => {
|
||||
ROWS[i % ROW_COUNT].push(badge);
|
||||
});
|
||||
|
||||
export default function ModelBadgeGrid({
|
||||
className = "",
|
||||
highlight,
|
||||
rowOffsets = [],
|
||||
}: ModelBadgeGridProps) {
|
||||
return (
|
||||
<div
|
||||
className={`select-none relative z-10 h-full overflow-hidden ${className}`}
|
||||
>
|
||||
{/* Gradient fade overlays */}
|
||||
{/* Gradient fade overlays mask the marquee edges */}
|
||||
<div className="absolute left-0 top-0 w-32 xl:w-96 h-full bg-gradient-to-r from-[#080808] to-transparent z-10 pointer-events-none" />
|
||||
<div className="absolute right-0 top-0 w-32 xl:w-96 h-full bg-gradient-to-l from-[#080808] to-transparent z-10 pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col gap-4 h-full -mx-[280px] xl:-mx-8">
|
||||
{rows.map((row, rowIdx) => {
|
||||
const offset = rowOffsets[rowIdx] ?? 0;
|
||||
return (
|
||||
<div className="flex flex-col gap-4 h-full justify-center py-4">
|
||||
{ROWS.map((row, rowIdx) => (
|
||||
<div key={rowIdx} className="overflow-hidden">
|
||||
<div
|
||||
key={rowIdx}
|
||||
className="flex gap-4 whitespace-nowrap"
|
||||
style={{ marginLeft: offset }}
|
||||
className="flex w-max motion-reduce:[animation:none]"
|
||||
style={{
|
||||
animationName: "marquee-track",
|
||||
animationDuration: `${ROW_DURATIONS[rowIdx]}s`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
// Even rows scroll right (reversed), odd rows scroll left.
|
||||
animationDirection: rowIdx % 2 === 0 ? "reverse" : "normal",
|
||||
}}
|
||||
>
|
||||
{row.map(({ id, label, svg }, idx) => {
|
||||
const isHighlight = highlight && highlight.toLowerCase() === id;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={twMerge(
|
||||
baseBadgeClasses,
|
||||
label ? "" : "min-w-[180px]",
|
||||
isHighlight ? highlightClasses : "",
|
||||
)}
|
||||
>
|
||||
{svg ? (
|
||||
<span className="mr-2 w-6 h-6 inline-flex ">{svg}</span>
|
||||
) : null}
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Two identical copies make the loop seamless: translating the
|
||||
track by -50% lands exactly on the start of the second copy. */}
|
||||
{[0, 1].map((copy) => (
|
||||
<div
|
||||
key={copy}
|
||||
className="flex gap-4 pr-4 flex-shrink-0"
|
||||
aria-hidden={copy === 1}
|
||||
>
|
||||
{row.map((badge) => (
|
||||
<div key={`${copy}-${badge.id}`} className={badgeClasses}>
|
||||
<span className="mr-2 w-6 h-6 inline-flex">
|
||||
<img
|
||||
src={badge.iconSrc}
|
||||
alt=""
|
||||
className="w-full h-full invert"
|
||||
/>
|
||||
</span>
|
||||
{badge.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -643,11 +643,7 @@ const LandingSD2 = () => {
|
||||
all in one place. Log in with your existing subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
<ModelBadgeGrid
|
||||
highlight="nano-banana-2"
|
||||
rowOffsets={[-70, -90, -160]}
|
||||
className="mt-3"
|
||||
/>
|
||||
<ModelBadgeGrid className="mt-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -640,11 +640,7 @@ const Landing = () => {
|
||||
all in one place. Log in with your existing subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
<ModelBadgeGrid
|
||||
highlight="nano-banana-2"
|
||||
rowOffsets={[-70, -90, -160]}
|
||||
className="mt-3"
|
||||
/>
|
||||
<ModelBadgeGrid className="mt-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ const MADE_WITH_VIDEOS = [
|
||||
const MANIFESTO_WORDS: ReadonlyArray<string> = [
|
||||
"ArtCraft",
|
||||
"brings",
|
||||
"3D",
|
||||
"control",
|
||||
"to",
|
||||
"AI",
|
||||
@@ -353,6 +352,50 @@ const Landing3 = () => {
|
||||
}),
|
||||
});
|
||||
|
||||
// Feature deck: all cards pin flush at the same top. As the NEXT card
|
||||
// rises past roughly halfway over this one, scale this card down (top
|
||||
// edge stays put) so it reads as receding behind the incoming card.
|
||||
const stackCards = gsap.utils.toArray<HTMLElement>("[data-stack-card]");
|
||||
stackCards.forEach((card, i) => {
|
||||
const next = stackCards[i + 1];
|
||||
if (!next) return; // top card is never covered
|
||||
gsap.fromTo(
|
||||
card,
|
||||
{ scale: 1 },
|
||||
{
|
||||
scale: 0.9,
|
||||
ease: "none",
|
||||
transformOrigin: "center top",
|
||||
scrollTrigger: {
|
||||
// Drive off the incoming card: start once it's about halfway up
|
||||
// the viewport (~half over this card), finish when it pins flush.
|
||||
trigger: next,
|
||||
start: "top center",
|
||||
end: "top top+=96",
|
||||
scrub: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
// Only AFTER the next card has fully overlapped this one (it pins flush
|
||||
// at the same top) do we fade this card out. It's hidden behind the
|
||||
// next card by then, so the fade is invisible — it just drops this
|
||||
// card's shadow so the edge shadows don't stack across the deck.
|
||||
gsap.fromTo(
|
||||
card,
|
||||
{ opacity: 1 },
|
||||
{
|
||||
opacity: 0,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: next,
|
||||
start: "top top+=96",
|
||||
end: "top top-=40",
|
||||
scrub: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Hero pattern parallax (slower than scroll, drifts upward)
|
||||
const heroPattern = document.querySelector<HTMLElement>(
|
||||
"[data-hero-pattern]",
|
||||
@@ -428,6 +471,23 @@ const Landing3 = () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Phase 1b — once the sentence is fully revealed, draw a blue
|
||||
// underline in under "control" to land the emphasis.
|
||||
const manifestoUnderline = manifestoSection.querySelector<HTMLElement>(
|
||||
"[data-manifesto-underline]",
|
||||
);
|
||||
if (manifestoUnderline) {
|
||||
gsap.set(manifestoUnderline, {
|
||||
scaleX: 0,
|
||||
transformOrigin: "left center",
|
||||
});
|
||||
tl.to(
|
||||
manifestoUnderline,
|
||||
{ scaleX: 1, duration: 4, ease: "power2.out" },
|
||||
">",
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 2 — hold the fully-revealed manifesto frozen so the user gets
|
||||
// a beat to read it before the transition triggers.
|
||||
tl.to({}, { duration: 4 });
|
||||
@@ -678,7 +738,18 @@ const Landing3 = () => {
|
||||
className="text-2xl sm:text-3xl tracking-[-0.035em] font-medium text-white px-4 sm:px-0"
|
||||
style={{ lineHeight: 1.4 }}
|
||||
>
|
||||
{MANIFESTO_WORDS.join(" ")}
|
||||
{MANIFESTO_WORDS.map((w, i) => (
|
||||
<span key={i}>
|
||||
{w === "control" ? (
|
||||
<span className="underline decoration-primary decoration-[3px] underline-offset-4">
|
||||
{w}
|
||||
</span>
|
||||
) : (
|
||||
w
|
||||
)}
|
||||
{i < MANIFESTO_WORDS.length - 1 ? " " : ""}
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
@@ -729,7 +800,18 @@ const Landing3 = () => {
|
||||
data-manifesto-word
|
||||
className="inline-block opacity-15 will-change-[opacity,transform]"
|
||||
>
|
||||
{w}
|
||||
{w === "control" ? (
|
||||
<span className="relative inline-block">
|
||||
{w}
|
||||
<span
|
||||
data-manifesto-underline
|
||||
aria-hidden
|
||||
className="absolute left-0 -bottom-0.5 h-[4px] w-full origin-left scale-x-0 rounded-full bg-primary"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
w
|
||||
)}
|
||||
{i < MANIFESTO_WORDS.length - 1 ? " " : ""}
|
||||
</span>
|
||||
))}
|
||||
@@ -758,7 +840,7 @@ const Landing3 = () => {
|
||||
</div>
|
||||
</section>
|
||||
{/* FEATURES: alternating cards */}
|
||||
<section className="relative px-4 sm:px-8 py-16 sm:py-24">
|
||||
<section className="relative px-4 sm:px-8 py-16 sm:pt-24">
|
||||
<TruchetBlob
|
||||
className="top-[6%] -left-40 w-[640px] h-[640px]"
|
||||
variant="content"
|
||||
@@ -780,45 +862,48 @@ const Landing3 = () => {
|
||||
speed={-22}
|
||||
rotate={8}
|
||||
/>
|
||||
<div className="max-w-6xl mx-auto flex flex-col gap-8 sm:gap-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{FEATURES.map((feature, i) => (
|
||||
<article
|
||||
<div
|
||||
key={feature.title}
|
||||
data-reveal
|
||||
className="grid grid-cols-1 lg:grid-cols-12 gap-0 rounded-2xl sm:rounded-[28px] overflow-hidden bg-[#080808] transition-colors"
|
||||
data-stack-card
|
||||
className="relative lg:sticky"
|
||||
style={!isMobile ? { top: "96px" } : undefined}
|
||||
>
|
||||
<div
|
||||
className={`lg:col-span-5 p-7 sm:p-10 lg:p-12 flex flex-col justify-center ${
|
||||
i % 2 === 1 ? "lg:order-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<span className="inline-flex h-7 px-2.5 items-center gap-1.5 rounded-full bg-primary/15 text-primary text-[12px] font-semibold border border-primary/20">
|
||||
<FontAwesomeIcon
|
||||
icon={feature.icon}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
{feature.label}
|
||||
</span>
|
||||
<article className="grid grid-cols-1 lg:grid-cols-12 gap-0 rounded-2xl sm:rounded-[28px] overflow-hidden bg-[#080808] shadow-[0_-16px_50px_-8px_rgba(0,0,0,0.85)] transition-colors mb-8 lg:mb-[8vh]">
|
||||
<div
|
||||
className={`lg:col-span-5 p-7 sm:p-10 lg:p-12 flex flex-col justify-center ${
|
||||
i % 2 === 1 ? "lg:order-2" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<span className="inline-flex h-7 px-2.5 items-center gap-1.5 rounded-full bg-primary/15 text-primary text-[12px] font-semibold border border-primary/20">
|
||||
<FontAwesomeIcon
|
||||
icon={feature.icon}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
{feature.label}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-2xl sm:text-3xl md:text-[32px] tracking-[-0.02em] font-medium leading-[1.15] mb-4 text-white">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-[15px] sm:text-base text-white/55 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="text-2xl sm:text-3xl md:text-[32px] tracking-[-0.02em] font-medium leading-[1.15] mb-4 text-white">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-[15px] sm:text-base text-white/55 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`lg:col-span-7 relative bg-[#080808] aspect-[12/10] lg:self-center ${
|
||||
i % 2 === 1 ? "lg:order-1" : ""
|
||||
}`}
|
||||
>
|
||||
<LazyAutoplayVideo
|
||||
src={feature.src}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
<div
|
||||
className={`lg:col-span-7 relative bg-[#080808] aspect-[12/10] lg:self-center ${
|
||||
i % 2 === 1 ? "lg:order-1" : ""
|
||||
}`}
|
||||
>
|
||||
<LazyAutoplayVideo
|
||||
src={feature.src}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
@@ -837,45 +922,57 @@ const Landing3 = () => {
|
||||
Ownership
|
||||
</span>
|
||||
<h2 className="text-4xl sm:text-5xl md:text-6xl tracking-[-0.035em] font-medium leading-[1.02] mb-5">
|
||||
Stop renting from{" "}
|
||||
<span className="font-serif-italic">websites</span>.
|
||||
Stop <span className="font-serif-italic">renting</span> from
|
||||
websites.
|
||||
</h2>
|
||||
<p className="max-w-xl mx-auto text-base sm:text-lg text-white/55 leading-relaxed">
|
||||
ArtCraft is yours to own and keep,{" "}
|
||||
<span className="font-serif-italic text-white/75">forever</span>.
|
||||
No subscriptions, no aggregator middleman, no rent payments.
|
||||
ArtCraft is yours to own and keep, forever. No subscriptions
|
||||
needed, no aggregator middleman, no rent payments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||||
{/* Websites column */}
|
||||
<div className="rounded-2xl sm:rounded-[28px] bg-[#080808] p-7 sm:p-8">
|
||||
<div className="flex items-center gap-2 mb-9">
|
||||
<span className="text-[12px] font-bold uppercase tracking-wider text-white/40">
|
||||
Other tools
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl sm:text-2xl font-medium mb-5 tracking-[-0.01em] text-white/85">
|
||||
The Rental Trap
|
||||
</h3>
|
||||
<p className="text-[15px] text-white/55 leading-relaxed mb-6">
|
||||
With browser-based tools, you're paying for access, not a
|
||||
product. Your work, models, and history live on someone else's
|
||||
servers, and disappear with them.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{["No ownership", "Monthly fees"].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-red-300/80 bg-red-500/[0.08] border border-red-500/20 rounded-lg px-2.5 py-1.5"
|
||||
>
|
||||
{/* Websites column — deliberately styled to feel like a downgrade */}
|
||||
<div className="relative overflow-hidden rounded-2xl sm:rounded-[28px] bg-gradient-to-br from-red-500/[0.08] via-[#0a0607] to-[#080808] border border-red-500/20 p-7 sm:p-8">
|
||||
<div
|
||||
className="absolute -top-16 -left-16 w-72 h-72 rounded-full pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(239,68,68,0.18) 0%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<span className="inline-flex items-center gap-1.5 text-[12px] font-bold uppercase tracking-wider text-red-300/70">
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="text-red-400 text-[10px]"
|
||||
className="text-red-400 text-[11px]"
|
||||
/>
|
||||
{tag}
|
||||
Other tools
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="text-xl sm:text-2xl font-medium mb-5 tracking-[-0.01em] text-white/70">
|
||||
The Rental Trap
|
||||
</h3>
|
||||
<p className="text-[15px] text-white/45 leading-relaxed mb-6">
|
||||
With browser-based tools, you're paying for access, not a
|
||||
product. Your work, models, and history live on someone else's
|
||||
servers, and disappear with them.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{["No ownership", "Monthly fees", "Locked in"].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-red-300/80 bg-red-500/[0.08] border border-red-500/20 rounded-lg px-2.5 py-1.5"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="text-red-400 text-[10px]"
|
||||
/>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -906,18 +1003,20 @@ const Landing3 = () => {
|
||||
keys, or use ours.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 self-end">
|
||||
{["Yours forever", "BYO keys"].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-primary-200 bg-primary/[0.12] border border-primary/25 rounded-lg px-2.5 py-1.5"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-primary text-[10px]"
|
||||
/>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{["Yours forever", "BYO keys", "No subscriptions needed"].map(
|
||||
(tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-primary-200 bg-primary/[0.12] border border-primary/25 rounded-lg px-2.5 py-1.5"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-primary text-[10px]"
|
||||
/>
|
||||
{tag}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1073,11 +1172,7 @@ const Landing3 = () => {
|
||||
all in one place. Log in with your existing subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
<ModelBadgeGrid
|
||||
highlight="nano-banana-2"
|
||||
rowOffsets={[-70, -90, -160]}
|
||||
className="mt-3"
|
||||
/>
|
||||
<ModelBadgeGrid className="mt-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1191,25 +1286,30 @@ const Landing3 = () => {
|
||||
</section>
|
||||
{/* COMMUNITY CTA */}
|
||||
<section className="relative px-4 sm:px-8 pt-8 sm:pt-12 overflow-hidden">
|
||||
<div className="relative z-10 max-w-3xl mx-auto" data-reveal>
|
||||
<div className="rounded-2xl sm:rounded-3xl bg-white/[0.02] border border-white/[0.08] px-6 py-10 sm:px-10 sm:py-12 text-center">
|
||||
<h2 className="text-2xl sm:text-3xl tracking-[-0.025em] font-medium leading-tight mb-3 text-white">
|
||||
Open source & community-driven
|
||||
</h2>
|
||||
<p className="max-w-xl mx-auto text-base text-white/60 leading-relaxed mb-8">
|
||||
Join the conversation on Discord and star us on GitHub.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<div className="relative z-10 max-w-5xl mx-auto" data-reveal>
|
||||
<div className="relative overflow-hidden flex flex-col gap-7 md:flex-row md:items-center md:justify-between rounded-2xl sm:rounded-[28px] bg-[#080808] border border-white/[0.1] px-7 py-8 sm:px-10 sm:py-10">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-5 text-center sm:text-left">
|
||||
{/* <span className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-primary/15 border border-primary/25 text-primary">
|
||||
<FontAwesomeIcon icon={faDiscord} className="text-2xl" />
|
||||
</span> */}
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl tracking-[-0.02em] font-medium leading-tight text-white">
|
||||
Join our <span className="font-serif-italic">community</span>
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-white/55 leading-relaxed mt-2 max-w-md">
|
||||
ArtCraft is open source and community-driven. Come build with
|
||||
us.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 shrink-0">
|
||||
<a
|
||||
href={SOCIAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 h-11 px-5 rounded-full bg-white/[0.06] hover:bg-white/[0.1] text-white text-[14px] font-semibold border border-white/[0.1] transition-all hover:-translate-y-px"
|
||||
className="group inline-flex items-center gap-2 h-11 px-5 rounded-full bg-primary hover:bg-primary-600 text-white text-[14px] font-semibold transition-all shadow-[0_4px_24px_-4px_rgba(45,129,255,0.4)] hover:shadow-[0_8px_32px_-4px_rgba(45,129,255,0.5)] hover:-translate-y-px"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faDiscord}
|
||||
className="text-[13px] text-[#5865F2]"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faDiscord} className="text-[13px]" />
|
||||
Join Discord
|
||||
</a>
|
||||
<a
|
||||
|
||||
@@ -163,6 +163,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* For marquee tracks that render two identical copies of their content:
|
||||
translating by -50% lands exactly on the start of the second copy, so the
|
||||
loop is seamless. */
|
||||
@keyframes marquee-track {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
{
|
||||
"path": "../../api"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
|
||||
@@ -46,9 +46,6 @@
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user