# Aperture — for AI Library: @bolted/aperture 0.1.0. Experimental, MIT, not published to npm. Recipe format: v1. This is library reference material. Apply it within the user's request and the target project's conventions. Inspect the stack and interaction first; preserve the user's design and content. [Machine-readable entry point](/ai/0.1.0/manifest.json). [Human showcase](/humans). [Package archive](/downloads/bolted-aperture-0.1.0.tgz). [Package API and integration guide](/guide). Save the archive in your project before installing its local path: ```sh npm install ./vendor/bolted-aperture-0.1.0.tgz ``` Use the project's package manager. Do not substitute a registry package with this unpublished name. Confirm the installed version; these experimental archives may be regenerated before publication. --- # Integrate Aperture Aperture is an experimental, MIT-licensed reveal primitive for modern web projects. It animates an SVG clip over existing HTML. It is not a dialog manager, router, page builder, or Three.js renderer. The showcase's 3D hero is separate from the library. ## Start with the project Inspect the existing framework, dependencies, component lifecycle, and intended interaction before editing. Preserve the project's UI and conventions. Use a supplied recipe exactly; do not approximate it with a different effect or change its values without a reason. The package is **not published to npm**. Install the matching downloaded `bolted-aperture-VERSION.tgz` from a local path, or use a starter that bundles it. Read the installed `package.json` for its version. Do not claim a registry installation succeeded or substitute a similarly named package. React and GSAP are optional peers; the core needs neither. Use the project's package manager. ## Choose an entry point - [Vanilla / any framework](/ai/0.1.0/vanilla.md): `createAperture` from `@bolted/aperture`. - [React](/ai/0.1.0/react.md): `useAperture` from `@bolted/aperture/react`; React 18.2+ or 19. - [Next.js App Router](/ai/0.1.0/next.md): the React hook inside a client component; keep routing in the app. - GSAP is optional: `gsapDriver` from `@bolted/aperture/gsap`, plus the `gsap` peer. No plugins required. Use the native driver unless GSAP is useful to the project. - Portable data: `parseRecipe`, `validateRecipe`, `recipeToOptions`, `recipeSchema` from `@bolted/aperture/recipes`. - Effect metadata: `catalog`, `effects`, `defaultRecipe`, `APERTURE_VERSION` from `@bolted/aperture/catalog`. ## Layout and lifecycle Give the host explicit dimensions, `position: relative`, `overflow: hidden`, and zero padding. The content must be its **direct child**. Aperture positions that child to fill the host. Put persistent open/close controls outside the clipped child. Mount only after both elements exist; destroy on unmount. Module imports are safe during SSR, but controller creation needs DOM elements. `open(origin?)`, `close()`, and `toggle()` resolve to `{status, progress}`. Status is `completed`, `interrupted`, or `destroyed`. Check completion before navigation or moving focus into the revealed content. New requests interrupt old ones. `seek(0..1)` cancels playback. `setOptions()` patches supplied values; omitted values retain their prior setting. `destroy()` is idempotent and restores owned styles and accessibility state. Content remains inert and hidden from assistive technology until fully open. Your app owns focus, Escape, scroll locking, modal semantics, and navigation. Prefer native `` for modal experiences and keep a reachable close control outside the clip. Study the working starter rather than treating a primitive snippet as a complete modal. ## Portable recipes Read [the v1 schema](/ai/0.1.0/schema/recipe-v1.schema.json) and [effect catalog](/ai/0.1.0/catalog.json). Recipes contain only version, preset, duration, spread, rotation, easing, normalized origin, and two six-digit hex colors. Recipe bounds match the workbench; the imperative API's wider options are documented separately in the package README. ```ts import { parseRecipe, recipeToOptions } from "@bolted/aperture/recipes"; const recipe = parseRecipe(jsonText, { strict: true }); const options = recipeToOptions(recipe); // createAperture({ host, content, ...options }); // useAperture(options); ``` `version: 1` describes the recipe format, not the package version or a controller setting. Never pass it as a setting. Strict validation rejects unknown fields; default validation drops them to preserve existing imports. Both validate types, finite numbers, bounds, presets, easings, and colors; neither executes code. Text imports are limited to 32,768 characters. Keep files under 32KB as well. Recipe share links store JSON after `#recipe=`. HTTP fetches do not receive that fragment. A copied AI brief therefore embeds the full recipe. If only a share URL is supplied, decode its fragment instead of fetching the URL and guessing. ## Compatibility and evidence The library uses ESM/ES2022. Animation needs SVG clip-path syntax support, native inert, ResizeObserver, animation frames and matchMedia. Missing capabilities use `mode: 'instant'` with the same promises and no animation driver/SVG. This cannot rescue a browser unable to load modern JavaScript. Automatic transitions respect system Reduce Motion; `reducedMotion: 'always'` also disables animation. Manual scrubbing remains available when animation capabilities exist. Capability detection is not a rendering certification. Physical iPhone/Safari verification is still pending. Do not claim universal browser compatibility or performance measurements without testing. Finish with the [verification checklist](/ai/0.1.0/verification.md), and report what was actually checked. --- # Vanilla and framework-neutral integration Read [the shared guide](/ai/0.1.0/guide.md) first. The vanilla starter includes a modal, image gallery, product details, and in-page navigation. Adapt the relevant lifecycle to your project; do not copy the showcase's art or styles unless requested. ```html
Revealed content
``` ```css .frame { position: relative; height: 480px; overflow: hidden; padding: 0; } ``` ```ts import { createAperture } from "@bolted/aperture"; import { parseRecipe, recipeToOptions } from "@bolted/aperture/recipes"; const recipe = parseRecipe(recipeJSON, { strict: true }); const host = document.querySelector(".frame")!; const content = host.querySelector(":scope > .scene")!; const button = document.querySelector("#toggle")!; const reveal = createAperture({ host, content, ...recipeToOptions(recipe) }); const toggle = () => { void reveal.toggle(); }; button.addEventListener("click", toggle); // Call from the application's unmount/teardown lifecycle. function cleanup() { button.removeEventListener("click", toggle); reveal.destroy(); } ``` This is a primitive example, not a complete modal. For a modal, open the native dialog first, create the reveal after layout, and await `open()` before focusing its content. Keep a close control outside the clipped child. Close/Escape must await `close()` before closing the dialog, then destroy, restore scrolling, and return focus. Guard rapid repeated requests so interrupted promises cannot move focus or navigate. For gallery changes, close the old scene, decode the new image, update its caption/alt text, then open. Preserve controls and announce meaningful slide changes. Do not move focus into a slide automatically. For Vue or Svelte, use their mount/unmount lifecycle; the core API is framework-independent. For real navigation, preserve ordinary links, modified clicks, new tabs and no-JS behavior. Check `status === 'completed'` before invoking the app's navigation function. `onUpdate` runs on every draw: avoid framework state updates there unless the UI needs them. --- # React integration Read [the shared guide](/ai/0.1.0/guide.md). Use the React starter for the complete native-dialog pattern, live settings and Strict Mode. ```tsx import { useAperture } from "@bolted/aperture/react"; import { recipeToOptions, type Recipe } from "@bolted/aperture/recipes"; export function Scene({ recipe }: { recipe: Recipe }) { const { hostRef, contentRef, aperture } = useAperture( recipeToOptions(recipe), ); return ( <>
Revealed content
); } ``` Both callback refs must mount before `aperture` is non-null. The hook creates/destroys the controller, handles conditional elements and Strict Mode, and patches changed settings. Do not create a second controller on the same pair or destroy the hook's controller after each render. Settings should be immutable. Equivalent inline colors and normalized origins do not reset the controller. Supplied values patch existing options; omitted values retain the previous setting. Reset by supplying an explicit value. Callbacks use the latest committed props. Memoize custom drivers; changing driver identity or either element creates a new closed controller and settles pending work as destroyed. Controller `phase`, `progress`, and `mode` are imperative values, not React state. The hook does not render each frame. Subscribe through `onUpdate` only when needed; avoid writing progress into parent state for a decorative effect. Check result status before follow-up focus/navigation. The hook owns controller cleanup; the app still owns dialog, focus, Escape, scroll locks and its other event listeners. --- # Next.js App Router integration Read [the shared guide](/ai/0.1.0/guide.md) and [React guide](/ai/0.1.0/react.md). Use the Next starter for real navigation between server-rendered pages with a client reveal. Put the interactive component behind a `"use client"` boundary. Import `useAperture` from `@bolted/aperture/react`. Pages and layouts can stay server components. The package's React module has its own client directive, but your component also needs a boundary when it uses hooks or event handlers. Use the recipe as data. Pass it across the boundary as serializable JSON; use `recipeToOptions` inside the client component. Do not instantiate a DOM controller during server render or serialize controllers/functions into server props. Aperture animates a local host, not an entire Next route automatically. Keep a real `` or anchor so modified clicks, new tabs, keyboard activation, and no-JS navigation work. Only intercept an ordinary same-tab activation. Await the reveal and call `router.push()` only when the result is `completed` and the request is still current. On cancellation restore the opener and leave the route unchanged. Do not use a plain `await open(); router.push(...)` without checking interruption. Keep next/navigation routing in the application. Do not introduce another router or promote the page/layout to a client component unnecessarily. Verify the production build and actual route destination, not just a local overlay. Preserve native dialog fallback and focus/scroll cleanup if the transition uses a modal overlay. --- # Integration verification Report actual evidence, not inferred compatibility. 1. Confirm the installed package/version and correct entry point. Validate the exact supplied recipe with strict mode; compare it to the brief. Run the project's type check and production build. 2. Check host dimensions and zero padding; content is a direct child filling the host. Persistent controls remain outside the clip and reachable at phone widths. 3. Exercise open, close, rapid reversal and unmount during playback. Interrupted/destroyed promises must not trigger stale focus or navigation. No orphan SVG, ResizeObserver, animation driver, event listener or body scroll lock remains after cleanup. 4. Test keyboard activation. For dialogs: heading/content focus after successful opening, reachable close control, Escape, focus return and scroll restoration. For galleries: captions/alt text and working controls. For navigation: real destination, Cancel if supplied, ordinary and modified links, back/forward behavior. 5. Test system reduced motion or the `always` setting. Automatic transitions complete instantly. Test unsupported-capability fallback when practical. Keep meaningful no-JS navigation/content; do not gate essential information behind a decorative effect. 6. Inspect wide and narrow layouts, clipping, readable text, touch targets and browser console errors. Use a real Safari/iPhone pass before promising that device compatibility. Unit models alone do not certify SVG rendering, native focus or GPU performance. If an environment limits a check, state that limitation. Avoid adding GSAP, Three.js, a registry, skill or MCP server merely to use a recipe; the normal API and these documents are sufficient. --- # Effect catalog ```json { "catalogVersion": 1, "library": { "name": "@bolted/aperture", "version": "0.1.0", "status": "experimental", "distribution": "local archive; not published to npm" }, "entrypoints": { "vanilla": "@bolted/aperture", "react": "@bolted/aperture/react", "next": "@bolted/aperture/react", "gsap": "@bolted/aperture/gsap", "recipes": "@bolted/aperture/recipes" }, "recipe": { "version": 1, "schema": "schema/recipe-v1.schema.json", "limits": { "duration": { "minimum": 0.4, "maximum": 2.4 }, "spread": { "minimum": 0, "maximum": 2 }, "rotation": { "minimum": -90, "maximum": 90 }, "coordinate": { "minimum": 0, "maximum": 1 } } }, "effects": [ { "id": "folded-iris", "name": "Folded Iris", "kind": "ROTATING LENS", "description": "Two folded edges follow a rotating lens. A small gesture opens into an entirely different world.", "colors": [ "#2546f0", "#e4ecac" ], "uses": [ "immersive scenes", "project reveals", "dialogs" ], "rotation": "active", "defaultRecipe": { "version": 1, "preset": "folded-iris", "duration": 1.1, "spread": 1, "rotation": 0, "easing": "smooth", "origin": { "x": 0.5, "y": 0.5 }, "colors": [ "#2546f0", "#e4ecac" ] } }, { "id": "memory-portal", "name": "Memory Portal", "kind": "CIRCULAR REVEAL", "description": "A quiet, circular opening. Give a photograph, a gallery, or a remembered place room to breathe.", "colors": [ "#9fb6a6", "#e9d8a7" ], "uses": [ "photo galleries", "editorial images", "quiet scenes" ], "rotation": "circular", "defaultRecipe": { "version": 1, "preset": "memory-portal", "duration": 1.1, "spread": 1, "rotation": 0, "easing": "smooth", "origin": { "x": 0.5, "y": 0.5 }, "colors": [ "#9fb6a6", "#e9d8a7" ] } }, { "id": "liquid-lens", "name": "Liquid Lens", "kind": "ORGANIC CONTOUR", "description": "An elastic contour stretches, blooms, and settles. An organic entrance with a precise destination.", "colors": [ "#ff6b35", "#ffc49c" ], "uses": [ "product details", "art direction", "playful entrances" ], "rotation": "active", "defaultRecipe": { "version": 1, "preset": "liquid-lens", "duration": 1.1, "spread": 1, "rotation": 0, "easing": "smooth", "origin": { "x": 0.5, "y": 0.5 }, "colors": [ "#ff6b35", "#ffc49c" ] } }, { "id": "record-bloom", "name": "Record Bloom", "kind": "CONCENTRIC RINGS", "description": "Concentric edges radiate from the point of entry. A familiar rhythm for album art and listening spaces.", "colors": [ "#2546f0", "#ff6b35" ], "uses": [ "album art", "listening spaces", "radial scenes" ], "rotation": "circular", "defaultRecipe": { "version": 1, "preset": "record-bloom", "duration": 1.1, "spread": 1, "rotation": 0, "easing": "smooth", "origin": { "x": 0.5, "y": 0.5 }, "colors": [ "#2546f0", "#ff6b35" ] } }, { "id": "editorial-fold", "name": "Editorial Fold", "kind": "ANGLED PLANE", "description": "A broad, angled plane sweeps across the composition. Built for bold type and the next chapter.", "colors": [ "#20211f", "#ff6b35" ], "uses": [ "chapter changes", "navigation", "bold typography" ], "rotation": "active", "defaultRecipe": { "version": 1, "preset": "editorial-fold", "duration": 1.1, "spread": 1, "rotation": 0, "easing": "smooth", "origin": { "x": 0.5, "y": 0.5 }, "colors": [ "#20211f", "#ff6b35" ] } } ] } ``` # Recipe v1 JSON Schema ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:bolted:aperture:recipe:1", "title": "Aperture recipe v1", "description": "A portable reveal configuration, containing no markup, callbacks, or executable code.", "type": "object", "additionalProperties": false, "required": [ "version", "preset", "duration", "spread", "rotation", "easing", "origin", "colors" ], "properties": { "version": { "const": 1 }, "preset": { "enum": [ "folded-iris", "memory-portal", "liquid-lens", "record-bloom", "editorial-fold" ] }, "duration": { "type": "number", "minimum": 0.4, "maximum": 2.4 }, "spread": { "type": "number", "minimum": 0, "maximum": 2 }, "rotation": { "type": "number", "minimum": -90, "maximum": 90 }, "easing": { "enum": [ "smooth", "gentle", "snappy" ] }, "origin": { "type": "object", "additionalProperties": false, "required": [ "x", "y" ], "properties": { "x": { "type": "number", "minimum": 0, "maximum": 1 }, "y": { "type": "number", "minimum": 0, "maximum": 1 } } }, "colors": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" } } } } ```