Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
513d2bc118 | ||
|
|
ca7c9f56bd | ||
|
|
2ee18084b7 | ||
|
|
097e746cda | ||
|
|
0c441d37c0 |
No files matched your search
@@ -43,7 +43,7 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Free up disk on the runner before BuildKit fills it up. The cargo
|
||||
# release build inside the Dockerfile produces multi-GB target dirs
|
||||
@@ -60,10 +60,10 @@ jobs:
|
||||
|
||||
# BuildKit is required for the `cache-from`/`cache-to` options on
|
||||
# docker/build-push-action.
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
name: Set up Docker Buildx
|
||||
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
- uses: docker/login-action@v3
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- id: meta
|
||||
name: Compute Docker tags
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/storytold/artcraft-services
|
||||
tags: |
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
type=raw,value=${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./build/service_cpu.Dockerfile
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Copy this file to `.env` and change the values.
|
||||
# `.env` is ignored by git; never commit real keys.
|
||||
|
||||
# ----- Local infrastructure (used by docker-compose.yml) -----
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change-me-root
|
||||
MYSQL_PASSWORD=change-me
|
||||
|
||||
# Credentials for the local S3-compatible storage (SeaweedFS).
|
||||
S3_ACCESS_KEY=artcraft
|
||||
S3_SECRET_KEY=change-me-please
|
||||
|
||||
# ----- storyteller-web settings -----
|
||||
|
||||
# Cookies are served over plain HTTP locally.
|
||||
COOKIE_SECURE=false
|
||||
COOKIE_DOMAIN=localhost
|
||||
COOKIE_SECRET=change-me
|
||||
SORT_KEY_SECRET=change-me
|
||||
WEBSITE_HOMEPAGE_REDIRECT=http://localhost:4201
|
||||
|
||||
ENABLE_PAGING=false
|
||||
NUM_WORKERS=4
|
||||
|
||||
# ----- Third-party providers -----
|
||||
# storyteller-web requires these to be set at startup. Placeholders let the server
|
||||
# start; features backed by a provider only work once you add a real key.
|
||||
|
||||
FAL_API_KEY=unset
|
||||
FAL_WEBHOOK_URL=http://localhost:12345/v1/webhooks/fal
|
||||
GMICLOUD_API_KEY=unset
|
||||
GROK_API_KEY=unset
|
||||
OPENAI_API_KEY=unset
|
||||
WORLDLABS_API_KEY=unset
|
||||
BEEBLE_API_KEY=unset
|
||||
BEEBLE_WEBHOOK_URL=http://localhost:12345/v1/webhooks/beeble
|
||||
SEEDANCE2PRO_COOKIES=unset
|
||||
SEEDANCE2PRO_WHITELIST_COOKIES=unset
|
||||
SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES=unset
|
||||
|
||||
# Email (Resend)
|
||||
RESEND_API_KEY=unset
|
||||
|
||||
# ----- Stripe (billing) -----
|
||||
|
||||
STRIPE_SECRET_KEY=unset
|
||||
STRIPE_SECRET_WEBHOOK_SIGNING_KEY=unset
|
||||
STRIPE_FAKEYOU_ACCOUNT_ID=unset
|
||||
STRIPE_PORTAL_DEFAULT_CONFIG_ID=unset
|
||||
STRIPE_CHECKOUT_SUCCESS_URL_PATH=/checkout_success
|
||||
STRIPE_CHECKOUT_CANCEL_URL_PATH=/checkout_cancel
|
||||
STRIPE_PORTAL_RETURN_URL_PATH=/portal_success
|
||||
|
||||
STRIPE_ARTCRAFT_ACCOUNT_ID=unset
|
||||
STRIPE_ARTCRAFT_SECRET_KEY=unset
|
||||
STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY=unset
|
||||
STRIPE_ARTCRAFT_CHECKOUT_SUCCESS_URL=http://localhost:4201/checkout_success
|
||||
STRIPE_ARTCRAFT_CHECKOUT_CANCEL_URL=http://localhost:4201/checkout_cancel
|
||||
STRIPE_ARTCRAFT_PORTAL_RETURN_URL=http://localhost:4201/portal_closed
|
||||
@@ -1,77 +0,0 @@
|
||||
# Self-hosting with Docker Compose
|
||||
|
||||
This directory runs the ArtCraft backend (`storyteller-web`) and the services it
|
||||
depends on with Docker Compose.
|
||||
|
||||
| Service | Image | Purpose |
|
||||
|-------------------|--------------------------|---------------------------------------------|
|
||||
| `storyteller-web` | built from this repo | HTTP API, on `http://localhost:12345` |
|
||||
| `mysql` | `mysql:8.4` | Accounts, media metadata, jobs, billing |
|
||||
| `migrate` | `mysql:8.4` | Applies `_database/sql/migrations`, exits |
|
||||
| `redis` | `redis:7` | Caching, rate limiting, job progress |
|
||||
| `elasticsearch` | `elasticsearch:8.15.3` | Search |
|
||||
| `seaweedfs` | `chrislusf/seaweedfs` | S3-compatible storage for media |
|
||||
| `storage-init` | `chrislusf/seaweedfs` | Creates the storage buckets, exits |
|
||||
|
||||
Only the API port (`12345`) is published to the host.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker with Docker Compose v2.23 or newer.
|
||||
- About 8 GB of memory for Docker and 20 GB of free disk. The first build
|
||||
compiles the Rust workspace in release mode and can take a long time.
|
||||
|
||||
## Usage
|
||||
|
||||
From this directory:
|
||||
|
||||
```bash
|
||||
cp .env.example .env # then edit the passwords and secrets
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Check that the server is up:
|
||||
|
||||
```bash
|
||||
curl http://localhost:12345/_status
|
||||
```
|
||||
|
||||
Stop everything with `docker compose down`. Add `-v` to also delete the
|
||||
database, search index and stored media.
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings live in `.env`, which git ignores. `docker-compose.yml` sets the
|
||||
connection settings for MySQL, Redis, Elasticsearch and storage, so you only
|
||||
need to change passwords and secrets.
|
||||
|
||||
`storyteller-web` requires credentials for every third-party provider (FAL,
|
||||
OpenAI, Stripe, Resend and others) to be present at startup. `.env.example`
|
||||
sets them to the placeholder `unset`, which lets the server start. Features
|
||||
that call a provider only work after you add a real key for it.
|
||||
|
||||
Provider webhooks, such as `FAL_WEBHOOK_URL`, must be reachable from the
|
||||
internet for asynchronous generation results to arrive.
|
||||
|
||||
## Database migrations
|
||||
|
||||
The `migrate` service applies every migration in `_database/sql/migrations` in
|
||||
order and records each one in `__diesel_schema_migrations`, the table the diesel
|
||||
CLI uses. It runs on every `docker compose up` and only applies migrations that
|
||||
have not run yet.
|
||||
|
||||
## Connecting the web app
|
||||
|
||||
To point the browser app at this server, run it from `frontend` as described in
|
||||
the repository README:
|
||||
|
||||
```bash
|
||||
VITE_USE_LOCAL_API=true npx nx dev artcraft-webapp
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only the HTTP API is included. Background workers (provider polling, video
|
||||
thumbnails, email, analytics) are not started.
|
||||
- Elasticsearch starts with no indexes. Index definitions are in
|
||||
`_database/elasticsearch/index_definitions`.
|
||||
@@ -1,163 +0,0 @@
|
||||
# Self-hosted ArtCraft backend (storyteller-web) with its dependencies.
|
||||
#
|
||||
# Usage (from this directory):
|
||||
# cp .env.example .env
|
||||
# docker compose up --build
|
||||
#
|
||||
# See README.md in this directory for details.
|
||||
|
||||
name: artcraft-selfhost
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: storyteller
|
||||
MYSQL_USER: storyteller
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
# NB: Use TCP (127.0.0.1), not the socket, so the check fails while the image's
|
||||
# temporary init server (which has networking disabled) is still running.
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "--silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# Applies the SQL migrations in `_database/sql/migrations` in order.
|
||||
# Applied versions are recorded in `__diesel_schema_migrations` (the same table
|
||||
# the diesel CLI uses), so re-running is safe and only applies new migrations.
|
||||
migrate:
|
||||
image: mysql:8.4
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MYSQL_PWD: ${MYSQL_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- ../../_database/sql/migrations:/migrations:ro
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -euo pipefail
|
||||
run_sql() { mysql -h mysql -u root storyteller "$$@"; }
|
||||
run_sql -e "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (
|
||||
version VARCHAR(50) PRIMARY KEY NOT NULL,
|
||||
run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"
|
||||
for dir in /migrations/*/; do
|
||||
name=$$(basename "$$dir")
|
||||
version=$$(echo "$${name%%_*}" | tr -d '-')
|
||||
if [ -n "$$(run_sql -N -e "SELECT 1 FROM __diesel_schema_migrations WHERE version = '$$version'")" ]; then
|
||||
continue
|
||||
fi
|
||||
echo "Applying migration $$name"
|
||||
run_sql < "$$dir/up.sql"
|
||||
run_sql -e "INSERT INTO __diesel_schema_migrations (version) VALUES ('$$version')"
|
||||
done
|
||||
echo "Migrations complete."
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: "false"
|
||||
ES_JAVA_OPTS: -Xms512m -Xmx512m
|
||||
volumes:
|
||||
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fs http://localhost:9200/_cluster/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# S3-compatible object storage (SeaweedFS) for uploaded and generated media.
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:3.97
|
||||
restart: unless-stopped
|
||||
command: server -dir=/data -s3 -s3.port=8333 -s3.config=/etc/seaweedfs/s3.json
|
||||
configs:
|
||||
- source: seaweedfs-s3
|
||||
target: /etc/seaweedfs/s3.json
|
||||
volumes:
|
||||
- seaweedfs-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8333/healthz || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# Creates the buckets storyteller-web expects.
|
||||
storage-init:
|
||||
image: chrislusf/seaweedfs:3.97
|
||||
depends_on:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -e
|
||||
for bucket in artcraft-private artcraft-public artcraft-public-gc; do
|
||||
echo "s3.bucket.create -name $$bucket" | weed shell -master=seaweedfs:9333
|
||||
done
|
||||
|
||||
storyteller-web:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: build/service_cpu.Dockerfile
|
||||
image: artcraft-services:local
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
storage-init:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_started
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
SERVER_ENVIRONMENT: development
|
||||
BIND_ADDRESS: 0.0.0.0:12345
|
||||
MYSQL_URL: mysql://storyteller:${MYSQL_PASSWORD}@mysql:3306/storyteller
|
||||
REDIS_0_URL: redis://redis:6379/0
|
||||
ELASTICSEARCH_URL: http://elasticsearch:9200
|
||||
S3_COMPATIBLE_ENDPOINT_URL: http://seaweedfs:8333
|
||||
ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
SECRET_KEY: ${S3_SECRET_KEY}
|
||||
REGION_NAME: us-east-1
|
||||
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME: artcraft-private
|
||||
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME: artcraft-public
|
||||
GC_ENABLED_PUBLIC_BUCKET_NAME: artcraft-public-gc
|
||||
AUDIO_UPLOADS_BUCKET_ROOT: /user_uploaded_audio
|
||||
ports:
|
||||
- "12345:12345"
|
||||
|
||||
configs:
|
||||
seaweedfs-s3:
|
||||
content: |
|
||||
{
|
||||
"identities": [
|
||||
{
|
||||
"name": "artcraft",
|
||||
"credentials": [{ "accessKey": "${S3_ACCESS_KEY}", "secretKey": "${S3_SECRET_KEY}" }],
|
||||
"actions": ["Admin", "Read", "Write", "List", "Tagging"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
elasticsearch-data:
|
||||
seaweedfs-data:
|
||||
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 118 KiB |
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
AppDemo,
|
||||
AppFamily,
|
||||
AppGallery,
|
||||
AppGetIt,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
LauncherDownloadButton,
|
||||
} from "@/components/apps/app-downloads";
|
||||
import { craftLauncherRecommended } from "@/components/apps/launcher-callout";
|
||||
import { craftLauncherDownloads } from "@/lib/craft-launcher";
|
||||
import {
|
||||
AppIcon,
|
||||
AppTab,
|
||||
@@ -85,16 +83,11 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
const release = craftAppRelease(app);
|
||||
// The hero's download is ArtCraft Launcher, which installs this app and keeps
|
||||
// it updated; the app's own installers are in #get-it, under the launcher.
|
||||
// An app with no release yet has nothing for the launcher to install.
|
||||
const launcherDownloads = release ? craftLauncherRecommended() : [];
|
||||
const allLauncherDownloads = craftLauncherDownloads();
|
||||
const hasDownload = release !== null;
|
||||
const launcherDownloads = craftLauncherRecommended();
|
||||
const hasDownload = launcherDownloads.length > 0 || release !== null;
|
||||
const [hero] = app.shots;
|
||||
const [before, accent, after] = app.headline;
|
||||
const shareUrl = siteUrl(craftAppPath(app));
|
||||
// A demo takes section 02 and pushes the rest down one.
|
||||
const sectionIndex = (n: number) =>
|
||||
String(n + (app.demo ? 1 : 0)).padStart(2, "0");
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
@@ -160,11 +153,7 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="flex flex-wrap items-start justify-center gap-3">
|
||||
{launcherDownloads.length > 0 ? (
|
||||
<LauncherDownloadButton
|
||||
recommended={launcherDownloads}
|
||||
allDownloads={allLauncherDownloads}
|
||||
fallbackHref="#get-it"
|
||||
/>
|
||||
<LauncherDownloadButton recommended={launcherDownloads} fallbackHref="#get-it" />
|
||||
) : (
|
||||
release && (
|
||||
<AppHeroDownloadButton
|
||||
@@ -200,11 +189,9 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
</div>
|
||||
</CampaignHero>
|
||||
|
||||
{app.demo && <AppDemo app={app} demo={app.demo} index="02" />}
|
||||
|
||||
<CampaignSection
|
||||
id="highlights"
|
||||
index={sectionIndex(2)}
|
||||
index="02"
|
||||
label="Highlights"
|
||||
annotation={app.category}
|
||||
title={
|
||||
@@ -216,9 +203,9 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
<TipGrid items={app.features} columns={3} />
|
||||
</CampaignSection>
|
||||
|
||||
<AppGallery app={app} index={sectionIndex(3)} />
|
||||
<AppGetIt app={app} index={sectionIndex(4)} />
|
||||
<AppFamily app={app} index={sectionIndex(5)} />
|
||||
<AppGallery app={app} index="03" />
|
||||
<AppGetIt app={app} index="04" />
|
||||
<AppFamily app={app} index="05" />
|
||||
|
||||
<ClosingCta
|
||||
eyebrow="Open source · Free"
|
||||
|
||||
@@ -28,7 +28,6 @@ import { trackAttrs } from "@/lib/analytics";
|
||||
import {
|
||||
CRAFT_LAUNCHER_NAME,
|
||||
CRAFT_LAUNCHER_REPO,
|
||||
craftLauncherDownloads,
|
||||
craftLauncherReleasePageUrl,
|
||||
} from "@/lib/craft-launcher";
|
||||
import {
|
||||
@@ -45,7 +44,7 @@ import { siteUrl } from "@/lib/links";
|
||||
|
||||
const TITLE = "Crafting Apps: open-source creative tools";
|
||||
const COVERAGE =
|
||||
"Image editing, vector illustration, video, photography, PDFs, motion graphics, page layout, interface design, beat making, word processing, audio, presentations, CAD and spreadsheets";
|
||||
"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 = {
|
||||
@@ -74,7 +73,6 @@ const PRINCIPLE_ICONS = [
|
||||
|
||||
export default function CraftingAppsPage() {
|
||||
const launcherDownloads = craftLauncherRecommended();
|
||||
const allLauncherDownloads = craftLauncherDownloads();
|
||||
return (
|
||||
<>
|
||||
<RevealManager />
|
||||
@@ -86,46 +84,15 @@ export default function CraftingAppsPage() {
|
||||
annotation={`${CRAFTING_APPS_COUNT_WORD} apps · Open source · Pure Rust`}
|
||||
title={
|
||||
<>
|
||||
{CRAFTING_APPS_COUNT_WORD} apps.{" "}
|
||||
<span className="whitespace-nowrap">
|
||||
One <ColorAccent>craft</ColorAccent>.
|
||||
</span>
|
||||
{CRAFTING_APPS_COUNT_WORD} apps. One <ColorAccent>craft</ColorAccent>.
|
||||
</>
|
||||
}
|
||||
lede={`${COVERAGE}. Native, open-source apps from the ArtCraft team, built in Rust and free to use.`}
|
||||
aside={
|
||||
<nav
|
||||
aria-label="Crafting Apps"
|
||||
data-reveal
|
||||
// Under the text: two even rows from sm up (7 + 6). Beside it from
|
||||
// xl up: centered rows of five.
|
||||
className="mt-12 grid w-fit grid-cols-4 gap-x-3 gap-y-6 sm:grid-cols-7 sm:gap-x-5 xl:mt-0 xl:flex xl:w-120 xl:shrink-0 xl:flex-wrap xl:justify-center"
|
||||
>
|
||||
{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)}`}
|
||||
{...trackAttrs("app_select", { app_name: app.slug })}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<div data-reveal className="mt-8 flex flex-wrap items-start gap-3">
|
||||
{launcherDownloads.length > 0 && (
|
||||
<LauncherDownloadButton
|
||||
recommended={launcherDownloads}
|
||||
allDownloads={allLauncherDownloads}
|
||||
fallbackHref={craftLauncherReleasePageUrl()}
|
||||
/>
|
||||
)}
|
||||
@@ -166,6 +133,29 @@ export default function CraftingAppsPage() {
|
||||
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)}`}
|
||||
{...trackAttrs("app_select", { app_name: app.slug })}
|
||||
>
|
||||
<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">
|
||||
|
||||
@@ -324,13 +324,6 @@ html[data-ruler="on"]::-webkit-scrollbar {
|
||||
.app-pdfcraft { --app: #12a58a; --app-ink: #0a7563; }
|
||||
.app-effectcraft { --app: #e0368f; --app-ink: #b0206c; }
|
||||
.app-designcraft { --app: #7bb51c; --app-ink: #457008; }
|
||||
.app-figcraft { --app: #02da96; --app-ink: #017a54; }
|
||||
.app-loopcraft { --app: #f0d824; --app-ink: #6f6300; }
|
||||
.app-wordcraft { --app: #4c6ef5; --app-ink: #253e99; }
|
||||
.app-soundcraft { --app: #14a9c4; --app-ink: #0b6f82; }
|
||||
.app-deckcraft { --app: #f26b1d; --app-ink: #ad4306; }
|
||||
.app-cadcraft { --app: #14a3c7; --app-ink: #09596d; }
|
||||
.app-gridcraft { --app: #1f9d55; --app-ink: #136938; }
|
||||
:root[data-theme="dark"] .app-photocraft { --app-ink: #7fb0ff; }
|
||||
:root[data-theme="dark"] .app-vectorcraft { --app-ink: #ff8a70; }
|
||||
:root[data-theme="dark"] .app-filmcraft { --app-ink: #b69cff; }
|
||||
@@ -338,13 +331,6 @@ html[data-ruler="on"]::-webkit-scrollbar {
|
||||
:root[data-theme="dark"] .app-pdfcraft { --app-ink: #3fd6b8; }
|
||||
:root[data-theme="dark"] .app-effectcraft { --app-ink: #ff7ab8; }
|
||||
:root[data-theme="dark"] .app-designcraft { --app-ink: #a6e04a; }
|
||||
:root[data-theme="dark"] .app-figcraft { --app-ink: #3fe6ae; }
|
||||
:root[data-theme="dark"] .app-loopcraft { --app-ink: #f5e04f; }
|
||||
:root[data-theme="dark"] .app-wordcraft { --app-ink: #8da2fb; }
|
||||
:root[data-theme="dark"] .app-soundcraft { --app-ink: #52d1ea; }
|
||||
:root[data-theme="dark"] .app-deckcraft { --app-ink: #ff9657; }
|
||||
:root[data-theme="dark"] .app-cadcraft { --app-ink: #4cc7e6; }
|
||||
:root[data-theme="dark"] .app-gridcraft { --app-ink: #5ee089; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .app-photocraft { --app-ink: #7fb0ff; }
|
||||
:root:not([data-theme="light"]) .app-vectorcraft { --app-ink: #ff8a70; }
|
||||
@@ -353,13 +339,6 @@ html[data-ruler="on"]::-webkit-scrollbar {
|
||||
:root:not([data-theme="light"]) .app-pdfcraft { --app-ink: #3fd6b8; }
|
||||
:root:not([data-theme="light"]) .app-effectcraft { --app-ink: #ff7ab8; }
|
||||
:root:not([data-theme="light"]) .app-designcraft { --app-ink: #a6e04a; }
|
||||
:root:not([data-theme="light"]) .app-figcraft { --app-ink: #3fe6ae; }
|
||||
:root:not([data-theme="light"]) .app-loopcraft { --app-ink: #f5e04f; }
|
||||
:root:not([data-theme="light"]) .app-wordcraft { --app-ink: #8da2fb; }
|
||||
:root:not([data-theme="light"]) .app-soundcraft { --app-ink: #52d1ea; }
|
||||
:root:not([data-theme="light"]) .app-deckcraft { --app-ink: #ff9657; }
|
||||
:root:not([data-theme="light"]) .app-cadcraft { --app-ink: #4cc7e6; }
|
||||
:root:not([data-theme="light"]) .app-gridcraft { --app-ink: #5ee089; }
|
||||
}
|
||||
|
||||
/* Plan cell: a color wash pooling under the index tab and fading into the
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
AppleIcon,
|
||||
ArrowDownIcon,
|
||||
ArrowDownToLineIcon,
|
||||
ChevronDownIcon,
|
||||
SquareTerminalIcon,
|
||||
} from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { WindowsIcon } from "@/components/icons";
|
||||
import { Button, type ButtonProps } from "@/components/ui";
|
||||
import { craftLauncherDownloads } from "@/lib/craft-launcher";
|
||||
import {
|
||||
CRAFT_DESKTOP_PLATFORMS,
|
||||
type CraftDesktopPlatform,
|
||||
@@ -163,14 +161,9 @@ export function AppHeroDownloadButton({
|
||||
* ArtCraft Launcher's CTA: a direct download for the visitor's system once
|
||||
* it is known (with the file's platform and build underneath), else a jump
|
||||
* to `fallbackHref`, where every platform is listed.
|
||||
*
|
||||
* On Linux, offers Flatpak as the default button action, plus a dropdown
|
||||
* arrow on the right to select and download other formats: Flatpak first,
|
||||
* then AppImage, then deb, then rpm.
|
||||
*/
|
||||
export function LauncherDownloadButton({
|
||||
recommended,
|
||||
allDownloads,
|
||||
fallbackHref,
|
||||
size = "lg",
|
||||
variant = "primary",
|
||||
@@ -178,7 +171,6 @@ export function LauncherDownloadButton({
|
||||
className,
|
||||
}: {
|
||||
recommended: CraftDownload[];
|
||||
allDownloads?: CraftDownload[];
|
||||
fallbackHref: string;
|
||||
size?: ButtonProps["size"];
|
||||
variant?: ButtonProps["variant"];
|
||||
@@ -187,158 +179,6 @@ export function LauncherDownloadButton({
|
||||
}) {
|
||||
const detected = useDetectedDesktop();
|
||||
const mine = detected && pickDownload(recommended, detected);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedDownload, setSelectedDownload] = useState<CraftDownload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (!menuRef.current?.contains(e.target as Node)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("pointerdown", onPointerDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [menuOpen]);
|
||||
|
||||
const isLinux = detected?.platform === "Linux";
|
||||
const downloads = allDownloads ?? (isLinux ? craftLauncherDownloads() : []);
|
||||
const linuxDownloads = isLinux
|
||||
? downloads.filter(
|
||||
(download) =>
|
||||
download.group === "Linux" &&
|
||||
(!detected.arch || !download.arch || download.arch === detected.arch),
|
||||
)
|
||||
: [];
|
||||
|
||||
const linuxOptions = isLinux
|
||||
? [...linuxDownloads].sort(
|
||||
(a, b) => linuxFormatPriority(a.fileName) - linuxFormatPriority(b.fileName),
|
||||
)
|
||||
: [];
|
||||
|
||||
const defaultLinuxDownload =
|
||||
linuxOptions.find((d) => d.fileName.endsWith(".flatpak")) ?? mine;
|
||||
const activeDownload = selectedDownload ?? defaultLinuxDownload ?? mine;
|
||||
|
||||
if (isLinux && linuxOptions.length > 0 && activeDownload) {
|
||||
const isPrimary = variant === "primary";
|
||||
return (
|
||||
<span className={twMerge("flex flex-col gap-2", className)}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={twMerge("relative inline-flex items-stretch", fullWidth ? "w-full" : "w-fit")}
|
||||
>
|
||||
<Button
|
||||
href={activeDownload.href}
|
||||
size={size}
|
||||
variant={variant}
|
||||
className={twMerge(
|
||||
fullWidth ? "flex-1 w-full" : undefined,
|
||||
"pr-4",
|
||||
)}
|
||||
>
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
Download Launcher
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMenuOpen((prev) => !prev)}
|
||||
aria-expanded={menuOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="Other Linux download formats"
|
||||
className={twMerge(
|
||||
"flex items-center justify-center transition-colors duration-150 cursor-pointer",
|
||||
size === "lg" ? "h-12 px-3.5" : size === "sm" ? "px-2 py-1.5" : "px-3 py-2",
|
||||
isPrimary
|
||||
? "bg-invert-bg text-invert-fg border-l border-invert-fg/20 hover:opacity-80"
|
||||
: "border border-line-strong border-l-0 bg-bg text-ink hover:bg-invert-bg hover:text-invert-fg",
|
||||
)}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
aria-hidden
|
||||
className={twMerge(
|
||||
"h-4 w-4 transition-transform duration-150",
|
||||
menuOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{menuOpen && (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full min-w-[280px] border border-line bg-bg-raised p-1 shadow-lg text-left"
|
||||
>
|
||||
<div className="border-b border-line px-3 py-2">
|
||||
<p className="hud-label text-[11px] text-faint">Select Linux format</p>
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
{linuxOptions.map((opt) => {
|
||||
const isDefault = opt.fileName.endsWith(".flatpak");
|
||||
const isSelected = activeDownload.fileName === opt.fileName;
|
||||
return (
|
||||
<li key={opt.href}>
|
||||
<a
|
||||
href={opt.href}
|
||||
download={opt.fileName}
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setSelectedDownload(opt);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
className={twMerge(
|
||||
"group/opt flex items-center justify-between gap-3 p-2.5 text-left transition-colors duration-150 hover:bg-bg-sunken",
|
||||
isSelected && "bg-ink/5",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="block text-sm font-medium text-ink">
|
||||
{opt.label}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="hud-label text-[10px] text-(--app-ink,var(--accent-ink))">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="block text-xs text-muted">
|
||||
{opt.description}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate font-mono text-[10px] text-faint">
|
||||
{opt.fileName}
|
||||
</span>
|
||||
</span>
|
||||
<ArrowDownToLineIcon
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-muted group-hover/opt:text-ink"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span aria-hidden={!activeDownload} className="hud-label min-h-4 w-0 min-w-full whitespace-nowrap text-faint">
|
||||
{activeDownload ? `For Linux · ${activeDownload.label}` : "\u00a0"}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={twMerge("flex flex-col gap-2", className)}>
|
||||
<Button
|
||||
@@ -360,15 +200,6 @@ export function LauncherDownloadButton({
|
||||
);
|
||||
}
|
||||
|
||||
function linuxFormatPriority(fileName: string): number {
|
||||
if (fileName.endsWith(".flatpak")) return 1;
|
||||
if (fileName.endsWith(".AppImage")) return 2;
|
||||
if (fileName.endsWith(".deb")) return 3;
|
||||
if (fileName.endsWith(".rpm")) return 4;
|
||||
if (fileName.endsWith(".tar.gz")) return 5;
|
||||
return 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown under the hero once the direct download is live, so visitors on
|
||||
* another machine, CPU or package format can still find their build.
|
||||
|
||||
@@ -2,11 +2,9 @@ import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowUpRightIcon, TerminalIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CampaignSection } from "@/components/campaign/sections";
|
||||
import DiscordButton from "@/components/discord-button";
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { SectionShell, SectionEyebrow } from "@/components/landing/section-shell";
|
||||
import LazyVideo from "@/components/lazy-video";
|
||||
import { Badge, Button, CopyButton } from "@/components/ui";
|
||||
import { trackAttrs } from "@/lib/analytics";
|
||||
import { CRAFT_LAUNCHER_RELEASE } from "@/lib/crafting-app-releases";
|
||||
@@ -17,7 +15,6 @@ import {
|
||||
craftAppName,
|
||||
craftAppRelease,
|
||||
craftAppRepo,
|
||||
craftDemoPosterUrl,
|
||||
craftReleaseDownloads,
|
||||
craftReleasePageUrl,
|
||||
craftReleasesUrl,
|
||||
@@ -25,7 +22,6 @@ import {
|
||||
craftShotUrl,
|
||||
formatList,
|
||||
type CraftApp,
|
||||
type CraftDemo,
|
||||
} from "@/lib/crafting-apps";
|
||||
import AppCard from "./app-card";
|
||||
import { AppPlatformDownloads, DownloadRow } from "./app-downloads";
|
||||
@@ -38,50 +34,6 @@ import { LauncherCallout } from "./launcher-callout";
|
||||
const CELL_HEADING_CLASSES =
|
||||
"mt-4 font-display text-3xl font-medium leading-[1.05] tracking-[-0.03em] text-ink-strong sm:text-4xl";
|
||||
|
||||
// A feature clip (apps with a `demo`), directly under the hero.
|
||||
export function AppDemo({
|
||||
app,
|
||||
demo,
|
||||
index,
|
||||
}: {
|
||||
app: CraftApp;
|
||||
demo: CraftDemo;
|
||||
index: string;
|
||||
}) {
|
||||
const [before, accent, after] = demo.headline;
|
||||
return (
|
||||
<CampaignSection
|
||||
id="demo"
|
||||
index={index}
|
||||
label={demo.label}
|
||||
annotation={craftAppName(app)}
|
||||
title={
|
||||
<>
|
||||
{before}
|
||||
<ColorAccent>{accent}</ColorAccent>
|
||||
{after}
|
||||
</>
|
||||
}
|
||||
lede={demo.lede}
|
||||
>
|
||||
<figure data-reveal className="border-t border-line">
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-bg-sunken">
|
||||
<LazyVideo
|
||||
src={demo.src}
|
||||
poster={craftDemoPosterUrl(app, demo)}
|
||||
label={demo.caption}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="flex items-center justify-between gap-4 border-t border-line px-6 py-3 md:px-10">
|
||||
<span className="hud-label text-muted">{demo.caption}</span>
|
||||
<span className="hud-label text-faint">Video</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</CampaignSection>
|
||||
);
|
||||
}
|
||||
|
||||
// The hero carries shot 01. Window captures pair up, and an odd one out
|
||||
// leads at full width; portrait pages (layout spreads) sit four across.
|
||||
export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
@@ -114,11 +66,7 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
rel="noopener noreferrer"
|
||||
className={twMerge(
|
||||
"group relative block overflow-hidden bg-bg-sunken",
|
||||
shot.portrait
|
||||
? "aspect-[7/9]"
|
||||
: shot.wide
|
||||
? "aspect-video"
|
||||
: "aspect-[16/10]",
|
||||
shot.portrait ? "aspect-[7/9]" : "aspect-[16/10]",
|
||||
)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
|
||||
@@ -65,7 +65,6 @@ export function LauncherCallout({ app }: { app: CraftApp }) {
|
||||
</p>
|
||||
<LauncherDownloadButton
|
||||
recommended={recommended}
|
||||
allDownloads={craftLauncherDownloads()}
|
||||
fallbackHref={craftLauncherReleasePageUrl()}
|
||||
fullWidth
|
||||
className="mt-4"
|
||||
|
||||
@@ -7,12 +7,10 @@ import { useEffect, useRef } from "react";
|
||||
// ambient motion.
|
||||
export default function LazyVideo({
|
||||
src,
|
||||
poster,
|
||||
className,
|
||||
label,
|
||||
}: {
|
||||
src: string;
|
||||
poster?: string;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
@@ -47,7 +45,6 @@ export default function LazyVideo({
|
||||
<video
|
||||
ref={ref}
|
||||
src={src}
|
||||
poster={poster}
|
||||
className={className}
|
||||
muted
|
||||
loop
|
||||
|
||||
@@ -16,7 +16,6 @@ export function PageHeader({
|
||||
title,
|
||||
lede,
|
||||
children,
|
||||
aside,
|
||||
align = "left",
|
||||
className,
|
||||
}: {
|
||||
@@ -29,31 +28,10 @@ export function PageHeader({
|
||||
lede?: ReactNode;
|
||||
/** Extra content under the lede (CTAs, back links, …). */
|
||||
children?: ReactNode;
|
||||
/** Beside the text from xl up (vertically centered), beneath it below. */
|
||||
aside?: ReactNode;
|
||||
align?: "left" | "center";
|
||||
className?: string;
|
||||
}) {
|
||||
const centered = align === "center";
|
||||
const text = (
|
||||
<>
|
||||
<h1
|
||||
data-reveal
|
||||
className="max-w-3xl font-display text-4xl font-medium leading-[1.02] tracking-[-0.035em] text-ink-strong sm:text-5xl md:text-6xl"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{lede && (
|
||||
<p
|
||||
data-reveal
|
||||
className="mt-5 max-w-xl text-lg leading-relaxed text-muted"
|
||||
>
|
||||
{lede}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<SectionShell id={id} className={className}>
|
||||
<SectionEyebrow index={index} label={label} annotation={annotation} />
|
||||
@@ -61,11 +39,23 @@ export function PageHeader({
|
||||
className={twMerge(
|
||||
"px-6 py-14 md:px-10 md:py-20",
|
||||
centered && "flex flex-col items-center text-center",
|
||||
aside && "xl:flex xl:items-center xl:justify-between xl:gap-12",
|
||||
)}
|
||||
>
|
||||
{aside ? <div className="min-w-0">{text}</div> : text}
|
||||
{aside}
|
||||
<h1
|
||||
data-reveal
|
||||
className="max-w-3xl font-display text-4xl font-medium leading-[1.02] tracking-[-0.035em] text-ink-strong sm:text-5xl md:text-6xl"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{lede && (
|
||||
<p
|
||||
data-reveal
|
||||
className="mt-5 max-w-xl text-lg leading-relaxed text-muted"
|
||||
>
|
||||
{lede}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</SectionShell>
|
||||
);
|
||||
|
||||
@@ -105,20 +105,20 @@ export const CRAFT_RELEASE_ASSETS: readonly CraftReleaseAsset[] = [
|
||||
description: "Unzip and run on Windows on ARM.",
|
||||
arch: "aarch64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.flatpak",
|
||||
group: "Linux",
|
||||
label: "Flatpak (x86_64)",
|
||||
description: "Sandboxed. Install with flatpak install --user.",
|
||||
arch: "x86_64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.AppImage",
|
||||
group: "Linux",
|
||||
label: "AppImage (x86_64)",
|
||||
description: "Any distro. Mark it executable and run.",
|
||||
arch: "x86_64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.flatpak",
|
||||
group: "Linux",
|
||||
label: "Flatpak (x86_64)",
|
||||
description: "Sandboxed. Install with flatpak install --user.",
|
||||
arch: "x86_64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.deb",
|
||||
@@ -141,20 +141,20 @@ export const CRAFT_RELEASE_ASSETS: readonly CraftReleaseAsset[] = [
|
||||
description: "Plain binaries to unpack anywhere.",
|
||||
arch: "x86_64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.flatpak",
|
||||
group: "Linux",
|
||||
label: "Flatpak (ARM64)",
|
||||
description: "Sandboxed, on 64-bit ARM.",
|
||||
arch: "aarch64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.AppImage",
|
||||
group: "Linux",
|
||||
label: "AppImage (ARM64)",
|
||||
description: "Any distro on 64-bit ARM.",
|
||||
arch: "aarch64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.flatpak",
|
||||
group: "Linux",
|
||||
label: "Flatpak (ARM64)",
|
||||
description: "Sandboxed, on 64-bit ARM.",
|
||||
arch: "aarch64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.deb",
|
||||
@@ -208,24 +208,17 @@ export function craftRelease(
|
||||
// every app above, and the /apps pages recommend it first. Its releases ship
|
||||
// the same file set minus the macOS CLI, named artcraft-launcher-*.
|
||||
export const CRAFT_LAUNCHER_RELEASE: CraftRelease | null = craftRelease(
|
||||
"0.3.2",
|
||||
"0.2.0",
|
||||
CRAFT_RELEASE_ASSETS.filter((asset) => !asset.file.startsWith("{slug}-cli-")),
|
||||
);
|
||||
|
||||
// Every app must be listed, so a new slug fails the type check until it is.
|
||||
export const CRAFT_APP_RELEASES: Record<CraftAppSlug, CraftRelease | null> = {
|
||||
photocraft: craftRelease("0.7.0"),
|
||||
vectorcraft: craftRelease("0.9.0"),
|
||||
filmcraft: craftRelease("0.6.0"),
|
||||
lightcraft: craftRelease("0.6.0"),
|
||||
pdfcraft: craftRelease("0.6.0"),
|
||||
effectcraft: craftRelease("0.8.0"),
|
||||
designcraft: craftRelease("0.6.0"),
|
||||
figcraft: craftRelease("0.5.1"),
|
||||
loopcraft: null,
|
||||
wordcraft: craftRelease("0.5.0"),
|
||||
soundcraft: craftRelease("0.5.0"),
|
||||
deckcraft: craftRelease("0.5.0"),
|
||||
cadcraft: craftRelease("0.5.0"),
|
||||
gridcraft: craftRelease("0.5.0"),
|
||||
photocraft: craftRelease("0.5.0"),
|
||||
vectorcraft: craftRelease("0.7.0"),
|
||||
filmcraft: craftRelease("0.4.0"),
|
||||
lightcraft: craftRelease("0.4.0"),
|
||||
pdfcraft: craftRelease("0.4.0"),
|
||||
effectcraft: craftRelease("0.6.0"),
|
||||
designcraft: craftRelease("0.4.0"),
|
||||
};
|
||||
@@ -18,8 +18,7 @@ import { mediaUrl } from "./links";
|
||||
// release there is null show a Discord waitlist instead of installers.
|
||||
//
|
||||
// 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), the shots and,
|
||||
// for an app with a demo, its poster frame.
|
||||
// hicolor render at 256 px), og.jpg (1200×630 share card) and the shots.
|
||||
|
||||
export type CraftAppSlug =
|
||||
| "photocraft"
|
||||
@@ -28,14 +27,7 @@ export type CraftAppSlug =
|
||||
| "lightcraft"
|
||||
| "pdfcraft"
|
||||
| "effectcraft"
|
||||
| "designcraft"
|
||||
| "figcraft"
|
||||
| "loopcraft"
|
||||
| "wordcraft"
|
||||
| "soundcraft"
|
||||
| "deckcraft"
|
||||
| "cadcraft"
|
||||
| "gridcraft";
|
||||
| "designcraft";
|
||||
|
||||
export type CraftPlatform = "macOS" | "Windows" | "Linux" | "Web";
|
||||
|
||||
@@ -48,21 +40,6 @@ export type CraftShot = {
|
||||
alt: string;
|
||||
/** Portrait page (7:9) rather than a 16:10 window capture. */
|
||||
portrait?: boolean;
|
||||
/** 16:9 window capture rather than 16:10. */
|
||||
wide?: boolean;
|
||||
};
|
||||
|
||||
/** A feature clip with its own section under the hero. */
|
||||
export type CraftDemo = {
|
||||
/** Muted, looping 16:9 clip. */
|
||||
src: string;
|
||||
/** Still under public/images/apps/<slug>/, shown until the clip plays. */
|
||||
poster: string;
|
||||
label: string;
|
||||
/** Headline split around its one color-accented contrast word. */
|
||||
headline: [before: string, accent: string, after: string];
|
||||
lede: string;
|
||||
caption: string;
|
||||
};
|
||||
|
||||
export type CraftApp = {
|
||||
@@ -83,7 +60,6 @@ export type CraftApp = {
|
||||
features: Tip[];
|
||||
/** First shot is the hero. */
|
||||
shots: [CraftShot, ...CraftShot[]];
|
||||
demo?: CraftDemo;
|
||||
};
|
||||
|
||||
/** One release file, resolved to a URL for a specific app. */
|
||||
@@ -559,523 +535,9 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "figcraft",
|
||||
prefix: "Fig",
|
||||
category: "Interface design",
|
||||
status: "Early alpha",
|
||||
headline: ["Interface design, ", "rebuilt", " in pure Rust."],
|
||||
pitch: "Open-source interface design and prototyping, rebuilt in pure Rust.",
|
||||
lede: "Frames, auto layout, components and variants, vector networks, styles, variables and prototypes in a native app that opens instantly and works offline. No account, no telemetry, no subscription.",
|
||||
schemaCategory: "DesignApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.95",
|
||||
features: [
|
||||
{
|
||||
title: "Familiar by design",
|
||||
body: "The toolbar, layers panel, Design panel, menus and shortcuts sit where your hands expect them. Productive on day one, no retraining.",
|
||||
},
|
||||
{
|
||||
title: "Layouts that build themselves",
|
||||
body: "Auto layout in both directions with wrap and grid, padding, gaps and Hug, Fill and Fixed sizing, plus constraints and layout grids for everything else.",
|
||||
},
|
||||
{
|
||||
title: "Components that scale",
|
||||
body: "Instances with overrides that survive edits, component sets with variants and properties, and styles and variables with modes for light and dark.",
|
||||
},
|
||||
{
|
||||
title: "Real vector networks",
|
||||
body: "Pen and pencil, a shape builder, booleans whose operands stay editable, variable-width strokes and smooth corners.",
|
||||
},
|
||||
{
|
||||
title: "Prototype and hand off",
|
||||
body: "Connections, triggers and animated transitions, a presentation view, and Dev Mode with measurements, annotations and generated CSS, HTML, React, SwiftUI and Compose code.",
|
||||
},
|
||||
{
|
||||
title: "Agent-ready",
|
||||
body: "Every menu item, tool and gesture runs one of 400+ commands, so the CLI, a JSON control channel and an MCP server can do anything you can click.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "shot-1-hero.png",
|
||||
caption: "Ridgeline — auto layout screens in light and dark",
|
||||
alt: "FigCraft editing the Ridgeline hiking app: three phone screens on the canvas, the Featured trail card instance selected, the layers panel showing its auto layout tree, and the Design panel showing Fill and Hug sizing and the Theme variable mode",
|
||||
},
|
||||
{
|
||||
file: "components.webp",
|
||||
source: "shot-2-components.png",
|
||||
caption: "Components, variants and the Assets panel",
|
||||
alt: "FigCraft's Assets panel with live component thumbnails beside a Button component set of nine variants, and the Design panel listing its Style and State variant properties",
|
||||
},
|
||||
{
|
||||
file: "prototype.webp",
|
||||
source: "shot-3-prototype.png",
|
||||
caption: "Prototype connections and flows",
|
||||
alt: "Four onboarding phone screens in FigCraft's Prototype tab joined by connection curves, with the Prototype panel showing an On click, Navigate to interaction",
|
||||
},
|
||||
{
|
||||
file: "dev-mode.webp",
|
||||
source: "shot-4-devmode.png",
|
||||
caption: "Dev Mode with annotations and generated code",
|
||||
alt: "FigCraft's Dev Mode on the Ridgeline home screen with a Ready for dev badge, an annotation callout, and the Inspect panel showing generated React code for the selected trail row",
|
||||
},
|
||||
{
|
||||
file: "vector.webp",
|
||||
source: "shot-5-vector.png",
|
||||
caption: "Vector networks and variable-width strokes",
|
||||
alt: "FigCraft's vector edit mode on a wave path with a tapered gradient stroke, its anchor points and bezier handles visible, above a boolean-built mountain mark and a knot of excluded rings",
|
||||
},
|
||||
{
|
||||
file: "dashboard.webp",
|
||||
source: "shot-6-dashboard-light.png",
|
||||
caption: "Layout grids and styles, light theme",
|
||||
alt: "FigCraft in its light theme with a web analytics dashboard of KPI cards, bar, donut and area charts, and the frame's 12-column layout grid showing as tinted columns",
|
||||
},
|
||||
{
|
||||
file: "collab.webp",
|
||||
source: "shot-7-collab.png",
|
||||
caption: "Live session with cursor chat and spotlight",
|
||||
alt: "A FigCraft live session with three participant avatars, a spotlight banner with a Follow button, two collaborators' cursors and selections on the canvas, and a cursor chat bubble",
|
||||
},
|
||||
],
|
||||
demo: {
|
||||
src: "https://frontend-cdn.fakeyou.com/videos/figcraft-collab-web.mp4",
|
||||
poster: "collab-poster.webp",
|
||||
label: "Live sessions",
|
||||
headline: ["Design ", "together", ", in real time."],
|
||||
lede: "Host the open file from the app or a server of your own, share the invite link, and edit it together. Changes sync live with cursors, selections and cursor chat, and undo takes back only your own.",
|
||||
caption: "A host and a guest editing one file",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "loopcraft",
|
||||
prefix: "Loop",
|
||||
category: "Beat making & music production",
|
||||
status: "Early alpha",
|
||||
headline: ["Beats, patterns and ", "songs", ", in pure Rust."],
|
||||
pitch: "An open-source beat maker and music production studio, built in pure Rust.",
|
||||
lede: "A pattern-based music workstation: a step sequencer and piano roll for writing parts, a playlist for arranging them, and a mixer with effects for finishing them. Native, open source, and drivable end to end by AI agents.",
|
||||
schemaCategory: "MultimediaApplication",
|
||||
platforms: ["macOS", "Windows", "Linux"],
|
||||
rustVersion: "1.95",
|
||||
features: [
|
||||
{
|
||||
title: "Step sequencer",
|
||||
body: "Program drums in the channel rack, up to four bars a row, with swing, step fills and a graph editor for each step's velocity, pan, pitch and shift.",
|
||||
},
|
||||
{
|
||||
title: "A deep piano roll",
|
||||
body: "Draw, paint and slice notes, stamp chords and scale runs, then quantise, strum, arpeggiate or humanise them, with slide notes and ghost notes from other channels.",
|
||||
},
|
||||
{
|
||||
title: "Arrange in the playlist",
|
||||
body: "Place patterns and audio clips on tracks, resize, slice and duplicate them, drop time markers, and draw automation for any knob, fader or the tempo.",
|
||||
},
|
||||
{
|
||||
title: "Instruments and effects",
|
||||
body: "Twelve instruments, from a sampler and synths to drum machines and a slicer, and twenty-four effects, each with factory presets. The drum sounds are synthesised, not sampled.",
|
||||
},
|
||||
{
|
||||
title: "Record and mix",
|
||||
body: "Play parts in from a MIDI or computer keyboard, record audio takes, and mix on a master and twenty insert tracks with sends and sidechain compression.",
|
||||
},
|
||||
{
|
||||
title: "Agent-ready",
|
||||
body: "Every action is a command with an id, so a command-line tool, an MCP server and a control channel can write, arrange and export a track end to end.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "loopcraft-hero.png",
|
||||
caption: "First Light — channel rack, mixer and playlist",
|
||||
alt: "LoopCraft with its demo song open: the browser on the left, the channel rack with its step sequencer, the mixer with an effect rack, and the playlist with coloured pattern clips",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
file: "light.webp",
|
||||
source: "light-theme.png",
|
||||
caption: "Light theme",
|
||||
alt: "LoopCraft in its light theme: the same windows on white panels with a dark application bar",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
file: "piano-roll.webp",
|
||||
source: "piano-roll.png",
|
||||
caption: "Piano roll with a velocity lane",
|
||||
alt: "LoopCraft's piano roll showing a melody as green notes on a grid beside a piano keyboard, with a velocity lane underneath",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
file: "instruments.webp",
|
||||
source: "instrument-and-mixer.png",
|
||||
caption: "Instrument editor over the playlist and mixer",
|
||||
alt: "LoopCraft's three-oscillator instrument editor, a grid of knobs for the oscillators, an envelope and a filter, floating over the playlist and mixer",
|
||||
wide: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "wordcraft",
|
||||
prefix: "Word",
|
||||
category: "Word processing",
|
||||
status: "In development",
|
||||
headline: ["A fast, open-source ", "word processor", ", rebuilt in pure Rust."],
|
||||
pitch: "Fast, open-source word processing, rebuilt in pure Rust.",
|
||||
lede: "The ribbon, styles, tables, track changes, references and mail merge in a native word processor that reads and writes .docx. Open source, offline, and fast.",
|
||||
schemaCategory: "BusinessApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "The ribbon you know",
|
||||
body: "Tabs, groups, shortcuts and the live Styles gallery. Enter continues a list, Tab demotes it, and formatting works as your hands expect.",
|
||||
},
|
||||
{
|
||||
title: "Real document formats",
|
||||
body: "Opens and saves .docx natively, imports and exports .odt, .rtf, .html, .md and .txt, and exports clean PDF with selectable text.",
|
||||
},
|
||||
{
|
||||
title: "Instant layout",
|
||||
body: "Paragraph layout is cached: typing in a 188-page document re-lays it out in about 1.4 ms, with pages rendered on demand.",
|
||||
},
|
||||
{
|
||||
title: "Track changes & review",
|
||||
body: "Comment balloons in the margin, accept and reject changes, document comparison, plus offline spelling and grammar as you type.",
|
||||
},
|
||||
{
|
||||
title: "Academic & business references",
|
||||
body: "Generate tables of contents, footnotes, citations and bibliographies in APA, MLA, Chicago and IEEE styles, indexes, and captions.",
|
||||
},
|
||||
{
|
||||
title: "Agent-ready commands",
|
||||
body: "389 discrete commands drive the ribbon, keyboard shortcuts, a command-line tool, a JSON control channel and an MCP server.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "hero.png",
|
||||
caption: "The Open Studio Handbook — ribbon, styles and navigation",
|
||||
alt: "WordCraft with the Home tab of the ribbon open over a two-page document titled The Open Studio Handbook, the Navigation pane on the left, and the Styles gallery showing live previews",
|
||||
},
|
||||
{
|
||||
file: "review.webp",
|
||||
source: "review.png",
|
||||
caption: "Review and track changes",
|
||||
alt: "WordCraft's Review tab with Track Changes on, inline insertions and deletions, and margin comment balloons",
|
||||
},
|
||||
{
|
||||
file: "references.webp",
|
||||
source: "references.png",
|
||||
caption: "Table of contents and citations",
|
||||
alt: "WordCraft's References tab showing a table of contents with dot leaders on page one and citations on page two",
|
||||
},
|
||||
{
|
||||
file: "layout.webp",
|
||||
source: "layout.png",
|
||||
caption: "Drop caps and text wrap",
|
||||
alt: "WordCraft layout tab showing a three-line drop cap, text wrapping around an object, and margin line numbers",
|
||||
},
|
||||
{
|
||||
file: "dark.webp",
|
||||
source: "dark.png",
|
||||
caption: "Dark mode with formatting marks",
|
||||
alt: "WordCraft in dark mode with the Insert tab open and formatting marks visible",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "soundcraft",
|
||||
prefix: "Sound",
|
||||
category: "Digital audio workstation",
|
||||
status: "In development",
|
||||
headline: ["Professional audio recording and ", "mixing", ", rebuilt in pure Rust."],
|
||||
pitch: "Professional audio recording, editing and mixing in pure Rust.",
|
||||
lede: "Dedicated Edit and Mix windows, multitrack audio and MIDI, ten inserts and sends per track, surround panning, and offline AudioSuite in a native DAW built from scratch.",
|
||||
schemaCategory: "MultimediaApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "The studio workflow",
|
||||
body: "Dedicated Edit and Mix windows, Shuffle, Slip, Spot and Grid modes, comping playlists, memory locations, and single-key Commands Focus shortcuts.",
|
||||
},
|
||||
{
|
||||
title: "Full mixing console",
|
||||
body: "Ten inserts and ten sends per track, busses, aux inputs, VCAs, routing folders, solo-in-place, and automatic plugin delay compensation.",
|
||||
},
|
||||
{
|
||||
title: "Live automation",
|
||||
body: "Write, Touch, Latch and Trim automation passes recorded live for volume, pan, mutes, sends and every plugin parameter.",
|
||||
},
|
||||
{
|
||||
title: "Original plugins & CLAP support",
|
||||
body: "Built-in EQ, dynamic processors, reverbs, delays, modulation, synthesisers, plus support for loading external CLAP plugins.",
|
||||
},
|
||||
{
|
||||
title: "MIDI & notation",
|
||||
body: "Piano-roll MIDI editor with velocity lane, step input, quantize, transposition, score notation editor, and Audio-to-MIDI pitch detection.",
|
||||
},
|
||||
{
|
||||
title: "Surround & video sync",
|
||||
body: "Surround panning up to 9.1.6, live speaker rendering, and a dedicated picture track with video playback synchronized to the playhead.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "edit.png",
|
||||
caption: "Edit window — arranging and editing the demo session",
|
||||
alt: "SoundCraft Edit window showing audio waveforms, MIDI tracks, rulers, markers and transport controls for a multi-track session",
|
||||
},
|
||||
{
|
||||
file: "mix.webp",
|
||||
source: "mix.png",
|
||||
caption: "Mix console with inserts and sends",
|
||||
alt: "SoundCraft Mix window with ten inserts and sends per track, pan knobs, faders, meters and an EQ plugin window",
|
||||
},
|
||||
{
|
||||
file: "midi.webp",
|
||||
source: "midi.png",
|
||||
caption: "Piano-roll MIDI editor",
|
||||
alt: "SoundCraft MIDI editor showing note velocity lanes and piano roll editing",
|
||||
},
|
||||
{
|
||||
file: "automation.webp",
|
||||
source: "automation.png",
|
||||
caption: "Breakpoint automation and memory locations",
|
||||
alt: "SoundCraft displaying volume automation curves on a track alongside the Memory Locations window",
|
||||
},
|
||||
{
|
||||
file: "surround.webp",
|
||||
source: "surround.png",
|
||||
caption: "Immersive surround renderer",
|
||||
alt: "SoundCraft Renderer window displaying a 7.1.4 surround speaker layout and spatial panners",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "deckcraft",
|
||||
prefix: "Deck",
|
||||
category: "Presentations & slide shows",
|
||||
status: "In development",
|
||||
headline: ["Fast, dynamic slide ", "presentations", ", in pure Rust."],
|
||||
pitch: "Build and deliver dynamic slide presentations, in pure Rust.",
|
||||
lede: "Slide masters, themes, 150+ vector shapes, transitions, animations and presenter view in a native app that opens and saves presentation files. No subscription, no cloud.",
|
||||
schemaCategory: "BusinessApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "Master slides & themes",
|
||||
body: "Slide masters and eleven layouts with inheriting placeholders, eight original themes with font and colour schemes, sections, and presenter notes.",
|
||||
},
|
||||
{
|
||||
title: "Vector shapes & drawing",
|
||||
body: "Over 150 preset shapes with adjustment handles, sticky connectors, curve booleans, gradient fills, and outline styling.",
|
||||
},
|
||||
{
|
||||
title: "Transitions & animation",
|
||||
body: "Slide transitions including Morph, entrance, emphasis, exit and motion-path animations, with by-paragraph builds.",
|
||||
},
|
||||
{
|
||||
title: "Presenter view",
|
||||
body: "Full-screen slide show delivery with speaker notes, upcoming slide previews, elapsed timers, rehearsal timings, and pen drawing.",
|
||||
},
|
||||
{
|
||||
title: "Format compatibility",
|
||||
body: "Open and save presentation files (.pptx), export vector PDF slides and handouts, and render slide images.",
|
||||
},
|
||||
{
|
||||
title: "Automated with MCP",
|
||||
body: "Over 200 commands scriptable from the CLI, a JSON control channel, and an MCP server for autonomous agents.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "editor.png",
|
||||
caption: "Slide editor with process diagram",
|
||||
alt: "DeckCraft slide editor showing the ribbon, slide thumbnails, notes panel, and a vector process diagram on the canvas",
|
||||
},
|
||||
{
|
||||
file: "shape-format.webp",
|
||||
source: "shape-format.png",
|
||||
caption: "Shape formatting and geometry",
|
||||
alt: "DeckCraft Shape Format tab with selection handles and alignment tools on an active shape",
|
||||
},
|
||||
{
|
||||
file: "format-pane.webp",
|
||||
source: "format-pane.png",
|
||||
caption: "Format Shape pane and gradient stops",
|
||||
alt: "DeckCraft Format Shape pane configuring gradient fill angles, stops and transparency",
|
||||
},
|
||||
{
|
||||
file: "slide-sorter.webp",
|
||||
source: "slide-sorter.png",
|
||||
caption: "Slide Sorter view with transitions",
|
||||
alt: "DeckCraft Slide Sorter showing all slides in the deck with assigned transition badges",
|
||||
},
|
||||
{
|
||||
file: "dark-mode.webp",
|
||||
source: "dark-mode.png",
|
||||
caption: "Dark mode with chart slide",
|
||||
alt: "DeckCraft dark mode editing a presentation slide featuring a native chart",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "cadcraft",
|
||||
prefix: "CAD",
|
||||
category: "Computer-aided design & drafting",
|
||||
status: "In development",
|
||||
headline: ["Computer-aided design and drafting, ", "reimagined", " in pure Rust."],
|
||||
pitch: "Fast, open-source computer-aided design and drafting in pure Rust.",
|
||||
lede: "The command line, object snaps, layers, dimensions, hatches, blocks and drawings you already know. Fast, native, and built for both people and AI agents.",
|
||||
schemaCategory: "DesignApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "The drafting workflow",
|
||||
body: "Type commands, click points, direct distance entry, object snaps, polar tracking, ortho, grips, and right-click-to-repeat.",
|
||||
},
|
||||
{
|
||||
title: "Open drawing formats",
|
||||
body: "Reads and writes DXF (ASCII and binary, R12–2018), opens DWG (R13–2018), and exports to PDF, SVG, and PNG.",
|
||||
},
|
||||
{
|
||||
title: "Paper space & viewports",
|
||||
body: "Layout sheets with floating viewports, per-viewport layer overrides, page setups, and scale-exact plotting.",
|
||||
},
|
||||
{
|
||||
title: "Associative annotations",
|
||||
body: "Linear, aligned, angular and radial dimensions that follow geometry, multileaders, tables, and custom drafting fonts.",
|
||||
},
|
||||
{
|
||||
title: "Hatching & block libraries",
|
||||
body: "Pick-point boundary hatching with islands, pattern fills, block definitions with attributes, and nested blocks.",
|
||||
},
|
||||
{
|
||||
title: "Parametric constraints",
|
||||
body: "Geometric and dimensional constraints with expression parameters that automatically re-solve as you edit.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "ui-apartment.png",
|
||||
caption: "Apartment floor plan with hatching and dimensions",
|
||||
alt: "CADCraft displaying an architectural floor plan with pick-point wall hatching, room schedules, dimensions and the command line",
|
||||
},
|
||||
{
|
||||
file: "layout.webp",
|
||||
source: "ui-layout.png",
|
||||
caption: "Paper space layout with scaled viewport",
|
||||
alt: "CADCraft paper space layout showing a title block sheet with an active scaled model viewport",
|
||||
},
|
||||
{
|
||||
file: "bracket.webp",
|
||||
source: "ui-bracket.png",
|
||||
caption: "Mechanical part drawing with section hatch",
|
||||
alt: "CADCraft drafting a mechanical mounting bracket with section lines, dimensions and title block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "gridcraft",
|
||||
prefix: "Grid",
|
||||
category: "Spreadsheets & data analysis",
|
||||
status: "In development",
|
||||
headline: ["Fast, native spreadsheets, ", "rebuilt", " in pure Rust."],
|
||||
pitch: "Fast, open-source spreadsheets and data analysis, rebuilt in pure Rust.",
|
||||
lede: "Ribbon tabs, formula bar, 500+ calculation functions, dynamic arrays, PivotTables and charts in a native spreadsheet app that reads and writes .xlsx files.",
|
||||
schemaCategory: "BusinessApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
features: [
|
||||
{
|
||||
title: "The spreadsheet workflow",
|
||||
body: "Formula bar, Name Box, sheet tabs, fill handle, colored cell references, conditional formatting, and familiar shortcuts.",
|
||||
},
|
||||
{
|
||||
title: "Native XLSX compatibility",
|
||||
body: "Styles, themes, formulas, dynamic arrays, tables, validation, comments, sparklines and PivotTables round-trip cleanly.",
|
||||
},
|
||||
{
|
||||
title: "500+ calculation functions",
|
||||
body: "Full dependency-graph calculation engine with dynamic arrays, spilling, LET, LAMBDA, MAP, REDUCE, and table references.",
|
||||
},
|
||||
{
|
||||
title: "Charts & PivotTables",
|
||||
body: "Seventeen chart types with customizable formatting, plus PivotTables with filters, field lists, layouts, and date grouping.",
|
||||
},
|
||||
{
|
||||
title: "High-performance engine",
|
||||
body: "Copy-on-write workbooks with instant undo snapshots, incremental recalculation, and a virtualised grid.",
|
||||
},
|
||||
{
|
||||
title: "Agent-native automation",
|
||||
body: "Every ribbon button, menu item, and formula can be driven by AI agents over an MCP server, CLI, or JSON control channel.",
|
||||
},
|
||||
],
|
||||
shots: [
|
||||
{
|
||||
file: "hero.webp",
|
||||
source: "hero-sales.png",
|
||||
caption: "Quarterly sales dashboard with charts and sparklines",
|
||||
alt: "GridCraft sales dashboard featuring a styled table with conditional formatting, sparklines, and column and pie charts",
|
||||
},
|
||||
{
|
||||
file: "budget.webp",
|
||||
source: "budget.png",
|
||||
caption: "Household budget with conditional formatting",
|
||||
alt: "GridCraft budget sheet showing accounting currency formats, color scales, and comparison bar charts",
|
||||
},
|
||||
{
|
||||
file: "grades-formulas.webp",
|
||||
source: "grades-formulas.png",
|
||||
caption: "Formulas and function library",
|
||||
alt: "GridCraft formula bar and Formulas ribbon tab calculating averages and letter grades",
|
||||
},
|
||||
{
|
||||
file: "pivot.webp",
|
||||
source: "pivot.png",
|
||||
caption: "PivotTable and field list",
|
||||
alt: "GridCraft PivotTable report summarizing data with the PivotTable Fields pane open on the right",
|
||||
},
|
||||
{
|
||||
file: "format-chart.webp",
|
||||
source: "format-chart.png",
|
||||
caption: "Chart Design and Format Chart Area pane",
|
||||
alt: "GridCraft configuring a column chart with the Format Chart Area pane and highlighted source range",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const COUNT_WORDS = [
|
||||
"Zero",
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"Four",
|
||||
"Five",
|
||||
"Six",
|
||||
"Seven",
|
||||
"Eight",
|
||||
"Nine",
|
||||
"Ten",
|
||||
"Eleven",
|
||||
"Twelve",
|
||||
"Thirteen",
|
||||
"Fourteen",
|
||||
"Fifteen",
|
||||
"Sixteen",
|
||||
];
|
||||
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 =
|
||||
@@ -1156,10 +618,6 @@ export function craftShotSourceUrl(app: CraftApp, shot: CraftShot): string {
|
||||
return `https://raw.githubusercontent.com/storytold/${app.slug}/main/docs/images/${shot.source}`;
|
||||
}
|
||||
|
||||
export function craftDemoPosterUrl(app: CraftApp, demo: CraftDemo): string {
|
||||
return mediaUrl(`/images/apps/${app.slug}/${demo.poster}`);
|
||||
}
|
||||
|
||||
export function craftAppIconPath(app: CraftApp): string {
|
||||
return `/images/apps/${app.slug}/icon.webp`;
|
||||
}
|
||||
|
||||