diff --git a/.github/assets/hero-dark.png b/.github/assets/hero-dark.png new file mode 100644 index 00000000..871b356c Binary files /dev/null and b/.github/assets/hero-dark.png differ diff --git a/.github/assets/hero-light.png b/.github/assets/hero-light.png new file mode 100644 index 00000000..e4c4bb2b Binary files /dev/null and b/.github/assets/hero-light.png differ diff --git a/README.md b/README.md index 2bb1efdf..1c2ff7fb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ + + + Locus Design: design that keeps humans in the loop + + +

+ Docs · + DESIGN.md · + shadcn registry · + npm +

+ # Locus Design -Design that keeps humans in the loop. A design system for agent products: React components +A design system for agent products: React components on Radix, product blocks, Loop (agent UI with the human in the loop), brand assets, and one theme with light and dark modes. Icons are Phosphor (`@phosphor-icons/react`), bold weight. diff --git a/apps/design/package.json b/apps/design/package.json index 35681bad..4b9fd74d 100644 --- a/apps/design/package.json +++ b/apps/design/package.json @@ -9,7 +9,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run", "deploy": "pnpm registry && cf deploy", - "registry": "node ../../scripts/registry/build.mjs" + "registry": "node ../../scripts/registry/build.mjs", + "readme:hero": "node scripts/readme-hero.ts" }, "dependencies": { "@json-render/core": "catalog:", diff --git a/apps/design/plugins/raster.ts b/apps/design/plugins/raster.ts index f3808161..492a2add 100644 --- a/apps/design/plugins/raster.ts +++ b/apps/design/plugins/raster.ts @@ -36,9 +36,13 @@ export function svgToPng(svg: string, width: number) { return new Resvg(svg, { fitTo: { mode: "width", value: width } }).render().asPng(); } -export async function elementToPng(element: ReactNode, size: { width: number; height: number }) { +export async function elementToPng( + element: ReactNode, + size: { width: number; height: number }, + outputWidth = size.width +) { const svg = await satori(element, { ...size, fonts: loadFonts() }); - return svgToPng(svg, size.width); + return svgToPng(svg, outputWidth); } const icoHeaderBytes = 6; diff --git a/apps/design/scripts/readme-hero.ts b/apps/design/scripts/readme-hero.ts new file mode 100644 index 00000000..23a9c2ac --- /dev/null +++ b/apps/design/scripts/readme-hero.ts @@ -0,0 +1,24 @@ +import { writeFileSync } from "node:fs"; +import { createElement } from "react"; +import { elementToPng } from "../plugins/raster.ts"; +import { withStandaloneServer } from "../plugins/standalone-server.ts"; + +const heroModule = "/src/seo/build/readme-hero.tsx"; +const outputDir = new URL("../../../.github/assets/", import.meta.url); +const retinaScale = 2; + +type HeroModule = typeof import("../src/seo/build/readme-hero.tsx"); + +await withStandaloneServer(async (server) => { + const { ReadmeHero, readmeHeroSize } = (await server.ssrLoadModule(heroModule)) as HeroModule; + for (const theme of ["dark", "light"] as const) { + const png = await elementToPng( + createElement(ReadmeHero, { theme }), + readmeHeroSize, + readmeHeroSize.width * retinaScale + ); + const file = new URL(`hero-${theme}.png`, outputDir); + writeFileSync(file, png); + console.log(`readme hero: ${file.pathname}`); + } +}); diff --git a/apps/design/src/seo/build/readme-hero.tsx b/apps/design/src/seo/build/readme-hero.tsx new file mode 100644 index 00000000..92b41e92 --- /dev/null +++ b/apps/design/src/seo/build/readme-hero.tsx @@ -0,0 +1,195 @@ +import { darkColors, lightColors, type Palette } from "@locusgraph/tokens"; +import type * as React from "react"; + +export type HeroTheme = "dark" | "light"; + +export const readmeHeroSize = { width: 1280, height: 560 } as const; + +const fonts = { + heading: "Archivo", + body: "IBM Plex Sans", + mono: "IBM Plex Mono", +}; + +const palettes: Record = { dark: darkColors, light: lightColors }; + +function alpha(hex: string, amount: number) { + const byte = Math.round(amount * 255) + .toString(16) + .padStart(2, "0"); + return `${hex}${byte}`; +} + +function row(style: React.CSSProperties = {}): React.CSSProperties { + return { display: "flex", alignItems: "center", ...style }; +} + +function Wordmark({ colors }: { colors: Palette }) { + return ( +
+
+
+
+
+ locus + design +
+
+ ); +} + +function Field({ label, value, colors }: { label: string; value: string; colors: Palette }) { + return ( +
+ {label} + {value} +
+ ); +} + +function ApprovalCard({ colors, line }: { colors: Palette; line: string }) { + return ( +
+
+
+
+ Waiting + to run + send invoice +
+ + Writes data + +
+ + Makes a change. Check the inputs before you approve. + +
+ + + +
+
+ + Send INV-2041 + + Deny +
+
+
+ + Decision log + +
+ + Approved send invoice + Maya · 12:04 +
+
+
+ ); +} + +export function ReadmeHero({ theme }: { theme: HeroTheme }) { + const colors = palettes[theme]; + const line = alpha(colors.foreground, theme === "dark" ? 0.12 : 0.1); + return ( +
+
+ +
+ + Design that keeps humans in the loop. + + + Components, blocks and Loop, the agent UI. + +
+ + React 19 · Tailwind 4 · Light and dark + +
+ +
+ ); +}