@protolabsai/design
Advanced tools
| #!/usr/bin/env node | ||
| /** | ||
| * protolabs-sync-assets — fan this package's brand marks out into a consuming | ||
| * repo's served dirs. @protolabsai/design/assets is the one source of truth; the | ||
| * copies are generated, not committed (or, for GitHub-rendered galleries, a | ||
| * committed mirror guarded in CI). Recolor/replace a mark in this package, re-run, | ||
| * and every surface follows. | ||
| * | ||
| * Per-repo config: `brand-assets.config.json`, found by walking up from cwd, so it | ||
| * works whether invoked from the repo root or a nested package (e.g. an app's | ||
| * prebuild). Dest dirs resolve relative to that config file (the repo root). | ||
| * | ||
| * protolabs-sync-assets # every surface in the config | ||
| * protolabs-sync-assets docs # only the named surface(s) | ||
| * | ||
| * Config shape: | ||
| * { | ||
| * "surfaces": { | ||
| * "<name>": { "<dest-dir>": { "<src-name>": "<dest-name>", ... } } | ||
| * "<name>": { "<dest-dir>": "*" } // "*" mirrors every asset, same names | ||
| * } | ||
| * } | ||
| */ | ||
| import { readdirSync, readFileSync, copyFileSync, mkdirSync, existsSync, statSync } from "node:fs"; | ||
| import { dirname, join, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| const ASSETS = join(dirname(fileURLToPath(import.meta.url)), "..", "assets"); | ||
| function findConfig(start) { | ||
| let dir = resolve(start); | ||
| for (;;) { | ||
| const candidate = join(dir, "brand-assets.config.json"); | ||
| if (existsSync(candidate)) return candidate; | ||
| const parent = dirname(dir); | ||
| if (parent === dir) return null; // hit filesystem root | ||
| dir = parent; | ||
| } | ||
| } | ||
| const configPath = findConfig(process.cwd()); | ||
| if (!configPath) { | ||
| console.error("[sync-assets] no brand-assets.config.json found (searched up from cwd)"); | ||
| process.exit(1); | ||
| } | ||
| const root = dirname(configPath); | ||
| const { surfaces } = JSON.parse(readFileSync(configPath, "utf8")); | ||
| if (!surfaces || typeof surfaces !== "object") { | ||
| console.error(`[sync-assets] ${configPath} has no "surfaces" object`); | ||
| process.exit(1); | ||
| } | ||
| const requested = process.argv.slice(2); | ||
| const groups = requested.length ? requested : Object.keys(surfaces); | ||
| const allAssets = () => | ||
| readdirSync(ASSETS).filter((f) => statSync(join(ASSETS, f)).isFile()); | ||
| let count = 0; | ||
| for (const group of groups) { | ||
| const dirs = surfaces[group]; | ||
| if (!dirs) { | ||
| console.error(`[sync-assets] unknown surface "${group}" — known: ${Object.keys(surfaces).join(", ")}`); | ||
| process.exit(1); | ||
| } | ||
| for (const [destDir, map] of Object.entries(dirs)) { | ||
| const dest = join(root, destDir); | ||
| mkdirSync(dest, { recursive: true }); | ||
| const files = map === "*" | ||
| ? Object.fromEntries(allAssets().map((f) => [f, f])) | ||
| : map; | ||
| for (const [from, to] of Object.entries(files)) { | ||
| copyFileSync(join(ASSETS, from), join(dest, to)); | ||
| count++; | ||
| } | ||
| } | ||
| } | ||
| console.log(`[sync-assets] copied ${count} mark(s) from @protolabsai/design → ${groups.join(", ")}`); |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="protoLabs Icon"> | ||
| <rect x="16" y="16" width="224" height="224" rx="56" fill="#7c3aed" /> | ||
| <rect x="16" y="16" width="224" height="224" rx="56" fill="#9b87f2" /> | ||
| <g transform="translate(224, 32) scale(-8, 8)" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
@@ -4,0 +4,0 @@ <path d="M12 8V4H8" /> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="protoLabs Icon"> | ||
| <g transform="translate(224, 32) scale(-8, 8)" fill="none" stroke="#7c3aed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| <g transform="translate(224, 32) scale(-8, 8)" fill="none" stroke="#9b87f2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| <path d="M12 8V4H8" /> | ||
@@ -4,0 +4,0 @@ <rect width="16" height="12" x="4" y="8" rx="2" /> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="protoLabs Icon"> | ||
| <rect x="16" y="16" width="224" height="224" rx="56" fill="#7c3aed" /> | ||
| <rect x="16" y="16" width="224" height="224" rx="56" fill="#9b87f2" /> | ||
| <g transform="translate(224, 32) scale(-8, 8)" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
@@ -4,0 +4,0 @@ <path d="M12 8V4H8" /> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 80" role="img" aria-label="protoLabs Logo"> | ||
| <!-- Bot icon on rounded-square background --> | ||
| <rect x="4" y="12" width="56" height="56" rx="14" fill="#7c3aed" /> | ||
| <rect x="4" y="12" width="56" height="56" rx="14" fill="#9b87f2" /> | ||
| <g transform="translate(56, 16) scale(-2, 2)" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
@@ -15,3 +15,3 @@ <path d="M12 8V4H8" /> | ||
| <!-- Labs text --> | ||
| <text x="174" y="52" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-weight="700" font-size="36" fill="#a78bfa" letter-spacing="-0.02em">Labs</text> | ||
| <text x="174" y="52" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-weight="700" font-size="36" fill="#b8a6f5" letter-spacing="-0.02em">Labs</text> | ||
| </svg> |
@@ -8,3 +8,3 @@ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="330" viewBox="0 0 1200 330" role="img" aria-label="protoLabs Studio Logo"> | ||
| <!-- Rounded square background --> | ||
| <rect width="200" height="200" rx="50" fill="#7c3aed" /> | ||
| <rect width="200" height="200" rx="50" fill="#9b87f2" /> | ||
@@ -24,4 +24,4 @@ <!-- Bot icon (mirrored) --> | ||
| <text x="345" y="215" font-family="Inter, system-ui, -apple-system, sans-serif" font-size="160" font-weight="800" letter-spacing="-4" fill="#FFFFFF"> | ||
| proto<tspan fill="#a78bfa">Labs</tspan> | ||
| proto<tspan fill="#b8a6f5">Labs</tspan> | ||
| </text> | ||
| </svg> |
+1
-1
@@ -71,3 +71,3 @@ /* @protolabsai/design — base layer. | ||
| /* Subtle violet halo — hero containers + brand moments only. Never on dense UI. */ | ||
| /* Subtle lavender halo — hero containers + brand moments only. Never on dense UI. */ | ||
| .pl-glow { | ||
@@ -74,0 +74,0 @@ box-shadow: var(--pl-shadow-glow); |
+6
-6
| /* GENERATED from src/tokens.js by scripts/build.mjs — do not edit by hand. */ | ||
| :root { | ||
| --pl-color-brand-violet: #7c3aed; | ||
| --pl-color-brand-violet-light: #a78bfa; | ||
| --pl-color-brand-lavender: #9b87f2; | ||
| --pl-color-brand-lavender-light: #b8a6f5; | ||
| --pl-color-brand-indigo: #6366f1; | ||
@@ -17,5 +17,5 @@ --pl-color-brand-indigo-bright: #818cf8; | ||
| --pl-color-status-info: oklch(0.68 0.12 230); | ||
| --pl-gradient-brand: linear-gradient(135deg, #a78bfa 0%, #818cf8 50%, #6366f1 100%); | ||
| --pl-gradient-brand-light: linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #4f46e5 100%); | ||
| --pl-shadow-glow: 0 0 80px rgba(167, 139, 250, 0.08); | ||
| --pl-gradient-brand: linear-gradient(135deg, #9b87f2 0%, #818cf8 50%, #6366f1 100%); | ||
| --pl-gradient-brand-light: linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%); | ||
| --pl-shadow-glow: 0 0 80px rgba(155, 135, 242, 0.08); | ||
| --pl-font-sans: "Geist", system-ui, -apple-system, sans-serif; | ||
@@ -51,4 +51,4 @@ --pl-font-mono: "Geist Mono", ui-monospace, "SF Mono", monospace; | ||
| :root { | ||
| --pl-gradient-brand: linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #4f46e5 100%); | ||
| --pl-gradient-brand: linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%); | ||
| } | ||
| } |
+5
-5
| { | ||
| "color": { | ||
| "brand": { | ||
| "violet": "#7c3aed", | ||
| "violetLight": "#a78bfa", | ||
| "lavender": "#9b87f2", | ||
| "lavenderLight": "#b8a6f5", | ||
| "indigo": "#6366f1", | ||
@@ -23,7 +23,7 @@ "indigoBright": "#818cf8" | ||
| "gradient": { | ||
| "brand": "linear-gradient(135deg, #a78bfa 0%, #818cf8 50%, #6366f1 100%)", | ||
| "brandLight": "linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #4f46e5 100%)" | ||
| "brand": "linear-gradient(135deg, #9b87f2 0%, #818cf8 50%, #6366f1 100%)", | ||
| "brandLight": "linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%)" | ||
| }, | ||
| "shadow": { | ||
| "glow": "0 0 80px rgba(167, 139, 250, 0.08)" | ||
| "glow": "0 0 80px rgba(155, 135, 242, 0.08)" | ||
| }, | ||
@@ -30,0 +30,0 @@ "font": { |
+5
-1
| { | ||
| "name": "@protolabsai/design", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "publishConfig": { | ||
@@ -20,4 +20,8 @@ "access": "public", | ||
| "assets", | ||
| "bin", | ||
| "README.md" | ||
| ], | ||
| "bin": { | ||
| "protolabs-sync-assets": "./bin/sync-assets.mjs" | ||
| }, | ||
| "exports": { | ||
@@ -24,0 +28,0 @@ "./tokens": "./src/tokens.js", |
+27
-1
@@ -44,3 +44,3 @@ # @protolabsai/design | ||
| import { tokens } from "@protolabsai/design/tokens"; | ||
| tokens.color.brand.violet; // "#7c3aed" | ||
| tokens.color.brand.lavender; // "#9b87f2" | ||
| ``` | ||
@@ -55,2 +55,28 @@ | ||
| `assets/` is the **one source of truth** for the brand marks, same as `src/tokens.js` | ||
| is for the values. A browser can't load a favicon from `node_modules`, so each | ||
| surface needs the mark in its own served dir — but those copies are *generated, not | ||
| committed*. This package ships a bin, **`protolabs-sync-assets`**, that copies from | ||
| `assets/` into the served dirs a repo declares; the copies are gitignored, so there's | ||
| nothing to drift. Recolor/replace a mark **here**, re-sync, and every surface follows. | ||
| The fleet pattern — any repo consuming this package: | ||
| 1. Add a `brand-assets.config.json` at the repo root mapping surfaces → dest dirs: | ||
| ```json | ||
| { | ||
| "surfaces": { | ||
| "docs": { "docs/public": { "favicon.svg": "favicon.svg" } }, | ||
| "gallery": { "docs/assets/brand": "*" } | ||
| } | ||
| } | ||
| ``` | ||
| A map of `"*"` mirrors every asset (for a GitHub-rendered gallery, kept committed). | ||
| 2. Prefix builds with the sync: `"docs:build": "protolabs-sync-assets docs && vitepress build docs"`. | ||
| 3. Gitignore the generated served copies; for a committed mirror, guard it in CI with | ||
| `protolabs-sync-assets gallery && git diff --exit-code -- <dir>`. | ||
| The bin finds the config by walking up from cwd, so it works from the repo root or a | ||
| nested app's `prebuild`. See this repo's `brand-assets.config.json` for a live example. | ||
| **Fonts** — Geist + Geist Mono (self-host or): | ||
@@ -57,0 +83,0 @@ |
+6
-5
@@ -17,4 +17,4 @@ /** | ||
| brand: { | ||
| violet: "#7c3aed", // the brand accent | ||
| violetLight: "#a78bfa", // gradient start, hover | ||
| lavender: "#9b87f2", // the brand accent | ||
| lavenderLight: "#b8a6f5", // hover, link-lift | ||
| indigo: "#6366f1", // gradient end, secondary accent | ||
@@ -39,7 +39,8 @@ indigoBright: "#818cf8", // gradient midpoint | ||
| // The one gradient — hero text fills + large brand moments only. Not UI chrome. | ||
| brand: "linear-gradient(135deg, #a78bfa 0%, #818cf8 50%, #6366f1 100%)", | ||
| brandLight: "linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #4f46e5 100%)", | ||
| // Lavender → indigo-bright → indigo. | ||
| brand: "linear-gradient(135deg, #9b87f2 0%, #818cf8 50%, #6366f1 100%)", | ||
| brandLight: "linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%)", | ||
| }, | ||
| shadow: { | ||
| glow: "0 0 80px rgba(167, 139, 250, 0.08)", // subtle violet halo; brand moments only | ||
| glow: "0 0 80px rgba(155, 135, 242, 0.08)", // subtle lavender halo; brand moments only | ||
| }, | ||
@@ -46,0 +47,0 @@ font: { |
@@ -20,5 +20,5 @@ /** | ||
| brand: { | ||
| DEFAULT: c.brand.violet, | ||
| violet: c.brand.violet, | ||
| "violet-light": c.brand.violetLight, | ||
| DEFAULT: c.brand.lavender, | ||
| lavender: c.brand.lavender, | ||
| "lavender-light": c.brand.lavenderLight, | ||
| indigo: c.brand.indigo, | ||
@@ -25,0 +25,0 @@ "indigo-bright": c.brand.indigoBright, |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
1279193
0.34%18
5.88%389
23.1%113
29.89%2
100%