From 3b6b2ec359a08e7e8b8cdc9471f27eee52dbd497 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 27 Sep 2026 18:12:13 -0400 Subject: [PATCH 1/9] =?UTF-8?q?Make=20Slopcamera=E2=80=99s=20public=20site?= =?UTF-8?q?=20quiet=20and=20keep=20phone=20navigation=20available?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/DESIGN.md | 27 +++++ apps/web/bun.lock | 8 +- apps/web/package.json | 4 +- apps/web/scripts/site-contract.test.ts | 4 +- apps/web/scripts/site-contract.ts | 4 +- apps/web/scripts/site-template.test.ts | 3 +- apps/web/scripts/verify-site-current.mjs | 2 +- apps/web/src/example-gallery.css | 13 +-- apps/web/src/index.html | 108 ++++++++---------- apps/web/src/site-content.ts | 2 - apps/web/src/site-shell.stylex.ts | 16 ++- apps/web/src/styles.css | 33 ++---- apps/web/src/theme.ts | 4 +- .../lantern-material/lantern-material.css | 43 ++----- .../vendor/lantern-material/provenance.json | 4 +- .../product-marketing-preset.css | 107 +++++++---------- .../vendor/marketing-preset/provenance.json | 4 +- apps/web/vendor/paper-theme/paper-theme.css | 2 +- apps/web/vendor/paper-theme/provenance.json | 4 +- bun.lock | 8 +- 20 files changed, 176 insertions(+), 224 deletions(-) create mode 100644 apps/web/DESIGN.md diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md new file mode 100644 index 00000000..5997bc9c --- /dev/null +++ b/apps/web/DESIGN.md @@ -0,0 +1,27 @@ +# Slopcamera website + +The public site introduces a media tool to coding-agent users. Its first proof +is an actual rendered example with editable source, followed by a small +gallery, workflow, interfaces, installation, and limits. Documentation keeps +its existing tutorial, how-to, reference, and explanation organization. + +## Visual system + +Use the shared Catppuccin light/dark palette and Nebula Sans headings, with +monospace reserved for code. Design-kit v0.23.0 and its exact marketing, paper, +and Lantern snapshots supply flat backgrounds and thin rules. The gallery +keeps real media, with no decorative card chrome or floating hero artwork. +Interface examples use ruled rows; technique links remain plain and touch-sized. +The shared v0.20.0 footer follows the document and aligns with the 72rem column. + +## Intentional differences for native review + +The homepage loses its hero light field and contour pattern. Headings become +sans, backgrounds opaque, and shadows disappear. The existing media players, +copy controls, appearance control, keyboard focus, docs navigation, preview, +privacy restrictions, and release-derived install commands retain their behavior. +The ordinary header is explicitly in scope: phone navigation exposes all links +in a scrollable second row, with 44px targets, instead of hiding destinations. +Review 360px, 390px, and 1440px layouts in light/dark plus the existing +JavaScript-disabled and forced-color cases. Historical migration comparisons +retain their original inputs; this work claims current-design acceptance. diff --git a/apps/web/bun.lock b/apps/web/bun.lock index d53be42c..f6993fc3 100644 --- a/apps/web/bun.lock +++ b/apps/web/bun.lock @@ -5,10 +5,10 @@ "": { "name": "@hraness/slopcamera-web", "dependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.22.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.23.0", "@hraness/design-kit-articles": "github:hraness/design-kit#v0.21.0", "@hraness/design-kit-status": "github:hraness/design-kit#v0.21.0", - "@hraness/site-footer": "github:hraness/site-footer#v0.19.3", + "@hraness/site-footer": "github:hraness/site-footer#v0.20.0", "@hraness/ui": "github:hraness/ui#v0.5.16", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", "@resvg/resvg-js": "2.6.2", @@ -71,7 +71,7 @@ "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#985ead3", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-985ead3", "sha512-Na5TTU+iDxDR2GwhNwGhAYn0j8d/Bz3Shhhvp/Jqy/hdxRXShvHlnoh7uYqPpqBIVWtGxUwht9unQaSAZ4NtGQ=="], + "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#585fba2", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-585fba2", "sha512-VlxOg8iAQ59LzbkBnt1EB1ikiu8jEuxku9yK+skfuozYtFFxEq9/JHE1xiyYHzSR0EmNCMKt0AGJjpuTIief9w=="], "@hraness/design-kit-articles": ["@hraness/design-kit@github:hraness/design-kit#4c3f7c9", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-4c3f7c9", "sha512-7SfYsa39IMWkb67vXWBK5xJ5l7nrotVERD/KnhogvJObxLBfvxLLl6qBY9Q6QK0uxBE5LTzRT/huciboQf91nw=="], @@ -79,7 +79,7 @@ "@hraness/direct": ["@hraness/direct@github:hraness/direct#f19d0fd", { "peerDependencies": { "@antithesishq/bombadil": "0.7.2", "agent-browser": "0.32.3", "effect": "3.22.1", "react": ">=18 <20" }, "optionalPeers": ["@antithesishq/bombadil", "agent-browser", "effect", "react"] }, "hraness-direct-f19d0fd", "sha512-rRzriJYSuIobCJFRWNK2Ye4esZYRkfyamsQpIndWf/8Wa0G16f7MQ/bmKr5XFhOYx6MLc8xzkRgfqZdZ8mTAxQ=="], - "@hraness/site-footer": ["@hraness/site-footer@github:hraness/site-footer#4f38057", { "peerDependencies": { "react": ">=18 <20" }, "optionalPeers": ["react"] }, "hraness-site-footer-4f38057", "sha512-bFOLkhPmTpkESiDhmJhAPVIjLZHoaCSbD9BEW2D1h676n2J2H123wATpI5nVUUvZw7/mCVSy6hpUN6jq0N6lQQ=="], + "@hraness/site-footer": ["@hraness/site-footer@github:hraness/site-footer#4244dc5", { "peerDependencies": { "react": ">=18 <20" }, "optionalPeers": ["react"] }, "hraness-site-footer-4244dc5", "sha512-5F4KwQ0ZrjwQjHNDAkBuhI5U7d7+gPMSluyuP0ZHkKdpahbomcxr4UtmQVp7ZJcTHuodHyaRKkIz9SK5y82mOQ=="], "@hraness/ui": ["@hraness/ui@github:hraness/ui#fa6df70", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@hugeicons/react": "^1.1.9", "@stylexjs/stylex": "0.19.0", "clsx": "^2.1.1", "parse5": "8.0.1", "react-aria": "3.50.0", "react-aria-components": "1.19.0" }, "peerDependencies": { "@babel/core": "7.29.7", "@stylexjs/babel-plugin": "0.19.0", "@types/babel__core": "7.20.5", "@types/bun": "1.3.14", "@types/node": "^20.19.0 || >=22.12.0", "lightningcss": "1.33.0", "next": "16.2.12 || 16.3.3", "react": ">=18 <20", "react-dom": ">=18 <20", "typescript": "6.0.3", "vite": "7.3.6 || 8.2.1" }, "optionalPeers": ["@babel/core", "@stylexjs/babel-plugin", "@types/babel__core", "@types/bun", "@types/node", "lightningcss", "next", "typescript", "vite"] }, "hraness-ui-fa6df70", "sha512-EW2h6MeyImS8ESP2eIaT/29WdnpY3I60XRTqObi/0bbSDW2TLbBCl3GmpdbpA72ob7InsJ54bwNZeFisQmjKqg=="], diff --git a/apps/web/package.json b/apps/web/package.json index acea4aa9..96af3716 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,10 +6,10 @@ "type": "module", "packageManager": "bun@1.3.14", "dependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.22.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.23.0", "@hraness/design-kit-articles": "github:hraness/design-kit#v0.21.0", "@hraness/design-kit-status": "github:hraness/design-kit#v0.21.0", - "@hraness/site-footer": "github:hraness/site-footer#v0.19.3", + "@hraness/site-footer": "github:hraness/site-footer#v0.20.0", "@hraness/ui": "github:hraness/ui#v0.5.16", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", "@resvg/resvg-js": "2.6.2", diff --git a/apps/web/scripts/site-contract.test.ts b/apps/web/scripts/site-contract.test.ts index 660708e5..16ebad56 100644 --- a/apps/web/scripts/site-contract.test.ts +++ b/apps/web/scripts/site-contract.test.ts @@ -37,8 +37,8 @@ function completeFixture() { const finalCss = artifact(`assets/site-${digest}.css`, ".fixture{display:grid}") // Deliberately make canonical identity order differ from package name order. const packages = [ - { manifestSha256: "8".repeat(64), name: "@hraness/design-kit", version: "0.22.0" }, - { manifestSha256: "e".repeat(64), name: "@hraness/site-footer", version: "0.19.3" }, + { manifestSha256: "8".repeat(64), name: "@hraness/design-kit", version: "0.23.0" }, + { manifestSha256: "e".repeat(64), name: "@hraness/site-footer", version: "0.20.0" }, { manifestSha256: "a".repeat(64), name: "@hraness/ui", version: "0.5.16" }, ] const complete = { diff --git a/apps/web/scripts/site-contract.ts b/apps/web/scripts/site-contract.ts index d22c5fe0..6583b2e3 100644 --- a/apps/web/scripts/site-contract.ts +++ b/apps/web/scripts/site-contract.ts @@ -15,8 +15,8 @@ export const siteSha256 = (value: string | Uint8Array): string => createHash("sh const foundationRoot = "graphs/site-foundation/" const maxArtifactBytes = 16 * 1024 * 1024 const releases = [ - { name: "@hraness/design-kit", version: "0.22.0" }, - { name: "@hraness/site-footer", version: "0.19.3" }, + { name: "@hraness/design-kit", version: "0.23.0" }, + { name: "@hraness/site-footer", version: "0.20.0" }, { name: "@hraness/ui", version: "0.5.16" }, ] as const diff --git a/apps/web/scripts/site-template.test.ts b/apps/web/scripts/site-template.test.ts index 23a1ff9f..02b75e42 100644 --- a/apps/web/scripts/site-template.test.ts +++ b/apps/web/scripts/site-template.test.ts @@ -144,7 +144,8 @@ describe("ordinary shell authored contract (pure, process-free)", () => { } expect(recipes).toContain('const tablet = "@media (max-width: 48rem)"') expect(recipes).toContain('const phone = "@media (max-width: 34rem)"') - expect(recipes).toContain('display: { default: null, [phone]: "none" }') + expect(recipes).toContain('overflowX: "auto"') + expect(recipes).not.toContain('display: { default: null, [phone]: "none" }') expect(recipes).toContain('stylex.props(shell.primaryAction, shell.navigationAction)') expect(recipes).not.toMatch(/recovery[A-Z]?\w*:/u) expect(recipes).toContain('[forcedColors]: {\n default: "CanvasText",') diff --git a/apps/web/scripts/verify-site-current.mjs b/apps/web/scripts/verify-site-current.mjs index d4a4c335..a0442faa 100644 --- a/apps/web/scripts/verify-site-current.mjs +++ b/apps/web/scripts/verify-site-current.mjs @@ -16,7 +16,7 @@ const { chromium } = createRequire(join(appDirectory, "package.json"))("playwrig export const currentRoutes = ["/", "/docs", "/docs/tutorials/first-diagram", "/docs/reference/sdk", "/blog", "/blog/introducing-slopcamera"] export const missingRoute = "/not-a-page" -export const widths = [390, 1440] +export const widths = [360, 390, 1440] export const schemes = ["light", "dark"] const types = { ".html": "text/html; charset=utf-8", ".css": "text/css", ".js": "text/javascript", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".png": "image/png", ".webp": "image/webp", ".mp4": "video/mp4", ".json": "application/json" } diff --git a/apps/web/src/example-gallery.css b/apps/web/src/example-gallery.css index a49f4e2c..c56acc42 100644 --- a/apps/web/src/example-gallery.css +++ b/apps/web/src/example-gallery.css @@ -18,11 +18,8 @@ .slopcamera-work-gallery .slopcamera-example { margin: 0; - padding: .75rem; - background: var(--hraness-material-plane); - border: 1px solid var(--hraness-material-seam); - border-radius: var(--radius-lg, .875rem); - box-shadow: var(--hraness-material-lift); + padding-block-start: 1rem; + border-block-start: 1px solid var(--hraness-material-seam); } .slopcamera-work-gallery .slopcamera-example__media { @@ -56,11 +53,9 @@ display: inline-flex; align-items: center; min-block-size: 2.75rem; - padding-inline: 1rem; - border-radius: 999px; + padding-inline: 0; text-decoration: none; - background: var(--hraness-material-plane); - box-shadow: inset 0 1px 0 var(--hraness-material-edge), 0 2px 6px var(--hraness-material-shade); + border-block-end: 1px solid var(--hraness-material-seam); } .slopcamera-techniques ul a:hover { diff --git a/apps/web/src/index.html b/apps/web/src/index.html index 0ea6fded..99ab8b2e 100644 --- a/apps/web/src/index.html +++ b/apps/web/src/index.html @@ -1,5 +1,5 @@ - + @@ -59,60 +59,30 @@
-
- {{HERO_BACKDROP}} +
-

Slopcamera

+

Media studio for coding agents

Visual work your agent can keep revising.

Images, diagrams, animation, 3D scenes, and edited video, built from source files - so a change is a re-render, not a re-roll. + so your agent can revise a scene and render it again.

-

Free and open source under the MIT license · Bun 1.3.14 or newer · Blender, CadQuery, and Manim install separately

+

Free and open source under the MIT license · Bun 1.3.14 or newer on macOS, Linux, or Windows

{{EXAMPLE_HERO}}
-
-
Outputs
Four familiesImages, diagrams, animated loops, and video
-
Interfaces
Through your agentSkill and CLI; SDK and MCP too
-
Delivery
16:9, 9:16, 1:1, 4:5Clean and captioned cuts from one edit
-
Cost
FreeMIT license; model usage is paid
-
-
-
Author
Author editable scenes, diagrams, and motion. Direct a world camera or a native educational film.
-
Compose
Combine rendered shots, generated media, and footage with world-space screens, narration, and graphics.
-
Deliver
Review, export, and retain sources and project decisions for the next revision.
-
- -
-
-

Install Slopcamera for your agent.

-

Tell your agent: “install Slopcamera and its skill.” Or run these two commands. Needs Bun 1.3.14 or newer on macOS, Linux, or Windows. Release v{{RELEASE_VERSION}}.

-
-
-

Install the CLI, then its matching Agent Skill

- {{RELEASE_INSTALL_COMMANDS}} -

Start a new agent session after installation. The Agent Skill helps your agent choose commands and check local tools.

-
- Build from source -
-

For source-only capabilities or development, follow the complete source-install guide. It records the commit, installs locked dependencies, builds the SDK and CLI, and installs the guide from that same checkout. Native engines install separately.

-
-
-
-
-
-

Examples

-

Each example links to its guide and source files.

+

Examples

+

Made by agents, from source you can open.

+

Each render below links to the guide that made it and the source files your agent would edit next.

-

Reference: .diagram.json, render profiles, scenes, raster-to-SVG, Gateway, video, engines, MCP. Use cases · Capabilities.

+

Looking for formats and limits? See the capability reference and use cases.

+

How it works

From source to a finished film.

-

You describe the result. Your agent authors scenes, edits, and diagrams as files you can both reopen.

-

Move scenes between renderers with cameras intact, or mount images and video in a 3D world. Understand scene interchange.

+

You describe the result. Your agent writes scenes, edits, and diagrams as files you can both reopen, then renders them on your machine.

  1. - -
    Prepare the sources

    Tell your agent what you have, or start from scratch. It checks your machine's tools.

    + +
    Prepare the sources

    Tell your agent what you have, or start from scratch. It checks which tools your machine has.

  2. - +
    Direct the result

    Say what should change and what must stay the same. Your agent can also generate images, video, and voice through your Vercel AI Gateway account.

  3. - -
    Compose the film

    Shots, generated media, and your footage land in one project with captions, graphics, color, and sound.

    + +
    Compose the film

    Rendered shots, generated media, and your footage land in one project with captions, graphics, color, and sound.

  4. - +
    Review before final

    Look at the same frames your agent sees. Preview and final renders use the same timeline and composition.

  5. - -
    Deliver and revise

    Get every format you need. Sources stay editable, so a later change is a small edit.

    + +
    Deliver and revise

    Export 16:9, 9:16, 1:1, and 4:5 cuts, clean or captioned, from one edit. Sources stay editable, so a later change is a small edit.

@@ -169,13 +139,13 @@

From source t
+

Interfaces

Choose how your agent works.

Start with the Agent Skill. Scripts and programs can call the CLI or SDK, and MCP clients get a fixed toolset.

-

Agent Skill

Teaches your agent which commands fit each job and what to check before it renders.

@@ -183,7 +153,6 @@

Agent Skill

-

CLI

Every studio operation as a plain command, with JSON output for scripts.

@@ -191,7 +160,6 @@

CLI

-

TypeScript SDK

The same operations in your own TypeScript, for builders wiring Slopcamera into their programs.

@@ -199,7 +167,6 @@

TypeScript SDK

-

MCP

A fixed set of 17 tools for MCP clients: check and render diagrams, inspect and plan scenes, and make images in one folder. It does not expose every CLI command.

@@ -208,24 +175,44 @@

MCP

+
+
+

Install

+

Install Slopcamera for your agent.

+

Tell your agent: “install Slopcamera and its skill.” Or run these two commands. Needs Bun 1.3.14 or newer on macOS, Linux, or Windows. Release v{{RELEASE_VERSION}}.

+
+
+

Install the CLI, then its matching Agent Skill

+ {{RELEASE_INSTALL_COMMANDS}} +

Start a new agent session after installation. The Agent Skill helps your agent choose commands and check local tools. Blender, CadQuery, and Manim install separately.

+
+ Build from source +
+

For source-only capabilities or development, follow the complete source-install guide. It records the commit, installs locked dependencies, builds the SDK and CLI, and installs the guide from that same checkout. Native engines install separately.

+
+
+
+
+
+

Your files

What stays on your machine

-

Ask the agent for the same video twice; the second take differs. Slopcamera keeps the source beside every render; a revision changes what you named, nothing else. Sources, edits, and renders stay on your own storage.

+

Slopcamera keeps editable source beside each render. Sources, edits, and local renders stay on your own storage; generation and selected cloud analysis use the services described below.

Source media stays unchanged
Original recordings and imported media stay unchanged. Cuts, framing, overlays, and effects remain project decisions.
Project state stays on your machine
There is no Slopcamera account or hosted project database. You control source, project state, previews, and final files.
Gateway credentials stay local
Local processes use your Vercel AI Gateway credential without storing or printing it. Media uploads require acknowledgement.
-
Operations leave a record
Preview and final renders share the timeline and composition. Important operations save a record of their inputs and outputs, with no secrets in it.
+
Operations leave a record
Important operations save a record of their inputs and outputs, with no secrets in it.
-
-

Before you install.

+

FAQ

+

Common questions

Why not just ask the agent directly?

You can. Each retry regenerates the take: new timing, new framing, new problems. Slopcamera keeps the editable source beside the render, so your agent changes one field and re-renders it.

@@ -242,14 +229,13 @@

Before you

-
+

Ask your agent for a first render.

Install the CLI and its Agent Skill, then describe the result you want.

-

Free and open source under the MIT license · Bun 1.3.14 or newer

diff --git a/apps/web/src/site-content.ts b/apps/web/src/site-content.ts index aa052dd8..24dc1bcf 100644 --- a/apps/web/src/site-content.ts +++ b/apps/web/src/site-content.ts @@ -1,4 +1,3 @@ -import { HeroBackdrop } from "@hraness/design-kit/react" import { renderHranessSiteFooter } from "@hraness/site-footer" import { AskAiAboutThis } from "@hraness/ui" import { createElement } from "react" @@ -198,7 +197,6 @@ export function siteContentSlots(document: SiteDocument, assets: SiteAssets): Re ] } return [...common, - ["{{HERO_BACKDROP}}", renderToStaticMarkup(createElement(HeroBackdrop, { seed: "slopcamera-creative-tools" })), 1], ["{{ASK_AI_ABOUT_THIS}}", renderAskAiAboutThis("https://slopcamera.com/"), 1], ["{{RELEASE_VERSION}}", publishedRelease.version, 1], ["{{RELEASE_URL}}", publishedRelease.releaseUrl, 1], diff --git a/apps/web/src/site-shell.stylex.ts b/apps/web/src/site-shell.stylex.ts index f8637339..a79e4613 100644 --- a/apps/web/src/site-shell.stylex.ts +++ b/apps/web/src/site-shell.stylex.ts @@ -60,6 +60,7 @@ const shell = stylex.create({ backdropFilter: "var(--hraness-material-chrome-blur, blur(14px) saturate(1.4))", }, wordmark: { + minHeight: "2.75rem", fontSize: "1rem", fontWeight: 600, letterSpacing: "-0.01em", @@ -81,7 +82,8 @@ const shell = stylex.create({ }, navigation: { display: "flex", - flexWrap: "wrap", + flexWrap: "nowrap", + overflowX: "auto", alignItems: "center", rowGap: "0.25rem", columnGap: { default: "1.35rem", [tablet]: "1rem", [phone]: "0.85rem" }, @@ -91,15 +93,18 @@ const shell = stylex.create({ marginLeft: { default: 0, [phone]: "auto" }, }, navigationLink: { + display: "inline-flex", + alignItems: "center", + minHeight: "2.75rem", + flexShrink: 0, color: { default: "var(--muted)", ":hover": "var(--ink)" }, fontSize: "0.92rem", fontWeight: 500, textDecoration: "none", }, homeNavigationLink: { - // Only the homepage has an install action. Its other links leave the - // phone row, while the 404's Home and GitHub links stay visible. - display: { default: null, [phone]: "none" }, + // Keep every destination available in the scrollable phone navigation. + display: "inline-flex", }, primaryAction: { display: "inline-flex", @@ -146,7 +151,8 @@ const shell = stylex.create({ textDecoration: "none", }, navigationAction: { - minHeight: "2.25rem", + minHeight: "2.75rem", + flexShrink: 0, paddingTop: "0.4rem", paddingRight: "0.9rem", paddingBottom: "0.4rem", diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 7892e9c5..01fc2b90 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -104,17 +104,12 @@ html[data-theme="dark"]:not([data-hraness-theme="paper"]) { html { background: var(--paper); scroll-behavior: smooth; - /* Keep focus and scroll targets above the shared fixed footer bar. */ - scroll-padding-block-end: calc(1.75rem + 2 * clamp(0.25rem, 0.8vw, 0.375rem) + env(safe-area-inset-bottom, 0px) + 1px); -} - -@media (pointer: coarse) { - html { - scroll-padding-block-end: calc(44px + 2 * clamp(0.25rem, 0.8vw, 0.375rem) + env(safe-area-inset-bottom, 0px) + 1px); - } } body { + min-height: 100dvh; + display: flex; + flex-direction: column; margin: 0; background: var(--paper); color: var(--ink); @@ -124,6 +119,9 @@ body { -webkit-font-smoothing: antialiased; } +body > main { flex: 1; min-inline-size: 0; } +#hraness-site-footer { --hraness-site-footer-measure: 72rem; } + a { color: inherit; text-decoration-thickness: 1px; @@ -338,11 +336,11 @@ h3 { align-items: start; gap: 1.5rem 2rem; min-inline-size: 0; - padding: 1.75rem; - background: var(--hraness-material-plane); - border: 1px solid var(--hraness-material-seam); - border-radius: var(--radius-lg, .875rem); - box-shadow: var(--hraness-material-lift); + padding-block: 1.75rem; + background: transparent; + border-block-start: 1px solid var(--hraness-material-seam); + border-radius: 0; + box-shadow: none; } .interface-copy { @@ -356,7 +354,7 @@ h3 { border: 1px solid var(--hraness-material-seam); border-radius: .75rem; background: color-mix(in oklch, var(--background, Canvas) 70%, var(--hraness-material-plane)); - box-shadow: var(--hraness-material-inset); + box-shadow: none; } /* Editorial display digits for the workflow steps. */ @@ -493,10 +491,3 @@ h3 { --hraness-marketing-inverse: var(--night); --hraness-marketing-inverse-ink: var(--night-ink); } - -/* Decorative topic icons (agent-generated line art, public/icons/). */ -.slopcamera-topic-icon { display: block; width: 88px; height: 88px; margin: 0 0 18px; } - -/* The shared light field sits behind focusable hero content. */ -.slopcamera-product-hero { position: relative; isolation: isolate; } -.slopcamera-product-hero > :not([data-hraness-hero-backdrop]) { position: relative; z-index: 1; } diff --git a/apps/web/src/theme.ts b/apps/web/src/theme.ts index e723c855..586854af 100644 --- a/apps/web/src/theme.ts +++ b/apps/web/src/theme.ts @@ -1,4 +1,4 @@ -import { attachFoil, attachHeroLight, installAppearanceMenus } from "@hraness/design-kit/browser" +import { attachFoil, installAppearanceMenus } from "@hraness/design-kit/browser" import { installCopyCommands } from "./copy-command" import { installExamplePlayers } from "./example-player" @@ -12,8 +12,6 @@ function installPageControls(): void { installCopyCommands() installExamplePlayers() attachFoil(document.documentElement) - const hero = document.querySelector(".slopcamera-product-hero") - if (hero) attachHeroLight(hero) } installAppearanceMenus({ diff --git a/apps/web/vendor/lantern-material/lantern-material.css b/apps/web/vendor/lantern-material/lantern-material.css index 24b83049..c81a9256 100644 --- a/apps/web/vendor/lantern-material/lantern-material.css +++ b/apps/web/vendor/lantern-material/lantern-material.css @@ -12,12 +12,11 @@ --hraness-material-muted: var(--muted, var(--ui-muted-foreground, CanvasText)); --hraness-material-seam: color-mix(in oklch, var(--hraness-material-ink) 5%, var(--hraness-material-plane)); --hraness-material-outline: transparent; - --hraness-pattern-cells: linear-gradient(145deg, rgb(255 255 255 / 0.05), transparent 48%, rgb(0 0 0 / 0.035)); - --hraness-pattern-cells-size: var(--hraness-material-module) var(--hraness-material-module); --hraness-material-edge: rgb(255 255 255 / 0.14); --hraness-material-shade: rgb(0 0 0 / 0.14); --hraness-material-warm-plane: color-mix(in oklch, var(--hraness-material-warm) 12%, var(--hraness-material-plane)); --hraness-material-rest: inset 0 1px 0 var(--hraness-material-edge), 0 2px 5px rgb(0 0 0 / 0.05), 0 10px 28px -14px rgb(0 0 0 / 0.14); + --hraness-material-raised: inset 0 1px 0 var(--hraness-material-edge), 0 1px 3px rgb(0 0 0 / 0.08); --hraness-material-inset: inset 2px 3px 7px rgb(0 0 0 / 0.08), inset -1px -1px 2px rgb(255 255 255 / 0.1); --hraness-material-lift: inset 0 1px 0 var(--hraness-material-edge), -3px -3px 10px rgb(255 255 255 / 0.04), 2px 4px 8px rgb(0 0 0 / 0.07), 8px 18px 40px rgb(0 0 0 / 0.09); --hraness-material-focus: var(--focus, var(--ui-ring, Highlight)); @@ -26,28 +25,9 @@ } @layer components.hraness-design-kit.legacy { - /* Set the pattern on the palette/material boundary, or on one field. */ - :is([data-hraness-pattern="cells"], [data-hraness-pattern="weave"], [data-hraness-pattern="contour"], [data-hraness-pattern="mesh"]) { - --hraness-material-wall-images: initial; - --hraness-pattern-decoration: initial; - } - [data-hraness-pattern="cells"] { - --hraness-pattern-image: var(--hraness-pattern-cells); - --hraness-pattern-size: var(--hraness-pattern-cells-size, 48rem 48rem); - } - [data-hraness-pattern="weave"] { - --hraness-pattern-image: repeating-conic-gradient(from 45deg, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 5%, transparent) 0% 25%, transparent 0% 50%); - --hraness-pattern-size: 1.5rem 1.5rem; - } - [data-hraness-pattern="contour"] { - --hraness-pattern-image: repeating-radial-gradient(ellipse at 24% 62%, transparent 0 2.2rem, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 9%, transparent) 2.25rem 2.3rem, transparent 2.35rem 4.4rem); - --hraness-pattern-size: 100% 100%; - } - [data-hraness-pattern="mesh"] { - --hraness-pattern-image: radial-gradient(circle at center, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 18%, transparent) 1px, transparent 1.5px); - --hraness-pattern-size: 1.5rem 1.5rem; - } - [data-hraness-pattern="none"] { + /* Patterns are retired. The attribute values stay valid for compatibility, + and every one paints the uninterrupted palette background. */ + :is([data-hraness-pattern="cells"], [data-hraness-pattern="weave"], [data-hraness-pattern="contour"], [data-hraness-pattern="mesh"], [data-hraness-pattern="none"]) { --hraness-pattern-image: none; --hraness-pattern-decoration: none; --hraness-marketing-field-images: none; @@ -101,18 +81,11 @@ } } - /* Glazing uses shaded faces rather than drawn seams, with a warm band - * of transmitted light. Its edges suggest a rounded enclosure without - * distorting the document, blurring text, or masking brand artwork. */ + /* The wall is a flat palette plane. Its retired shaded faces, grain, + * patterns, and light washes no longer paint, whatever tokens say. */ :where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-wall) { background-color: var(--background, var(--ui-background, Canvas)); - background-image: var(--hraness-material-wall-images, - var(--hraness-material-wall-grain, none), - var(--hraness-pattern-image, var(--hraness-material-wall-cells, var(--hraness-pattern-cells))), - radial-gradient(ellipse at var(--hraness-hero-light-x, 68%) var(--hraness-hero-light-y, 32%), color-mix(in srgb, var(--hraness-material-warm) 22%, transparent), transparent 68%), - linear-gradient(110deg, color-mix(in srgb, var(--hraness-material-cool) 14%, transparent), transparent 38%, rgb(255 255 255 / 0.04) 65%, transparent)); - background-size: 64px 64px, var(--hraness-pattern-size, var(--hraness-material-wall-cell-size, var(--hraness-material-module)) var(--hraness-material-wall-cell-size, var(--hraness-material-module))), 100% 100%, 100% 100%; - background-position: 0 0, center top, center top, center top; + background-image: none; color: var(--hraness-material-ink); } @@ -206,7 +179,6 @@ --hraness-material-chrome-paint: var(--surface, var(--ui-background, Canvas)); --hraness-material-chrome-blur: none; } - :where([data-hraness-material="lantern"], [data-hraness-material="lantern"] *):is(.hraness-material-wall) { background-image: none; } } @media (forced-colors: active) { [data-hraness-material="lantern"] { @@ -220,6 +192,7 @@ --hraness-material-warm-plane: Canvas; --hraness-material-warm: Highlight; --hraness-material-edge: transparent; + --hraness-material-raised: none; --hraness-material-inset: none; --hraness-material-lift: none; } diff --git a/apps/web/vendor/lantern-material/provenance.json b/apps/web/vendor/lantern-material/provenance.json index 37a9f355..87508480 100644 --- a/apps/web/vendor/lantern-material/provenance.json +++ b/apps/web/vendor/lantern-material/provenance.json @@ -3,13 +3,13 @@ "contractVersion": 1, "source": { "repository": "https://github.com/hraness/design-kit", - "commit": "d38d13c07d7956d02ddfbca8d32aa2066d88fbd3", + "commit": "3df4c411c7f5e5cbc02448463571696f0d47cee5", "export": "@hraness/design-kit/lantern-material.css" }, "files": { "lantern-material.css": { "path": "src/lantern-material.css", - "sha256": "fc456d2c7b51b37b51d6b086be90f102ca856c07849a0d7487b2e74e8981f8ac" + "sha256": "7d455c4eabdf3204f2b5cf3b0497de1f722f007ab0bd25013e9ba5691a60c863" }, "check.mjs": { "path": "scripts/check-lantern-material-snapshot.mjs", diff --git a/apps/web/vendor/marketing-preset/product-marketing-preset.css b/apps/web/vendor/marketing-preset/product-marketing-preset.css index 073a559b..5fabb8cc 100644 --- a/apps/web/vendor/marketing-preset/product-marketing-preset.css +++ b/apps/web/vendor/marketing-preset/product-marketing-preset.css @@ -1,4 +1,7 @@ -/* Opt-in marketing contract 1. Asset URLs stay relative in packages and snapshots. */ +/* Opt-in marketing contract 1, Quiet direction. Asset URLs stay relative in + packages and snapshots. Instrument Serif stays vendored for products that + opt in explicitly; no preset uses it by default, so it is never downloaded + unless a product sets it as its display face. */ @font-face { font-family: "Instrument Serif"; src: url("./fonts/instrument-serif/instrument-serif-latin-400.woff2") format("woff2"); @@ -9,38 +12,39 @@ @layer components.hraness-design-kit.legacy { :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { - --hraness-marketing-display-font: "Instrument Serif", Georgia, serif; - --hraness-marketing-h1-size: clamp(3rem, 2.1rem + 4.2vw, 5.5rem); - --hraness-marketing-h1-leading: 1.02; - --hraness-marketing-h1-tracking: -.025em; - --hraness-marketing-h2-size: clamp(2.125rem, 1.45rem + 2.6vw, 3.5rem); - --hraness-marketing-h2-leading: 1.08; - --hraness-marketing-h2-tracking: -.02em; - --hraness-marketing-display-weight: 400; + /* Quiet display role: the body sans at a semibold weight with tight + tracking, not a serif. */ + --hraness-marketing-display-font: var(--font-text, "Nebula Sans", ui-sans-serif, system-ui, sans-serif); + --hraness-marketing-display-weight: 550; + --hraness-marketing-h1-size: clamp(2.375rem, 1.55rem + 3.6vw, 4.5rem); + --hraness-marketing-h1-leading: 1.04; + --hraness-marketing-h1-tracking: -.035em; + --hraness-marketing-h2-size: clamp(1.75rem, 1.3rem + 1.9vw, 2.75rem); + --hraness-marketing-h2-leading: 1.1; + --hraness-marketing-h2-tracking: -.028em; + --hraness-marketing-h3-size: clamp(1.25rem, 1.1rem + .6vw, 1.625rem); + --hraness-marketing-h3-leading: 1.2; --hraness-marketing-content-measure: 70rem; --hraness-marketing-header-measure: 76rem; --hraness-marketing-header-height: 3.25rem; --hraness-sticky-offset: calc(var(--hraness-marketing-header-height) + 1px); --hraness-marketing-gutter: 2rem; - --hraness-marketing-hero-space: clamp(4rem, 8vw, 7rem) clamp(4.5rem, 9vw, 8rem); + --hraness-marketing-hero-space: clamp(4rem, 8vw, 7rem) clamp(3rem, 6vw, 5rem); --hraness-marketing-story-space: 5rem; - --hraness-marketing-summary-leading: 1.65; + --hraness-marketing-summary-leading: 1.6; --hraness-marketing-header-action-height: 2.25rem; --hraness-marketing-field-ink: var(--foreground, var(--ui-foreground, CanvasText)); --hraness-marketing-field-muted: color-mix(in oklch, var(--foreground, var(--ui-foreground, CanvasText)) 12%, var(--muted, var(--ui-muted-foreground, CanvasText))); --hraness-marketing-link: var(--primary, var(--ui-primary, LinkText)); - --hraness-marketing-field-gradient: radial-gradient(ellipse at var(--hraness-hero-light-x, 68%) var(--hraness-hero-light-y, 32%), color-mix(in oklch, var(--primary, var(--ui-primary, CanvasText)) 9%, var(--background, Canvas)), var(--background, Canvas) 72%); - --hraness-marketing-field-images: var(--hraness-pattern-decoration, url("./marketing-assets/grain.svg"), var(--hraness-pattern-image, url("./marketing-assets/cells.svg")), var(--hraness-marketing-field-gradient)); - --hraness-marketing-field-size: 64px 64px, var(--hraness-pattern-size, 48rem 48rem), 100% 100%; - --hraness-material-wall-grain: url("./marketing-assets/grain.svg"); - --hraness-material-wall-cells: var(--hraness-pattern-image, url("./marketing-assets/cells.svg")); - --hraness-material-wall-cell-size: calc(var(--hraness-material-module, 6rem) * 8); + /* Retired textures: the field paints the flat palette background. */ + --hraness-marketing-field-images: none; + --hraness-marketing-field-size: auto; --hraness-marketing-terminal-background: var(--surface, var(--ui-card, Canvas)); --hraness-marketing-terminal-ink: var(--foreground, var(--ui-foreground, CanvasText)); --hraness-marketing-terminal-muted: var(--muted, var(--ui-muted-foreground, CanvasText)); --hraness-marketing-terminal-command: var(--primary, var(--ui-primary, LinkText)); - --hraness-marketing-terminal-chrome: linear-gradient(to bottom, color-mix(in oklch, var(--surface, Canvas) 96%, white), var(--surface, Canvas)); - --hraness-marketing-terminal-shadow: var(--hraness-material-lift, inset 0 1px 0 rgb(255 255 255 / 0.12), 0 2px 5px rgb(0 0 0 / 0.08), 0 24px 56px -24px rgb(0 0 0 / 0.26)); + --hraness-marketing-terminal-chrome: var(--surface, var(--ui-card, Canvas)); + --hraness-marketing-terminal-shadow: 0 0 0 1px color-mix(in srgb, var(--foreground, var(--ui-foreground, CanvasText)) 12%, transparent); } [data-hraness-marketing-preset="minimal"] { @@ -52,47 +56,22 @@ --hraness-marketing-h2-size: clamp(1.5rem, 1.15rem + 1.4vw, 2.125rem); --hraness-marketing-h2-leading: 1.15; --hraness-marketing-h2-tracking: -.02em; + --hraness-marketing-h3-size: clamp(1.375rem, 1.2rem + .7vw, 1.75rem); + --hraness-marketing-h3-leading: 1.2; --hraness-marketing-header-height: 2.5rem; --hraness-marketing-hero-space: 2rem 3rem; --hraness-marketing-story-space: 3rem; --hraness-marketing-field-images: none; } - :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { - --hraness-pattern-cells: url("./marketing-assets/cells.svg"); - --hraness-pattern-cells-size: 48rem 48rem; - } - /* Set the pattern on the palette/material boundary, or on one field. */ - :is([data-hraness-pattern="cells"], [data-hraness-pattern="weave"], [data-hraness-pattern="contour"], [data-hraness-pattern="mesh"]) { - --hraness-material-wall-images: initial; - --hraness-pattern-decoration: initial; - } - [data-hraness-pattern="cells"] { - --hraness-pattern-image: var(--hraness-pattern-cells); - --hraness-pattern-size: var(--hraness-pattern-cells-size, 48rem 48rem); - } - [data-hraness-pattern="weave"] { - --hraness-pattern-image: repeating-conic-gradient(from 45deg, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 5%, transparent) 0% 25%, transparent 0% 50%); - --hraness-pattern-size: 1.5rem 1.5rem; - } - [data-hraness-pattern="contour"] { - --hraness-pattern-image: repeating-radial-gradient(ellipse at 24% 62%, transparent 0 2.2rem, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 9%, transparent) 2.25rem 2.3rem, transparent 2.35rem 4.4rem); - --hraness-pattern-size: 100% 100%; - } - [data-hraness-pattern="mesh"] { - --hraness-pattern-image: radial-gradient(circle at center, color-mix(in srgb, var(--primary, var(--ui-primary, CanvasText)) 18%, transparent) 1px, transparent 1.5px); - --hraness-pattern-size: 1.5rem 1.5rem; - } - [data-hraness-pattern="none"] { + /* Patterns are retired. The attribute values stay valid for compatibility, + and every one paints the uninterrupted palette background. */ + :is([data-hraness-pattern="cells"], [data-hraness-pattern="weave"], [data-hraness-pattern="contour"], [data-hraness-pattern="mesh"], [data-hraness-pattern="none"]) { --hraness-pattern-image: none; --hraness-pattern-decoration: none; --hraness-marketing-field-images: none; --hraness-material-wall-images: none; } - :is([data-hraness-pattern="cells"], [data-hraness-pattern="weave"], [data-hraness-pattern="contour"], [data-hraness-pattern="mesh"]) { - --hraness-marketing-field-images: url("./marketing-assets/grain.svg"), var(--hraness-pattern-image), var(--hraness-marketing-field-gradient); - --hraness-marketing-field-size: 64px 64px, var(--hraness-pattern-size), 100% 100%; - } /* Role-local bindings override the legacy role defaults as well as serving compiled recipes. Application headings outside these hooks stay untouched. */ @@ -118,8 +97,10 @@ --hraness-marketing-frame-radius: 1.125rem; --hraness-marketing-section-space: var(--hraness-marketing-story-space); --hraness-marketing-action-height: 2.625rem; - --hraness-marketing-line: color-mix(in srgb, var(--hraness-marketing-ink) 5%, transparent); - --hraness-marketing-chrome-rule: 1px solid transparent; + --hraness-marketing-line: color-mix(in srgb, var(--hraness-marketing-ink) 12%, transparent); + --hraness-marketing-chrome-rule: 1px solid var(--hraness-marketing-line); + --hraness-marketing-chrome-shadow: none; + --hraness-marketing-surface-shadow: 0 0 0 1px var(--hraness-marketing-line); --hraness-marketing-shadow: var(--hraness-marketing-terminal-shadow); --hraness-marketing-frame-chrome: var(--hraness-marketing-terminal-chrome); } @@ -172,6 +153,12 @@ :is([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) .hraness-marketing-header .hraness-marketing-action { min-block-size: var(--hraness-marketing-header-action-height); } + /* Phone headers keep 44px targets on both rows. */ + @media (max-width: 48rem) { + :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { + --hraness-marketing-header-action-height: 2.75rem; + } + } @media (pointer: coarse) { :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { --hraness-marketing-header-action-height: 3rem; @@ -185,12 +172,11 @@ padding-inline: var(--hraness-marketing-gutter); } - /* This is a background on a flow container, never an overlay on content. */ + /* The field is a flow container on the flat palette background. The retired + grain, cell, and pattern textures never paint, whatever tokens say. */ :is([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]).hraness-marketing-field, :is([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) .hraness-marketing-field { - background-image: var(--hraness-marketing-field-images); - background-position: 0 0, center top, 0 0; - background-size: var(--hraness-marketing-field-size); + background-image: none; color: var(--hraness-marketing-field-ink); position: relative; } @@ -251,24 +237,15 @@ @media (max-width: 760px) { :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { --hraness-marketing-gutter: 1.25rem; - --hraness-marketing-hero-space: 3.5rem 4.5rem; + --hraness-marketing-hero-space: 2.75rem 3rem; --hraness-marketing-story-space: 3.5rem; - --hraness-marketing-field-size: 64px 64px, var(--hraness-pattern-size, 36rem 36rem), 100% 100%; --hraness-material-module: 4.5rem; - --hraness-pattern-cells-size: 36rem 36rem; } [data-hraness-marketing-preset="minimal"] { --hraness-marketing-hero-space: 2rem 3rem; --hraness-marketing-story-space: 3rem; } } - @media (prefers-reduced-transparency: reduce) { - :is([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]).hraness-marketing-field, - :is([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) .hraness-marketing-field { - background-image: none; - background-color: var(--background, Canvas); - } - } @media (forced-colors: active) { :where([data-hraness-marketing-preset="editorial"], [data-hraness-marketing-preset="minimal"]) { --hraness-marketing-field-images: none; diff --git a/apps/web/vendor/marketing-preset/provenance.json b/apps/web/vendor/marketing-preset/provenance.json index a032cdd7..ec9a4580 100644 --- a/apps/web/vendor/marketing-preset/provenance.json +++ b/apps/web/vendor/marketing-preset/provenance.json @@ -3,13 +3,13 @@ "contractVersion": 1, "source": { "repository": "https://github.com/hraness/design-kit", - "commit": "d38d13c07d7956d02ddfbca8d32aa2066d88fbd3", + "commit": "3df4c411c7f5e5cbc02448463571696f0d47cee5", "export": "@hraness/design-kit/product-marketing-preset.css" }, "files": { "product-marketing-preset.css": { "path": "src/product-marketing-preset.css", - "sha256": "ab78b17a454385c190e36172ea660aa396cd08e7ef371e249b91342ec411044d" + "sha256": "f410fdaec2bc7b5d3316b499689b060c34cb38e66662e0ed2bbfbd7dfa70ef4e" }, "fonts/instrument-serif/instrument-serif-latin-400.woff2": { "path": "src/fonts/instrument-serif/instrument-serif-latin-400.woff2", diff --git a/apps/web/vendor/paper-theme/paper-theme.css b/apps/web/vendor/paper-theme/paper-theme.css index 653f7d86..fbd0ba0e 100644 --- a/apps/web/vendor/paper-theme/paper-theme.css +++ b/apps/web/vendor/paper-theme/paper-theme.css @@ -28,7 +28,7 @@ --ui-radius: .875rem; --control-radius: .625rem; --hraness-paper-heading-size: clamp(1.75rem, 3vw, 2.375rem); - --hraness-paper-section-heading-size: clamp(1.25rem, 2.2vw, 1.75rem); + --hraness-paper-section-heading-size: var(--hraness-type-h2-size, clamp(1.75rem, 1.45rem + .9vw, 2.125rem)); --hraness-paper-summary-size: 1rem; --hraness-paper-heading-leading: 1.15; --hraness-paper-heading-tracking: -.035em; diff --git a/apps/web/vendor/paper-theme/provenance.json b/apps/web/vendor/paper-theme/provenance.json index 21343d74..4920b5a9 100644 --- a/apps/web/vendor/paper-theme/provenance.json +++ b/apps/web/vendor/paper-theme/provenance.json @@ -3,13 +3,13 @@ "contractVersion": 1, "source": { "repository": "https://github.com/hraness/design-kit", - "commit": "d38d13c07d7956d02ddfbca8d32aa2066d88fbd3", + "commit": "3df4c411c7f5e5cbc02448463571696f0d47cee5", "export": "@hraness/design-kit/paper-theme.css" }, "files": { "paper-theme.css": { "path": "src/paper-theme.css", - "sha256": "9c2ef8f896b807de825f82c3c0e103a38931b3a94588fc54d3e3d9012a96a523" + "sha256": "fad45fb28c0bb83b552061d1d1fa66ffe1f3bd0e1532db62fa8cd9c182da5b32" }, "LICENSE": { "path": "LICENSE", diff --git a/bun.lock b/bun.lock index 8a193ce8..be5a4472 100644 --- a/bun.lock +++ b/bun.lock @@ -52,10 +52,10 @@ "name": "@hraness/slopcamera-web", "version": "0.0.0", "dependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.22.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.23.0", "@hraness/design-kit-articles": "github:hraness/design-kit#v0.21.0", "@hraness/design-kit-status": "github:hraness/design-kit#v0.21.0", - "@hraness/site-footer": "github:hraness/site-footer#v0.19.3", + "@hraness/site-footer": "github:hraness/site-footer#v0.20.0", "@hraness/ui": "github:hraness/ui#v0.5.16", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", "@resvg/resvg-js": "2.6.2", @@ -188,7 +188,7 @@ "@hraness/algal": ["@hraness/algal@github:hraness/algal#c46264a", { "bin": { "algal": "./cli.ts", "morphogen": "./cli.ts" } }, "hraness-algal-c46264a", "sha512-7NwCCh55A4RPdiCmfnvJJVumA1AU/3OA93rG3qEV3a/EjkiE9XBRZkbOdM81loFXvGmmBBqwRq4OtJXTY9aPgw=="], - "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#985ead3", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-985ead3", "sha512-Na5TTU+iDxDR2GwhNwGhAYn0j8d/Bz3Shhhvp/Jqy/hdxRXShvHlnoh7uYqPpqBIVWtGxUwht9unQaSAZ4NtGQ=="], + "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#585fba2", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-585fba2", "sha512-VlxOg8iAQ59LzbkBnt1EB1ikiu8jEuxku9yK+skfuozYtFFxEq9/JHE1xiyYHzSR0EmNCMKt0AGJjpuTIief9w=="], "@hraness/design-kit-articles": ["@hraness/design-kit@github:hraness/design-kit#4c3f7c9", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-4c3f7c9", "sha512-7SfYsa39IMWkb67vXWBK5xJ5l7nrotVERD/KnhogvJObxLBfvxLLl6qBY9Q6QK0uxBE5LTzRT/huciboQf91nw=="], @@ -196,7 +196,7 @@ "@hraness/direct": ["@hraness/direct@github:hraness/direct#af954e7", { "peerDependencies": { "@antithesishq/bombadil": "0.7.2", "agent-browser": "0.32.3", "effect": "3.22.1", "react": ">=18 <20" }, "optionalPeers": ["@antithesishq/bombadil", "agent-browser", "effect", "react"] }, "hraness-direct-af954e7", "sha512-q1iauLS/UaP0gLknME4Roca46yosjsc2oQ3ULrH1cxiUBwbfRMREf0BeiQtiKJGPOkPsHNXUEJM6I68IgRqm/Q=="], - "@hraness/site-footer": ["@hraness/site-footer@github:hraness/site-footer#4f38057", { "peerDependencies": { "react": ">=18 <20" }, "optionalPeers": ["react"] }, "hraness-site-footer-4f38057", "sha512-bFOLkhPmTpkESiDhmJhAPVIjLZHoaCSbD9BEW2D1h676n2J2H123wATpI5nVUUvZw7/mCVSy6hpUN6jq0N6lQQ=="], + "@hraness/site-footer": ["@hraness/site-footer@github:hraness/site-footer#4244dc5", { "peerDependencies": { "react": ">=18 <20" }, "optionalPeers": ["react"] }, "hraness-site-footer-4244dc5", "sha512-5F4KwQ0ZrjwQjHNDAkBuhI5U7d7+gPMSluyuP0ZHkKdpahbomcxr4UtmQVp7ZJcTHuodHyaRKkIz9SK5y82mOQ=="], "@hraness/slopcamera-scene": ["@hraness/slopcamera-scene@workspace:packages/scene"], From 1fd008ce40fff2da0b8736996c3d876bea9efac2 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 27 Sep 2026 18:12:37 -0400 Subject: [PATCH 2/9] Give section labels and secondary links their quiet typography --- apps/web/src/styles.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 01fc2b90..d4cf1a40 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -491,3 +491,16 @@ h3 { --hraness-marketing-inverse: var(--night); --hraness-marketing-inverse-ink: var(--night-ink); } + +.slopcamera-eyebrow { + color: var(--muted); + font: .75rem/1.5 var(--font-mono); + letter-spacing: .035em; +} + +.slopcamera-text-link { + display: inline-flex; + align-items: center; + min-block-size: 2.75rem; + text-decoration: underline; +} From 5209a047923d9802fe0e5aa0cf3496c2cdce13a8 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 27 Sep 2026 18:23:26 -0400 Subject: [PATCH 3/9] Align quiet site acceptance with released foundations --- apps/web/scripts/build-site.ts | 4 ++-- apps/web/scripts/lantern-material.test.ts | 6 +++--- apps/web/scripts/lantern-material.ts | 2 +- apps/web/scripts/site-copy-content.test.ts | 7 +++++-- apps/web/scripts/site-examples-browser-contract.test.ts | 7 +++---- apps/web/scripts/site-examples-profile.ts | 4 ++-- scripts/legacy-identity.inventory.json | 8 ++++---- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/web/scripts/build-site.ts b/apps/web/scripts/build-site.ts index 24eae744..4c6ce925 100644 --- a/apps/web/scripts/build-site.ts +++ b/apps/web/scripts/build-site.ts @@ -23,8 +23,8 @@ import { renderStatusPage } from "../src/status-page-content" import type { BlogPageContent, SiteAssets } from "../src/site-content" const packages = [ - { name: "@hraness/design-kit", version: "0.22.0" }, - { name: "@hraness/site-footer", version: "0.19.3" }, + { name: "@hraness/design-kit", version: "0.23.0" }, + { name: "@hraness/site-footer", version: "0.20.0" }, { name: "@hraness/ui", version: "0.5.16" }, ] as const const fontFiles = [ diff --git a/apps/web/scripts/lantern-material.test.ts b/apps/web/scripts/lantern-material.test.ts index 49c58fcc..f5d34a55 100644 --- a/apps/web/scripts/lantern-material.test.ts +++ b/apps/web/scripts/lantern-material.test.ts @@ -20,7 +20,7 @@ describe("Lantern material admission and scope", () => { test("admits only the complete released finite inventory and rejects changed ownership or bytes", async () => { const admitted = await snapshotLanternMaterial(vendor) - expect(admitted.sourceCommit).toBe("d38d13c07d7956d02ddfbca8d32aa2066d88fbd3") + expect(admitted.sourceCommit).toBe("3df4c411c7f5e5cbc02448463571696f0d47cee5") expect([...admitted.files.keys()].sort()).toEqual(["LICENSE", "check.d.mts", "check.mjs", "lantern-material.css"]) const root = await realpath(await mkdtemp(join(tmpdir(), "slopcamera-lantern-"))) try { @@ -50,8 +50,8 @@ describe("Lantern material admission and scope", () => { ]) expect(home.match(/data-hraness-material="lantern"/gu)).toHaveLength(1) expect(home).toContain('class="topbar hraness-material-chrome {{SITE_HEADER_CLASS}}"') - expect(home.match(/hraness-material-wall/gu)).toHaveLength(1) - expect(home).toContain('class="hraness-marketing-hero slopcamera-product-hero hraness-material-wall"') + expect(home).not.toContain("hraness-material-wall") + expect(home).toContain('class="hraness-marketing-hero slopcamera-product-hero"') expect(home).toContain("{{EXAMPLE_HERO}}") expect(home).not.toContain("hraness-material-terminal") expect(home).not.toContain('class="hraness-marketing-field"') diff --git a/apps/web/scripts/lantern-material.ts b/apps/web/scripts/lantern-material.ts index d910d1de..5c015d87 100644 --- a/apps/web/scripts/lantern-material.ts +++ b/apps/web/scripts/lantern-material.ts @@ -4,7 +4,7 @@ import { checkLanternMaterialSnapshot } from "../vendor/lantern-material/check.m import { readPreviewFile } from "./preview-file" import { siteSha256 } from "./site-contract" -export const currentLanternMaterialRevision = "d38d13c07d7956d02ddfbca8d32aa2066d88fbd3" +export const currentLanternMaterialRevision = "3df4c411c7f5e5cbc02448463571696f0d47cee5" export const historicalLanternMaterialRevision = "0e089bc18f9a0409f0e74b1fb7192f468956e386" export type LanternMaterialRevision = typeof currentLanternMaterialRevision | typeof historicalLanternMaterialRevision diff --git a/apps/web/scripts/site-copy-content.test.ts b/apps/web/scripts/site-copy-content.test.ts index 8a0299c1..0342bcbe 100644 --- a/apps/web/scripts/site-copy-content.test.ts +++ b/apps/web/scripts/site-copy-content.test.ts @@ -80,7 +80,7 @@ describe("media studio public copy (pure, process-free)", () => { const commands = ["slopcamera diagram init first.diagram.json", "slopcamera diagram check first.diagram.json --strict", "slopcamera diagram render first.diagram.json"] expect(html).toContain("{{EXAMPLE_HERO}}") expect(html).toContain("{{EXAMPLE_GALLERY}}") - expect(html).toContain("docs/tutorials/first-diagram.md") + expect(html).toContain('href="/docs/tutorials/first-diagram"') for (const source of [readme, diagramSession, homeMarkdown]) { const positions = commands.map(command => source.indexOf(command)) expect(positions.every(position => position >= 0)).toBe(true) @@ -126,7 +126,10 @@ describe("media studio public copy (pure, process-free)", () => { const routes = JSON.parse(config).redirects.filter((route: { has?: unknown }) => route.has === undefined) expect(routes).toEqual([]) const links = [...html.matchAll(/href="(https:\/\/github.com\/hraness\/slopcamera\/blob\/main\/docs\/[^"#]+)(?:#[^"]*)?"/gu)] - expect(links.length).toBeGreaterThanOrEqual(5) + expect(links.length).toBeGreaterThanOrEqual(1) + const firstPartyLinks = [...html.matchAll(/href="(\/docs\/[^"#]+)"/gu)] + expect(firstPartyLinks.length).toBeGreaterThanOrEqual(10) + for (const [, route] of firstPartyLinks) expect(await Bun.file(new URL(`../src${route}.md`, import.meta.url)).exists()).toBe(true) const allowed = new Set(["README.md", "tutorials/first-diagram.md", "tutorials/first-native-film.md", "spatial-scenes.md", "studio.md", "directing-video.md", "how-to/edit-video.md", "how-to/generate-media.md", "how-to/educational-video.md", "how-to/run-workflows.md", "reference/capabilities.md", "architecture.md", "how-to/use-current-source.md"]) for (const [, link] of links) expect(allowed.has(link!.split("/docs/")[1]!)).toBe(true) expect(html).not.toMatch(/ { test("current hero and trust literals match authored content before reduced-motion enhancement", async () => { const source = await readFile(new URL("../src/index.html", import.meta.url), "utf8") expect(source.split("{{EXAMPLE_HERO}}")).toHaveLength(2) - // The shared inert hero artwork is a build-time slot that site-content.ts fills; it is not - // authored hero copy, and portfolio-surfaces-v3 checks the rendered artwork separately. - expect(source.split("{{HERO_BACKDROP}}")).toHaveLength(2) - const tree = parseFragment(source.replace("{{EXAMPLE_HERO}}", () => renderExampleHero()).replace(/^[ \t]*\{\{HERO_BACKDROP\}\}\r?\n/mu, "")) + // The quiet hero keeps its real media proof without the retired decorative backdrop. + expect(source).not.toContain("{{HERO_BACKDROP}}") + const tree = parseFragment(source.replace("{{EXAMPLE_HERO}}", () => renderExampleHero())) type Node = DefaultTreeAdapterMap["node"] type Element = DefaultTreeAdapterMap["element"] const descendants = (node: Node): Element[] => "childNodes" in node diff --git a/apps/web/scripts/site-examples-profile.ts b/apps/web/scripts/site-examples-profile.ts index e475fbdf..94a7f0c5 100644 --- a/apps/web/scripts/site-examples-profile.ts +++ b/apps/web/scripts/site-examples-profile.ts @@ -18,7 +18,7 @@ export const examplesIslands: readonly ExamplesIsland[] = [ { "selector": ".hraness-marketing-hero", "baseline": "
\n
\n

Slopcamera

\n

Direct scenes and films with your coding agent

\n

\n Slopcamera is a local visual studio for coding agents. Author scenes, combine\n generated and recorded media, and export images, diagrams, animation,\n and video from retained sources.\n

\n \n

Free and open source under the MIT license · Verified release · Bun 1.3.14 or newer · Recording needs macOS

\n
\n
\n
\n
\n \n slopcamera · your first local diagram\n
\n
\n
#    __\n#  _|__|_\n# |  (o) |  slopcamera\n# |______|\n\n$ slopcamera diagram init first.diagram.json\n# An editable starter, ready for your labels.\n$ slopcamera diagram check first.diagram.json --strict\nValid diagram.\n$ slopcamera diagram render first.diagram.json\n# example-flow.tldr\n# example-flow.light.svg + example-flow.dark.svg\n# example-flow.light.png + example-flow.dark.png
\n
\n
\n An included input, five local outputs, no model account. Follow the complete tutorial; rendering again replaces the derived files.\n Illustrative session · included starter\n
\n
\n
\n
\n
Outputs
Four familiesImages, diagrams, animated loops, and video
\n
Interfaces
Start with your agentAgent Skill and CLI; SDK and a fixed MCP subset
\n
Delivery
16:9, 9:16, 1:1, 4:5Clean and captioned cuts from one edit
\n
Cost
FreeMIT license, no account
\n
\n
", - "current": "
\n
\n

Slopcamera

\n

Visual work your agent can keep revising.

\n

\n Images, diagrams, animation, 3D scenes, and edited video, built from source files\n so a change is a re-render, not a re-roll.\n

\n \n

Free and open source under the MIT license · Bun 1.3.14 or newer · Blender, CadQuery, and Manim install separately

\n
\n
\n
An optical instrument, in motion

A camera move around original brass and glass; the Blender scene stays editable.

6s · Silent

Follow the guideView sourceOpen video

\n
\n
\n
Outputs
Four familiesImages, diagrams, animated loops, and video
\n
Interfaces
Through your agentSkill and CLI; SDK and MCP too
\n
Delivery
16:9, 9:16, 1:1, 4:5Clean and captioned cuts from one edit
\n
Cost
FreeMIT license; model usage is paid
\n
\n
" + "current": "
\n
\n

Media studio for coding agents

\n

Visual work your agent can keep revising.

\n

\n Images, diagrams, animation, 3D scenes, and edited video, built from source files\n so your agent can revise a scene and render it again.\n

\n \n

Free and open source under the MIT license · Bun 1.3.14 or newer on macOS, Linux, or Windows

\n
\n
\n
An optical instrument, in motion

A camera move around original brass and glass; the Blender scene stays editable.

6s · Silent

Follow the guideView sourceOpen video

\n
\n
" }, { "selector": "#examples", @@ -55,7 +55,7 @@ export const examplesIslands: readonly ExamplesIsland[] = [ { "selector": "#design", "baseline": "
\n
\n

Retain the sources behind the result.

\n

Keep sources, edits, and outputs in your own storage. Rendered derivatives connect native and portable assets without erasing their differences.

\n
\n
\n
Source media stays unchanged
Original recordings and imported media stay unchanged. Cuts, framing, overlays, and effects remain project decisions.
\n
Project state stays on your machine
There is no Slopcamera account or hosted project database. You control source, project state, previews, and final files.
\n
Gateway credentials stay local
Local processes use your Vercel AI Gateway credential without storing or printing it. Media uploads require acknowledgement.
\n
Operations leave receipts
Preview and final renders share the timeline and composition. Important operations retain secret-free input and output receipts.
\n
\n

Native Python execution requires separate authorization. Loading a custom Bun workflow evaluates its module, including during check and plan; review the source before invoking it. Both run as your current user, without an operating-system sandbox. Runtime hashes identify observed tools; they do not make arbitrary source hermetic. Read the security policy and privacy guide.

\n

Find a path through the documentation index: learn with a first diagram, run a workflow, look up capabilities, or understand the architecture.

\n
", - "current": "
\n
\n

What stays on your machine

\n

Ask the agent for the same video twice; the second take differs. Slopcamera keeps the source beside every render; a revision changes what you named, nothing else. Sources, edits, and renders stay on your own storage.

\n
\n
\n
Source media stays unchanged
Original recordings and imported media stay unchanged. Cuts, framing, overlays, and effects remain project decisions.
\n
Project state stays on your machine
There is no Slopcamera account or hosted project database. You control source, project state, previews, and final files.
\n
Gateway credentials stay local
Local processes use your Vercel AI Gateway credential without storing or printing it. Media uploads require acknowledgement.
\n
Operations leave a record
Preview and final renders share the timeline and composition. Important operations save a record of their inputs and outputs, with no secrets in it.
\n
\n

Native engine jobs run only with --allow-trusted-code. They and custom Bun workflows run as your current user, without an operating-system sandbox, so review their source first. Read the security policy and privacy guide.

\n

Find your way in the docs: a first diagram, a workflow, capabilities, or the architecture.

\n
" + "current": "
\n
\n

Your files

\n

What stays on your machine

\n

Slopcamera keeps editable source beside each render. Sources, edits, and local renders stay on your own storage; generation and selected cloud analysis use the services described below.

\n
\n
\n
Source media stays unchanged
Original recordings and imported media stay unchanged. Cuts, framing, overlays, and effects remain project decisions.
\n
Project state stays on your machine
There is no Slopcamera account or hosted project database. You control source, project state, previews, and final files.
\n
Gateway credentials stay local
Local processes use your Vercel AI Gateway credential without storing or printing it. Media uploads require acknowledgement.
\n
Operations leave a record
Important operations save a record of their inputs and outputs, with no secrets in it.
\n
\n

Native engine jobs run only with --allow-trusted-code. They and custom Bun workflows run as your current user, without an operating-system sandbox, so review their source first. Read the security policy and privacy guide.

\n
" }, ] export const examplesHomeIds = ["native-product", "premiere-wall", "island-pulse", "interference-field", "native-fluid", "compute-temple"] as const diff --git a/scripts/legacy-identity.inventory.json b/scripts/legacy-identity.inventory.json index 5770cebc..2f353d0c 100644 --- a/scripts/legacy-identity.inventory.json +++ b/scripts/legacy-identity.inventory.json @@ -1485,9 +1485,9 @@ ] }, { - "identityLineCount": 5, - "identityLinesSha256": "a1b58e2baaa64392de9e7d9f6bdde7f3bf9c455eea732f096b5989d5db2c9446", - "occurrenceCount": 7, + "identityLineCount": 6, + "identityLinesSha256": "f700451272b581928d6de554644c8b94f24a036f8f76d963b45301be3fc2a925", + "occurrenceCount": 8, "path": "apps/web/scripts/site-examples-profile.ts", "categories": [ "native-film-studio", @@ -1739,7 +1739,7 @@ }, { "identityLineCount": 4, - "identityLinesSha256": "31a588a1da828bc1e9f5fcb1c0d3f2b2790b07e6bb3541e476e2ee8b4a763780", + "identityLinesSha256": "19e324955414c3274688459121534cebb6583f34365c5a122babc761b542d235", "occurrenceCount": 4, "path": "apps/web/src/index.html", "categories": [ From 23f8bf5a59dd82c9462129239ff6542d46db0875 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 27 Sep 2026 18:33:32 -0400 Subject: [PATCH 4/9] Admit exact quiet foundation assets without retired textures --- apps/web/scripts/build-site.ts | 3 ++- apps/web/scripts/site-contract.test.ts | 16 ++++++++++++++++ apps/web/scripts/site-contract.ts | 11 +++++------ apps/web/site.test.ts | 18 +++++++++--------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/apps/web/scripts/build-site.ts b/apps/web/scripts/build-site.ts index 4c6ce925..30e9e8b1 100644 --- a/apps/web/scripts/build-site.ts +++ b/apps/web/scripts/build-site.ts @@ -112,7 +112,8 @@ export async function buildSite(appDirectory: string, assets: SiteAssets): Promi return { path: below(root, join(presetRoot, path)), sha256: siteSha256(bytes), bytes: bytes.byteLength } }) fonts.push(...presetAssets.filter(item => item.path.endsWith(".woff2"))) - const images = presetAssets.filter(item => item.path.endsWith(".svg")) + // Quiet emits no decorative textures; snapshot bytes remain captured source inputs. + const images: typeof presetAssets = [] // The shared metallic wordmark paints the canonical product mark; its bytes // are admitted once as the foil mask image. const wordmarkMaskPath = join(app, "src/marks/slopcamera.svg") diff --git a/apps/web/scripts/site-contract.test.ts b/apps/web/scripts/site-contract.test.ts index 16ebad56..509e1be1 100644 --- a/apps/web/scripts/site-contract.test.ts +++ b/apps/web/scripts/site-contract.test.ts @@ -351,3 +351,19 @@ describe("site shell artifact publication (pure synthetic controls)", () => { } }) }) + + +test("Quiet admits no decorative textures and rejects reintroduced texture bytes", () => { + const quiet = foundationOutput(); + quiet.output = quiet.output.filter(item => !item.fileName?.endsWith(".svg")); + const css = quiet.output.find(item => item.fileName?.endsWith(".css"))!; + css.source = String(css.source).replace(/\.texture\d+\{[^}]+\}/gu, ""); + const inspect = (source: string) => [...source.matchAll(/url\(([^)]+)\)/gu)].map(match => match[1]!); + expect(snapshotWithInspection(quiet, fontHashes, entrypoint, inspect, []).artifacts).toHaveLength(16); + expect(() => snapshotWithInspection(foundationOutput(), fontHashes, entrypoint, inspect, [])).toThrow(); + const unapproved = structuredClone(quiet); + unapproved.output.push({ type: "asset", fileName: "assets/unapproved.svg", source: imageSources[0]! }); + expect(() => snapshotWithInspection(unapproved, fontHashes, entrypoint, inspect, [])).toThrow(); + css.source += ".unexpected{background:url(./texture-0.svg)}"; + expect(() => snapshotWithInspection(quiet, fontHashes, entrypoint, inspect, [])).toThrow("multiplicity"); +}); diff --git a/apps/web/scripts/site-contract.ts b/apps/web/scripts/site-contract.ts index 6583b2e3..971ac15b 100644 --- a/apps/web/scripts/site-contract.ts +++ b/apps/web/scripts/site-contract.ts @@ -80,7 +80,6 @@ function capturedFoundation(value: SiteFoundation): SiteArtifact[] { assert.equal(scripts.length, 1, "Site foundation requires one captured private entry") assert.equal(css.length, 1, "Site foundation requires one captured stylesheet") assert.equal(fonts.length, 14, "Site foundation requires fourteen captured fonts") - assert.ok(images.length >= 2, "Site foundation requires two captured textures plus one mask per foil mark") assert.equal(input.artifacts.length, 16 + images.length, "Site foundation artifact inventory must account for every foil mask") assert.equal(input.privateScriptPath, `${foundationRoot}${scripts[0]!.path}`) assert.equal(input.cssPath, `${foundationRoot}${css[0]!.path}`) @@ -94,9 +93,9 @@ export function snapshotSiteFoundation(value: unknown, fontHashes: readonly stri assert.equal(fontHashes.length, 14) fontHashes.forEach(digest) assert.equal(new Set(fontHashes).size, 14, "Site foundation font inputs must be distinct") - assert.equal(imageHashes.length, 2) + assert.ok(imageHashes.length === 0 || imageHashes.length === 2, "Site textures must match either Quiet or the historical paired texture profile") imageHashes.forEach(digest) - assert.equal(new Set(imageHashes).size, 2, "Site foundation texture inputs must be distinct") + assert.equal(new Set(imageHashes).size, imageHashes.length, "Site foundation texture inputs must be distinct") maskHashes.forEach(digest) assert.equal(new Set(maskHashes).size, maskHashes.length, "Site foundation mask inputs must be distinct") for (const maskHash of maskHashes) { @@ -105,7 +104,7 @@ export function snapshotSiteFoundation(value: unknown, fontHashes: readonly stri const results = Array.isArray(value) ? value : [value] assert.equal(results.length, 1, "Site foundation requires one Vite output") const output = record(results[0]).output - assert.ok(Array.isArray(output) && output.length === 18 + maskHashes.length, "Site foundation must emit one private entry, one CSS asset, fourteen WOFF2 assets and two SVG textures plus one mask per foil mark") + assert.ok(Array.isArray(output) && output.length === 16 + imageHashes.length + maskHashes.length, "Site foundation must emit one private entry, one CSS asset, fourteen WOFF2 assets and the exact approved SVG textures and foil masks") const chunks = output.map(record).filter(item => item.type === "chunk") assert.equal(chunks.length, 1, "Site foundation requires exactly one captured private entry") const chunk = chunks[0]! @@ -217,8 +216,8 @@ export function projectSiteArtifacts(value: unknown, expected: Readonly<{ return false }) assert.equal(rendererEntries, 1, "Site generation requires one captured renderer entry") - const maskCount = captured.filter(item => item.path.endsWith(".svg")).length - 2 - assert.equal(publicArtifacts.length, 18 + maskCount + expected.documents.length) + const imageCount = captured.filter(item => item.path.endsWith(".svg")).length + assert.equal(publicArtifacts.length, 16 + imageCount + expected.documents.length) for (const path of allowed) assert.ok(publicArtifacts.some(item => item.path === path), `Missing public site artifact: ${path}`) return sorted(publicArtifacts) } diff --git a/apps/web/site.test.ts b/apps/web/site.test.ts index 28c92f51..3b0177ab 100644 --- a/apps/web/site.test.ts +++ b/apps/web/site.test.ts @@ -865,9 +865,9 @@ describe("static Slopcamera site", () => { const artifacts = builtAssets.siteArtifacts const paths = artifacts.map(item => item.path) const blogDocuments = [blogIndexDocument, ...blogPosts.map(blogDocumentForPost)] - expect(artifacts).toHaveLength(19 + 2 + docPages.length + blogDocuments.length) + expect(artifacts).toHaveLength(17 + 2 + docPages.length + blogDocuments.length) expect(paths).toEqual([...paths].sort()) - expect(new Set(paths).size).toBe(19 + 2 + docPages.length + blogDocuments.length) + expect(new Set(paths).size).toBe(17 + 2 + docPages.length + blogDocuments.length) expect(paths.filter(path => path.endsWith(".html"))).toEqual([ "404.html", ...docPages.map(docsDocumentForPage), ...blogDocuments, "index.html", ].sort()) @@ -886,10 +886,10 @@ describe("static Slopcamera site", () => { const fonts = paths.filter(path => path.endsWith(".woff2")) expect(fonts).toHaveLength(14) const images = paths.filter(path => path.endsWith(".svg")) - expect(images).toHaveLength(3) + expect(images).toHaveLength(1) for (const name of ["grain.svg", "cells.svg"]) { const expected = await readFile(join(appDirectory, "vendor/marketing-preset/marketing-assets", name)) - expect(images.some(path => artifacts.find(item => item.path === path)!.sha256 === new Bun.CryptoHasher("sha256").update(expected).digest("hex"))).toBe(true) + expect(images.some(path => artifacts.find(item => item.path === path)!.sha256 === new Bun.CryptoHasher("sha256").update(expected).digest("hex"))).toBe(false) } // The third SVG is the foil wordmark mask: the canonical product mark bytes. const markBytes = await readFile(join(appDirectory, "src/marks/slopcamera.svg")) @@ -897,7 +897,7 @@ describe("static Slopcamera site", () => { const masks = images.filter(path => artifacts.find(item => item.path === path)!.sha256 === markSha256) const textures = images.filter(path => !masks.includes(path)) expect(masks).toHaveLength(1) - expect(textures).toHaveLength(2) + expect(textures).toHaveLength(0) const foundation = await readBuilt(builtAssets.siteFoundationPath.slice(1)) const union = await readBuilt(builtAssets.stylesPath.slice(1)) expect(foundation.match(/@font-face\b/gu)).toHaveLength(14) @@ -1440,10 +1440,10 @@ describe("static Slopcamera site", () => { const localLockfile = await readFile(join(appDirectory, "bun.lock"), "utf8") expect(manifest.dependencies).toEqual({ - "@hraness/design-kit": "github:hraness/design-kit#v0.22.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.23.0", "@hraness/design-kit-articles": "github:hraness/design-kit#v0.21.0", "@hraness/design-kit-status": "github:hraness/design-kit#v0.21.0", - "@hraness/site-footer": "github:hraness/site-footer#v0.19.3", + "@hraness/site-footer": "github:hraness/site-footer#v0.20.0", "@hraness/ui": "github:hraness/ui#v0.5.16", "@hraness/web-discovery": "github:hraness/web-discovery#v0.9.0", "@resvg/resvg-js": "2.6.2", @@ -1469,9 +1469,9 @@ describe("static Slopcamera site", () => { }) expect(rootManifest.workspaces?.catalog?.["posthog-js"]).toBeUndefined() expect(rootManifest.workspaces?.catalog?.["@hraness/design-kit"]).toBeUndefined() - expect(localLockfile).toContain('"@hraness/design-kit": "github:hraness/design-kit#v0.22.0"') + expect(localLockfile).toContain('"@hraness/design-kit": "github:hraness/design-kit#v0.23.0"') expect(localLockfile).toContain( - '"@hraness/site-footer": "github:hraness/site-footer#v0.19.3"', + '"@hraness/site-footer": "github:hraness/site-footer#v0.20.0"', ) expect(localLockfile).toContain('"@hraness/ui": "github:hraness/ui#v0.5.16"') expect(localLockfile).toContain('"@hraness/design-kit-articles": "github:hraness/design-kit#v0.21.0"') From a81e7a55e7befc101311ec649ed7cb2c92712766 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 27 Sep 2026 18:54:23 -0400 Subject: [PATCH 5/9] Align site contracts with proof-first layout and flow footer --- apps/web/site.test.ts | 101 ++++++++---------------------------------- 1 file changed, 19 insertions(+), 82 deletions(-) diff --git a/apps/web/site.test.ts b/apps/web/site.test.ts index 3b0177ab..39304049 100644 --- a/apps/web/site.test.ts +++ b/apps/web/site.test.ts @@ -191,52 +191,6 @@ function assertCombinedSiteCssBudget(styles: string, foundation: string): number return bytes } -/** Evaluate one CSS length expression (calc, min, max, clamp, env fallback, - * px, rem at 16px, vw) at a viewport width. Only the closed grammar the shared - * footer and the host scroll padding use is accepted; nothing is evaluated as - * source text. */ -function evaluateCssLength(expression: string, viewportWidth: number): number { - const tokens = expression.match(/[a-z][a-z-]*\(|[a-z][a-z-]*|\(|\)|,|[+*/-]|\d*\.?\d+(?:px|rem|vw)?/gu) ?? [] - let index = 0 - const peek = () => tokens[index] - const take = (expected?: string): string => { - const token = tokens[index++] - if (token === undefined || (expected !== undefined && token !== expected)) throw new Error(`Unexpected CSS token ${String(token)} in ${expression}`) - return token - } - const sum = (): number => { - let value = product() - while (peek() === "+" || peek() === "-") value = take() === "+" ? value + product() : value - product() - return value - } - const product = (): number => { - let value = primary() - while (peek() === "*" || peek() === "/") value = take() === "*" ? value * primary() : value / primary() - return value - } - const list = (): number[] => { - const values = [sum()] - while (peek() === ",") { take(","); values.push(sum()) } - take(")") - return values - } - const primary = (): number => { - const token = take() - if (token === "(" || token === "calc(") { const value = sum(); take(")"); return value } - if (token === "max(") return Math.max(...list()) - if (token === "min(") return Math.min(...list()) - if (token === "clamp(") { const [low, preferred, high] = list() as [number, number, number]; return Math.min(Math.max(preferred, low), high) } - if (token === "env(") { take("safe-area-inset-bottom"); take(","); const [fallback] = list(); return fallback! } - const length = /^(\d*\.?\d+)(px|rem|vw)?$/u.exec(token) - if (length === null) throw new Error(`Unexpected CSS token ${token} in ${expression}`) - const value = Number(length[1]) - return length[2] === "rem" ? value * 16 : length[2] === "vw" ? value * viewportWidth / 100 : value - } - const value = sum() - if (index !== tokens.length) throw new Error(`Trailing CSS tokens in ${expression}`) - return value -} - /** The installed footer's custom-property tokens for the classes the rendered * footer element actually carries, split by pointer context. */ function installedFooterTokens(stylesheet: string, footerClasses: ReadonlySet): Record<"fine" | "coarse", Record> { @@ -1094,13 +1048,13 @@ describe("static Slopcamera site", () => { expect(client).not.toMatch(/fetch\(|XMLHttpRequest|WebSocket|EventSource|sendBeacon/) }) - test("uses one install, examples, workflow, interfaces, and design information architecture", async () => { + test("presents examples and workflow before installation and trust details", async () => { const html = await readSource("index.html") const sections = [ - 'id="install"', 'id="examples"', 'id="workflow"', 'id="interfaces"', + 'id="install"', 'id="design"', 'id="questions"', 'id="closing"', @@ -1120,7 +1074,6 @@ describe("static Slopcamera site", () => { expect(navigation).toContain('class="site-action {{SITE_NAVIGATION_ACTION_CLASS}}" data-emphasis="primary" href="#install"') expect(html).not.toContain('class="docs-index"') for (const role of [ - "pillars", "install", "primitives", "section", @@ -1190,7 +1143,7 @@ describe("static Slopcamera site", () => { const html = await readSource("index.html") const searchableHtml = html.replace(/\s+/gu, " ") - for (const claim of ["Source", "Project", "Operations", "Outputs"]) { + for (const claim of ["Source media stays unchanged", "Project state stays on your machine", "Operations leave a record", "inputs and outputs"]) { expect(html).toContain(claim) } expect(searchableHtml).toContain("Native engine jobs run only with --allow-trusted-code") @@ -1308,7 +1261,7 @@ describe("static Slopcamera site", () => { expect(await readSource("preview.html")).not.toContain("data-hraness-marketing-preset") expect(html).not.toContain("Ben Guo") expect(html).not.toContain('class="hraness-marketing-hero__eyebrow"') - expect(html).toContain('class="hraness-marketing-hero slopcamera-product-hero hraness-material-wall" data-align="start"') + expect(html).toContain('class="hraness-marketing-hero slopcamera-product-hero" data-align="start"') expect(css).toContain("grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr)") for (const declaration of ["white-space: pre", "overflow-wrap: normal", "word-break: normal", "overflow: auto"]) { expect(css).toContain(declaration) @@ -1378,12 +1331,7 @@ describe("static Slopcamera site", () => { expect(icon).toContain('stop-color="#f6b94a"') }) - test("keeps the scrollport's optimal viewing region above the shared fixed footer bar", async () => { - // The shared footer bar is position: fixed with its own in-flow footprint, - // but Chromium's sequential focus only scrolls a target that leaves the - // layout viewport. The host's scroll-padding-block-end must equal the - // installed footer's exact bar height for both pointer contexts at every - // width, so focused and scrolled-to content is never obscured by the bar. + test("keeps the shared footer in document flow without fixed-bar scroll compensation", async () => { const [css, footerCss, html] = await Promise.all([ readSource("styles.css"), readFile(fileURLToPath(import.meta.resolve("@hraness/site-footer/stylex.css")), "utf8"), @@ -1392,33 +1340,22 @@ describe("static Slopcamera site", () => { const footerClasses = new Set(/