Unify
Connect an app to Unify in six steps. About ten minutes for a new app.
Unify is published to GitHub Packages. Point the @nashyvan scope there; the token comes from the environment, never the file.
@nashyvan:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}export NPM_TOKEN=$(gh auth token) # a token with read:packages
pnpm add @nashyvan/unify// next.config.ts
const nextConfig: NextConfig = {
output: "standalone",
// Unify ships TypeScript source; Next compiles it with the app.
transpilePackages: ["@nashyvan/unify"],
};One import replaces @import "tailwindcss" and every token file. Pick a hue on the theming page.
/* 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
Geist fonts, the pre-paint theme script (no flash of the wrong theme) and the toaster.
// 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>
);
}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.
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 — 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 } });
}Installs inside Docker need the token as a build secret.
# 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: readDependabot watches the package daily and opens one PR per release; the app's CI checks it.
# .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"] }# 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