Skip to content

Commit 16c39fb

Browse files
committed
homepage redesign, white-surface refactor, UXD application form
Visual system - swap cream/sand palette for white surface + phase-tint accents (--color-surface, --color-surface-dim); buttons + cards lose rounding - new gradient utilities: gradient-rule[-vertical], gradient-underline (form inputs), gradient-frame (2px gradient border on cards) - brackets refactor: link-brackets → brackets base + brackets-text / brackets-box variants with tunable --bracket-stroke / --bracket-leg - new Eyebrow.tsx atom; new SectionTitle.astro atom unifying Fraunces+brackets headings across all section components Homepage - new section order: skillsCloud → latestUpdates (broadsheet wire on phase-gradient banner) → howWeWork (phases timeline + modalities row) → projectsRoll (4 featured, 2-col, no images) → organisationsRoll → uxdCta - ProjectsRollSection: new hideImages + wide flags (schema + pagesCMS); wide locks card titles at text-3xl across breakpoints - ProjectCard: pulsing green active dot (matches /projects/[slug]); drop the featured ring border Pro bono UXD application form - new UxdApplicationForm.tsx with Brevo submission + Altcha challenge - api/altcha/* + api/apply.ts endpoints; .env.example, Brevo/Altcha deps - new applyForUxd + uxdCta section types (Sections.astro, content.config.ts, .pages.yml); wired to /get-in-touch#apply + homepage Misc - project detail page polish (status colophon, corner-bracket hero frame) - atom + organism updates to clear cream/sand tokens and rounded utilities
1 parent e5b9e46 commit 16c39fb

41 files changed

Lines changed: 1496 additions & 251 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# UXD application form (src/pages/api/apply.ts + /api/altcha/challenge.ts)
2+
3+
# Altcha — privacy-friendly spam check.
4+
# Generate a 256-bit hex string. Same key used by both endpoints.
5+
# openssl rand -hex 32
6+
ALTCHA_HMAC_KEY=
7+
8+
# Brevo — transactional email + (optional) contact list.
9+
# Required: API key with permission to send transactional email.
10+
BREVO_API_KEY=
11+
12+
# Where the application notification is delivered.
13+
BREVO_NOTIFY_TO=hello@draftlab.org
14+
15+
# Verified sender in Brevo. Must be authenticated for the sending domain.
16+
BREVO_NOTIFY_FROM=apply@draftlab.org
17+
BREVO_NOTIFY_FROM_NAME=Draftlab
18+
19+
# Optional. If set (and >0), the applicant's email + name + project
20+
# are upserted into this Brevo contact list. Leave blank to skip.
21+
BREVO_APPLICATIONS_LIST_ID=

.pages.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,16 @@ components:
420420
label: Featured Only
421421
type: boolean
422422
default: false
423+
- name: hideImages
424+
label: Hide Images
425+
type: boolean
426+
default: false
427+
description: Suppress card images so all blocks stay the same size.
428+
- name: wide
429+
label: Wide Cards
430+
type: boolean
431+
default: false
432+
description: Two cards per row in a landscape (wider-than-tall) aspect ratio.
423433

424434
phases_overview_section:
425435
type: object

package-lock.json

Lines changed: 91 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
"@astrojs/netlify": "^7.0.8",
1414
"@astrojs/react": "^5.0.4",
1515
"@astrojs/sitemap": "^3.7.2",
16+
"@getbrevo/brevo": "^5.0.4",
1617
"@headlessui/react": "^2.2.9",
1718
"@stefanprobst/rehype-extract-toc": "^3.0.0",
1819
"@tailwindcss/vite": "^4.1.18",
1920
"@types/canvas-confetti": "^1.9.0",
2021
"@types/react": "^19.2.9",
2122
"@types/react-dom": "^19.2.3",
23+
"altcha": "^3.0.4",
24+
"altcha-lib": "^2.0.3",
2225
"astro": "^6.2.1",
2326
"astro-expressive-code": "^0.41.6",
2427
"canvas-confetti": "^1.9.4",

src/components/atoms/Banner.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
2-
import IconInformationCircle from '~icons/heroicons/information-circle-20-solid';
3-
import IconExclamationTriangle from '~icons/heroicons/exclamation-triangle-20-solid';
42
import IconCheckCircle from '~icons/heroicons/check-circle-20-solid';
3+
import IconExclamationTriangle from '~icons/heroicons/exclamation-triangle-20-solid';
4+
import IconInformationCircle from '~icons/heroicons/information-circle-20-solid';
55
import IconXCircle from '~icons/heroicons/x-circle-20-solid';
66
import IconXMark from '~icons/heroicons/x-mark-20-solid';
77

src/components/atoms/Eyebrow.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { ReactNode } from 'react';
2+
3+
type Tag = 'span' | 'p' | 'div' | 'legend' | 'header' | 'aside';
4+
type Tone = 'muted' | 'subtle' | 'ink' | 'white';
5+
6+
interface Props {
7+
rule?: boolean;
8+
tone?: Tone;
9+
as?: Tag;
10+
className?: string;
11+
children: ReactNode;
12+
}
13+
14+
const TONE_CLASS: Record<Tone, string> = {
15+
muted: 'text-ink-muted',
16+
subtle: 'text-ink-muted/70',
17+
ink: 'text-ink',
18+
white: 'text-white',
19+
};
20+
21+
const TEXT_CLASS = 'font-mono text-[0.6rem] tracking-[0.32em] uppercase';
22+
23+
export function Eyebrow({
24+
rule = false,
25+
tone = 'muted',
26+
as,
27+
className = '',
28+
children,
29+
}: Props) {
30+
const textClass = `${TEXT_CLASS} ${TONE_CLASS[tone]}`;
31+
32+
if (rule) {
33+
const Wrapper = as ?? 'div';
34+
return (
35+
<Wrapper className={`flex items-center gap-3 ${className}`.trim()}>
36+
<span className="block w-8 gradient-rule" aria-hidden="true" />
37+
<span className={textClass}>{children}</span>
38+
</Wrapper>
39+
);
40+
}
41+
42+
const Tag = as ?? 'span';
43+
return <Tag className={`${textClass} ${className}`.trim()}>{children}</Tag>;
44+
}
45+
46+
export default Eyebrow;

src/components/atoms/Link.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const variants = {
2020
nav: 'text-xl',
2121
footer: 'text-white text-sm hover:underline underline-offset-4 transition',
2222
unstyled: '',
23-
brackets: 'link-brackets'
23+
brackets: 'brackets-text'
2424
};
2525
2626
const classes = `${variants[variant]} ${className}`;

src/components/atoms/SectionTitle.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const { title, as: Tag = 'h2', class: className = '', id } = Astro.props;
1212
<Tag
1313
id={id}
1414
class:list={[
15-
'link-brackets is-active inline-block w-fit font-serif text-xl font-bold tracking-tight md:text-2xl',
15+
'brackets-text is-active inline-block w-fit font-serif text-xl font-bold tracking-tight md:text-2xl',
1616
className,
1717
]}
1818
>

src/components/atoms/SkillIcon.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ if (iconData) {
8686
{title && (
8787
<span
8888
aria-hidden="true"
89-
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 -translate-x-1/2 translate-y-1 border border-ink/20 bg-cream px-2 py-1 pb-[0.22rem] font-mono text-[0.65rem] leading-none font-medium tracking-[0.14em] whitespace-nowrap text-ink uppercase opacity-0 shadow-[2px_2px_0_color-mix(in_srgb,var(--color-ink)_10%,transparent)] transition duration-150 ease-out group-focus-within/skill-tooltip:translate-y-0 group-focus-within/skill-tooltip:opacity-100 group-hover/skill-tooltip:translate-y-0 group-hover/skill-tooltip:opacity-100 motion-reduce:translate-y-0 motion-reduce:transition-none"
89+
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 -translate-x-1/2 translate-y-1 border border-ink/20 bg-white px-2 py-1 pb-[0.22rem] font-mono text-[0.65rem] leading-none font-medium tracking-[0.14em] whitespace-nowrap text-ink uppercase opacity-0 shadow-[2px_2px_0_color-mix(in_srgb,var(--color-ink)_10%,transparent)] transition duration-150 ease-out group-focus-within/skill-tooltip:translate-y-0 group-focus-within/skill-tooltip:opacity-100 group-hover/skill-tooltip:translate-y-0 group-hover/skill-tooltip:opacity-100 motion-reduce:translate-y-0 motion-reduce:transition-none"
9090
>
9191
{title}
9292
</span>

src/components/molecules/FilterDropdown.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
ListboxOption,
55
ListboxOptions,
66
} from '@headlessui/react';
7-
import IconChevronDown from '~icons/heroicons/chevron-down-20-solid';
87
import IconCheck from '~icons/heroicons/check-20-solid';
8+
import IconChevronDown from '~icons/heroicons/chevron-down-20-solid';
99

1010
export interface FilterOption {
1111
value: string;
@@ -59,9 +59,9 @@ export default function FilterDropdown({
5959

6060
return (
6161
<div className={`relative ${className}`}>
62-
<label className="mb-1 block text-sm font-medium text-gray-700">
62+
<span className="mb-1 block text-sm font-medium text-gray-700">
6363
{label}
64-
</label>
64+
</span>
6565
<Listbox value={selected || ''} onChange={handleChange}>
6666
{({ open }) => (
6767
<>

0 commit comments

Comments
 (0)