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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
!/docs/
/docs/*
!/docs/adoption.md
!/docs/components.md
!/docs/releasing.md
!/docs/design-locks/
/docs/design-locks/*
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Contributing

Use Node.js 24+, pnpm 10.33.2 and `pnpm install --frozen-lockfile`, then `pnpm dev` for the demo app. The landing page, playground and showcase share one Next.js app. Run `pnpm check` before opening a pull request. Keep the library and demo responsibilities separate.
Use Node.js 24+, pnpm 10.33.2 and `pnpm install --frozen-lockfile`, then `pnpm dev` for the demo app. The landing page, playground, showcase and `/components` guide share one Next.js app. Run `pnpm check` before opening a pull request. Keep the library and demo responsibilities separate.

The demo deliberately consumes the pinned public alpha from npm. Local library edits do not change that installed demo version. Use `pnpm pack:local` and an independent consumer to verify a new package build before release. Do not silently replace the demo's published dependency with a source alias.
The demo consumes the built workspace package through `workspace:*`. Its `dev`, `build` and `typecheck` scripts prepare the library's ignored `dist` exports before running Next.js, including from `apps/demo`. Rebuild with `pnpm --filter react-glaze build` after editing library source while the demo server is running.

Use `pnpm pack:local` and an independent consumer that installs the resulting tarball to verify a new package build before release. That archive check verifies packaging separately from workspace development; see the [release procedure](docs/releasing.md).

For optical changes, include a minimal reproduction and before/after browser evidence. Distinguish native Safari, Playwright WebKit, viewport emulation, Simulator and physical devices. Do not infer FPS from JavaScript submission timings.

Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[![React Glaze - Liquid glass for React](apps/demo/public/brand/readme-banner.png)](https://react-glaze.app/)

[Website](https://react-glaze.app/) · [Playground](https://react-glaze.app/playground) · [Showcase](https://react-glaze.app/showcase) · [npm](https://www.npmjs.com/package/react-glaze)
[Website](https://react-glaze.app/) · [Components](https://react-glaze.app/components) · [Liquid Glass editor](https://react-glaze.app/components?component=glass) · [Showcase](https://react-glaze.app/showcase) · [npm](https://www.npmjs.com/package/react-glaze)

A configurable React 19 component that refracts the page behind your content. Use a native button, link or ordinary wrapper, keep your own CSS, and adjust the material, shape and optional pointer-driven rim light.
Liquid glass for React 19: a configurable wrapper, morphing menus, spring-driven actions and composable surfaces. Keep native content and your own CSS, and tune the material and motion.

**Experimental.** Desktop Chrome, Firefox and Safari are the targets; iPhone Safari is experimental. Background capture is asynchronous and has [documented limitations](packages/react/README.md#known-limits).

Expand Down Expand Up @@ -35,6 +35,12 @@ export function SaveButton() {

No provider, background image prop or stylesheet import is required. Your content controls the size; your CSS controls layout and typography. Next.js App Router can render the library's client boundary from a server component. Event handlers belong in your own client component.

## Morphing components

`LiquidGlassActions` opens a row, column or fan of native buttons. `LiquidGlassMenu` turns one trigger into a menu panel. Both use `LiquidGlassGroup` and `LiquidGlassSurface`, which are also available for building your own interactions. Adjust action size, signed trigger distance, refraction, exact duration and bounce independently.

[Component guide and examples](docs/components.md) · [Interactive components](https://react-glaze.app/components)

## Customize

- **Materials:** reference, quiet, frosted and Optical Type presets, with individual optical controls.
Expand All @@ -50,12 +56,13 @@ Use Node.js 24 or newer:

```sh
pnpm install --frozen-lockfile
pnpm --filter react-glaze build
pnpm dev
```

Open http://127.0.0.1:8862. One Next.js app serves the landing page, `/playground` and `/showcase`.
Open http://127.0.0.1:8862. One Next.js app serves the landing page, `/components` and `/showcase`. `/playground` remains a compatibility redirect to `/components?component=glass`.

The playground includes material/rim controls, custom backgrounds, generated JSX/JSON and shareable URLs. Save one configuration locally for your next visit; explicit shared URLs take priority. Uploaded images stay in the current tab. Saved configurations belong to the browser origin; using the same host and port as the previous preview preserves access to them.
The Liquid Glass editor at `/components?component=glass` includes material/rim controls, custom backgrounds, generated JSX/JSON and shareable URLs. Save one configuration locally for your next visit; explicit shared URLs take priority. Uploaded images stay in the current tab. Saved configurations belong to the browser origin; using the same host and port as the previous preview preserves access to them.

The Roam showcase exercises responsive photographs, image changes, navigation, native dialogs and trip planning over real page content. Its saved trips stay local to your browser.

Expand All @@ -66,7 +73,7 @@ pnpm check
pnpm pack:local
```

`packages/react` contains the library. `apps/demo` is a Next.js consumer pinned to the published `react-glaze@0.1.1` package. Its import resolves to the registry package, while the local library is built and tested separately. The workspace uses one pnpm lockfile.
`packages/react` contains the library. `apps/demo` consumes its built workspace package during development and CI. An independently installed tarball consumer verifies the distributable boundary separately. The workspace uses one pnpm lockfile. Build the package before starting the demo after library changes.

Checks cover package tests and public types, playground configuration/storage tests, workspace type checks and the demo production build. Local archives are written to unique directories so previous builds remain available. See the [demo guide](apps/demo/README.md) for routes and styling, and the [design lock](docs/design-locks/2026-09-09-demo.md) for the landing direction and verification record.

Expand Down
18 changes: 11 additions & 7 deletions apps/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ Open http://127.0.0.1:8862.
| Route | Purpose |
| --- | --- |
| `/` | One-screen introduction, real glass demo, installation and links |
| `/playground` | Material/shape/rim editor, JSX/JSON export, shareable configuration and optional local save |
| `/components?component=glass` | Material/shape/rim editor, JSX/JSON export, shareable configuration and optional local save |
| `/showcase` | Roam travel demo: photographs, gallery, filters, saved places and native trip-planning dialog |
| `/components` | Interactive split actions, morphing menu, Liquid Glass editor and group/surface examples with copyable TSX |
| `/playground` | Compatibility redirect to `/components?component=glass` |
| `/quality.html` | Preserved optical quality fixture |
| `/research.html` | Preserved material research fixture |

The app uses the actual published `react-glaze@0.0.0-alpha.1` dependency. The monorepo disables automatic workspace linking so the demo exercises its public artifact. The local package remains available for independent builds, tests and archives.
The app consumes `react-glaze` through `workspace:*`, using the library's built exports in `packages/react/dist`. The demo's `dev`, `build` and `typecheck` scripts build that dependency first, including when run directly from `apps/demo` after a fresh workspace install. After editing library source while the dev server is running, run `pnpm --filter react-glaze build` again to update those exports.

The playground retains the owner's exact initial/reset configuration. Explicit `config` query parameters win over a manually saved local configuration. The storage key remains `react-glaze:playground:config:v1`. Save is manual, Reset does not overwrite the saved copy, Delete saved leaves the current preview intact, and uploaded image bytes are not stored. Storage belongs to an origin, so changing host or port does not transfer previous settings. Old `/?config=...` links redirect to `/playground` with their configuration preserved.
Use `pnpm pack:local` and an independently installed tarball consumer to verify the distributable package separately. The workspace demo does not establish that an archive contains everything an external application needs. See the [release procedure](../../docs/releasing.md).

The Liquid Glass editor retains the owner's exact initial/reset configuration. `/components` selects split actions by default; `component=actions|menu|glass` is addressable, and a `config` query without an explicit valid component selects the editor. Explicit `config` query parameters win over a manually saved local configuration. The storage key remains `react-glaze:playground:config:v1`. Save is manual, Reset does not overwrite the saved copy, Delete saved leaves the current preview intact, and uploaded image bytes are not stored. Storage belongs to an origin, so changing host or port does not transfer previous settings. Old `/?config=...` and `/playground` links redirect to `/components?component=glass` with their query values preserved.

Roam retains `liquid-showcase-trip-v1` for its local trip state. Photographs and fonts retain their [source attribution](ASSETS.md).

Expand All @@ -31,23 +35,23 @@ Generated with `@larsen-utvikling/create-next-app@0.6.0`, pnpm and the recommend

The single stylesheet chain is `src/app/layout.tsx` -> `globals.css` -> `src/lib/design-system/index.css`. Structural/type/motion tokens come from the template. The color file is the actual Canonical Palette download using #4655F5, automatic secondary, weak neutral tint, shadcn/ui HSL values, media dark mode and no Tailwind integration. [Token guide](DESIGN.md).

The playground and showcase retain their visual identity in scoped route styles. Their palette variables and generic controls cannot leak into the landing page. The playground mounts its browser-only editor within a client-only loader; the route retains its own server-rendered metadata.
The editor and showcase retain their visual identity in scoped route styles. Their palette variables and generic controls cannot leak into the landing page. The editor mounts its browser-only implementation within a client-only loader after its first selection; the Components route retains its own server-rendered metadata.

[Design lock and verification](../../docs/design-locks/2026-09-09-demo.md).

## Verify

Production traffic and adoption actions use Vercel Web Analytics. See the [event definitions, collection boundaries and dashboard](docs/analytics.md).

Run `pnpm check` at the root for package/config/storage tests, type checks and the production build. Use `pnpm --filter demo build` to build only the site, and `pnpm --filter demo start` to preview that production build.
Run `pnpm check` at the root for package/config/storage tests, type checks and the production build. Use `pnpm --filter demo build` for the site's production build, including its workspace package prerequisite, and `pnpm --filter demo start` to preview it. From `apps/demo`, the equivalent commands are `pnpm build` and `pnpm start`; `pnpm typecheck` also prepares the library before checking the app.

Desktop Chrome, Firefox and Safari are the package targets; iPhone Safari remains experimental. Local builds, browser checks, physical-device checks and deployment are separate evidence. No npm release or site deployment is triggered by these commands.


## Identity and search metadata

The shared Join logo appears in the landing, playground and showcase branding. [Brand assets and regeneration](docs/brand.md) describe the single SVG geometry source, outlined wordmarks, native icons and README/social graphic.
The shared Join logo appears in the landing, Components route and showcase branding. [Brand assets and regeneration](docs/brand.md) describe the single SVG geometry source, outlined wordmarks, native icons and README/social graphic.

Next.js file conventions serve `favicon.ico`, `icon.svg`, `apple-icon.png`, `manifest.webmanifest`, `robots.txt`, `sitemap.xml`, Open Graph and Twitter images. Metadata helpers define each route's title, description and production canonical URL. Child routes preserve the actual native social image metadata from their parent. The safely serialized JSON-LD graph describes the website and the open-source package without invented reviews, ratings or release claims.

The three current routes are included in the sitemap. Historical research and quality fixtures remain accessible with `X-Robots-Tag: noindex, follow`; they are not blocked in robots.txt. The manifest uses browser display and does not promise an offline application. Verification and search-engine indexing are separate outcomes.
The three public routes are included in the sitemap. Historical research and quality fixtures remain accessible with `X-Robots-Tag: noindex, follow`; they are not blocked in robots.txt. The manifest uses browser display and does not promise an offline application. Verification and search-engine indexing are separate outcomes.
8 changes: 4 additions & 4 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --hostname 127.0.0.1 --port 8862",
"build": "next build",
"dev": "pnpm --filter react-glaze build && next dev --hostname 127.0.0.1 --port 8862",
"build": "pnpm --filter react-glaze build && next build",
"start": "next start --hostname 127.0.0.1 --port 8862",
"lint": "eslint",
"typecheck": "next typegen && tsc --noEmit"
"typecheck": "pnpm --filter react-glaze build && next typegen && tsc --noEmit"
},
"dependencies": {
"@vercel/analytics": "^2.0.1",
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-glaze": "0.1.2"
"react-glaze": "workspace:*"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
67 changes: 67 additions & 0 deletions apps/demo/src/app/components/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { ResolvingMetadata } from "next";
import { Suspense } from "react";
import Link from "next/link";
import { BrandIcon } from "@/components/brand/BrandIcon";
import { ComponentsExplorer } from "@/components/components/ComponentsExplorer";
import { pageMetadata } from "@/lib/seo";

export async function generateMetadata(
_props: unknown,
parent: ResolvingMetadata,
) {
return pageMetadata("/components", await parent);
}

function ComponentsExplorerFallback() {
return (
<div className="components-playground" aria-busy="true">
<p className="components-example-status" role="status">
Loading the component explorer...
</p>
</div>
);
}

export default function ComponentsPage() {
return (
<div className="components-page">
<header className="components-header">
<Link href="/" className="landing-brand" aria-label="React Glaze home">
<BrandIcon />
<span>React Glaze</span>
</Link>
<nav aria-label="React Glaze">
<Link href="/">Home</Link>
<Link href="/components" aria-current="page">
Components
</Link>
<Link href="/showcase">Showcase</Link>
</nav>
</header>
<main>
<div className="components-intro">
<p className="components-eyebrow">REACT GLAZE / COMPONENTS</p>
<h1>
One material.
<br />
<span>Made to move.</span>
</h1>
<p>
Composable glass surfaces and fluid menus.
<br />
Native controls, shared motion, your content.
</p>
</div>
<Suspense fallback={<ComponentsExplorerFallback />}>
<ComponentsExplorer />
</Suspense>
</main>
<footer className="components-footer">
<span>React 19 / Open source / MIT</span>
<Link href="https://github.com/Stianlars1/react-glaze">
View the source
</Link>
</footer>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/demo/src/app/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function manifest(): MetadataRoute.Manifest {
id: "/",
name: SITE_NAME,
short_name: SITE_NAME,
description: "Liquid glass for React. Explore the playground and showcase.",
description: "Liquid glass for React. Explore components and showcase.",
lang: "en",
start_url: "/",
scope: "/",
Expand Down
32 changes: 18 additions & 14 deletions apps/demo/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@ import { redirect } from "next/navigation";
import { MaterialStage } from "@/components/landing/MaterialStage";
import { InstallCommand } from "@/components/landing/InstallCommand";
import { BrandIcon } from "@/components/brand/BrandIcon";
import { pageMetadata } from "@/lib/seo";
import { PACKAGE_URL, pageMetadata, REPOSITORY_URL } from "@/lib/seo";
import { ResourceLink } from "@/components/analytics/ResourceLink";
import { redirectToComponents, type RouteSearchParams } from "@/lib/components-navigation";

export const metadata = pageMetadata("/");

export default async function Home({
searchParams,
}: {
searchParams: Promise<{ config?: string | string[] }>;
searchParams: Promise<RouteSearchParams>;
}) {
const { config } = await searchParams;
const query = await searchParams;
const { config } = query;
if (config !== undefined) {
const query = new URLSearchParams({
config: Array.isArray(config) ? config[0] : config,
});
redirect(`/playground?${query}`);
redirect(redirectToComponents(query));
}

return (
Expand All @@ -28,9 +27,14 @@ export default async function Home({
<BrandIcon />
<span>React Glaze</span>
</Link>
<ResourceLink destination="github" className="landing-github" href="https://github.com/Stianlars1/react-glaze">
GitHub
</ResourceLink>
<nav className="landing-resources" aria-label="Project resources">
<ResourceLink destination="github" className="landing-resource-link" href={REPOSITORY_URL}>
GitHub
</ResourceLink>
<ResourceLink destination="npm" className="landing-resource-link" href={PACKAGE_URL}>
npm
</ResourceLink>
</nav>
</header>

<main className="landing-main">
Expand All @@ -39,15 +43,15 @@ export default async function Home({
<h1>Liquid glass for React.</h1>
<p className="landing-description">
Wrap a button or a card.<br />
Keep your content and your own CSS.
Compose a menu. Keep your own CSS.
</p>
</div>
<div className="landing-entry-points">
<InstallCommand />
<nav className="landing-actions" aria-label="Explore React Glaze">
<Link href="/playground" className="landing-route-link">
<span>Playground</span>
<span className="landing-route-detail">Tune the glass</span>
<Link href="/components" className="landing-route-link">
<span>Components</span>
<span className="landing-route-detail">Explore and customize</span>
</Link>
<Link href="/showcase" className="landing-route-link">
<span>Showcase</span>
Expand Down
17 changes: 8 additions & 9 deletions apps/demo/src/app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { ResolvingMetadata } from "next";
import { PlaygroundLoader } from "@/components/playground/PlaygroundLoader";
import { pageMetadata } from "@/lib/seo";
import { redirect } from "next/navigation";
import { redirectToComponents, type RouteSearchParams } from "@/lib/components-navigation";

export async function generateMetadata(_props: unknown, parent: ResolvingMetadata) {
return pageMetadata("/playground", await parent);
}

export default function PlaygroundPage() {
return <PlaygroundLoader />;
export default async function PlaygroundPage({
searchParams,
}: {
searchParams: Promise<RouteSearchParams>;
}) {
redirect(redirectToComponents(await searchParams));
}
Loading
Loading