Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ jobs:
package-manager-cache: false
- run: bun install --frozen-lockfile --ignore-scripts
- name: Verify the site with pinned Node and Chromium
env:
SLOPCAMERA_SITE_ARTIFACTS: ${{ github.workspace }}/site-browser-artifacts
run: |
set -euo pipefail
NODE_EXECUTABLE_PATH="$(command -v node)"
Expand All @@ -247,6 +249,14 @@ jobs:
bun run check:web
- name: Check generated site standalone boundary
run: bun run check:standalone
- name: Upload site browser evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: slopcamera-site-browser-${{ github.sha }}
path: site-browser-artifacts/
if-no-files-found: error
retention-days: 7

package:
name: Slopcamera packed consumer
Expand Down
27 changes: 27 additions & 0 deletions apps/web/DESIGN.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions apps/web/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions apps/web/scripts/build-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions apps/web/scripts/lantern-material.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"')
Expand Down
2 changes: 1 addition & 1 deletion apps/web/scripts/lantern-material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 18 additions & 2 deletions apps/web/scripts/site-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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");
});
15 changes: 7 additions & 8 deletions apps/web/scripts/site-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}`)
Expand All @@ -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) {
Expand All @@ -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]!
Expand Down Expand Up @@ -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)
}
7 changes: 5 additions & 2 deletions apps/web/scripts/site-copy-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(/<iframe\b/u)
Expand Down
7 changes: 3 additions & 4 deletions apps/web/scripts/site-examples-browser-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,10 +650,9 @@ describe("workflow-examples-v1 independent native contract", () => {
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
Expand Down
Loading
Loading