Skip to content

Commit e1994f0

Browse files
authored
Merge pull request #2 from draftlab-org/feature/gradients
Feature/gradients
2 parents 7ed4a8d + bee178d commit e1994f0

9 files changed

Lines changed: 177 additions & 67 deletions

File tree

src/components/molecules/ProjectCard.astro

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import { Image } from 'astro:assets';
33
import draftlabLogo from '@assets/draftlab-logo.svg';
44
import GradientLink from '@components/atoms/GradientLink.astro';
55
import SkillIcon from '@components/atoms/SkillIcon.astro';
6+
import {
7+
getOrderedActivePhases,
8+
getRadialMeshGradient,
9+
type Phase,
10+
} from '@utils/phaseGradient';
611
import type { ImageMetadata } from 'astro';
712
8-
const PHASE_ORDER = ['understand', 'define', 'deliver', 'sustain'] as const;
9-
1013
export interface Props {
1114
name: string;
1215
slug: string;
1316
description: string;
1417
client?: string;
1518
clientLogo?: ImageMetadata;
16-
phases: Array<'understand' | 'define' | 'deliver' | 'sustain'>;
19+
phases: Phase[];
1720
skills?: string[];
1821
projectStatus?: 'active' | 'complete';
1922
image?: ImageMetadata;
@@ -34,23 +37,9 @@ const {
3437
} = Astro.props;
3538
3639
const isActive = projectStatus === 'active';
37-
const phaseSet = new Set(phases);
38-
39-
const phaseLightVar: Record<(typeof PHASE_ORDER)[number], string> = {
40-
understand: 'var(--color-phase-understand-light)',
41-
define: 'var(--color-phase-define-light)',
42-
deliver: 'var(--color-phase-deliver-light)',
43-
sustain: 'var(--color-phase-sustain-light)',
44-
};
4540
46-
const orderedActivePhases = PHASE_ORDER.filter((p) => phaseSet.has(p));
47-
const gradientStops =
48-
orderedActivePhases.length === 0
49-
? ['var(--color-white)', 'var(--color-white)']
50-
: orderedActivePhases.length === 1
51-
? [phaseLightVar[orderedActivePhases[0]], 'var(--color-white)']
52-
: orderedActivePhases.map((p) => phaseLightVar[p]);
53-
const cardGradient = `linear-gradient(var(--phase-gradient-method) var(--phase-gradient-angle), ${gradientStops.join(', ')})`;
41+
const orderedActivePhases = getOrderedActivePhases(phases);
42+
const cardGradient = getRadialMeshGradient(slug, orderedActivePhases, 'light');
5443
---
5544

5645
<a

src/components/organisms/Footer.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const copyrightText = `© ${currentYear} ${siteConfig.title}.`;
3535
const { html: footerBottomHtml } = await renderMarkdown(siteConfig.footer.bottom);
3636
---
3737

38-
<footer class={`gradient-phase text-white ${className}`}>
38+
<footer class={`mesh-phase text-white ${className}`}>
3939
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
4040
<!-- Main footer content: 3 columns on desktop, stack on mobile -->
4141
<div class="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">

src/components/organisms/Hero.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const sizes = {
3232
};
3333
3434
const backgrounds = {
35-
white: 'bg-white/90',
35+
white: 'bg-transparent',
3636
gray: 'bg-gray-50',
3737
gradient: 'bg-gradient-to-br from-blue-50 to-indigo-100',
3838
highlight: ''

src/components/sections/CalEmbedSection.astro

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,65 @@ const {
1616
const containerId = `cal-inline-${namespace}`;
1717
---
1818

19-
<div class="mx-auto -my-6 w-full max-w-5xl sm:-my-8">
20-
<div
21-
id={containerId}
22-
class="w-full overflow-scroll"
23-
style={`min-height:${minHeight}`}
24-
></div>
19+
<div class="mx-auto w-full sm:-my-8">
20+
<div
21+
id={containerId}
22+
class="w-full overflow-scroll"
23+
style={`min-height:${minHeight}`}
24+
>
25+
</div>
2526
</div>
2627

2728
<script
28-
is:inline
29-
type="text/javascript"
30-
define:vars={{ containerId, calLink, namespace, origin }}
29+
is:inline
30+
type="text/javascript"
31+
define:vars={{ containerId, calLink, namespace, origin }}
3132
>
32-
(function (C, A, L) {
33-
let p = function (a, ar) { a.q.push(ar); };
34-
let d = C.document;
35-
C.Cal = C.Cal || function () {
36-
let cal = C.Cal;
37-
let ar = arguments;
38-
if (!cal.loaded) {
39-
cal.ns = {};
40-
cal.q = cal.q || [];
41-
d.head.appendChild(d.createElement("script")).src = A;
42-
cal.loaded = true;
43-
}
44-
if (ar[0] === L) {
45-
const api = function () { p(api, arguments); };
46-
const ns = ar[1];
47-
api.q = api.q || [];
48-
if (typeof ns === "string") {
49-
cal.ns[ns] = cal.ns[ns] || api;
50-
p(cal.ns[ns], ar);
51-
p(cal, ["initNamespace", ns]);
52-
} else {
53-
p(cal, ar);
54-
}
55-
return;
56-
}
57-
p(cal, ar);
58-
};
59-
})(window, origin + "/embed/embed.js", "init");
33+
(function (C, A, L) {
34+
let p = function (a, ar) {
35+
a.q.push(ar);
36+
};
37+
let d = C.document;
38+
C.Cal =
39+
C.Cal ||
40+
function () {
41+
let cal = C.Cal;
42+
let ar = arguments;
43+
if (!cal.loaded) {
44+
cal.ns = {};
45+
cal.q = cal.q || [];
46+
d.head.appendChild(d.createElement('script')).src = A;
47+
cal.loaded = true;
48+
}
49+
if (ar[0] === L) {
50+
const api = function () {
51+
p(api, arguments);
52+
};
53+
const ns = ar[1];
54+
api.q = api.q || [];
55+
if (typeof ns === 'string') {
56+
cal.ns[ns] = cal.ns[ns] || api;
57+
p(cal.ns[ns], ar);
58+
p(cal, ['initNamespace', ns]);
59+
} else {
60+
p(cal, ar);
61+
}
62+
return;
63+
}
64+
p(cal, ar);
65+
};
66+
})(window, origin + '/embed/embed.js', 'init');
6067

61-
Cal("init", namespace, { origin: origin });
68+
Cal('init', namespace, { origin: origin });
6269

63-
Cal.ns[namespace]("inline", {
64-
elementOrSelector: "#" + containerId,
65-
config: { layout: "month_view", useSlotsViewOnSmallScreen: "true" },
66-
calLink: calLink,
67-
});
70+
Cal.ns[namespace]('inline', {
71+
elementOrSelector: '#' + containerId,
72+
config: { layout: 'month_view', useSlotsViewOnSmallScreen: 'true' },
73+
calLink: calLink,
74+
});
6875

69-
Cal.ns[namespace]("ui", { hideEventTypeDetails: false, layout: "month_view" });
76+
Cal.ns[namespace]('ui', {
77+
hideEventTypeDetails: false,
78+
layout: 'month_view',
79+
});
7080
</script>

src/content/pages/get-in-touch.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ description: Book a 30-minute conversation with Draftlab.
44
sections:
55
- type: hero
66
title: Get in touch
7+
background:
8+
bgColor: phase-gradient-light
79
subtitle: Book a 30-minute conversation. We'll listen to where you are, what you're working on, and figure out together whether we're the right fit.
810
- type: calEmbed
911
calLink: book/30min

src/layouts/SectionLayout.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ const backgrounds = {
5959
white: 'bg-white',
6060
gray: 'bg-gray-50',
6161
gradient: 'bg-gradient-to-br from-blue-50 to-indigo-100',
62-
'phase-gradient': 'gradient-phase',
63-
'phase-gradient-light': 'gradient-phase-light',
62+
'phase-gradient': 'mesh-phase',
63+
'phase-gradient-light': 'mesh-phase-light',
6464
};
6565
6666
const bgClass = backgrounds[bgColor as keyof typeof backgrounds] || "";

src/styles/colors.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@
9696
var(--color-phase-understand-dark), var(--color-phase-define-dark),
9797
var(--color-phase-deliver-dark), var(--color-phase-sustain-dark);
9898

99+
/* Mesh-gradient knobs. Drive the `mesh-phase{,-light,-dark}` utilities
100+
in components.css. Position is the blob centre in % of the box;
101+
fade is the radial stop where the blob reaches transparent.
102+
Override at any scope (e.g. inside a section) to retune that
103+
element's mesh without touching the global look. */
104+
--mesh-understand-x: 15%;
105+
--mesh-understand-y: 25%;
106+
--mesh-understand-fade: 60%;
107+
--mesh-define-x: 78%;
108+
--mesh-define-y: 18%;
109+
--mesh-define-fade: 58%;
110+
--mesh-deliver-x: 22%;
111+
--mesh-deliver-y: 82%;
112+
--mesh-deliver-fade: 62%;
113+
--mesh-sustain-x: 85%;
114+
--mesh-sustain-y: 78%;
115+
--mesh-sustain-fade: 60%;
116+
99117
/* Semantic tokens. `surface` is the page-bg colour; body has no explicit
100118
background, so this resolves visually to white. Kept as a token so
101119
`gradient-frame` (which paints surface as the inner of its 2-layer bg)

src/styles/components.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@
1515
background-image: linear-gradient(var(--phase-gradient-method) var(--phase-gradient-angle), var(--phase-gradient-stops-dark));
1616
}
1717

18+
/* Mesh-phase backgrounds: four radial blobs (one per phase) over a solid
19+
floor, driven by the `--mesh-*` knobs in colors.css. Positions and
20+
fade radii are tweakable globally there or per-element via inline
21+
style / a parent rule. The floor fills any region the blobs don't
22+
reach. Use these on full-area backgrounds (footer, full-bleed
23+
sections); for thin lines and bg-clip-text, keep the linear
24+
gradient utilities above. */
25+
@utility mesh-phase {
26+
background:
27+
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand), transparent var(--mesh-understand-fade)),
28+
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define), transparent var(--mesh-define-fade)),
29+
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver), transparent var(--mesh-deliver-fade)),
30+
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain), transparent var(--mesh-sustain-fade)),
31+
var(--color-phase-deliver);
32+
}
33+
34+
@utility mesh-phase-light {
35+
background:
36+
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand-light), transparent var(--mesh-understand-fade)),
37+
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define-light), transparent var(--mesh-define-fade)),
38+
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver-light), transparent var(--mesh-deliver-fade)),
39+
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain-light), transparent var(--mesh-sustain-fade)),
40+
var(--color-white);
41+
}
42+
43+
@utility mesh-phase-dark {
44+
background:
45+
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand-dark), transparent var(--mesh-understand-fade)),
46+
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define-dark), transparent var(--mesh-define-fade)),
47+
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver-dark), transparent var(--mesh-deliver-fade)),
48+
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain-dark), transparent var(--mesh-sustain-fade)),
49+
var(--color-phase-deliver-dark);
50+
}
51+
1852
/* Phase-gradient horizontal rule. 2px tall by default so the four-stop
1953
gradient is actually visible at typical widths. Apply to a span/div with
2054
explicit width (e.g. flex-1 or w-8). For a vertical rule, see below. */

src/utils/phaseGradient.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export const PHASE_ORDER = [
2+
'understand',
3+
'define',
4+
'deliver',
5+
'sustain',
6+
] as const;
7+
8+
export type Phase = (typeof PHASE_ORDER)[number];
9+
10+
export type PhaseVariant = 'light' | 'main' | 'dark';
11+
12+
export function getOrderedActivePhases(slugs: readonly string[]): Phase[] {
13+
const set = new Set(slugs);
14+
return PHASE_ORDER.filter((p): p is Phase => set.has(p));
15+
}
16+
17+
// FNV-1a + xorshift32. Deterministic, zero-alloc, ~10 lines.
18+
function hashStr(s: string): number {
19+
let h = 2166136261;
20+
for (let i = 0; i < s.length; i++) {
21+
h = Math.imul(h ^ s.charCodeAt(i), 16777619);
22+
}
23+
return h >>> 0;
24+
}
25+
26+
function makeRng(seed: number): () => number {
27+
let s = seed || 1;
28+
return () => {
29+
s = Math.imul(s ^ (s >>> 15), 2246822507);
30+
s = Math.imul(s ^ (s >>> 13), 3266489909);
31+
s ^= s >>> 16;
32+
return (s >>> 0) / 4294967296;
33+
};
34+
}
35+
36+
// Per-phase blob centred at a seed-derived random spot, fading to transparent
37+
// over a seed-derived radius. Stacked with a white floor so transparent
38+
// regions resolve to white rather than the parent background.
39+
export function getRadialMeshGradient(
40+
seed: string,
41+
phases: readonly Phase[],
42+
variant: PhaseVariant = 'light',
43+
): string {
44+
if (phases.length === 0) return 'white';
45+
const rand = makeRng(hashStr(seed));
46+
const layers: string[] = [];
47+
for (const phase of phases) {
48+
const x = Math.round(rand() * 100);
49+
const y = Math.round(rand() * 100);
50+
const fade = Math.round(45 + rand() * 30);
51+
layers.push(
52+
`radial-gradient(at ${x}% ${y}%, var(--color-phase-${phase}-${variant}), transparent ${fade}%)`,
53+
);
54+
}
55+
layers.push('white');
56+
return layers.join(', ');
57+
}

0 commit comments

Comments
 (0)