465eb05aaf
twenty-website had accumulated structural problems that were cheaper to fix by rebuilding than to refactor in place: - Drift had no guardrails. Values were matched at call sites instead of single-sourced, so things silently diverged — e.g. the radius token base was wrong for days (every radius() consumer rendered double) because nothing measured it against the old site's CSS variables. - A whole tree escaped quality checks. src/lib/ (~9.8k lines) was never format-checked, because oxfmt silently ignores directories named lib/. - Inconsistent rhythm. Hero spacing varied 24–88px between pages (CEO-flagged), because section spacing wasn't a token. - Over-extraction. -config.ts sprawl pulled single-component configuration out into the wrong place. The goal: a ground-up rebuild where drift is structurally impossible, held to a Linear / Railway / Notion / Attio quality bar. The old site is treated as source of intent only — nothing is blindly ported; every piece is re-decided and A/B-verified. **Rebuild** A full rebuild on Next 16 + Turbopack + Linaria (zero-runtime CSS), ~1,100 files. Marketing pages (home, product, pricing, partners + marketplace, customers/case-studies, why-twenty, releases, legal), the interactive AppPreview product mockup, the platform/visuals WebGL system (engine + rigs, three code-split off every initial chunk), and the standalone /halftone studio (the dev tool that generates the site's halftone art — engine, exporters, and full UI ported as an isolated island). **Architecture & guarantees** - Parity by construction. src/tokens/definitions.ts is the only file with raw values; the :root CSS-variable block is generated from it at build time and accessors derive var names through the same helpers — derived alpha tokens appear in served CSS without ever being hand-written. - Mobile-first by API shape. mediaUp() is the only media helper (no max-width helper exists, on purpose). - Section rhythm is a token (RHYTHM.section) — the hero-spacing inconsistency class is fixed by construction. - Fluid type ramps interpolate font-size and line-height between designed endpoints [390px → md]; TYPE_SCALE is the single source. - three.js never enters an initial chunk — confined to platform/visuals heavy zones, reached only via dynamic(ssr:false), enforced by check-visual-bundle.
91 lines
2.2 KiB
TypeScript
91 lines
2.2 KiB
TypeScript
import { type PartnerCountryValue } from './data/partner-country-options';
|
|
import { type PartnerLanguageValue } from './data/partner-language-options';
|
|
import { type PartnerScopeValue } from './data/partner-scope-options';
|
|
import { type PartnerTeamTypeValue } from './data/partner-team-type-options';
|
|
|
|
export type CountryFieldValue = PartnerCountryValue | '';
|
|
|
|
export type PartnerApplicationState = {
|
|
stepIndex: number;
|
|
|
|
// Identity
|
|
name: string;
|
|
email: string;
|
|
company: string;
|
|
website: string;
|
|
|
|
// Profile
|
|
linkedin: string;
|
|
city: string;
|
|
country: CountryFieldValue;
|
|
languages: PartnerLanguageValue[];
|
|
|
|
// Expertise & experience
|
|
typeOfTeam: PartnerTeamTypeValue | '';
|
|
partnerScope: PartnerScopeValue[];
|
|
skills: string[];
|
|
applicationNotes: string;
|
|
|
|
// Commercials
|
|
hourlyRate: string;
|
|
projectBudgetMin: string;
|
|
calendarLink: string;
|
|
|
|
// Meta
|
|
fieldErrors: Partial<Record<string, string>>;
|
|
submitError: string | null;
|
|
isSubmitting: boolean;
|
|
isSubmitted: boolean;
|
|
};
|
|
|
|
// The scalar (string-valued) fields a single SET_FIELD action can target;
|
|
// multi-value fields (languages, partnerScope, skills) have their own actions.
|
|
export type ScalarFieldName =
|
|
| 'name'
|
|
| 'email'
|
|
| 'company'
|
|
| 'website'
|
|
| 'linkedin'
|
|
| 'city'
|
|
| 'country'
|
|
| 'typeOfTeam'
|
|
| 'applicationNotes'
|
|
| 'hourlyRate'
|
|
| 'projectBudgetMin'
|
|
| 'calendarLink';
|
|
|
|
export type PartnerApplicationAction =
|
|
| { type: 'SET_FIELD'; field: ScalarFieldName; value: string }
|
|
| { type: 'TOGGLE_SCOPE'; value: PartnerScopeValue }
|
|
| { type: 'TOGGLE_LANGUAGE'; value: PartnerLanguageValue }
|
|
| { type: 'SET_SKILLS'; value: string[] }
|
|
| { type: 'GO_NEXT' }
|
|
| { type: 'GO_BACK' }
|
|
| { type: 'SET_SUBMITTING'; value: boolean }
|
|
| { type: 'SET_SUBMIT_ERROR'; value: string | null }
|
|
| { type: 'SET_SUBMITTED' }
|
|
| { type: 'RESET' };
|
|
|
|
export const INITIAL_PARTNER_APPLICATION_STATE: PartnerApplicationState = {
|
|
stepIndex: 0,
|
|
name: '',
|
|
email: '',
|
|
company: '',
|
|
website: '',
|
|
linkedin: '',
|
|
city: '',
|
|
country: '',
|
|
languages: [],
|
|
typeOfTeam: '',
|
|
partnerScope: [],
|
|
skills: [],
|
|
applicationNotes: '',
|
|
hourlyRate: '',
|
|
projectBudgetMin: '',
|
|
calendarLink: '',
|
|
fieldErrors: {},
|
|
submitError: null,
|
|
isSubmitting: false,
|
|
isSubmitted: false,
|
|
};
|