From 6c602bc48e2ac4564743eb898d1625c934387417 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Mon, 12 Oct 2026 11:11:17 +0200 Subject: [PATCH 1/2] feat(shipyard): shipyard's website and docs at coss.com/shipyard A new app, apps/shipyard, served under /shipyard like coss ui under /ui. The landing page is the one from berthd.app, under coss.com's header, rails and theme: the dithered harbour that comes alive at night, the six chapters with the app's line scenes and loops, the install line. Its own type and tokens stay; the header shows "coss.com shipyard", Docs, Download, the docs' search, Shipyard's repository and its stars, and the theme switch. The docs move here from the cosscom/shipyard repository's docs/ folder, on the same Fumadocs setup as coss ui's docs (sidebar, search, table of contents, Copy Markdown, .md routes), with Shipyard's own pieces ported: callouts, the stage, the path of cards, screenshots, diagrams, the line scenes, and the reference tables made linkable. The shared header takes the product's repository for its GitHub link, and the products list has shipyard. Co-Authored-By: Claude Fable 5.1 --- apps/shipyard/.gitignore | 44 + apps/shipyard/README.md | 27 + apps/shipyard/app/api/raw/[...slug]/route.ts | 50 + apps/shipyard/app/apple-icon.png | Bin 0 -> 5548 bytes apps/shipyard/app/docs.css | 325 +++++ apps/shipyard/app/docs/[[...slug]]/page.tsx | 102 ++ apps/shipyard/app/docs/layout.tsx | 18 + apps/shipyard/app/globals.css | 12 + apps/shipyard/app/icon.svg | 4 + apps/shipyard/app/landing.css | 766 ++++++++++ apps/shipyard/app/layout.tsx | 53 + apps/shipyard/app/not-found.tsx | 44 + apps/shipyard/app/opengraph-image.jpg | Bin 0 -> 185579 bytes apps/shipyard/app/page.tsx | 515 +++++++ apps/shipyard/app/sitemap.ts | 14 + apps/shipyard/app/twitter-image.jpg | Bin 0 -> 185579 bytes apps/shipyard/components/command-menu.tsx | 172 +++ apps/shipyard/components/docs-copy-page.tsx | 21 + apps/shipyard/components/docs-sidebar.tsx | 66 + apps/shipyard/components/docs-toc.tsx | 74 + apps/shipyard/components/docs/callout.tsx | 53 + apps/shipyard/components/docs/cards.tsx | 51 + apps/shipyard/components/docs/diagram.css | 339 +++++ apps/shipyard/components/docs/diagram.tsx | 183 +++ apps/shipyard/components/docs/scenes.css | 439 ++++++ apps/shipyard/components/docs/scenes.tsx | 969 +++++++++++++ apps/shipyard/components/docs/shot.tsx | 53 + apps/shipyard/components/docs/stage.tsx | 26 + apps/shipyard/components/landing/runtime.tsx | 105 ++ apps/shipyard/components/main-nav.tsx | 38 + apps/shipyard/components/mobile-nav.tsx | 128 ++ apps/shipyard/components/site-footer.tsx | 38 + apps/shipyard/components/site-header.tsx | 33 + .../content/docs/(root)/changelog.mdx | 1277 +++++++++++++++++ apps/shipyard/content/docs/(root)/index.mdx | 79 + apps/shipyard/content/docs/(root)/meta.json | 4 + .../content/docs/concepts/architecture.mdx | 109 ++ apps/shipyard/content/docs/concepts/boxes.mdx | 125 ++ .../content/docs/concepts/laptop-agent.mdx | 236 +++ apps/shipyard/content/docs/concepts/meta.json | 11 + .../docs/concepts/projects-and-worktrees.mdx | 164 +++ .../content/docs/concepts/security.mdx | 422 ++++++ .../docs/concepts/sessions-and-agents.mdx | 239 +++ .../content/docs/contributing/meta.json | 1 + .../content/docs/contributing/releasing.mdx | 198 +++ .../docs/getting-started/add-a-box.mdx | 513 +++++++ .../docs/getting-started/first-agent.mdx | 146 ++ .../docs/getting-started/first-project.mdx | 101 ++ .../content/docs/getting-started/install.mdx | 326 +++++ .../content/docs/getting-started/meta.json | 4 + .../content/docs/guides/agent-browser.mdx | 323 +++++ .../docs/guides/agent-integrations.mdx | 177 +++ .../content/docs/guides/artifacts.mdx | 139 ++ .../content/docs/guides/automations.mdx | 350 +++++ apps/shipyard/content/docs/guides/browser.mdx | 167 +++ apps/shipyard/content/docs/guides/files.mdx | 175 +++ apps/shipyard/content/docs/guides/home.mdx | 84 ++ apps/shipyard/content/docs/guides/hooks.mdx | 136 ++ apps/shipyard/content/docs/guides/kits.mdx | 287 ++++ apps/shipyard/content/docs/guides/labs.mdx | 341 +++++ .../shipyard/content/docs/guides/leadline.mdx | 159 ++ apps/shipyard/content/docs/guides/linear.mdx | 88 ++ apps/shipyard/content/docs/guides/meta.json | 32 + .../content/docs/guides/more-computers.mdx | 89 ++ .../content/docs/guides/offline-queue.mdx | 78 + .../content/docs/guides/orchestration.mdx | 288 ++++ apps/shipyard/content/docs/guides/phone.mdx | 105 ++ apps/shipyard/content/docs/guides/plugins.mdx | 591 ++++++++ .../content/docs/guides/project-config.mdx | 223 +++ .../content/docs/guides/resource-guard.mdx | 50 + .../content/docs/guides/review-a-pr.mdx | 238 +++ apps/shipyard/content/docs/guides/runs.mdx | 221 +++ apps/shipyard/content/docs/guides/secrets.mdx | 88 ++ .../content/docs/guides/task-templates.mdx | 44 + .../content/docs/guides/team-setup.mdx | 545 +++++++ .../content/docs/guides/terminals.mdx | 60 + apps/shipyard/content/docs/guides/themes.mdx | 141 ++ .../content/docs/guides/visual-diffs.mdx | 198 +++ apps/shipyard/content/docs/meta.json | 11 + .../content/docs/reference/app-api.mdx | 271 ++++ .../content/docs/reference/berthd.mdx | 188 +++ apps/shipyard/content/docs/reference/cli.mdx | 220 +++ .../content/docs/reference/config.mdx | 236 +++ .../content/docs/reference/events.mdx | 255 ++++ .../shipyard/content/docs/reference/files.mdx | 151 ++ .../shipyard/content/docs/reference/meta.json | 12 + .../content/docs/reference/plugin-sdk.mdx | 350 +++++ apps/shipyard/fonts/LICENSE-inter.txt | 93 ++ .../shipyard/fonts/LICENSE-jetbrains-mono.txt | 93 ++ apps/shipyard/fonts/inter-latin-wght.woff2 | Bin 0 -> 48256 bytes .../fonts/jetbrains-mono-latin-wght.woff2 | Bin 0 -> 40404 bytes apps/shipyard/hooks/use-is-mac.ts | 11 + apps/shipyard/lib/config.ts | 34 + apps/shipyard/lib/docs.ts | 3 + apps/shipyard/lib/fonts.ts | 17 + apps/shipyard/lib/rehype-nowrap-tokens.ts | 50 + apps/shipyard/lib/rehype-reference.ts | 227 +++ apps/shipyard/lib/shots.json | 38 + apps/shipyard/lib/source.ts | 7 + apps/shipyard/lib/utils.ts | 6 + apps/shipyard/mdx-components.tsx | 279 ++++ apps/shipyard/next.config.ts | 19 + apps/shipyard/package.json | 42 + apps/shipyard/postcss.config.mjs | 3 + apps/shipyard/public/assets/clips.js | 191 +++ .../public/assets/film/hero-loop-poster.webp | Bin 0 -> 146062 bytes .../shipyard/public/assets/film/hero-loop.mp4 | Bin 0 -> 1196962 bytes .../public/assets/film/hero-loop.webm | Bin 0 -> 1366789 bytes apps/shipyard/public/assets/film/poster.webp | Bin 0 -> 180200 bytes apps/shipyard/public/assets/harbour.js | 201 +++ apps/shipyard/public/assets/harbour/day.webp | Bin 0 -> 114304 bytes .../shipyard/public/assets/harbour/night.webp | Bin 0 -> 164082 bytes apps/shipyard/public/assets/hero-loop.js | 232 +++ .../public/assets/loops/artifacts.mp4 | Bin 0 -> 173364 bytes .../public/assets/loops/artifacts.webp | Bin 0 -> 60478 bytes apps/shipyard/public/assets/loops/ask.mp4 | Bin 0 -> 199515 bytes apps/shipyard/public/assets/loops/ask.webp | Bin 0 -> 42078 bytes apps/shipyard/public/assets/loops/compare.mp4 | Bin 0 -> 495412 bytes .../shipyard/public/assets/loops/compare.webp | Bin 0 -> 75400 bytes apps/shipyard/public/assets/loops/groups.mp4 | Bin 0 -> 325303 bytes apps/shipyard/public/assets/loops/groups.webp | Bin 0 -> 47692 bytes apps/shipyard/public/assets/loops/start.mp4 | Bin 0 -> 577628 bytes apps/shipyard/public/assets/loops/start.webp | Bin 0 -> 150508 bytes apps/shipyard/public/assets/loops/steps.mp4 | Bin 0 -> 237347 bytes apps/shipyard/public/assets/loops/steps.webp | Bin 0 -> 62136 bytes apps/shipyard/public/assets/loops/themes.mp4 | Bin 0 -> 710971 bytes apps/shipyard/public/assets/loops/themes.webp | Bin 0 -> 63422 bytes apps/shipyard/public/assets/scenes.css | 555 +++++++ apps/shipyard/public/assets/scenes.json | 8 + apps/shipyard/public/assets/seam.svg | 1 + .../public/shots/agent-browser-dark.webp | Bin 0 -> 18478 bytes .../public/shots/agent-browser-light.webp | Bin 0 -> 19006 bytes .../public/shots/browser-sandbox-dark.webp | Bin 0 -> 43192 bytes .../public/shots/browser-sandbox-light.webp | Bin 0 -> 46616 bytes .../public/shots/chat-artifacts-dark.webp | Bin 0 -> 101722 bytes .../public/shots/chat-artifacts-light.webp | Bin 0 -> 112952 bytes .../public/shots/chat-background-dark.webp | Bin 0 -> 48662 bytes .../public/shots/chat-background-light.webp | Bin 0 -> 58378 bytes .../public/shots/chat-question-dark.webp | Bin 0 -> 61276 bytes .../public/shots/chat-question-light.webp | Bin 0 -> 66134 bytes apps/shipyard/public/shots/kits-dark.webp | Bin 0 -> 33150 bytes apps/shipyard/public/shots/kits-light.webp | Bin 0 -> 36990 bytes .../public/shots/service-terminal-dark.webp | Bin 0 -> 65514 bytes .../public/shots/service-terminal-light.webp | Bin 0 -> 68176 bytes .../public/shots/tab-groups-dark.webp | Bin 0 -> 85464 bytes .../public/shots/tab-groups-light.webp | Bin 0 -> 91792 bytes apps/shipyard/public/shots/themes-dark.webp | Bin 0 -> 64936 bytes apps/shipyard/public/shots/themes-light.webp | Bin 0 -> 68518 bytes apps/shipyard/source.config.ts | 40 + apps/shipyard/tsconfig.json | 21 + biome.json | 1 + bun.lock | 33 + packages/ui/src/lib/config.ts | 4 + packages/ui/src/shared/github-link-client.tsx | 10 +- packages/ui/src/shared/github-link.tsx | 17 +- packages/ui/src/shared/site-header.tsx | 5 +- 156 files changed, 18062 insertions(+), 7 deletions(-) create mode 100644 apps/shipyard/.gitignore create mode 100644 apps/shipyard/README.md create mode 100644 apps/shipyard/app/api/raw/[...slug]/route.ts create mode 100644 apps/shipyard/app/apple-icon.png create mode 100644 apps/shipyard/app/docs.css create mode 100644 apps/shipyard/app/docs/[[...slug]]/page.tsx create mode 100644 apps/shipyard/app/docs/layout.tsx create mode 100644 apps/shipyard/app/globals.css create mode 100644 apps/shipyard/app/icon.svg create mode 100644 apps/shipyard/app/landing.css create mode 100644 apps/shipyard/app/layout.tsx create mode 100644 apps/shipyard/app/not-found.tsx create mode 100644 apps/shipyard/app/opengraph-image.jpg create mode 100644 apps/shipyard/app/page.tsx create mode 100644 apps/shipyard/app/sitemap.ts create mode 100644 apps/shipyard/app/twitter-image.jpg create mode 100644 apps/shipyard/components/command-menu.tsx create mode 100644 apps/shipyard/components/docs-copy-page.tsx create mode 100644 apps/shipyard/components/docs-sidebar.tsx create mode 100644 apps/shipyard/components/docs-toc.tsx create mode 100644 apps/shipyard/components/docs/callout.tsx create mode 100644 apps/shipyard/components/docs/cards.tsx create mode 100644 apps/shipyard/components/docs/diagram.css create mode 100644 apps/shipyard/components/docs/diagram.tsx create mode 100644 apps/shipyard/components/docs/scenes.css create mode 100644 apps/shipyard/components/docs/scenes.tsx create mode 100644 apps/shipyard/components/docs/shot.tsx create mode 100644 apps/shipyard/components/docs/stage.tsx create mode 100644 apps/shipyard/components/landing/runtime.tsx create mode 100644 apps/shipyard/components/main-nav.tsx create mode 100644 apps/shipyard/components/mobile-nav.tsx create mode 100644 apps/shipyard/components/site-footer.tsx create mode 100644 apps/shipyard/components/site-header.tsx create mode 100644 apps/shipyard/content/docs/(root)/changelog.mdx create mode 100644 apps/shipyard/content/docs/(root)/index.mdx create mode 100644 apps/shipyard/content/docs/(root)/meta.json create mode 100644 apps/shipyard/content/docs/concepts/architecture.mdx create mode 100644 apps/shipyard/content/docs/concepts/boxes.mdx create mode 100644 apps/shipyard/content/docs/concepts/laptop-agent.mdx create mode 100644 apps/shipyard/content/docs/concepts/meta.json create mode 100644 apps/shipyard/content/docs/concepts/projects-and-worktrees.mdx create mode 100644 apps/shipyard/content/docs/concepts/security.mdx create mode 100644 apps/shipyard/content/docs/concepts/sessions-and-agents.mdx create mode 100644 apps/shipyard/content/docs/contributing/meta.json create mode 100644 apps/shipyard/content/docs/contributing/releasing.mdx create mode 100644 apps/shipyard/content/docs/getting-started/add-a-box.mdx create mode 100644 apps/shipyard/content/docs/getting-started/first-agent.mdx create mode 100644 apps/shipyard/content/docs/getting-started/first-project.mdx create mode 100644 apps/shipyard/content/docs/getting-started/install.mdx create mode 100644 apps/shipyard/content/docs/getting-started/meta.json create mode 100644 apps/shipyard/content/docs/guides/agent-browser.mdx create mode 100644 apps/shipyard/content/docs/guides/agent-integrations.mdx create mode 100644 apps/shipyard/content/docs/guides/artifacts.mdx create mode 100644 apps/shipyard/content/docs/guides/automations.mdx create mode 100644 apps/shipyard/content/docs/guides/browser.mdx create mode 100644 apps/shipyard/content/docs/guides/files.mdx create mode 100644 apps/shipyard/content/docs/guides/home.mdx create mode 100644 apps/shipyard/content/docs/guides/hooks.mdx create mode 100644 apps/shipyard/content/docs/guides/kits.mdx create mode 100644 apps/shipyard/content/docs/guides/labs.mdx create mode 100644 apps/shipyard/content/docs/guides/leadline.mdx create mode 100644 apps/shipyard/content/docs/guides/linear.mdx create mode 100644 apps/shipyard/content/docs/guides/meta.json create mode 100644 apps/shipyard/content/docs/guides/more-computers.mdx create mode 100644 apps/shipyard/content/docs/guides/offline-queue.mdx create mode 100644 apps/shipyard/content/docs/guides/orchestration.mdx create mode 100644 apps/shipyard/content/docs/guides/phone.mdx create mode 100644 apps/shipyard/content/docs/guides/plugins.mdx create mode 100644 apps/shipyard/content/docs/guides/project-config.mdx create mode 100644 apps/shipyard/content/docs/guides/resource-guard.mdx create mode 100644 apps/shipyard/content/docs/guides/review-a-pr.mdx create mode 100644 apps/shipyard/content/docs/guides/runs.mdx create mode 100644 apps/shipyard/content/docs/guides/secrets.mdx create mode 100644 apps/shipyard/content/docs/guides/task-templates.mdx create mode 100644 apps/shipyard/content/docs/guides/team-setup.mdx create mode 100644 apps/shipyard/content/docs/guides/terminals.mdx create mode 100644 apps/shipyard/content/docs/guides/themes.mdx create mode 100644 apps/shipyard/content/docs/guides/visual-diffs.mdx create mode 100644 apps/shipyard/content/docs/meta.json create mode 100644 apps/shipyard/content/docs/reference/app-api.mdx create mode 100644 apps/shipyard/content/docs/reference/berthd.mdx create mode 100644 apps/shipyard/content/docs/reference/cli.mdx create mode 100644 apps/shipyard/content/docs/reference/config.mdx create mode 100644 apps/shipyard/content/docs/reference/events.mdx create mode 100644 apps/shipyard/content/docs/reference/files.mdx create mode 100644 apps/shipyard/content/docs/reference/meta.json create mode 100644 apps/shipyard/content/docs/reference/plugin-sdk.mdx create mode 100644 apps/shipyard/fonts/LICENSE-inter.txt create mode 100644 apps/shipyard/fonts/LICENSE-jetbrains-mono.txt create mode 100644 apps/shipyard/fonts/inter-latin-wght.woff2 create mode 100644 apps/shipyard/fonts/jetbrains-mono-latin-wght.woff2 create mode 100644 apps/shipyard/hooks/use-is-mac.ts create mode 100644 apps/shipyard/lib/config.ts create mode 100644 apps/shipyard/lib/docs.ts create mode 100644 apps/shipyard/lib/fonts.ts create mode 100644 apps/shipyard/lib/rehype-nowrap-tokens.ts create mode 100644 apps/shipyard/lib/rehype-reference.ts create mode 100644 apps/shipyard/lib/shots.json create mode 100644 apps/shipyard/lib/source.ts create mode 100644 apps/shipyard/lib/utils.ts create mode 100644 apps/shipyard/mdx-components.tsx create mode 100644 apps/shipyard/next.config.ts create mode 100644 apps/shipyard/package.json create mode 100644 apps/shipyard/postcss.config.mjs create mode 100644 apps/shipyard/public/assets/clips.js create mode 100644 apps/shipyard/public/assets/film/hero-loop-poster.webp create mode 100644 apps/shipyard/public/assets/film/hero-loop.mp4 create mode 100644 apps/shipyard/public/assets/film/hero-loop.webm create mode 100644 apps/shipyard/public/assets/film/poster.webp create mode 100644 apps/shipyard/public/assets/harbour.js create mode 100644 apps/shipyard/public/assets/harbour/day.webp create mode 100644 apps/shipyard/public/assets/harbour/night.webp create mode 100644 apps/shipyard/public/assets/hero-loop.js create mode 100644 apps/shipyard/public/assets/loops/artifacts.mp4 create mode 100644 apps/shipyard/public/assets/loops/artifacts.webp create mode 100644 apps/shipyard/public/assets/loops/ask.mp4 create mode 100644 apps/shipyard/public/assets/loops/ask.webp create mode 100644 apps/shipyard/public/assets/loops/compare.mp4 create mode 100644 apps/shipyard/public/assets/loops/compare.webp create mode 100644 apps/shipyard/public/assets/loops/groups.mp4 create mode 100644 apps/shipyard/public/assets/loops/groups.webp create mode 100644 apps/shipyard/public/assets/loops/start.mp4 create mode 100644 apps/shipyard/public/assets/loops/start.webp create mode 100644 apps/shipyard/public/assets/loops/steps.mp4 create mode 100644 apps/shipyard/public/assets/loops/steps.webp create mode 100644 apps/shipyard/public/assets/loops/themes.mp4 create mode 100644 apps/shipyard/public/assets/loops/themes.webp create mode 100644 apps/shipyard/public/assets/scenes.css create mode 100644 apps/shipyard/public/assets/scenes.json create mode 100644 apps/shipyard/public/assets/seam.svg create mode 100644 apps/shipyard/public/shots/agent-browser-dark.webp create mode 100644 apps/shipyard/public/shots/agent-browser-light.webp create mode 100644 apps/shipyard/public/shots/browser-sandbox-dark.webp create mode 100644 apps/shipyard/public/shots/browser-sandbox-light.webp create mode 100644 apps/shipyard/public/shots/chat-artifacts-dark.webp create mode 100644 apps/shipyard/public/shots/chat-artifacts-light.webp create mode 100644 apps/shipyard/public/shots/chat-background-dark.webp create mode 100644 apps/shipyard/public/shots/chat-background-light.webp create mode 100644 apps/shipyard/public/shots/chat-question-dark.webp create mode 100644 apps/shipyard/public/shots/chat-question-light.webp create mode 100644 apps/shipyard/public/shots/kits-dark.webp create mode 100644 apps/shipyard/public/shots/kits-light.webp create mode 100644 apps/shipyard/public/shots/service-terminal-dark.webp create mode 100644 apps/shipyard/public/shots/service-terminal-light.webp create mode 100644 apps/shipyard/public/shots/tab-groups-dark.webp create mode 100644 apps/shipyard/public/shots/tab-groups-light.webp create mode 100644 apps/shipyard/public/shots/themes-dark.webp create mode 100644 apps/shipyard/public/shots/themes-light.webp create mode 100644 apps/shipyard/source.config.ts create mode 100644 apps/shipyard/tsconfig.json diff --git a/apps/shipyard/.gitignore b/apps/shipyard/.gitignore new file mode 100644 index 000000000..cccfd3167 --- /dev/null +++ b/apps/shipyard/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# fumadocs +.source \ No newline at end of file diff --git a/apps/shipyard/README.md b/apps/shipyard/README.md new file mode 100644 index 000000000..723879805 --- /dev/null +++ b/apps/shipyard/README.md @@ -0,0 +1,27 @@ +# shipyard + +Shipyard's website and docs at [coss.com/shipyard](https://coss.com/shipyard): +the landing page, and the docs at `/shipyard/docs` on the same Fumadocs setup +as coss ui's. + +- `app/page.tsx`, `app/landing.css`: the landing page, as it was at + berthd.app. The harbour at the top is dithered on a canvas and comes alive + at night (`public/assets/harbour.js`, `hero-loop.js`); the chapters' loops + and line scenes load once the page has painted (`clips.js`, + `scenes.css`/`scenes.json`). `components/landing/runtime.tsx` brings them + in. The page keeps its own type (Inter, JetBrains Mono, in `fonts/`) and + tokens; the header, the rails and the docs are coss's. +- `content/docs/`: the docs, moved here from the `cosscom/shipyard` + repository's `docs/` folder. `components/docs/` are its own pieces + (callouts, the stage, the path of cards, screenshots in `public/shots/`, + diagrams and the line scenes); `lib/rehype-reference.ts` makes the + reference tables linkable. +- The app's downloads, the live demo (`berthd.app/demo/`), the box's + install line (`berthd.app/install`), the plugin listing and the + **Review in Shipyard** bounce page stay on berthd.app: they are built and + published from the `cosscom/shipyard` repository. + +```sh +bun install +bun run dev --filter=shipyard # http://localhost:4100/shipyard +``` diff --git a/apps/shipyard/app/api/raw/[...slug]/route.ts b/apps/shipyard/app/api/raw/[...slug]/route.ts new file mode 100644 index 000000000..b2eb6fbaf --- /dev/null +++ b/apps/shipyard/app/api/raw/[...slug]/route.ts @@ -0,0 +1,50 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { type NextRequest, NextResponse } from "next/server"; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ slug: string[] }> }, +) { + const { slug } = await params; + + // Construct the file path from the slug + // If we have ['docs', 'filename'], look in (root) directory + let filePath: string; + if (slug.length === 2 && slug[0] === "docs") { + filePath = path.join( + process.cwd(), + "content", + "docs", + "(root)", + slug[1] || "", + ); + } else { + filePath = path.join(process.cwd(), "content", ...slug); + } + + // Check for both .mdx and .md extensions + const mdxPath = filePath.endsWith(".mdx") ? filePath : `${filePath}.mdx`; + const mdPath = filePath.endsWith(".md") ? filePath : `${filePath}.md`; + + try { + let content: string; + + // Try .mdx first, then .md + try { + content = await fs.readFile(mdxPath, "utf-8"); + } catch { + content = await fs.readFile(mdPath, "utf-8"); + } + + // Return raw markdown with appropriate headers + return new NextResponse(content, { + headers: { + "Cache-Control": "public, max-age=3600", + "Content-Type": "text/plain; charset=utf-8", + }, + }); + } catch { + return NextResponse.json({ error: "File not found" }, { status: 404 }); + } +} diff --git a/apps/shipyard/app/apple-icon.png b/apps/shipyard/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4402aaa8e6791330b4aab50aaf949bb2f26f1608 GIT binary patch literal 5548 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 u;Ao5g00030|JyyS%K!iX21!IgR09A_C)va * { + margin: 0; +} +.berth-callout-text > * + * { + margin-top: 0.5em; +} +.berth-callout-text a { + color: var(--foreground); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--foreground) 35%, transparent); + text-underline-offset: 3px; +} +.berth-callout-text code { + font-size: 0.86em; + padding: 0.1em 0.32em; + border-radius: 5px; + background: var(--berth-code-bg); + border: 1px solid var(--berth-border-soft); + color: var(--foreground); + overflow-wrap: anywhere; +} +.berth-stage { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px 24px; + margin: 8px 0 36px; + padding: 20px 20px 18px 22px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); +} +.berth-stage-title { + margin: 0 0 10px; + font-size: 14px; + font-weight: 600; + color: var(--foreground); +} +.berth-stage-text { + font-size: 0.9375rem; + line-height: 1.6; +} +.berth-stage-text > :first-child { + margin-top: 0; +} +.berth-stage-text > :last-child { + margin-bottom: 0; +} +.berth-stage-text figure.shiki { + margin: 0 0 12px; + background: var(--background) !important; +} +.berth-stage-art { + align-self: end; + margin: 0 -6px -6px 0; +} +@media (max-width: 720px) { + .berth-stage { + grid-template-columns: minmax(0, 1fr); + padding: 16px; + } + .berth-stage-art { + order: -1; + justify-self: start; + margin: -4px 0 0 -10px; + } + .berth-stage-art svg { + width: 184px; + height: auto; + } +} +.berth-path { + counter-reset: berth-step; +} +.berth-path a[data-card] { + counter-increment: berth-step; + position: relative; + padding-left: 52px; +} +.berth-path a[data-card]::before { + content: counter(berth-step); + position: absolute; + left: 16px; + top: 15px; + display: grid; + place-items: center; + width: 22px; + height: 22px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--card); + font: 500 12px / 1 var(--font-mono); + color: var(--muted-foreground); +} +.berth-nw { + white-space: nowrap; +} +.berth-path a[data-card]::before { + top: 15px; +} +.berth-stage-text a { + color: var(--foreground); + font-weight: 500; + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--foreground) 35%, transparent); + text-underline-offset: 3px; +} +.berth-stage-text :not(pre) > code { + font-size: 0.86em; + padding: 0.1em 0.34em; + border-radius: 5px; + background: var(--background); + border: 1px solid var(--berth-border-soft); + color: var(--foreground); +} +.berth-stage-text p { + color: var(--berth-fg-2); +} +.berth-stage-text figure.shiki { + --berth-code-ground: var(--background); +} +dl.berth-ref { + margin: 1.25rem 0 1.75rem; + border: 1px solid var(--border); + border-radius: 12px; +} +.berth-ref-item { + position: relative; + padding: 11px 40px 12px 16px; + scroll-margin-top: 7rem; + transition: background-color 0.15s; +} +.berth-ref-item + .berth-ref-item { + border-top: 1px solid var(--berth-border-soft); +} +.berth-ref-item:first-child { + border-radius: 11px 11px 0 0; +} +.berth-ref-item:last-child { + border-radius: 0 0 11px 11px; +} +.berth-ref-item:only-child { + border-radius: 11px; +} +.berth-ref-sig { + margin: 0; + font-weight: 400; + line-height: 1.55; +} +.berth-ref-sig code { + padding: 0; + border: 0; + background: none; + font-size: 13.5px; + font-weight: 500; + color: var(--foreground); +} +.berth-ref-desc { + margin: 3px 0 0; + padding: 0; + font-size: 0.875rem; + line-height: 1.6; + color: var(--berth-fg-2); +} +.berth-ref-desc > :first-child { + margin-top: 0; +} +a.berth-ref-anchor { + position: absolute; + top: 10px; + right: 10px; + display: grid; + place-items: center; + width: 24px; + height: 24px; + border-radius: 6px; + font: 500 13px / 1 var(--font-mono); + color: var(--muted-foreground); + text-decoration: none; + opacity: 0; + transition: + opacity 0.15s, + background-color 0.15s, + color 0.15s; +} +.berth-ref-item:hover a.berth-ref-anchor, +a.berth-ref-anchor:focus-visible, +.berth-ref-item:target a.berth-ref-anchor { + opacity: 1; +} +a.berth-ref-anchor:hover { + color: var(--foreground); + background: var(--accent); +} +@media (hover: none) { + a.berth-ref-anchor { + opacity: 0.6; + } +} +.berth-ref-item:target, +table.berth-fields tr:target > td { + background: var(--accent); +} +table.berth-fields tr { + scroll-margin-top: 7rem; +} +a.berth-field-link { + text-decoration: none; + font-weight: inherit; +} +a.berth-field-link:hover code { + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--foreground) 40%, transparent); + text-underline-offset: 3px; +} +#nd-page .berth-stage-text figure.shiki pre { + width: auto; + min-width: 0; + padding-right: 0; + white-space: pre-wrap; +} +#nd-page .berth-stage-text figure.shiki .line { + white-space: pre-wrap; + overflow-wrap: anywhere; + padding-left: calc(var(--padding-left) + 2ch); + text-indent: -2ch; +} +#nd-page .berth-stage-text figure.shiki .line > span { + display: inline-block; + max-width: 100%; + text-indent: 0; +} +#nd-page .berth-stage-text figure.shiki .line > span:only-child { + display: inline; +} +.berth-shot { + margin: 1.5rem 0 1.75rem; +} +.berth-shot img { + display: block; + width: 100%; + height: auto; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--berth-code-bg); +} +.berth-shot-light, +.berth-shot-dark { + display: block; +} +.berth-shot-dark, +.dark .berth-shot-light { + display: none; +} +.dark .berth-shot-dark { + display: block; +} +.berth-shot-caption { + margin: 10px 2px 0; + max-width: 68ch; + font-size: 13.5px; + line-height: 1.55; + color: var(--muted-foreground); +} diff --git a/apps/shipyard/app/docs/[[...slug]]/page.tsx b/apps/shipyard/app/docs/[[...slug]]/page.tsx new file mode 100644 index 000000000..677b35f55 --- /dev/null +++ b/apps/shipyard/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,102 @@ +import { Card, CardFrame, CardPanel } from "@coss/ui/components/card"; +import { ScrollArea } from "@coss/ui/components/scroll-area"; +import { notFound } from "next/navigation"; +import { DocsCopyPage } from "@/components/docs-copy-page"; +import { DocsTableOfContents } from "@/components/docs-toc"; +import { SiteFooter } from "@/components/site-footer"; +import { source } from "@/lib/source"; +import { mdxComponents } from "@/mdx-components"; + +export const revalidate = false; +export const dynamic = "force-static"; +export const dynamicParams = false; + +export function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: { + params: Promise<{ slug?: string[] }>; +}) { + const params = await props.params; + const page = source.getPage(params.slug); + + if (!page) { + notFound(); + } + + const doc = page.data; + + if (!doc.title || !doc.description) { + notFound(); + } + + return { + description: doc.description, + title: `${doc.title} - Shipyard docs`, + }; +} + +export default async function Page(props: { + params: Promise<{ slug?: string[] }>; +}) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) { + notFound(); + } + + const doc = page.data; + const rawContent = await page.data.getText("raw"); + const MDX = doc.body; + + return ( +
+
+ + + +
+
+
+
+

+ {doc.title} +

+ {doc.description && ( +

+ {doc.description} +

+ )} +
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ {doc.toc?.length ? : null} +
+ +
+
+ ); +} diff --git a/apps/shipyard/app/docs/layout.tsx b/apps/shipyard/app/docs/layout.tsx new file mode 100644 index 000000000..0da66e977 --- /dev/null +++ b/apps/shipyard/app/docs/layout.tsx @@ -0,0 +1,18 @@ +import { SidebarProvider } from "@coss/ui/components/sidebar"; +import { DocsSidebar } from "@/components/docs-sidebar"; +import { source } from "@/lib/source"; + +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + +
{children}
+
+
+ ); +} diff --git a/apps/shipyard/app/globals.css b/apps/shipyard/app/globals.css new file mode 100644 index 000000000..a2ca26cce --- /dev/null +++ b/apps/shipyard/app/globals.css @@ -0,0 +1,12 @@ +@import "@coss/ui/globals.css"; +@import "./landing.css"; +@import "./docs.css"; +@import "../components/docs/diagram.css"; +@import "../components/docs/scenes.css"; + +@layer base { + a:active, + button:active { + @apply opacity-72 md:opacity-100; + } +} diff --git a/apps/shipyard/app/icon.svg b/apps/shipyard/app/icon.svg new file mode 100644 index 000000000..df2be7ab9 --- /dev/null +++ b/apps/shipyard/app/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/shipyard/app/landing.css b/apps/shipyard/app/landing.css new file mode 100644 index 000000000..4d834ceeb --- /dev/null +++ b/apps/shipyard/app/landing.css @@ -0,0 +1,766 @@ +/* Shipyard's landing page, as it was at berthd.app (the repository's + site/styles.css), scoped to .shipyard-landing under coss.com's layout and + header. Its own type (Inter, JetBrains Mono) and tokens (Shipyard Light + and Shipyard Dark, by the site's theme class) and coss-ui's buttons. The + harbour at the top is the only picture that isn't a screenshot. The few + url()s here are under the site's base path (next.config.ts). */ + +.shipyard-landing { + --bg: #ffffff; + --card: #ffffff; + --muted-bg: #f4f4f6; + --border: #e6e6ea; + --input: #dcdce1; + --fg: #26272b; + --fg-2: #45474e; + --muted: #62646d; + --primary: #26272b; + --primary-fg: #ffffff; + --amber: #c27a12; + --ring: #8b8d96; + --sea: #eef1f6; + --blue: #3b82f6; + --green: #16a34a; + --shadow: + 0 1px 2px rgb(20 22 30 / 0.04), 0 24px 60px -28px rgb(20 22 30 / 0.28); + --sans: + var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", + sans-serif; + --mono: var(--font-jetbrains-mono), ui-monospace, "SF Mono", Menlo, monospace; + --band: clamp(340px, 58vh, 600px); + color-scheme: light; +} +/* Night: the system's dark mode, unless the page was put to day; or the + page put to night by "Close the laptop". */ +.dark .shipyard-landing { + /* coss.com's dark ground, so the page sits flush under its header. */ + --bg: #141414; + --card: #1f2024; + --muted-bg: #222327; + --border: #2c2d33; + --input: #34353c; + --fg: #e3e4e8; + --fg-2: #c0c2c9; + --muted: #9a9ca5; + --primary: #e3e4e8; + --primary-fg: #18191c; + --amber: #e8a33d; + --ring: #6b6d76; + --sea: #131824; + --green: #4ade80; + --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 30px 80px -30px rgb(0 0 0 / 0.75); + color-scheme: dark; +} +.shipyard-landing { + position: relative; + margin: 0; + background: var(--bg); + color: var(--fg); + font: 15px / 1.6 var(--sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} +.shipyard-landing img, +.shipyard-landing svg, +.shipyard-landing canvas { + display: block; +} +.shipyard-landing a { + color: inherit; +} +.shipyard-landing :focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + border-radius: 6px; +} +.shipyard-landing h1, +.shipyard-landing h2 { + margin: 0; + font-weight: 600; + letter-spacing: -0.022em; + text-wrap: balance; +} +.shipyard-landing p { + margin: 0; +} +.shipyard-landing code { + font-family: var(--mono); +} +.shipyard-landing .wrap { + width: min(1040px, 100% - 32px); + margin-inline: auto; +} +/* coss-ui buttons: default (primary) and outline, size default. */ +.shipyard-landing .btn { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 14px; + border: 1px solid var(--input); + border-radius: 10px; + background: var(--card); + color: var(--fg); + font: 500 14px / 1 var(--sans); + text-decoration: none; + white-space: nowrap; + box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); + transition: + background-color 0.15s, + border-color 0.15s; +} +.shipyard-landing .btn:hover { + background: var(--muted-bg); +} +.shipyard-landing .btn.primary { + border-color: var(--primary); + background: var(--primary); + color: var(--primary-fg); + box-shadow: + inset 0 1px rgb(255 255 255 / 0.16), + 0 1px 2px rgb(0 0 0 / 0.12); +} +.shipyard-landing .btn.primary:hover { + opacity: 0.9; +} +.shipyard-landing .btn svg { + width: 16px; + height: 16px; + margin-inline: -2px; +} +.shipyard-landing .actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} +.shipyard-landing .site-header-inner { + display: flex; + align-items: center; + gap: 8px; + height: 100%; + max-width: 1416px; + margin: 0 auto; + padding: 0 16px; +} +/* coss-ui's ghost button, size sm. */ +/* A hairline between the page's links and the repository's. */ +/* The theme toggle is an icon, as on coss.com; its wording is for readers. */ +@media (min-width: 640px) { + .shipyard-landing .site-header-inner { + padding: 0 24px; + } +} +.shipyard-landing .rule { + pointer-events: none; + max-width: 1416px; + margin: 0 auto; +} +.shipyard-landing .rule { + position: relative; + z-index: 7; + height: 0; + --mark-top: -3.5px; +} +.shipyard-landing .marks::before, +.shipyard-landing .marks::after { + content: ""; + position: absolute; + top: var(--mark-top); + width: 8px; + height: 8px; + border: 1px solid var(--border); + border-radius: 2px; + background: var(--bg); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); +} +.shipyard-landing .marks::before { + left: -15.5px; +} +.shipyard-landing .marks::after { + right: -15.5px; +} +/* Narrower than the header's width plus the rails, they'd sit at the window's + edge or past it. */ +@media (max-width: 1441px) { + .shipyard-landing .marks::before, + .shipyard-landing .marks::after { + display: none; + } +} +.shipyard-landing .chapter.ruled { + position: relative; +} +.shipyard-landing .chapter.ruled::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 1px; + background: color-mix(in srgb, var(--border) 64%, transparent); +} +/* Hero: the harbour band, then the pitch at its foot, where it dissolves. */ +.shipyard-landing .hero { + position: relative; + /* The whole page: the harbour and what's under it fill the window. */ + min-height: 100svh; + padding-bottom: 48px; +} +.shipyard-landing .harbour { + position: relative; + height: var(--band); + overflow: hidden; +} +.shipyard-landing .harbour picture, +.shipyard-landing .harbour img { + width: 100%; + height: 100%; +} +.shipyard-landing .harbour img { + object-fit: cover; + object-position: 50% 42%; + -webkit-mask-image: linear-gradient(black 50%, transparent); + mask-image: linear-gradient(black 50%, transparent); + transition: opacity 0.4s; +} +.shipyard-landing .harbour canvas { + position: absolute; + inset: 0; + image-rendering: pixelated; + opacity: 0; + transition: opacity 0.4s; +} +.shipyard-landing .harbour.drawn canvas { + opacity: 1; +} +.shipyard-landing .harbour.drawn img { + opacity: 0; +} +/* The loop, at night: over the drawn harbour in the same box (so nothing + moves), dissolving through the same dots (a mask from assets/hero-loop.js). + It brings its own beam, so the CSS one rests. */ +.shipyard-landing .loop { + position: absolute; + inset: 0; + opacity: 0; + transition: opacity 0.8s; + -webkit-mask: + var(--mask, linear-gradient(black 50%, transparent)) 0 0 / 100% 100% + no-repeat; + mask: + var(--mask, linear-gradient(black 50%, transparent)) 0 0 / 100% 100% + no-repeat; +} +/* The video itself is a pixel, under the canvas its frames are drawn on. */ +.shipyard-landing .loop video { + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 1px; +} +.shipyard-landing .loop canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} +.shipyard-landing .harbour.live .loop { + opacity: 1; +} +.shipyard-landing .harbour.live > canvas { + transition-delay: 0.8s; +} +.shipyard-landing .harbour.live > canvas, +.shipyard-landing .harbour.live .beam { + opacity: 0 !important; +} +.shipyard-landing .harbour .loop canvas { + opacity: 1; + image-rendering: auto; + transition: none; +} +.shipyard-landing .hero-copy { + position: relative; + width: min(720px, 100% - 32px); + margin: calc(var(--band) * -0.3) auto 0; + text-align: center; +} + +.shipyard-landing .hero h1 { + font-size: clamp(32px, 5.2vw, 54px); + line-height: 1.06; + text-shadow: + 0 0 10px var(--bg), + 0 0 24px var(--bg); +} +.shipyard-landing .lede { + max-width: 560px; + margin: 16px auto 24px; + color: var(--fg-2); + font-size: 17px; + text-shadow: 0 0 8px var(--bg); + text-wrap: balance; +} +.shipyard-landing .install { + display: flex; + align-items: center; + gap: 10px; + width: fit-content; + max-width: 100%; + margin: 18px auto 0; + padding: 4px 4px 4px 12px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--muted-bg); + text-align: left; +} +.shipyard-landing .install-label { + flex: none; + color: var(--muted); + font-size: 12px; +} +.shipyard-landing .install code { + overflow-x: auto; + font-size: 13px; + white-space: nowrap; + scrollbar-width: none; +} +.shipyard-landing .copy { + flex: none; + height: 28px; + padding: 0 10px; + border: 1px solid var(--input); + border-radius: 8px; + background: var(--card); + color: var(--fg); + font: 500 12.5px var(--sans); + cursor: pointer; +} +.shipyard-landing .copy:hover { + background: var(--bg); +} +.shipyard-landing .note { + margin-top: 14px; + color: var(--muted); + font-size: 13px; +} +.shipyard-landing .note a { + display: inline-flex; + align-items: center; + gap: 2px; + color: var(--fg-2); + font-weight: 500; + text-decoration: none; +} +.shipyard-landing .note a:hover { + color: var(--fg); +} +.shipyard-landing .note svg { + width: 13px; + height: 13px; +} +/* The Linux app is an alpha: a quiet line under the Mac download. */ +.shipyard-landing .note.linux-alpha { + margin-top: 4px; + font-size: 12px; +} +.shipyard-landing .note.linux-alpha a { + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: var(--muted); +} +.shipyard-landing .kicker { + display: flex; + align-items: center; + gap: 10px; + color: var(--muted); + font: 500 13px / 1 var(--mono); +} +/* A segmented switch over several views. */ +.shipyard-landing .seg { + display: flex; + width: fit-content; + margin: 0 auto 18px; + padding: 3px; + border: 1px solid var(--border); + border-radius: 11px; + background: var(--muted-bg); +} +.shipyard-landing .seg button { + height: 30px; + padding: 0 14px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--muted); + font: 500 13.5px var(--sans); + cursor: pointer; +} +.shipyard-landing .seg button:hover { + color: var(--fg); +} +.shipyard-landing .seg button[aria-pressed="true"] { + background: var(--card); + color: var(--fg); + box-shadow: + 0 1px 2px rgb(0 0 0 / 0.08), + 0 0 0 1px var(--border); +} +.shipyard-landing .stack { + display: grid; +} +.shipyard-landing .stack > [aria-hidden="true"] { + opacity: 0; + visibility: hidden; + transition: + opacity 0.3s, + visibility 0s 0.3s; +} +.shipyard-landing .sr { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +@media (max-width: 640px) { + .shipyard-landing { + --band: clamp(300px, 52vh, 420px); + } + .shipyard-landing .hero-copy { + margin-top: calc(var(--band) * -0.22); + } + .shipyard-landing .harbour img { + object-position: 8% 42%; + } + .shipyard-landing .lede { + font-size: 16px; + } + .shipyard-landing .install-label { + display: none; + } + /* The install section is a scroll away, and the width is for the brand. */ +} +@media (prefers-reduced-motion: reduce) { + .shipyard-landing .harbour canvas, + .shipyard-landing .harbour img { + transition: none; + } +} +/* The lighthouse's beam: a wedge of halftone dots from the lamp, sweeping + slowly. Only transform moves; it rests when the page is hidden and for + reduced motion. Brighter at night. */ +.shipyard-landing .beam { + position: absolute; + z-index: 1; + width: min(70vw, 900px); + height: 220px; + margin-top: -110px; + transform-origin: 0 50%; + background: linear-gradient( + to right, + rgb(255 236 190 / 0.9), + rgb(255 236 190 / 0.35) 40%, + transparent 85% + ); + clip-path: polygon(0 48%, 100% 0, 100% 100%, 0 52%); + -webkit-mask: + radial-gradient(circle at 1px 1px, #000 0.8px, transparent 1px) 0 0 / 3px + 3px; + mask: + radial-gradient(circle at 1px 1px, #000 0.8px, transparent 1px) 0 0 / 3px + 3px; + opacity: 0; + pointer-events: none; + animation: sweep 11s ease-in-out infinite alternate; + transition: opacity 0.8s; +} +.shipyard-landing .harbour.drawn .beam { + opacity: 0.32; +} +.dark .shipyard-landing .harbour.drawn .beam { + opacity: 0.85; +} +@keyframes sweep { + from { + transform: rotate(-14deg); + } + to { + transform: rotate(10deg); + } +} +.shipyard-landing[data-page-hidden] .beam { + animation-play-state: paused; +} +/* "Close the laptop": puts the page to night, and says what keeps going. */ +/* Until the script shows it, the switch keeps its place, so the header + never moves. */ +@keyframes pulse { + 50% { + opacity: 0.35; + } +} +@media (prefers-reduced-motion: reduce) { + .shipyard-landing .beam { + animation: none; + transform: rotate(-4deg); + } +} + +@keyframes film-in { + from { + opacity: 0; + transform: translateY(8px) scale(0.985); + } +} + +@media (prefers-reduced-motion: reduce) { + .shipyard-landing .loop, + .shipyard-landing .harbour.live > canvas { + transition: none; + } +} +/* Below the harbour: the film's story in six chapters. Each opens with one + of the app's line scenes (assets/scenes.css), its one amber light, then the + film's caption and a few words, and a short loop of the real app. */ +.shipyard-landing .chapter { + padding-top: clamp(72px, 10vw, 128px); +} +.shipyard-landing .chapter .wrap { + width: min(1120px, 100% - 32px); +} +/* The hero already fills the window; the first chapter follows closer. */ +.shipyard-landing #start { + padding-top: clamp(40px, 6vw, 72px); +} +.shipyard-landing .chapter-head { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); + gap: 14px 64px; + align-items: end; + margin-bottom: clamp(28px, 4vw, 40px); +} +.shipyard-landing .chapter-mark { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 16px; + /* Room for what rises above the scene's box: a mast, a beam, a sun. */ + padding-top: 14px; + margin-bottom: 6px; +} +.shipyard-landing .chapter-mark.centered { + flex-direction: column; + gap: 10px; + margin-bottom: 18px; +} +/* A line scene's slot, its size kept until the drawing arrives. */ +.shipyard-landing .scene { + --muted-foreground: var(--muted); + --warning: var(--amber); + flex: none; + display: block; + width: 144px; + aspect-ratio: 160 / 52; +} +.shipyard-landing .scene.dawn { + width: 208px; +} +.shipyard-landing .scene svg { + width: 100%; + height: auto; +} +.shipyard-landing .chapter h2 { + font-size: clamp(30px, 4.2vw, 48px); + line-height: 1.05; +} +.shipyard-landing .chapter-head > p { + max-width: 470px; + color: var(--fg-2); + font-size: 16.5px; + text-wrap: pretty; +} +/* A loop: its poster sizes the frame; the video fades in over it once it + plays. The footage is the app in Shipyard Dark, so the frame is dark too. */ +.shipyard-landing .clip { + position: relative; + margin: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 14px; + background: #1d1e22; + box-shadow: var(--shadow); +} +.shipyard-landing .clip img { + width: 100%; + height: auto; +} +/* A poster not asked for yet keeps its frame, and shows no alt text. */ +.shipyard-landing .clip img[data-src] { + visibility: hidden; +} +.shipyard-landing .clip video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; + transition: opacity 0.4s; +} +.shipyard-landing .clip.playing video { + opacity: 1; +} +.shipyard-landing .views .seg { + margin-bottom: 16px; +} +.shipyard-landing .stack > .clip { + grid-area: 1 / 1; + transition: opacity 0.3s; +} +/* It keeps going: the night comes in through the harbour's dots, whatever + the theme, and goes out the same way. */ +.shipyard-landing .chapter.after-dark { + padding-top: clamp(56px, 7vw, 96px); +} +.shipyard-landing .night-in, +.shipyard-landing .night-out { + height: 64px; + background: #18191c; + -webkit-mask: url("/shipyard/assets/seam.svg") 0 0 / 16px 64px repeat-x; + mask: url("/shipyard/assets/seam.svg") 0 0 / 16px 64px repeat-x; +} +.shipyard-landing .night-out { + transform: scaleY(-1); +} +.shipyard-landing .night-body { + --bg: #18191c; + --card: #1f2024; + --muted-bg: #222327; + --border: #2c2d33; + --input: #34353c; + --fg: #e3e4e8; + --fg-2: #c0c2c9; + --muted: #9a9ca5; + --amber: #e8a33d; + --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 30px 80px -30px rgb(0 0 0 / 0.75); + padding: clamp(40px, 6vw, 72px) 0 clamp(48px, 7vw, 88px); + background: var(--bg); + color: var(--fg); + color-scheme: dark; +} +.shipyard-landing .night-body .clip { + border-color: #2c2d33; +} +.shipyard-landing .boxes { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px 10px; + margin-top: 18px; + color: var(--fg-2); + font: 500 12.5px / 1 var(--mono); +} +.shipyard-landing .boxes span { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 12px 7px 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--card); +} +.shipyard-landing .boxes i { + width: 7px; + height: 7px; + border-radius: 50%; + background: #4ade80; + box-shadow: 0 0 8px rgb(74 222 128 / 0.6); + animation: pulse 2.6s ease-in-out infinite; +} +.shipyard-landing .boxes span + span i { + animation-delay: -1.3s; +} +.shipyard-landing[data-page-hidden] .boxes i { + animation-play-state: paused; +} +/* Install: the end, at dawn. */ +.shipyard-landing .install-ch { + padding-bottom: clamp(88px, 11vw, 136px); + text-align: center; +} +.shipyard-landing .lede-2 { + max-width: 460px; + margin: 14px auto 26px; + color: var(--fg-2); + font-size: 17px; + text-wrap: balance; +} +.shipyard-landing .ends { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px 24px; + margin-top: 26px; + font-size: 14.5px; +} +.shipyard-landing .ends a { + display: inline-flex; + align-items: center; + gap: 2px; + color: var(--fg-2); + font-weight: 500; + text-decoration: none; +} +.shipyard-landing .ends a:hover { + color: var(--fg); +} +.shipyard-landing .ends svg { + width: 14px; + height: 14px; +} +@media (max-width: 760px) { + .shipyard-landing .chapter-head { + grid-template-columns: 1fr; + gap: 12px; + } + .shipyard-landing .scene { + width: 120px; + } + .shipyard-landing .scene.dawn { + width: 168px; + } + /* On a phone, the frame closes in on the app's main pane. */ + .shipyard-landing .clip img, + .shipyard-landing .clip video { + aspect-ratio: 4 / 3; + height: auto; + object-fit: cover; + object-position: 64% 40%; + } + .shipyard-landing .clip video { + height: 100%; + } + .shipyard-landing .views .seg button { + padding: 0 10px; + font-size: 13px; + } +} +@media (prefers-reduced-motion: reduce) { + .shipyard-landing .boxes i { + animation: none; + } + .shipyard-landing .clip video, + .shipyard-landing .stack > .clip { + transition: none; + } +} +@media (max-width: 760px) { + .shipyard-landing .night-body .clip img, + .shipyard-landing .night-body .clip video { + object-position: 12% 50%; + } +} +/* A phone's header: the brand a touch smaller, and the links as icons + (styles for 640px and under, above). */ diff --git a/apps/shipyard/app/layout.tsx b/apps/shipyard/app/layout.tsx new file mode 100644 index 000000000..d5c721fd0 --- /dev/null +++ b/apps/shipyard/app/layout.tsx @@ -0,0 +1,53 @@ +import "./globals.css"; + +import { fontMono, fontSans } from "@coss/ui/fonts"; +import { ThemeProvider } from "@coss/ui/shared/theme-provider"; +import type { Metadata } from "next"; +import { SiteHeader } from "@/components/site-header"; +import { appConfig } from "@/lib/config"; +import { inter, jetbrainsMono } from "@/lib/fonts"; + +export const metadata: Metadata = { + description: appConfig.description, + metadataBase: new URL("https://coss.com"), + openGraph: { + description: appConfig.description, + siteName: "Shipyard", + title: "Shipyard - coding agents on your own boxes", + type: "website", + url: appConfig.url, + }, + title: { + default: "Shipyard - coding agents on your own boxes", + template: "%s - Shipyard", + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + +
+