Sign In

@protolabsai/design

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@protolabsai/design - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+78
bin/sync-assets.mjs
#!/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(", ")}`);
+1
-1
<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>

@@ -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);

/* 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%);
}
}
{
"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": {

{
"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",

@@ -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 @@

@@ -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,