mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
feat(web): consume the brand pack instead of retyping it
The site had been copying brand facts out of a generator that could emit them. The four glyph hexes were hand-typed into globals.css in both themes, and the splash duration sat in a TypeScript constant under a comment asking the next person to keep two repositories in agreement by reading pack.json. That comment was doing the job a build should do. styles/brand.css is now the generated projection, committed here because this repository cannot run the generator, and imported ahead of Tailwind. The four gradients read var(--sg-glyph-*); there are zero hardcoded glyph hexes left in globals.css. hero-splash reads --sg-dur-splash rather than declaring 1337, and the literal that remains is a fallback for a missing stylesheet, announced in the console, not a second copy of the value. The file is held to the pack by lane 3 of check_consumer_projections in such-graphics, default closed behind SG_CONSUMER_ROOT_SUCH_SOFTWARE_SITE. Do not hand-edit it: regenerate with `such-graphics project-brand` and copy the result. Verified in the built stylesheet rather than assumed: 84 --sg-* definitions land, --sg-dur-splash:1337ms among them, and .hero-title compiles to the var() form.
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
/* The generated brand tokens. Source of truth is such-graphics
|
||||||
|
packs/such-software/pack.json; regenerate with `such-graphics project-brand` and copy
|
||||||
|
the result here. Never hand-edit styles/brand.css: the pack gate compares it. */
|
||||||
|
@import "../styles/brand.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -290,10 +295,11 @@
|
|||||||
and the symmetric cubic-bezier easing removes the hard turnaround the old ease-in-out had
|
and the symmetric cubic-bezier easing removes the hard turnaround the old ease-in-out had
|
||||||
at each end of the travel. */
|
at each end of the travel. */
|
||||||
/* The brand's four-colour system (S green, U magenta, C blue, H teal), one per glyph.
|
/* The brand's four-colour system (S green, U magenta, C blue, H teal), one per glyph.
|
||||||
Light theme uses the light palette; .dark swaps in the dark palette, which is the
|
These were four hand-typed hexes per theme, copied out of the pack and free to drift
|
||||||
same hues lifted for contrast on near-black -- from packs/such-software/pack.json. */
|
from it. They now come from the generated tokens above, so the pack is the only place
|
||||||
|
the values exist. */
|
||||||
.hero-title {
|
.hero-title {
|
||||||
background-image: linear-gradient(110deg, #1a5c38 0%, #b83670 30%, #176a92 55%, #12857e 80%, #1a5c38 100%);
|
background-image: linear-gradient(110deg, var(--sg-glyph-s) 0%, var(--sg-glyph-u) 30%, var(--sg-glyph-c) 55%, var(--sg-glyph-h) 80%, var(--sg-glyph-s) 100%);
|
||||||
background-size: 300% auto;
|
background-size: 300% auto;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -301,16 +307,16 @@
|
|||||||
animation: hero-sheen 18s cubic-bezier(0.37, 0, 0.63, 1) infinite;
|
animation: hero-sheen 18s cubic-bezier(0.37, 0, 0.63, 1) infinite;
|
||||||
}
|
}
|
||||||
.dark .hero-title {
|
.dark .hero-title {
|
||||||
background-image: linear-gradient(110deg, #35925c 0%, #e8689e 30%, #4aade0 55%, #3ad4c6 80%, #35925c 100%);
|
background-image: linear-gradient(110deg, var(--sg-glyph-s-dark) 0%, var(--sg-glyph-u-dark) 30%, var(--sg-glyph-c-dark) 55%, var(--sg-glyph-h-dark) 80%, var(--sg-glyph-s-dark) 100%);
|
||||||
}
|
}
|
||||||
.brand-gradient-text {
|
.brand-gradient-text {
|
||||||
background-image: linear-gradient(90deg, #1a5c38, #b83670, #176a92, #12857e);
|
background-image: linear-gradient(90deg, var(--sg-glyph-s), var(--sg-glyph-u), var(--sg-glyph-c), var(--sg-glyph-h));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
.dark .brand-gradient-text {
|
.dark .brand-gradient-text {
|
||||||
background-image: linear-gradient(90deg, #35925c, #e8689e, #4aade0, #3ad4c6);
|
background-image: linear-gradient(90deg, var(--sg-glyph-s-dark), var(--sg-glyph-u-dark), var(--sg-glyph-c-dark), var(--sg-glyph-h-dark));
|
||||||
}
|
}
|
||||||
@keyframes hero-sheen {
|
@keyframes hero-sheen {
|
||||||
0%, 100% { background-position: 0% center; }
|
0%, 100% { background-position: 0% center; }
|
||||||
|
|||||||
@@ -5,8 +5,31 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
// How long the splash holds before it hands over. This MUST match the pack's
|
// How long the splash holds before it hands over. This MUST match the pack's
|
||||||
// motion.splash.ms: the morph asset is generated to end exactly on this number, so a gate
|
// motion.splash.ms: the morph asset is generated to end exactly on this number, so a gate
|
||||||
// that closes early cuts the last letter mid-draw and one that closes late holds a still
|
// that closes early cuts the last letter mid-draw and one that closes late holds a still
|
||||||
// frame. See such-graphics packs/such-software/pack.json.
|
// frame.
|
||||||
const SPLASH_MS = 1337;
|
//
|
||||||
|
// It used to say that, and then hardcode 1337 next to a comment asking the next person to
|
||||||
|
// keep two repositories in agreement by hand. The pack has always been able to emit the
|
||||||
|
// number: it reaches us as `--sg-dur-splash` in the generated brand.css, so now we read it.
|
||||||
|
// The literal below is a fallback for the case where the stylesheet did not load, not a
|
||||||
|
// second copy of the value.
|
||||||
|
const SPLASH_FALLBACK_MS = 1337;
|
||||||
|
|
||||||
|
function splashMs(): number {
|
||||||
|
if (typeof window === "undefined") return SPLASH_FALLBACK_MS;
|
||||||
|
const raw = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--sg-dur-splash")
|
||||||
|
.trim();
|
||||||
|
const value = Number.parseFloat(raw);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) {
|
||||||
|
console.warn(
|
||||||
|
`hero-splash: --sg-dur-splash is ${raw || "unset"}, so the generated brand tokens are ` +
|
||||||
|
`not loaded; holding for ${SPLASH_FALLBACK_MS}ms instead.`,
|
||||||
|
);
|
||||||
|
return SPLASH_FALLBACK_MS;
|
||||||
|
}
|
||||||
|
// CSS times are "1337ms" or "1.337s"; only the latter needs scaling.
|
||||||
|
return raw.endsWith("ms") ? value : value * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
// Long enough to read as a cross-fade, short enough that it does not feel like a second
|
// Long enough to read as a cross-fade, short enough that it does not feel like a second
|
||||||
// gate stacked on the first.
|
// gate stacked on the first.
|
||||||
@@ -27,7 +50,7 @@ const BURST_CELLS = [
|
|||||||
*
|
*
|
||||||
* Four dots become the four pillar symbols become the word SUCH, then the page takes over.
|
* Four dots become the four pillar symbols become the word SUCH, then the page takes over.
|
||||||
* The whole thing is one generated SVG (morph_splash*.svg) whose CSS timeline is scaled to
|
* The whole thing is one generated SVG (morph_splash*.svg) whose CSS timeline is scaled to
|
||||||
* land on SPLASH_MS, so this component only has to hold it and then get out of the way.
|
* land on that duration, so this component only has to hold it and then get out of the way.
|
||||||
*
|
*
|
||||||
* It is a FIXED overlay from its first paint, never a block in document flow. That is not
|
* It is a FIXED overlay from its first paint, never a block in document flow. That is not
|
||||||
* cosmetic: in flow, removing it pulled a viewport of height out of the page and shifted
|
* cosmetic: in flow, removing it pulled a viewport of height out of the page and shifted
|
||||||
@@ -63,7 +86,7 @@ export function HeroSplash({ onEnter, leaving }: { onEnter: () => void; leaving?
|
|||||||
// The gate: hand over when the morph ends.
|
// The gate: hand over when the morph ends.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (leaving) return;
|
if (leaving) return;
|
||||||
const id = window.setTimeout(() => enterRef.current(), SPLASH_MS);
|
const id = window.setTimeout(() => enterRef.current(), splashMs());
|
||||||
return () => window.clearTimeout(id);
|
return () => window.clearTimeout(id);
|
||||||
}, [leaving]);
|
}, [leaving]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/* pack=such-software version=3.0.0 source=02fb904f79a118d7e33d470cbbf51aa1a3ed0123 */
|
||||||
|
:root {
|
||||||
|
/* GENERATED by such_graphics.style.tokens from the style pack -- do not edit */
|
||||||
|
--sg-accent: #e8b23a;
|
||||||
|
--sg-bg: #f7f8fa;
|
||||||
|
--sg-bg-dark: #0a0a0a;
|
||||||
|
--sg-crit: #b83670;
|
||||||
|
--sg-faint: #aab2bf;
|
||||||
|
--sg-field: #1a5c38;
|
||||||
|
--sg-field-dark: #45c99e;
|
||||||
|
--sg-field-hi: #2f8f5e;
|
||||||
|
--sg-field-hi-dark: #7ddfbe;
|
||||||
|
--sg-frame: #1a5c38;
|
||||||
|
--sg-frame-dark: #45c99e;
|
||||||
|
--sg-glyph-c: #176a92;
|
||||||
|
--sg-glyph-c-dark: #4aade0;
|
||||||
|
--sg-glyph-h: #12857e;
|
||||||
|
--sg-glyph-h-dark: #3ad4c6;
|
||||||
|
--sg-glyph-s: #1a5c38;
|
||||||
|
--sg-glyph-s-dark: #35925c;
|
||||||
|
--sg-glyph-u: #b83670;
|
||||||
|
--sg-glyph-u-dark: #e8689e;
|
||||||
|
--sg-ink: #0a0a14;
|
||||||
|
--sg-ink-dark: #f7f8fa;
|
||||||
|
--sg-muted: #5b6472;
|
||||||
|
--sg-ok: #237456;
|
||||||
|
--sg-spark: #0a0a14;
|
||||||
|
--sg-spark-dark: #ffffff;
|
||||||
|
--sg-warn: #b8863a;
|
||||||
|
--sg-font-body: 'Manrope', 'DM Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
--sg-font-display: 'Manrope', 'DM Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
--sg-font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
--sg-fs-body: 13px;
|
||||||
|
--sg-fs-caption: 11px;
|
||||||
|
--sg-fs-label: 12px;
|
||||||
|
--sg-fs-subhead: 15px;
|
||||||
|
--sg-fs-title: 34px;
|
||||||
|
--sg-fs-wordmark: 78px;
|
||||||
|
--sg-dur-draw: 1600ms;
|
||||||
|
--sg-motion-draw-curve-name: ease-in-out;
|
||||||
|
--sg-motion-draw-curve: ease-in-out;
|
||||||
|
--sg-dur-fade: 400ms;
|
||||||
|
--sg-motion-fade-curve-name: ease-out;
|
||||||
|
--sg-motion-fade-curve: ease-out;
|
||||||
|
--sg-dur-frame: 900ms;
|
||||||
|
--sg-motion-frame-curve-name: ease-out;
|
||||||
|
--sg-motion-frame-curve: ease-out;
|
||||||
|
--sg-dur-grow: 520ms;
|
||||||
|
--sg-motion-grow-curve-name: ease-out-back;
|
||||||
|
--sg-motion-grow-curve: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
--sg-dur-morph: 900ms;
|
||||||
|
--sg-motion-morph-curve-name: ease-in-out;
|
||||||
|
--sg-motion-morph-curve: ease-in-out;
|
||||||
|
--sg-dur-pulse: 3000ms;
|
||||||
|
--sg-motion-pulse-curve-name: linear;
|
||||||
|
--sg-motion-pulse-curve: linear;
|
||||||
|
--sg-dur-settle: 420ms;
|
||||||
|
--sg-motion-settle-curve-name: ease-in-out;
|
||||||
|
--sg-motion-settle-curve: ease-in-out;
|
||||||
|
--sg-dur-splash: 1337ms;
|
||||||
|
--sg-motion-splash-curve-name: ease-in-out;
|
||||||
|
--sg-motion-splash-curve: ease-in-out;
|
||||||
|
--sg-dur-symbol-hold: 1100ms;
|
||||||
|
--sg-motion-symbol-hold-curve-name: linear;
|
||||||
|
--sg-motion-symbol-hold-curve: linear;
|
||||||
|
--sg-corner-lg: 24px;
|
||||||
|
--sg-corner-md: 16px;
|
||||||
|
--sg-corner-sm: 8px;
|
||||||
|
--sg-touch-min: 48px;
|
||||||
|
--sg-role-accent-color: var(--sg-glyph-u);
|
||||||
|
--sg-role-accent-font-family: var(--sg-font-display);
|
||||||
|
--sg-role-accent-font-size: var(--sg-fs-subhead);
|
||||||
|
--sg-role-body-color: var(--sg-ink);
|
||||||
|
--sg-role-body-font-family: var(--sg-font-body);
|
||||||
|
--sg-role-body-font-size: var(--sg-fs-body);
|
||||||
|
--sg-role-caption-color: var(--sg-muted);
|
||||||
|
--sg-role-caption-font-family: var(--sg-font-mono);
|
||||||
|
--sg-role-caption-font-size: var(--sg-fs-caption);
|
||||||
|
--sg-role-subtitle-color: var(--sg-muted);
|
||||||
|
--sg-role-subtitle-font-family: var(--sg-font-display);
|
||||||
|
--sg-role-subtitle-font-size: var(--sg-fs-subhead);
|
||||||
|
--sg-role-title-color: var(--sg-ink);
|
||||||
|
--sg-role-title-font-family: var(--sg-font-display);
|
||||||
|
--sg-role-title-font-size: var(--sg-fs-title);
|
||||||
|
--sg-role-wordmark-color: var(--sg-ink);
|
||||||
|
--sg-role-wordmark-font-family: var(--sg-font-display);
|
||||||
|
--sg-role-wordmark-font-size: var(--sg-fs-wordmark);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user