@protolabsai/design
Advanced tools
+88
-3
| /* GENERATED from src/tokens.js by scripts/build.mjs — do not edit by hand. */ | ||
| :root { | ||
| color-scheme: dark; | ||
| --pl-color-brand-lavender: #9b87f2; | ||
| --pl-color-brand-lavender-light: #b8a6f5; | ||
| --pl-color-brand-lavender-deep: #5b3fc9; | ||
| --pl-color-brand-indigo: #6366f1; | ||
| --pl-color-brand-indigo-bright: #818cf8; | ||
| --pl-color-brand-indigo-deep: #4f46e5; | ||
| --pl-color-bg: #0a0a0c; | ||
@@ -11,7 +14,14 @@ --pl-color-bg-raised: #131316; | ||
| --pl-color-bg-hover: #19191d; | ||
| --pl-color-bg-inset: #060608; | ||
| --pl-color-overlay: rgba(0, 0, 0, 0.6); | ||
| --pl-color-fg: #ededed; | ||
| --pl-color-fg-muted: #a1a1aa; | ||
| --pl-color-fg-subtle: #71717a; | ||
| --pl-color-fg-on-accent: #0a0a0c; | ||
| --pl-color-border: rgba(255, 255, 255, 0.08); | ||
| --pl-color-border-strong: rgba(255, 255, 255, 0.18); | ||
| --pl-color-overlay: rgba(0, 0, 0, 0.6); | ||
| --pl-color-accent: #9b87f2; | ||
| --pl-color-accent-hover: #b8a6f5; | ||
| --pl-color-accent-fg: #b8a6f5; | ||
| --pl-color-focus: #9b87f2; | ||
| --pl-color-status-success: oklch(0.72 0.13 145); | ||
@@ -51,8 +61,83 @@ --pl-color-status-warning: oklch(0.78 0.12 75); | ||
| /* Light-ground sites: the dark-mode gradient stops wash out, so swap to the | ||
| * saturated variant. The rest of the palette is dark-first by design. */ | ||
| /* Light mode — the user's OS preference drives it. Apps inherit automatically; | ||
| * marketing pins dark with [data-theme="dark"] below (attribute selectors beat | ||
| * this media block on specificity). */ | ||
| @media (prefers-color-scheme: light) { | ||
| :root { | ||
| color-scheme: light; | ||
| --pl-color-bg: #f6f7f9; | ||
| --pl-color-bg-raised: #ffffff; | ||
| --pl-color-bg-subtle: #eceef2; | ||
| --pl-color-bg-hover: #e4e7ec; | ||
| --pl-color-bg-inset: #eef0f4; | ||
| --pl-color-overlay: rgba(20, 20, 30, 0.35); | ||
| --pl-color-fg: #18181b; | ||
| --pl-color-fg-muted: #52525b; | ||
| --pl-color-fg-subtle: #8a8a94; | ||
| --pl-color-fg-on-accent: #ffffff; | ||
| --pl-color-border: rgba(0, 0, 0, 0.10); | ||
| --pl-color-border-strong: rgba(0, 0, 0, 0.20); | ||
| --pl-color-accent: #6366f1; | ||
| --pl-color-accent-hover: #4f46e5; | ||
| --pl-color-accent-fg: #4f46e5; | ||
| --pl-color-focus: #6366f1; | ||
| --pl-color-status-success: oklch(0.62 0.15 150); | ||
| --pl-color-status-warning: oklch(0.74 0.16 92); | ||
| --pl-color-status-error: oklch(0.58 0.20 25); | ||
| --pl-color-status-info: oklch(0.58 0.17 255); | ||
| --pl-gradient-brand: linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%); | ||
| --pl-shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.12); | ||
| } | ||
| } | ||
| /* Explicit theme force — wins over the OS preference above. */ | ||
| :root[data-theme="light"] { | ||
| color-scheme: light; | ||
| --pl-color-bg: #f6f7f9; | ||
| --pl-color-bg-raised: #ffffff; | ||
| --pl-color-bg-subtle: #eceef2; | ||
| --pl-color-bg-hover: #e4e7ec; | ||
| --pl-color-bg-inset: #eef0f4; | ||
| --pl-color-overlay: rgba(20, 20, 30, 0.35); | ||
| --pl-color-fg: #18181b; | ||
| --pl-color-fg-muted: #52525b; | ||
| --pl-color-fg-subtle: #8a8a94; | ||
| --pl-color-fg-on-accent: #ffffff; | ||
| --pl-color-border: rgba(0, 0, 0, 0.10); | ||
| --pl-color-border-strong: rgba(0, 0, 0, 0.20); | ||
| --pl-color-accent: #6366f1; | ||
| --pl-color-accent-hover: #4f46e5; | ||
| --pl-color-accent-fg: #4f46e5; | ||
| --pl-color-focus: #6366f1; | ||
| --pl-color-status-success: oklch(0.62 0.15 150); | ||
| --pl-color-status-warning: oklch(0.74 0.16 92); | ||
| --pl-color-status-error: oklch(0.58 0.20 25); | ||
| --pl-color-status-info: oklch(0.58 0.17 255); | ||
| --pl-gradient-brand: linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%); | ||
| --pl-shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.12); | ||
| } | ||
| :root[data-theme="dark"] { | ||
| color-scheme: dark; | ||
| --pl-color-bg: #0a0a0c; | ||
| --pl-color-bg-raised: #131316; | ||
| --pl-color-bg-subtle: #1c1c22; | ||
| --pl-color-bg-hover: #19191d; | ||
| --pl-color-bg-inset: #060608; | ||
| --pl-color-overlay: rgba(0, 0, 0, 0.6); | ||
| --pl-color-fg: #ededed; | ||
| --pl-color-fg-muted: #a1a1aa; | ||
| --pl-color-fg-subtle: #71717a; | ||
| --pl-color-fg-on-accent: #0a0a0c; | ||
| --pl-color-border: rgba(255, 255, 255, 0.08); | ||
| --pl-color-border-strong: rgba(255, 255, 255, 0.18); | ||
| --pl-color-accent: #9b87f2; | ||
| --pl-color-accent-hover: #b8a6f5; | ||
| --pl-color-accent-fg: #b8a6f5; | ||
| --pl-color-focus: #9b87f2; | ||
| --pl-color-status-success: oklch(0.72 0.13 145); | ||
| --pl-color-status-warning: oklch(0.78 0.12 75); | ||
| --pl-color-status-error: oklch(0.64 0.16 25); | ||
| --pl-color-status-info: oklch(0.68 0.12 230); | ||
| --pl-gradient-brand: linear-gradient(135deg, #9b87f2 0%, #818cf8 50%, #6366f1 100%); | ||
| --pl-shadow-popover: 0 8px 28px rgba(0, 0, 0, 0.5); | ||
| } |
+43
-2
@@ -6,4 +6,6 @@ { | ||
| "lavenderLight": "#b8a6f5", | ||
| "lavenderDeep": "#5b3fc9", | ||
| "indigo": "#6366f1", | ||
| "indigoBright": "#818cf8" | ||
| "indigoBright": "#818cf8", | ||
| "indigoDeep": "#4f46e5" | ||
| }, | ||
@@ -14,7 +16,14 @@ "bg": "#0a0a0c", | ||
| "bgHover": "#19191d", | ||
| "bgInset": "#060608", | ||
| "overlay": "rgba(0, 0, 0, 0.6)", | ||
| "fg": "#ededed", | ||
| "fgMuted": "#a1a1aa", | ||
| "fgSubtle": "#71717a", | ||
| "fgOnAccent": "#0a0a0c", | ||
| "border": "rgba(255, 255, 255, 0.08)", | ||
| "borderStrong": "rgba(255, 255, 255, 0.18)", | ||
| "overlay": "rgba(0, 0, 0, 0.6)", | ||
| "accent": "#9b87f2", | ||
| "accentHover": "#b8a6f5", | ||
| "accentFg": "#b8a6f5", | ||
| "focus": "#9b87f2", | ||
| "status": { | ||
@@ -66,3 +75,35 @@ "success": "oklch(0.72 0.13 145)", | ||
| "linear": "linear" | ||
| }, | ||
| "light": { | ||
| "color": { | ||
| "bg": "#f6f7f9", | ||
| "bgRaised": "#ffffff", | ||
| "bgSubtle": "#eceef2", | ||
| "bgHover": "#e4e7ec", | ||
| "bgInset": "#eef0f4", | ||
| "overlay": "rgba(20, 20, 30, 0.35)", | ||
| "fg": "#18181b", | ||
| "fgMuted": "#52525b", | ||
| "fgSubtle": "#8a8a94", | ||
| "fgOnAccent": "#ffffff", | ||
| "border": "rgba(0, 0, 0, 0.10)", | ||
| "borderStrong": "rgba(0, 0, 0, 0.20)", | ||
| "accent": "#6366f1", | ||
| "accentHover": "#4f46e5", | ||
| "accentFg": "#4f46e5", | ||
| "focus": "#6366f1", | ||
| "status": { | ||
| "success": "oklch(0.62 0.15 150)", | ||
| "warning": "oklch(0.74 0.16 92)", | ||
| "error": "oklch(0.58 0.20 25)", | ||
| "info": "oklch(0.58 0.17 255)" | ||
| } | ||
| }, | ||
| "gradient": { | ||
| "brand": "linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%)" | ||
| }, | ||
| "shadow": { | ||
| "popover": "0 8px 24px rgba(0, 0, 0, 0.12)" | ||
| } | ||
| } | ||
| } |
+1
-1
| { | ||
| "name": "@protolabsai/design", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "publishConfig": { | ||
@@ -5,0 +5,0 @@ "access": "public", |
+81
-18
@@ -6,8 +6,17 @@ /** | ||
| * Nothing downstream (CSS vars, Tailwind preset, docs) is hand-maintained — | ||
| * it all derives from this object, so the spec and the shipped values can't | ||
| * it all derives from these objects, so the spec and the shipped values can't | ||
| * drift apart again (which is exactly what bit us before this package existed). | ||
| * | ||
| * Locked 2026-05-29. Where visual-identity.md (spec) and the shipped site | ||
| * (styles.css) disagreed, the shipped site won: 4px radius, #0a0a0c ground, | ||
| * #ededed foreground, bordered (not filled) primary buttons. | ||
| * Locked 2026-05-29 (dark). Light mode added 2026-06-09. | ||
| * | ||
| * THEMING / WHITE-LABEL | ||
| * -------------------- | ||
| * `tokens` holds the DEFAULT (protoLabs.studio dark) theme. `light` holds the light-mode | ||
| * COLOR overrides (same keys). The build emits: | ||
| * :root { ...dark... } — default | ||
| * @media (prefers-color-scheme: light){:root} — OS preference drives light | ||
| * :root[data-theme="light"|"dark"] — explicit force (beats the media query) | ||
| * Marketing stays dark by setting [data-theme="dark"]; apps inherit OS preference. | ||
| * White-label: a consumer overrides the `--pl-color-*` (esp. accent/bg/fg) under | ||
| * their own scope — our values are the default, theirs win where set. | ||
| */ | ||
@@ -17,18 +26,34 @@ | ||
| color: { | ||
| // ── Brand marks (theme-invariant — the literal brand colors) ── | ||
| brand: { | ||
| lavender: "#9b87f2", // the brand accent | ||
| lavenderLight: "#b8a6f5", // hover, link-lift | ||
| indigo: "#6366f1", // gradient end, secondary accent | ||
| // Lavender leads on dark; indigo leads on light. Each anchor has a | ||
| // light / base / deep step (the deep steps clear AA on white). | ||
| lavender: "#9b87f2", // base — brand accent + dark-mode interactive | ||
| lavenderLight: "#b8a6f5", // hover / link-lift on dark | ||
| lavenderDeep: "#5b3fc9", // deep step — light grounds / white-label | ||
| indigo: "#6366f1", // secondary anchor + light-mode interactive accent | ||
| indigoBright: "#818cf8", // gradient midpoint | ||
| indigoDeep: "#4f46e5", // deep step — light-mode links / gradient end | ||
| }, | ||
| bg: "#0a0a0c", // default dark ground | ||
| bgRaised: "#131316", // cards, slight elevation | ||
| bgSubtle: "#1c1c22", // one step above raised — table stripes, list hover targets | ||
| bgHover: "#19191d", // interactive hover wash on raised/ground surfaces | ||
| fg: "#ededed", // primary text | ||
| fgMuted: "#a1a1aa", // labels, secondary text — lifted one step for legibility on the dark ground | ||
| // ── Surfaces ── | ||
| bg: "#0a0a0c", // default ground | ||
| bgRaised: "#131316", // cards, panels, slight elevation | ||
| bgSubtle: "#1c1c22", // table stripes, list hover targets | ||
| bgHover: "#19191d", // interactive hover wash | ||
| bgInset: "#060608", // sunken — code wells, deep inputs | ||
| overlay: "rgba(0, 0, 0, 0.6)", // modal / drawer scrim | ||
| // ── Text (three tiers + on-accent) ── | ||
| fg: "#ededed", // primary | ||
| fgMuted: "#a1a1aa", // secondary / labels | ||
| fgSubtle: "#71717a", // tertiary / placeholder / disabled | ||
| fgOnAccent: "#0a0a0c", // text/icon on an accent fill | ||
| // ── Lines ── | ||
| border: "rgba(255, 255, 255, 0.08)", // subtle 1px dividers | ||
| borderStrong: "rgba(255, 255, 255, 0.18)", // emphasized borders, buttons | ||
| overlay: "rgba(0, 0, 0, 0.6)", // modal / drawer scrim | ||
| // Status — low-chroma, never neon. They mean something; not decoration. | ||
| // ── Interactive accent (the white-label swap point; = brand lavender) ── | ||
| accent: "#9b87f2", | ||
| accentHover: "#b8a6f5", | ||
| accentFg: "#b8a6f5", // accent as text (links, active labels) on this ground | ||
| focus: "#9b87f2", // focus-visible ring | ||
| // ── Status — low-chroma, never neon. They mean something; not decoration. ── | ||
| status: { | ||
@@ -43,3 +68,2 @@ success: "oklch(0.72 0.13 145)", | ||
| // The one gradient — hero text fills + large brand moments only. Not UI chrome. | ||
| // Lavender → indigo-bright → indigo. | ||
| brand: "linear-gradient(135deg, #9b87f2 0%, #818cf8 50%, #6366f1 100%)", | ||
@@ -50,4 +74,3 @@ brandLight: "linear-gradient(135deg, #7355e8 0%, #6366f1 50%, #4f46e5 100%)", | ||
| glow: "0 0 80px rgba(155, 135, 242, 0.08)", // subtle lavender halo; brand moments only | ||
| // The one UI shadow family — reserved for popovers, command palette, dialogs, | ||
| // drawers, toasts (visual-identity §Geometry). Never on flat content. | ||
| // The one UI shadow family — popovers, command palette, dialogs, drawers, toasts. | ||
| popover: "0 8px 28px rgba(0, 0, 0, 0.5)", | ||
@@ -78,2 +101,42 @@ }, | ||
| /** | ||
| * Light-mode COLOR overrides (apps; marketing stays dark). Same keys as | ||
| * tokens.color, minus `brand` (the marks are invariant). Status colors are | ||
| * darkened for contrast on a light ground; the accent darkens to clear AA on | ||
| * white. Surfaces invert their elevation relation (cards become the lightest). | ||
| */ | ||
| export const light = { | ||
| color: { | ||
| // Cool-neutral surfaces (matching the cool-purple brand), cards the lightest. | ||
| bg: "#f6f7f9", // cool off-white ground | ||
| bgRaised: "#ffffff", // cards / panels — the lightest surface (elevation flips) | ||
| bgSubtle: "#eceef2", // table stripes, list hover targets | ||
| bgHover: "#e4e7ec", // interactive hover wash | ||
| bgInset: "#eef0f4", // sunken — code wells, deep inputs | ||
| overlay: "rgba(20, 20, 30, 0.35)", // slightly cool scrim | ||
| fg: "#18181b", // primary | ||
| fgMuted: "#52525b", // secondary / labels | ||
| fgSubtle: "#8a8a94", // tertiary / placeholder | ||
| fgOnAccent: "#ffffff", // text on the indigo accent fill | ||
| border: "rgba(0, 0, 0, 0.10)", | ||
| borderStrong: "rgba(0, 0, 0, 0.20)", | ||
| // Indigo leads light: base indigo for fills, indigo-deep for links/accent text (AA). | ||
| accent: "#6366f1", | ||
| accentHover: "#4f46e5", | ||
| accentFg: "#4f46e5", | ||
| focus: "#6366f1", | ||
| // Status — used as accents (dots, borders, badge tints), so they read as | ||
| // their hue rather than chase body-text AA. Lifted a couple steps from the | ||
| // over-dark first pass; warning is a true yellow, not amber/brown. | ||
| status: { | ||
| success: "oklch(0.62 0.15 150)", | ||
| warning: "oklch(0.74 0.16 92)", | ||
| error: "oklch(0.58 0.20 25)", | ||
| info: "oklch(0.58 0.17 255)", | ||
| }, | ||
| }, | ||
| gradient: { brand: tokens.gradient.brandLight }, | ||
| shadow: { popover: "0 8px 24px rgba(0, 0, 0, 0.12)" }, | ||
| }; | ||
| export default tokens; |
1287331
0.58%590
46.4%