Skip to content
Lockerverse SDK
Esc
↑↓navigate↵open⌘Jpreview
Theme workbench14 examples

Styling / Development

Theme workbench

Apply one token set to payment, signup, and launcher UI.

All examples
Previewlight site theme
Component
Surface
Loading payment…
CodeApp.tsx
import {
  LockerverseDonateLauncher,
  LockerversePayment,
  type LockerverseStyle,
  type LockerverseTheme,
  useLockerverseWidget,
} from "@lockerverse/react";
import "@lockerverse/react/styles.css";
import {
  LockerverseSignup,
  useLockerverseSignup,
} from "@lockerverse/react/signup";
import "@lockerverse/react/signup.css";
import { useState } from "react";
import { exampleRuntime } from "../example-runtime";

type ComponentName = "launcher" | "payment" | "signup";
type SurfaceName = "ink" | "paper" | "site";

const surfaceStyles = {
  ink: {
    "--lockerverse-bg": "#12100d",
    "--lockerverse-border": "#433c34",
    "--lockerverse-control": "#1c1915",
    "--lockerverse-muted": "#afa79e",
    "--lockerverse-placeholder": "#7c746b",
    "--lockerverse-subtle": "#29251f",
    "--lockerverse-text": "#fff9ef",
  },
  paper: {
    "--lockerverse-bg": "#fffaf0",
    "--lockerverse-border": "#d9cbb7",
    "--lockerverse-control": "#f5eddf",
    "--lockerverse-muted": "#756a5e",
    "--lockerverse-placeholder": "#9c9083",
    "--lockerverse-subtle": "#eee3d3",
    "--lockerverse-text": "#211c17",
  },
  site: {},
} satisfies Record<SurfaceName, LockerverseStyle>;

function PaymentPreview({
  style,
  theme,
}: {
  style: LockerverseStyle;
  theme: LockerverseTheme;
}) {
  const widget = useLockerverseWidget({
    apiBaseUrl: exampleRuntime.apiBaseUrl,
    communitySlug: exampleRuntime.communitySlug,
    environment: "development",
    sentryDsn: null,
    widgetSlug: exampleRuntime.widgetSlug,
  });
  return (
    <LockerversePayment
      selection={[{ productSlug: "adult", quantity: 1 }]}
      style={style}
      theme={theme}
      widget={widget}
    />
  );
}

function SignupPreview({
  style,
  theme,
}: {
  style: LockerverseStyle;
  theme: LockerverseTheme;
}) {
  const widget = useLockerverseSignup({
    apiBaseUrl: exampleRuntime.apiBaseUrl,
    communitySlug: exampleRuntime.communitySlug,
    environment: "development",
    sentryDsn: null,
    signupSlug: exampleRuntime.signupSlug,
  });
  return <LockerverseSignup style={style} theme={theme} widget={widget} />;
}

function LauncherPreview({
  style,
  theme,
}: {
  style: LockerverseStyle;
  theme: LockerverseTheme;
}) {
  const widget = useLockerverseWidget({
    apiBaseUrl: exampleRuntime.apiBaseUrl,
    communitySlug: exampleRuntime.communitySlug,
    environment: "development",
    sentryDsn: null,
    widgetSlug: exampleRuntime.widgetSlug,
  });
  return (
    <LockerverseDonateLauncher
      buttonLabel="Donate"
      position="inline"
      style={style}
      theme={theme}
      widget={widget}
    />
  );
}

export default function ThemingExample({
  theme = "light",
}: {
  theme?: LockerverseTheme;
}) {
  const [component, setComponent] = useState<ComponentName>("payment");
  const [surface, setSurface] = useState<SurfaceName>("site");
  const [accent, setAccent] = useState("#f1511b");
  const [radius, setRadius] = useState(10);
  const style = {
    ...surfaceStyles[surface],
    "--lockerverse-accent": accent,
    "--lockerverse-radius": `${radius}px`,
  } satisfies LockerverseStyle;

  return (
    <div className="theme-workbench">
      <div className="theme-workbench__controls">
        <fieldset>
          <legend>Component</legend>
          <div className="theme-workbench__options">
            {(["payment", "signup", "launcher"] as const).map((name) => (
              <button
                aria-pressed={component === name}
                key={name}
                onClick={() => setComponent(name)}
                type="button"
              >
                {name}
              </button>
            ))}
          </div>
        </fieldset>
        <fieldset>
          <legend>Surface</legend>
          <div className="theme-workbench__options">
            {(["site", "paper", "ink"] as const).map((name) => (
              <button
                aria-pressed={surface === name}
                key={name}
                onClick={() => setSurface(name)}
                type="button"
              >
                {name}
              </button>
            ))}
          </div>
        </fieldset>
        <label className="theme-workbench__color">
          <span>Accent</span>
          <input
            aria-label="Theme accent color"
            onChange={(event) => setAccent(event.currentTarget.value)}
            type="color"
            value={accent}
          />
          <code>{accent}</code>
        </label>
        <label className="theme-workbench__radius">
          <span>Radius</span>
          <input
            max="24"
            min="0"
            onChange={(event) => setRadius(event.currentTarget.valueAsNumber)}
            type="range"
            value={radius}
          />
          <code>{radius}px</code>
        </label>
      </div>
      <div className="theme-workbench__preview" data-component={component}>
        {component === "payment" ? (
          <PaymentPreview style={style} theme={theme} />
        ) : null}
        {component === "signup" ? (
          <SignupPreview style={style} theme={theme} />
        ) : null}
        {component === "launcher" ? (
          <LauncherPreview style={style} theme={theme} />
        ) : null}
      </div>
    </div>
  );
}