Unify

Getting started

Connect an app to Unify in six steps. About ten minutes for a new app.

1. Registry access

Unify is published to GitHub Packages. Point the @nashyvan scope there; the token comes from the environment, never the file.

.npmrc
@nashyvan:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
Terminal
export NPM_TOKEN=$(gh auth token)   # a token with read:packages
pnpm add @nashyvan/unify
next.config.ts
// next.config.ts
const nextConfig: NextConfig = {
  output: "standalone",
  // Unify ships TypeScript source; Next compiles it with the app.
  transpilePackages: ["@nashyvan/unify"],
};

3. Styles and accent

One import replaces @import "tailwindcss" and every token file. Pick a hue on the theming page.

app/globals.css
/* app/globals.css — the only stylesheet import. It brings Tailwind too. */
@import "@nashyvan/unify/styles.css";

/* The app's accent (and, optionally, a neutral tint). */
:root {
  --accent-h: 185;
  --accent-c: 0.12;
  --tone: 0.004;
}

Delete the app's own tokens

Remove hand-written colour, radius and shadow variables and any dark: variants — the tokens switch themes by themselves.

4. Root layout

Geist fonts, the pre-paint theme script (no flash of the wrong theme) and the toaster.

app/layout.tsx
// app/layout.tsx
import { ThemeScript, Toaster } from "@nashyvan/unify";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const sans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const mono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeScript />            {/* or defaultTheme="dark" */}
      </head>
      <body className={`${sans.variable} ${mono.variable}`}>
        {children}
        <Toaster />
      </body>
    </html>
  );
}
app/(app)/receipts/page.tsx
import { Button, Card, CardHeader, CardTitle, PageHeader } from "@nashyvan/unify";

export default function Page() {
  return (
    <>
      <PageHeader title="Receipts" actions={<Button>New receipt</Button>} />
      <Card>
        <CardHeader><CardTitle>October</CardTitle></CardHeader>
      </Card>
    </>
  );
}

Browse every component with its code on Components.

6. App icon

Generated, not drawn: the favicon, the Apple touch icon and the in-app logo come from one spec. Delete app/favicon.ico.

lib/brand.ts + app/icon.tsx
// lib/brand.ts — one spec for favicon, touch icon and the in-app lockup
export const ICON = { hue: 185, chroma: 0.12, glyph: '<path d="…"/>' };

// app/icon.tsx
import { appIconSvg } from "@nashyvan/unify";
import { ICON } from "@/lib/brand";
export const contentType = "image/svg+xml";
export default function Icon() {
  return new Response(appIconSvg(ICON), { headers: { "Content-Type": contentType } });
}

In CI and Docker

Installs inside Docker need the token as a build secret.

Dockerfile + deploy.yml
# Dockerfile, deps stage: the token is a build secret, never a layer
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm install --frozen-lockfile

# .github/workflows/deploy.yml, docker/build-push-action
secrets: |
  npm_token=${{ secrets.GITHUB_TOKEN }}     # needs permissions: packages: read

Staying up to date

Dependabot watches the package daily and opens one PR per release; the app's CI checks it.

.github/dependabot.yml
# .github/dependabot.yml
registries:
  github-npm:
    type: npm-registry
    url: https://npm.pkg.github.com
    token: ${{ secrets.DEPENDABOT_PACKAGES_TOKEN }}
updates:
  - package-ecosystem: npm
    directory: /
    registries: [github-npm]
    schedule: { interval: daily }
    groups:
      unify: { patterns: ["@nashyvan/unify"] }
Terminal
# Work on Unify and an app side by side (do not commit this)
pnpm link ~/Developer/Personal/developer/packages/unify
# … and back to the published version
pnpm unlink @nashyvan/unify && pnpm install