Add VectorCraft, EffectCraft, DesignCraft and app icons to Craft Apps pages
- Rename DrawCraft to VectorCraft (repo renamed); /apps/drawcraft
permanently redirects to /apps/vectorcraft, since the Craft READMEs
still link the old page
- Add EffectCraft (motion graphics & VFX) and DesignCraft (page layout &
publishing) pages with copy, screenshots and share images
- Show each app's icon: an icon row in the /apps header, on lineup and
family cards, and above the wordmark in each app hero
- PhotoCraft installers (v0.1.1-rc.4): hero download button, macOS /
Windows / Linux downloads, "Installers ready" badge, release JSON-LD
- Portrait gallery layout for DesignCraft's magazine pages
- Share images now carry the app icon; hub share image is the icon row
- Press kit: Crafting Apps icons (1024 px PNG from each repo)
- Count-driven hub copy ("Seven apps"), 4-column lineup, colors for the
two new apps
No files matched your search
@@ -25,6 +25,13 @@ const nextConfig: NextConfig = {
|
||||
permanent: false,
|
||||
}),
|
||||
),
|
||||
// DrawCraft was renamed VectorCraft; the Crafting Apps READMEs still
|
||||
// link the old page.
|
||||
{
|
||||
source: "/apps/drawcraft",
|
||||
destination: "/apps/vectorcraft",
|
||||
permanent: true,
|
||||
},
|
||||
// Legacy share-link form (`/media?media=<token>`) from the Vite site;
|
||||
// the canonical route is /media/<token>. Mirrored in netlify.toml so
|
||||
// the edge answers it before the Next runtime.
|
||||
|
||||
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -1,11 +1,13 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowDownToLineIcon } from "lucide-react";
|
||||
import {
|
||||
AppFamily,
|
||||
AppGallery,
|
||||
AppGetIt,
|
||||
} from "@/components/apps/app-sections";
|
||||
import {
|
||||
AppIcon,
|
||||
AppTab,
|
||||
AppWordmark,
|
||||
ColorAccent,
|
||||
@@ -24,7 +26,9 @@ import {
|
||||
craftAppName,
|
||||
craftAppOgImage,
|
||||
craftAppPath,
|
||||
craftAppIconUrl,
|
||||
craftAppRepo,
|
||||
craftReleasePageUrl,
|
||||
craftShotUrl,
|
||||
getCraftApp,
|
||||
} from "@/lib/crafting-apps";
|
||||
@@ -81,8 +85,13 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
description: app.lede,
|
||||
url: shareUrl,
|
||||
image: siteUrl(craftAppOgImage(app)),
|
||||
thumbnailUrl: siteUrl(craftAppIconUrl(app)),
|
||||
screenshot: app.shots.map((shot) => siteUrl(craftShotUrl(app, shot))),
|
||||
codeRepository: craftAppRepo(app),
|
||||
sameAs: craftAppRepo(app),
|
||||
...(app.release && {
|
||||
softwareVersion: app.release.version,
|
||||
downloadUrl: craftReleasePageUrl(app, app.release),
|
||||
}),
|
||||
isAccessibleForFree: true,
|
||||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||||
publisher: { "@type": "Organization", name: "ArtCraft", url: SITE_URL },
|
||||
@@ -101,9 +110,12 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
label={name}
|
||||
annotation={`${app.category} · ${app.status}`}
|
||||
kicker={
|
||||
<span className="flex items-center gap-3">
|
||||
<AppTab>{craftAppIndex(app)}</AppTab>
|
||||
<AppWordmark app={app} className="text-2xl" />
|
||||
<span className="flex flex-col items-center gap-5">
|
||||
<AppIcon app={app} priority className="h-24 w-24 md:h-28 md:w-28" />
|
||||
<span className="flex items-center gap-3">
|
||||
<AppTab>{craftAppIndex(app)}</AppTab>
|
||||
<AppWordmark app={app} className="text-2xl" />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
title={
|
||||
@@ -120,8 +132,6 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
src={craftShotUrl(app, hero)}
|
||||
alt={hero.alt}
|
||||
fetchPriority="high"
|
||||
width={1600}
|
||||
height={1000}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
}
|
||||
@@ -130,7 +140,18 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<DiscordButton size="lg">Join the Discord</DiscordButton>
|
||||
{app.release && (
|
||||
<Button href="#get-it" size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
Download {name}
|
||||
</Button>
|
||||
)}
|
||||
<DiscordButton
|
||||
size="lg"
|
||||
variant={app.release ? "secondary" : "primary"}
|
||||
>
|
||||
Join the Discord
|
||||
</DiscordButton>
|
||||
<Button
|
||||
href={craftAppRepo(app)}
|
||||
variant="secondary"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
BotIcon,
|
||||
CodeXmlIcon,
|
||||
@@ -17,22 +18,26 @@ import {
|
||||
import DiscordButton from "@/components/discord-button";
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { SectionShell, SectionEyebrow } from "@/components/landing/section-shell";
|
||||
import { ColorAccent } from "@/components/apps/app-wordmark";
|
||||
import { AppIcon, ColorAccent, appThemeClass } from "@/components/apps/app-wordmark";
|
||||
import { PageHeader } from "@/components/page/page-header";
|
||||
import RevealManager from "@/components/reveal-manager";
|
||||
import { Button } from "@/components/ui";
|
||||
import {
|
||||
CRAFTING_APPS,
|
||||
CRAFTING_APPS_COUNT_WORD,
|
||||
CRAFTING_APPS_GITHUB_ORG,
|
||||
CRAFTING_APPS_OG_IMAGE,
|
||||
CRAFTING_APPS_PRINCIPLES,
|
||||
craftAppIndex,
|
||||
craftAppName,
|
||||
craftAppPath,
|
||||
} from "@/lib/crafting-apps";
|
||||
import { siteUrl } from "@/lib/links";
|
||||
|
||||
const TITLE = "Crafting Apps: open-source creative tools";
|
||||
const DESCRIPTION =
|
||||
"Image editing, vector illustration, video, photography and PDFs: five native, open-source apps from the ArtCraft team, built in Rust and free to use.";
|
||||
const COVERAGE =
|
||||
"Image editing, vector illustration, video, photography, PDFs, motion graphics and page layout";
|
||||
const DESCRIPTION = `${COVERAGE}: ${CRAFTING_APPS_COUNT_WORD.toLowerCase()} native, open-source apps from the ArtCraft team, built in Rust and free to use.`;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
@@ -67,13 +72,13 @@ export default function CraftingAppsPage() {
|
||||
id="apps"
|
||||
index="01"
|
||||
label="Crafting Apps"
|
||||
annotation="Five apps · Open source · Pure Rust"
|
||||
annotation={`${CRAFTING_APPS_COUNT_WORD} apps · Open source · Pure Rust`}
|
||||
title={
|
||||
<>
|
||||
Five apps. One <ColorAccent>craft</ColorAccent>.
|
||||
{CRAFTING_APPS_COUNT_WORD} apps. One <ColorAccent>craft</ColorAccent>.
|
||||
</>
|
||||
}
|
||||
lede="Image editing, vector illustration, video, photography and PDFs. Native, open-source apps from the ArtCraft team, built in Rust and free to use."
|
||||
lede={`${COVERAGE}. Native, open-source apps from the ArtCraft team, built in Rust and free to use.`}
|
||||
>
|
||||
<div data-reveal className="mt-8 flex flex-wrap gap-3">
|
||||
<DiscordButton size="lg">Join the Discord</DiscordButton>
|
||||
@@ -90,9 +95,31 @@ export default function CraftingAppsPage() {
|
||||
</div>
|
||||
<ShareBar
|
||||
url={siteUrl("/apps")}
|
||||
text="Crafting Apps: five open-source creative apps from ArtCraft"
|
||||
text={`Crafting Apps: ${CRAFTING_APPS_COUNT_WORD.toLowerCase()} open-source creative apps from ArtCraft`}
|
||||
className="mt-6"
|
||||
/>
|
||||
<nav
|
||||
aria-label="Crafting Apps"
|
||||
data-reveal
|
||||
className="mt-12 flex flex-wrap gap-x-3 gap-y-6 sm:gap-x-5"
|
||||
>
|
||||
{CRAFTING_APPS.map((app) => (
|
||||
<Link
|
||||
key={app.slug}
|
||||
href={craftAppPath(app)}
|
||||
className={`group flex w-18 flex-col items-center gap-2 sm:w-20 ${appThemeClass(app)}`}
|
||||
>
|
||||
<AppIcon
|
||||
app={app}
|
||||
priority
|
||||
className="h-14 w-14 transition-transform duration-300 ease-out group-hover:-translate-y-1 sm:h-20 sm:w-20"
|
||||
/>
|
||||
<span className="text-xs font-medium text-muted group-hover:text-(--app-ink)">
|
||||
{craftAppName(app)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</PageHeader>
|
||||
|
||||
<SectionShell id="lineup">
|
||||
@@ -103,7 +130,7 @@ export default function CraftingAppsPage() {
|
||||
/>
|
||||
<div
|
||||
data-reveal-group
|
||||
className="grid gap-px bg-line sm:grid-cols-2 lg:grid-cols-3"
|
||||
className="grid gap-px bg-line sm:grid-cols-2 xl:grid-cols-4"
|
||||
>
|
||||
{CRAFTING_APPS.map((app) => (
|
||||
<AppCard key={app.slug} app={app} index={craftAppIndex(app)} />
|
||||
|
||||
@@ -313,25 +313,32 @@ html[data-ruler="on"]::-webkit-scrollbar {
|
||||
:root:not([data-theme="light"]) .plan-enterprise { --plan-ink: #7ea3ee; }
|
||||
}
|
||||
|
||||
/* Crafting Apps color identities, same contract as the plan colors:
|
||||
/* Crafting Apps color identities (each app's icon tile color), same
|
||||
contract as the plan colors:
|
||||
`--app` is the saturated surface color (black text on it passes AA),
|
||||
`--app-ink` the contrast-safe text shade per theme. */
|
||||
.app-photocraft { --app: #2f7bf5; --app-ink: #1a5bc4; }
|
||||
.app-drawcraft { --app: #e8573f; --app-ink: #b83a24; }
|
||||
.app-vectorcraft { --app: #e8573f; --app-ink: #b83a24; }
|
||||
.app-filmcraft { --app: #8b5cf6; --app-ink: #6a3fd6; }
|
||||
.app-lightcraft { --app: #f2a516; --app-ink: #8a5800; }
|
||||
.app-printcraft { --app: #12a58a; --app-ink: #0a7563; }
|
||||
.app-effectcraft { --app: #e0368f; --app-ink: #b0206c; }
|
||||
.app-designcraft { --app: #7bb51c; --app-ink: #457008; }
|
||||
:root[data-theme="dark"] .app-photocraft { --app-ink: #7fb0ff; }
|
||||
:root[data-theme="dark"] .app-drawcraft { --app-ink: #ff8a70; }
|
||||
:root[data-theme="dark"] .app-vectorcraft { --app-ink: #ff8a70; }
|
||||
:root[data-theme="dark"] .app-filmcraft { --app-ink: #b69cff; }
|
||||
:root[data-theme="dark"] .app-lightcraft { --app-ink: #ffc14d; }
|
||||
:root[data-theme="dark"] .app-printcraft { --app-ink: #3fd6b8; }
|
||||
:root[data-theme="dark"] .app-effectcraft { --app-ink: #ff7ab8; }
|
||||
:root[data-theme="dark"] .app-designcraft { --app-ink: #a6e04a; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .app-photocraft { --app-ink: #7fb0ff; }
|
||||
:root:not([data-theme="light"]) .app-drawcraft { --app-ink: #ff8a70; }
|
||||
:root:not([data-theme="light"]) .app-vectorcraft { --app-ink: #ff8a70; }
|
||||
:root:not([data-theme="light"]) .app-filmcraft { --app-ink: #b69cff; }
|
||||
:root:not([data-theme="light"]) .app-lightcraft { --app-ink: #ffc14d; }
|
||||
:root:not([data-theme="light"]) .app-printcraft { --app-ink: #3fd6b8; }
|
||||
:root:not([data-theme="light"]) .app-effectcraft { --app-ink: #ff7ab8; }
|
||||
:root:not([data-theme="light"]) .app-designcraft { --app-ink: #a6e04a; }
|
||||
}
|
||||
|
||||
/* Plan cell: a color wash pooling under the index tab and fading into the
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
craftShotUrl,
|
||||
type CraftApp,
|
||||
} from "@/lib/crafting-apps";
|
||||
import { AppTab, AppWordmark, appThemeClass } from "./app-wordmark";
|
||||
import { AppIcon, AppTab, AppWordmark, appThemeClass } from "./app-wordmark";
|
||||
|
||||
// Lineup cell: color tab + category, the hero screenshot, wordmark, pitch,
|
||||
// and (unless compact) platform/status chips. The whole cell is the link; a
|
||||
// hairline in the app's color draws across the top on hover.
|
||||
// Lineup cell: color tab + category, the hero screenshot, the app icon
|
||||
// straddling the screenshot's bottom edge, wordmark, pitch, and (unless
|
||||
// compact) status/platform chips. The whole cell is the link; a hairline in
|
||||
// the app's color draws across the top on hover.
|
||||
export default function AppCard({
|
||||
app,
|
||||
index,
|
||||
@@ -47,22 +48,25 @@ export default function AppCard({
|
||||
alt={hero.alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width={1600}
|
||||
height={1000}
|
||||
className="h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.03]"
|
||||
/>
|
||||
</div>
|
||||
<div className={twMerge("flex flex-1 flex-col", compact ? "p-6" : "p-6 md:p-8")}>
|
||||
<h3 className="leading-none">
|
||||
<AppWordmark
|
||||
app={app}
|
||||
className={compact ? "text-2xl" : "text-3xl sm:text-4xl"}
|
||||
/>
|
||||
<div className={twMerge("flex flex-1 flex-col", compact ? "px-6 pb-6" : "px-6 pb-6 md:px-8 md:pb-8")}>
|
||||
<AppIcon
|
||||
app={app}
|
||||
className={twMerge(
|
||||
"relative transition-transform duration-300 ease-out group-hover:-translate-y-1",
|
||||
compact ? "-mt-7 h-14 w-14" : "-mt-9 h-18 w-18",
|
||||
)}
|
||||
/>
|
||||
<h3 className="mt-4 leading-none">
|
||||
<AppWordmark app={app} className={compact ? "text-2xl" : "text-3xl"} />
|
||||
</h3>
|
||||
<p className="mt-3 leading-relaxed text-muted">{app.pitch}</p>
|
||||
{!compact && (
|
||||
<div className="mt-6 flex flex-wrap gap-1.5">
|
||||
<Badge label={app.status} className="text-(--app-ink)" />
|
||||
{app.release && <Badge label="Installers ready" className="text-(--app-ink)" />}
|
||||
{app.platforms.map((platform) => (
|
||||
<Badge key={platform} label={platform} />
|
||||
))}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
craftAppIndex,
|
||||
craftAppName,
|
||||
craftAppRepo,
|
||||
craftReleaseDownloads,
|
||||
craftReleasePageUrl,
|
||||
craftShotSourceUrl,
|
||||
craftShotUrl,
|
||||
formatList,
|
||||
@@ -25,9 +27,11 @@ import { ColorAccent } from "./app-wordmark";
|
||||
const CELL_HEADING_CLASSES =
|
||||
"mt-4 font-display text-3xl font-medium leading-[1.05] tracking-[-0.03em] text-ink-strong sm:text-4xl";
|
||||
|
||||
// The hero carries shot 01; the rest lead with a full-width shot, then pair.
|
||||
// The hero carries shot 01. Window captures lead with a full-width shot,
|
||||
// then pair; portrait pages (layout spreads) sit four across.
|
||||
export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
const [, ...shots] = app.shots;
|
||||
const portrait = shots.every((shot) => shot.portrait);
|
||||
return (
|
||||
<SectionShell id="gallery">
|
||||
<SectionEyebrow
|
||||
@@ -35,18 +39,27 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
label="Screenshots"
|
||||
annotation={`Captured in ${craftAppName(app)}`}
|
||||
/>
|
||||
<div data-reveal-group className="grid gap-px bg-line md:grid-cols-2">
|
||||
<div
|
||||
data-reveal-group
|
||||
className={twMerge(
|
||||
"grid gap-px bg-line",
|
||||
portrait ? "grid-cols-2 lg:grid-cols-4" : "md:grid-cols-2",
|
||||
)}
|
||||
>
|
||||
{shots.map((shot, i) => (
|
||||
<figure
|
||||
key={shot.file}
|
||||
data-reveal
|
||||
className={twMerge("bg-bg", i === 0 && "md:col-span-2")}
|
||||
className={twMerge("bg-bg", !portrait && i === 0 && "md:col-span-2")}
|
||||
>
|
||||
<a
|
||||
href={craftShotSourceUrl(app, shot)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative block aspect-[16/10] overflow-hidden bg-bg-sunken"
|
||||
className={twMerge(
|
||||
"group relative block overflow-hidden bg-bg-sunken",
|
||||
shot.portrait ? "aspect-[7/9]" : "aspect-[16/10]",
|
||||
)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
@@ -54,8 +67,6 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
alt={shot.alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width={1600}
|
||||
height={1000}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<span className="hud-label absolute right-3 bottom-3 flex items-center gap-1 bg-bg/80 px-2 py-1 text-ink opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||
@@ -63,7 +74,12 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
<ArrowUpRightIcon aria-hidden className="h-3 w-3" />
|
||||
</span>
|
||||
</a>
|
||||
<figcaption className="flex items-center justify-between gap-4 border-t border-line px-6 py-3 md:px-10">
|
||||
<figcaption
|
||||
className={twMerge(
|
||||
"flex items-center justify-between gap-4 border-t border-line py-3",
|
||||
portrait ? "px-4 md:px-6" : "px-6 md:px-10",
|
||||
)}
|
||||
>
|
||||
<span className="hud-label text-muted">{shot.caption}</span>
|
||||
<span className="hud-label text-faint">
|
||||
{String(i + 2).padStart(2, "0")}
|
||||
@@ -77,8 +93,8 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
}
|
||||
|
||||
// Installers (or, until there are any, the Discord waitlist) beside
|
||||
// build-from-source. Filling `downloads` in crafting-apps.ts flips the left
|
||||
// cell to download buttons.
|
||||
// build-from-source. Giving an app a `release` in crafting-apps.ts flips the
|
||||
// left cell to download buttons.
|
||||
export function AppGetIt({ app, index }: { app: CraftApp; index: string }) {
|
||||
const name = craftAppName(app);
|
||||
const command = craftAppBuildCommand(app);
|
||||
@@ -94,19 +110,32 @@ export function AppGetIt({ app, index }: { app: CraftApp; index: string }) {
|
||||
<div className="grid gap-px bg-line md:grid-cols-2">
|
||||
<div data-reveal className="flex flex-col bg-bg p-6 md:p-10">
|
||||
<p className="hud-label text-faint">Installers</p>
|
||||
{app.downloads ? (
|
||||
{app.release ? (
|
||||
<>
|
||||
<h3 className={CELL_HEADING_CLASSES}>
|
||||
Download <ColorAccent>{name}</ColorAccent>.
|
||||
</h3>
|
||||
<p className="mt-4 max-w-md leading-relaxed text-muted">
|
||||
Version {app.release.version}, free. Questions or feedback?
|
||||
The team is in the ArtCraft Discord.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{app.downloads.map((download) => (
|
||||
{craftReleaseDownloads(app, app.release).map((download) => (
|
||||
<Button key={download.href} href={download.href} size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
{download.label}
|
||||
{download.platform}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href={craftReleasePageUrl(app, app.release)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hud-label mt-5 flex w-fit items-center gap-1.5 text-muted hover:text-ink"
|
||||
>
|
||||
All builds and checksums
|
||||
<ArrowUpRightIcon aria-hidden className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -202,7 +231,7 @@ export function AppFamily({ app, index }: { app: CraftApp; index: string }) {
|
||||
</div>
|
||||
<div
|
||||
data-reveal-group
|
||||
className="grid gap-px border-t border-line bg-line sm:grid-cols-2 lg:grid-cols-4"
|
||||
className="grid gap-px border-t border-line bg-line sm:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{siblings.map((sibling) => (
|
||||
<AppCard
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { CraftApp } from "@/lib/crafting-apps";
|
||||
import { craftAppIconUrl, type CraftApp } from "@/lib/crafting-apps";
|
||||
|
||||
// The app's engraved-animal icon: a rounded tile with transparent corners,
|
||||
// so the drop shadow follows the tile shape. Decorative — it always sits
|
||||
// beside the app's name.
|
||||
export function AppIcon({
|
||||
app,
|
||||
priority = false,
|
||||
className,
|
||||
}: {
|
||||
app: CraftApp;
|
||||
/** Above the fold: load eagerly at high priority. */
|
||||
priority?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={craftAppIconUrl(app)}
|
||||
alt=""
|
||||
width={256}
|
||||
height={256}
|
||||
loading={priority ? "eager" : "lazy"}
|
||||
fetchPriority={priority ? "high" : undefined}
|
||||
decoding="async"
|
||||
className={twMerge(
|
||||
"h-14 w-14 shrink-0 drop-shadow-[0_6px_14px_rgba(0,0,0,0.28)]",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Scopes the app's color identity (`--app`, `--app-ink` in globals.css).
|
||||
export function appThemeClass(app: CraftApp): string {
|
||||
@@ -37,11 +68,11 @@ export function AppWordmark({
|
||||
// serif italic used elsewhere on the site.
|
||||
export function ColorAccent({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="text-[var(--app-ink,var(--accent-ink))]">{children}</span>
|
||||
<span className="text-(--app-ink,var(--accent-ink))">{children}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Saturated index tab in the app's color (black text passes AA on all five).
|
||||
// Saturated index tab in the app's color (black text passes AA on every app).
|
||||
export function AppTab({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<span className="hud-label bg-(--app) px-2 py-1 font-bold text-black">
|
||||
|
||||
@@ -9,15 +9,20 @@ import { mediaUrl } from "./links";
|
||||
// Facts (counts, timings, formats) come from each repo's README — keep them
|
||||
// in sync when the READMEs change.
|
||||
//
|
||||
// No installers are published yet. When an app ships a release, give it
|
||||
// `downloads` and the page swaps its "coming soon" cell for the buttons.
|
||||
// Apps without a published GitHub release show a Discord waitlist. When one
|
||||
// ships, give it `release` and its page offers the installers instead.
|
||||
//
|
||||
// Assets live in public/images/apps/<slug>/: icon.webp (the repo's 512 px
|
||||
// hicolor render at 256 px), og.jpg (1200×630 share card) and the shots.
|
||||
|
||||
export type CraftAppSlug =
|
||||
| "photocraft"
|
||||
| "drawcraft"
|
||||
| "vectorcraft"
|
||||
| "filmcraft"
|
||||
| "lightcraft"
|
||||
| "printcraft";
|
||||
| "printcraft"
|
||||
| "effectcraft"
|
||||
| "designcraft";
|
||||
|
||||
export type CraftPlatform = "macOS" | "Windows" | "Linux" | "Web";
|
||||
|
||||
@@ -28,15 +33,24 @@ export type CraftShot = {
|
||||
source: string;
|
||||
caption: string;
|
||||
alt: string;
|
||||
/** Portrait page (7:9) rather than a 16:10 window capture. */
|
||||
portrait?: boolean;
|
||||
};
|
||||
|
||||
export type CraftRelease = {
|
||||
/** Tag without the leading "v", e.g. "0.1.1-rc.4". */
|
||||
version: string;
|
||||
/** Asset names are `<slug>-<version>-<suffix>`. */
|
||||
files: { platform: Exclude<CraftPlatform, "Web">; suffix: string }[];
|
||||
};
|
||||
|
||||
export type CraftApp = {
|
||||
slug: CraftAppSlug;
|
||||
/** Prefix before "Craft" — the wordmark sets "Craft" in the serif. */
|
||||
/** Prefix before "Craft" — the wordmark colors "Craft". */
|
||||
prefix: string;
|
||||
category: string;
|
||||
status: string;
|
||||
/** Headline split around its one serif-italic contrast word. */
|
||||
/** Headline split around its one color-accented contrast word. */
|
||||
headline: [before: string, accent: string, after: string];
|
||||
/** One line for cards and share text. */
|
||||
pitch: string;
|
||||
@@ -48,7 +62,7 @@ export type CraftApp = {
|
||||
features: Tip[];
|
||||
/** First shot is the hero. */
|
||||
shots: [CraftShot, ...CraftShot[]];
|
||||
downloads?: { label: string; href: string }[];
|
||||
release?: CraftRelease;
|
||||
};
|
||||
|
||||
export const CRAFTING_APPS_GITHUB_ORG = "https://github.com/storytold";
|
||||
@@ -65,6 +79,14 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
schemaCategory: "DesignApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
release: {
|
||||
version: "0.1.1-rc.4",
|
||||
files: [
|
||||
{ platform: "macOS", suffix: "macos-universal.dmg" },
|
||||
{ platform: "Windows", suffix: "windows-x64.msi" },
|
||||
{ platform: "Linux", suffix: "linux-x86_64.AppImage" },
|
||||
],
|
||||
},
|
||||
features: [
|
||||
{
|
||||
title: "Familiar by design",
|
||||
@@ -119,8 +141,8 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "drawcraft",
|
||||
prefix: "Draw",
|
||||
slug: "vectorcraft",
|
||||
prefix: "Vector",
|
||||
category: "Vector illustration",
|
||||
status: "In development",
|
||||
headline: ["Vector illustration, ", "reimagined", " in pure Rust."],
|
||||
@@ -159,26 +181,26 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "shot-1-neon.png",
|
||||
caption: "Neon Drive — made in DrawCraft",
|
||||
alt: "DrawCraft editing a synthwave poster titled Neon Drive with an outer glow in the Appearance panel",
|
||||
caption: "Neon Drive — made in VectorCraft",
|
||||
alt: "VectorCraft editing a synthwave poster titled Neon Drive with an outer glow in the Appearance panel",
|
||||
},
|
||||
{
|
||||
file: "ribbons.webp",
|
||||
source: "shot-2-ribbons.png",
|
||||
caption: "Live blends between editable spines",
|
||||
alt: "DrawCraft showing Live Blends: 70 smooth-color steps between two ribbon paths",
|
||||
alt: "VectorCraft showing live blends: 70 smooth-color steps between two ribbon paths",
|
||||
},
|
||||
{
|
||||
file: "sheet.webp",
|
||||
source: "shot-3-sheet.png",
|
||||
caption: "Booleans, mesh, repeat and envelope",
|
||||
alt: "DrawCraft feature sheet with exact booleans, a gradient mesh, a live radial repeat and an envelope distort",
|
||||
alt: "VectorCraft feature sheet with exact booleans, a gradient mesh, a live radial repeat and an envelope distort",
|
||||
},
|
||||
{
|
||||
file: "bezier.webp",
|
||||
source: "shot-4-bezier.png",
|
||||
caption: "Direct selection on bezier anchors",
|
||||
alt: "DrawCraft direct-selecting the anchors and handles of a crescent moon path",
|
||||
alt: "VectorCraft direct-selecting the anchors and handles of a crescent moon path",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -374,8 +396,152 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "effectcraft",
|
||||
prefix: "Effect",
|
||||
category: "Motion graphics & VFX",
|
||||
status: "In development",
|
||||
headline: ["Motion graphics and visual ", "effects", ", in pure Rust."],
|
||||
pitch: "Open-source motion graphics and visual effects, built in pure Rust.",
|
||||
lede: "Compositions, layers, keyframes, 259 effects, expressions, 3D cameras and lights, and a render queue in a native compositor written in pure Rust. Young, moving fast, and already usable.",
|
||||
schemaCategory: "MultimediaApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.95",
|
||||
features: [
|
||||
{
|
||||
title: "Animate the way you know",
|
||||
body: "Familiar panels and keyframes: linear, bezier, hold and eased keys, roving keys, and a graph editor with value and speed curves.",
|
||||
},
|
||||
{
|
||||
title: "Layers of every kind",
|
||||
body: "Solids, shapes, text, footage, nested compositions, nulls, adjustment layers, cameras and lights, with parenting, track mattes and 38 blend modes.",
|
||||
},
|
||||
{
|
||||
title: "259 effects",
|
||||
body: "Blur, color correction, distortion, generators, keying, particles and simulation, stylize and time effects, each with sensible defaults.",
|
||||
},
|
||||
{
|
||||
title: "Real 3D",
|
||||
body: "3D layers with cameras, depth of field and lights that cast soft ray-traced shadows, plus orbit, pan and dolly tools.",
|
||||
},
|
||||
{
|
||||
title: "Expressions",
|
||||
body: "JavaScript expressions with wiggle, loops and layer references, edited inline and linked straight to any property.",
|
||||
},
|
||||
{
|
||||
title: "Export anywhere",
|
||||
body: "H.264 and ProRes with alpha, PNG, TIFF and 32-bit EXR sequences, GIF, and Lottie import and export for the web. No FFmpeg inside.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "effectcraft-hero.png",
|
||||
caption: "An animated title on the timeline",
|
||||
alt: "EffectCraft's composition panel showing an animated EFFECTCRAFT title, with the project panel, a timeline of text, shape and solid layers, and the properties panel",
|
||||
},
|
||||
{
|
||||
file: "graph-editor.webp",
|
||||
source: "effectcraft-graph-editor.png",
|
||||
caption: "Graph editor with eased keyframes",
|
||||
alt: "EffectCraft's graph editor showing an eased value curve for a text animator",
|
||||
},
|
||||
{
|
||||
file: "effects.webp",
|
||||
source: "effectcraft-effects.png",
|
||||
caption: "Particle simulation effects",
|
||||
alt: "EffectCraft's effect controls for a particle simulation bursting from the center of the composition",
|
||||
},
|
||||
{
|
||||
file: "3d.webp",
|
||||
source: "effectcraft-3d.png",
|
||||
caption: "3D cameras, lights and soft shadows",
|
||||
alt: "EffectCraft's 3D showcase: intersecting cards lit by a spot light with soft shadows on a gridded floor, seen from a custom camera view",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "designcraft",
|
||||
prefix: "Design",
|
||||
category: "Page layout & publishing",
|
||||
status: "In development",
|
||||
headline: ["Page layout and ", "publishing", ", rebuilt in pure Rust."],
|
||||
pitch: "Open-source page layout and publishing, rebuilt in pure Rust.",
|
||||
lede: "Spreads and parent pages, threaded stories, styles, swatches and text wrap in a native layout app with a professional paragraph composer. No subscription, no licence server, no telemetry.",
|
||||
schemaCategory: "DesignApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "The layout tools you know",
|
||||
body: "Spreads and parent pages, frames and threaded stories, paragraph and character styles, swatches and text wrap, laid out the way you expect.",
|
||||
},
|
||||
{
|
||||
title: "Beautiful type",
|
||||
body: "A Knuth–Plass paragraph composer, dictionary hyphenation, optical margin alignment and baseline grids. Line breaks match on screen and in PDF.",
|
||||
},
|
||||
{
|
||||
title: "Print-ready PDF",
|
||||
body: "Real selectable text with embedded fonts, CMYK and spot colors, bleed, crop marks and archival PDF/A output.",
|
||||
},
|
||||
{
|
||||
title: "Fast",
|
||||
body: "Multithreaded SIMD rendering, copy-on-write documents with instant undo, and cached composition.",
|
||||
},
|
||||
{
|
||||
title: "Open formats",
|
||||
body: "A documented native format, PNG export, and layout interchange import and export for moving work between tools.",
|
||||
},
|
||||
{
|
||||
title: "Agent-native",
|
||||
body: "Every menu item, tool, panel and dialog can be driven over a JSON control channel and an MCP server.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "ui-spread.png",
|
||||
caption: "Threaded columns and text wrap — Quarterly",
|
||||
alt: "DesignCraft showing a magazine spread: a threaded three-column story with its thread line, a wrapped pull quote, and the Properties panel's text frame options",
|
||||
},
|
||||
{
|
||||
file: "cover.webp",
|
||||
source: "page-cover.png",
|
||||
caption: "Cover",
|
||||
alt: "Magazine cover for The Spring Issue: a dusk landscape of layered purple hills under a pale sun, with the serif headline The Quiet Art of Layout",
|
||||
portrait: true,
|
||||
},
|
||||
{
|
||||
file: "feature.webp",
|
||||
source: "page-2.png",
|
||||
caption: "Styles",
|
||||
alt: "Feature opener titled Notes on the Grid with a kicker rule, italic deck, landscape picture, caption and two columns of justified body text",
|
||||
portrait: true,
|
||||
},
|
||||
{
|
||||
file: "columns.webp",
|
||||
source: "page-3.png",
|
||||
caption: "Threading and wrap",
|
||||
alt: "Three columns of justified, hyphenated body text wrapping around a shaded pull quote",
|
||||
portrait: true,
|
||||
},
|
||||
{
|
||||
file: "swatches.webp",
|
||||
source: "page-4.png",
|
||||
caption: "Swatches",
|
||||
alt: "Coming Next page titled Color, Ink and Paper on a plum background with four labeled color swatches",
|
||||
portrait: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const COUNT_WORDS = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"];
|
||||
|
||||
/** "Seven" — the family size as a capitalized word, for headlines. */
|
||||
export const CRAFTING_APPS_COUNT_WORD =
|
||||
COUNT_WORDS[CRAFTING_APPS.length] ?? String(CRAFTING_APPS.length);
|
||||
|
||||
const LIST_FORMAT = new Intl.ListFormat("en", { type: "conjunction" });
|
||||
|
||||
/** "a, b and c" — Oxford comma per the en locale. */
|
||||
@@ -451,6 +617,34 @@ export function craftShotSourceUrl(app: CraftApp, shot: CraftShot): string {
|
||||
return `https://raw.githubusercontent.com/storytold/${app.slug}/main/docs/images/${shot.source}`;
|
||||
}
|
||||
|
||||
export function craftAppIconPath(app: CraftApp): string {
|
||||
return `/images/apps/${app.slug}/icon.webp`;
|
||||
}
|
||||
|
||||
export function craftAppIconUrl(app: CraftApp): string {
|
||||
return mediaUrl(craftAppIconPath(app));
|
||||
}
|
||||
|
||||
/** The repo's 1024 px icon render (press kit download). */
|
||||
export function craftAppIconSourceUrl(app: CraftApp): string {
|
||||
return `https://raw.githubusercontent.com/storytold/${app.slug}/main/assets/app-icon/${app.slug}-1024.png`;
|
||||
}
|
||||
|
||||
export function craftReleasePageUrl(app: CraftApp, release: CraftRelease): string {
|
||||
return `${craftAppRepo(app)}/releases/tag/v${release.version}`;
|
||||
}
|
||||
|
||||
export function craftReleaseDownloads(
|
||||
app: CraftApp,
|
||||
release: CraftRelease,
|
||||
): { platform: string; href: string }[] {
|
||||
const base = `${craftAppRepo(app)}/releases/download/v${release.version}`;
|
||||
return release.files.map(({ platform, suffix }) => ({
|
||||
platform,
|
||||
href: `${base}/${app.slug}-${release.version}-${suffix}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function craftAppOgImage(app: CraftApp): string {
|
||||
return mediaUrl(`/images/apps/${app.slug}/og.jpg`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
import {
|
||||
CRAFTING_APPS,
|
||||
craftAppIconPath,
|
||||
craftAppIconSourceUrl,
|
||||
craftAppName,
|
||||
craftShotPath,
|
||||
craftShotSourceUrl,
|
||||
@@ -108,6 +110,18 @@ export const PRESS_KIT_CATEGORIES: PressKitCategory[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Crafting Apps icons",
|
||||
description: "App icons for every Crafting App, 1024 px PNG",
|
||||
assets: CRAFTING_APPS.map((app) => ({
|
||||
type: "image" as const,
|
||||
title: `${craftAppName(app)} icon`,
|
||||
thumbnail: craftAppIconPath(app),
|
||||
downloadUrl: craftAppIconSourceUrl(app),
|
||||
downloadLabel: "1024 px PNG",
|
||||
containThumbnail: true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "Screenshots & media",
|
||||
description: "High-resolution screenshots and promotional images",
|
||||
|
||||