Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 8 additions & 19 deletions src/components/molecules/ProjectCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { Image } from 'astro:assets';
import draftlabLogo from '@assets/draftlab-logo.svg';
import GradientLink from '@components/atoms/GradientLink.astro';
import SkillIcon from '@components/atoms/SkillIcon.astro';
import {
getOrderedActivePhases,
getRadialMeshGradient,
type Phase,
} from '@utils/phaseGradient';
import type { ImageMetadata } from 'astro';

const PHASE_ORDER = ['understand', 'define', 'deliver', 'sustain'] as const;

export interface Props {
name: string;
slug: string;
description: string;
client?: string;
clientLogo?: ImageMetadata;
phases: Array<'understand' | 'define' | 'deliver' | 'sustain'>;
phases: Phase[];
skills?: string[];
projectStatus?: 'active' | 'complete';
image?: ImageMetadata;
Expand All @@ -34,23 +37,9 @@ const {
} = Astro.props;

const isActive = projectStatus === 'active';
const phaseSet = new Set(phases);

const phaseLightVar: Record<(typeof PHASE_ORDER)[number], string> = {
understand: 'var(--color-phase-understand-light)',
define: 'var(--color-phase-define-light)',
deliver: 'var(--color-phase-deliver-light)',
sustain: 'var(--color-phase-sustain-light)',
};

const orderedActivePhases = PHASE_ORDER.filter((p) => phaseSet.has(p));
const gradientStops =
orderedActivePhases.length === 0
? ['var(--color-white)', 'var(--color-white)']
: orderedActivePhases.length === 1
? [phaseLightVar[orderedActivePhases[0]], 'var(--color-white)']
: orderedActivePhases.map((p) => phaseLightVar[p]);
const cardGradient = `linear-gradient(var(--phase-gradient-method) var(--phase-gradient-angle), ${gradientStops.join(', ')})`;
const orderedActivePhases = getOrderedActivePhases(phases);
const cardGradient = getRadialMeshGradient(slug, orderedActivePhases, 'light');
---

<a
Expand Down
2 changes: 1 addition & 1 deletion src/components/organisms/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const copyrightText = `© ${currentYear} ${siteConfig.title}.`;
const { html: footerBottomHtml } = await renderMarkdown(siteConfig.footer.bottom);
---

<footer class={`gradient-phase text-white ${className}`}>
<footer class={`mesh-phase text-white ${className}`}>
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<!-- Main footer content: 3 columns on desktop, stack on mobile -->
<div class="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
Expand Down
2 changes: 1 addition & 1 deletion src/components/organisms/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const sizes = {
};

const backgrounds = {
white: 'bg-white/90',
white: 'bg-transparent',
gray: 'bg-gray-50',
gradient: 'bg-gradient-to-br from-blue-50 to-indigo-100',
highlight: ''
Expand Down
98 changes: 54 additions & 44 deletions src/components/sections/CalEmbedSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,65 @@ const {
const containerId = `cal-inline-${namespace}`;
---

<div class="mx-auto -my-6 w-full max-w-5xl sm:-my-8">
<div
id={containerId}
class="w-full overflow-scroll"
style={`min-height:${minHeight}`}
></div>
<div class="mx-auto w-full sm:-my-8">
<div
id={containerId}
class="w-full overflow-scroll"
style={`min-height:${minHeight}`}
>
</div>
</div>

<script
is:inline
type="text/javascript"
define:vars={{ containerId, calLink, namespace, origin }}
is:inline
type="text/javascript"
define:vars={{ containerId, calLink, namespace, origin }}
>
(function (C, A, L) {
let p = function (a, ar) { a.q.push(ar); };
let d = C.document;
C.Cal = C.Cal || function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () { p(api, arguments); };
const ns = ar[1];
api.q = api.q || [];
if (typeof ns === "string") {
cal.ns[ns] = cal.ns[ns] || api;
p(cal.ns[ns], ar);
p(cal, ["initNamespace", ns]);
} else {
p(cal, ar);
}
return;
}
p(cal, ar);
};
})(window, origin + "/embed/embed.js", "init");
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement('script')).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const ns = ar[1];
api.q = api.q || [];
if (typeof ns === 'string') {
cal.ns[ns] = cal.ns[ns] || api;
p(cal.ns[ns], ar);
p(cal, ['initNamespace', ns]);
} else {
p(cal, ar);
}
return;
}
p(cal, ar);
};
})(window, origin + '/embed/embed.js', 'init');

Cal("init", namespace, { origin: origin });
Cal('init', namespace, { origin: origin });

Cal.ns[namespace]("inline", {
elementOrSelector: "#" + containerId,
config: { layout: "month_view", useSlotsViewOnSmallScreen: "true" },
calLink: calLink,
});
Cal.ns[namespace]('inline', {
elementOrSelector: '#' + containerId,
config: { layout: 'month_view', useSlotsViewOnSmallScreen: 'true' },
calLink: calLink,
});

Cal.ns[namespace]("ui", { hideEventTypeDetails: false, layout: "month_view" });
Cal.ns[namespace]('ui', {
hideEventTypeDetails: false,
layout: 'month_view',
});
</script>
2 changes: 2 additions & 0 deletions src/content/pages/get-in-touch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ description: Book a 30-minute conversation with Draftlab.
sections:
- type: hero
title: Get in touch
background:
bgColor: phase-gradient-light
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.
- type: calEmbed
calLink: book/30min
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/SectionLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const backgrounds = {
white: 'bg-white',
gray: 'bg-gray-50',
gradient: 'bg-gradient-to-br from-blue-50 to-indigo-100',
'phase-gradient': 'gradient-phase',
'phase-gradient-light': 'gradient-phase-light',
'phase-gradient': 'mesh-phase',
'phase-gradient-light': 'mesh-phase-light',
};

const bgClass = backgrounds[bgColor as keyof typeof backgrounds] || "";
Expand Down
18 changes: 18 additions & 0 deletions src/styles/colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@
var(--color-phase-understand-dark), var(--color-phase-define-dark),
var(--color-phase-deliver-dark), var(--color-phase-sustain-dark);

/* Mesh-gradient knobs. Drive the `mesh-phase{,-light,-dark}` utilities
in components.css. Position is the blob centre in % of the box;
fade is the radial stop where the blob reaches transparent.
Override at any scope (e.g. inside a section) to retune that
element's mesh without touching the global look. */
--mesh-understand-x: 15%;
--mesh-understand-y: 25%;
--mesh-understand-fade: 60%;
--mesh-define-x: 78%;
--mesh-define-y: 18%;
--mesh-define-fade: 58%;
--mesh-deliver-x: 22%;
--mesh-deliver-y: 82%;
--mesh-deliver-fade: 62%;
--mesh-sustain-x: 85%;
--mesh-sustain-y: 78%;
--mesh-sustain-fade: 60%;

/* Semantic tokens. `surface` is the page-bg colour; body has no explicit
background, so this resolves visually to white. Kept as a token so
`gradient-frame` (which paints surface as the inner of its 2-layer bg)
Expand Down
34 changes: 34 additions & 0 deletions src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@
background-image: linear-gradient(var(--phase-gradient-method) var(--phase-gradient-angle), var(--phase-gradient-stops-dark));
}

/* Mesh-phase backgrounds: four radial blobs (one per phase) over a solid
floor, driven by the `--mesh-*` knobs in colors.css. Positions and
fade radii are tweakable globally there or per-element via inline
style / a parent rule. The floor fills any region the blobs don't
reach. Use these on full-area backgrounds (footer, full-bleed
sections); for thin lines and bg-clip-text, keep the linear
gradient utilities above. */
@utility mesh-phase {
background:
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand), transparent var(--mesh-understand-fade)),
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define), transparent var(--mesh-define-fade)),
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver), transparent var(--mesh-deliver-fade)),
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain), transparent var(--mesh-sustain-fade)),
var(--color-phase-deliver);
}

@utility mesh-phase-light {
background:
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand-light), transparent var(--mesh-understand-fade)),
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define-light), transparent var(--mesh-define-fade)),
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver-light), transparent var(--mesh-deliver-fade)),
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain-light), transparent var(--mesh-sustain-fade)),
var(--color-white);
}

@utility mesh-phase-dark {
background:
radial-gradient(at var(--mesh-understand-x) var(--mesh-understand-y), var(--color-phase-understand-dark), transparent var(--mesh-understand-fade)),
radial-gradient(at var(--mesh-define-x) var(--mesh-define-y), var(--color-phase-define-dark), transparent var(--mesh-define-fade)),
radial-gradient(at var(--mesh-deliver-x) var(--mesh-deliver-y), var(--color-phase-deliver-dark), transparent var(--mesh-deliver-fade)),
radial-gradient(at var(--mesh-sustain-x) var(--mesh-sustain-y), var(--color-phase-sustain-dark), transparent var(--mesh-sustain-fade)),
var(--color-phase-deliver-dark);
}

/* Phase-gradient horizontal rule. 2px tall by default so the four-stop
gradient is actually visible at typical widths. Apply to a span/div with
explicit width (e.g. flex-1 or w-8). For a vertical rule, see below. */
Expand Down
57 changes: 57 additions & 0 deletions src/utils/phaseGradient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export const PHASE_ORDER = [
'understand',
'define',
'deliver',
'sustain',
] as const;

export type Phase = (typeof PHASE_ORDER)[number];

export type PhaseVariant = 'light' | 'main' | 'dark';

export function getOrderedActivePhases(slugs: readonly string[]): Phase[] {
const set = new Set(slugs);
return PHASE_ORDER.filter((p): p is Phase => set.has(p));
}

// FNV-1a + xorshift32. Deterministic, zero-alloc, ~10 lines.
function hashStr(s: string): number {
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h = Math.imul(h ^ s.charCodeAt(i), 16777619);
}
return h >>> 0;
}

function makeRng(seed: number): () => number {
let s = seed || 1;
return () => {
s = Math.imul(s ^ (s >>> 15), 2246822507);
s = Math.imul(s ^ (s >>> 13), 3266489909);
s ^= s >>> 16;
return (s >>> 0) / 4294967296;
};
}

// Per-phase blob centred at a seed-derived random spot, fading to transparent
// over a seed-derived radius. Stacked with a white floor so transparent
// regions resolve to white rather than the parent background.
export function getRadialMeshGradient(
seed: string,
phases: readonly Phase[],
variant: PhaseVariant = 'light',
): string {
if (phases.length === 0) return 'white';
const rand = makeRng(hashStr(seed));
const layers: string[] = [];
for (const phase of phases) {
const x = Math.round(rand() * 100);
const y = Math.round(rand() * 100);
const fade = Math.round(45 + rand() * 30);
layers.push(
`radial-gradient(at ${x}% ${y}%, var(--color-phase-${phase}-${variant}), transparent ${fade}%)`,
);
}
layers.push('white');
return layers.join(', ');
}
Loading