diff --git a/cms/public/icon.png b/cms/public/icon.png index d14ecd6..c64b93d 100644 Binary files a/cms/public/icon.png and b/cms/public/icon.png differ diff --git a/cms/public/logo.webp b/cms/public/logo.webp index 307b221..ab69ce7 100644 Binary files a/cms/public/logo.webp and b/cms/public/logo.webp differ diff --git a/cms/src/blocks/PhilosophyBlock.ts b/cms/src/blocks/PhilosophyBlock.ts index 6b8673b..274eddd 100644 --- a/cms/src/blocks/PhilosophyBlock.ts +++ b/cms/src/blocks/PhilosophyBlock.ts @@ -18,7 +18,7 @@ export const PhilosophyBlock: Block = { name: 'title', type: 'text', localized: true, - required: true, + required: false, }, { name: 'text', diff --git a/cms/src/blocks/ServicesBlock.ts b/cms/src/blocks/ServicesBlock.ts index 3baaf8a..c5aa288 100644 --- a/cms/src/blocks/ServicesBlock.ts +++ b/cms/src/blocks/ServicesBlock.ts @@ -29,7 +29,7 @@ export const ServicesBlock: Block = { name: 'title', type: 'text', admin: { - width: '70%', + width: '60%', }, label: { de: 'Titel', @@ -39,19 +39,32 @@ export const ServicesBlock: Block = { required: true, }, { - name: 'icon', + name: 'eyebrow', + type: 'text', + admin: { + width: '20%', + }, + label: { + de: 'Eyebrow-Kürzel', + en: 'Eyebrow tag', + }, + localized: true, + required: true, + }, + { + name: 'visualization', type: 'select', admin: { - width: '30%', + width: '20%', }, label: { - de: 'Icon', - en: 'Icon', + de: 'Visualisierung', + en: 'Visualization', }, options: [ - { label: 'Mobile Phone', value: 'mobile-phone' }, - { label: 'Laptop', value: 'laptop' }, - { label: 'Laptop Code', value: 'laptop-code' }, + { label: { de: 'Mobile App', en: 'Mobile App' }, value: 'mobile' }, + { label: { de: 'Website / CMS', en: 'Website / CMS' }, value: 'web' }, + { label: { de: 'Webanwendung', en: 'Web App' }, value: 'webapp' }, ], required: true, }, @@ -77,6 +90,15 @@ export const ServicesBlock: Block = { relationTo: 'pages', required: true, }, + { + name: 'isHero', + type: 'checkbox', + defaultValue: false, + label: { + de: 'Als Hero anzeigen', + en: 'Show as hero', + }, + }, ], }, ], diff --git a/cms/src/globals/Labels.ts b/cms/src/globals/Labels.ts index 376dd55..613cb41 100644 --- a/cms/src/globals/Labels.ts +++ b/cms/src/globals/Labels.ts @@ -60,18 +60,43 @@ const Labels: GlobalConfig = { ...(['web-app', 'website', 'app', 'seo', 'cms'] satisfies Project['tags']).map((tag) => labelField(tag), ), + labelField('period'), + labelField('tags'), + labelField('customer'), + labelField('testimonials-eyebrow'), + labelField('testimonials-heading'), ], }, { name: 'articles', type: 'group', - fields: [labelField('written-by'), labelField('last-updated-at')], + fields: [ + labelField('written-by'), + labelField('last-updated-at'), + labelField('published-at'), + labelField('tags'), + ], + }, + { + name: 'authors', + type: 'group', + fields: [labelField('profession'), labelField('social')], }, { name: 'testimonials', type: 'group', fields: [labelField('testimonials-title', 'Testimonials section title')], }, + { + name: 'footer', + type: 'group', + fields: [ + labelField('site'), + labelField('social'), + labelField('contact'), + labelField('write-us'), + ], + }, { name: 'not-found-page', type: 'group', diff --git a/cms/src/payload-types.ts b/cms/src/payload-types.ts index 79754af..113c082 100644 --- a/cms/src/payload-types.ts +++ b/cms/src/payload-types.ts @@ -329,9 +329,11 @@ export interface ServicesBlock { services?: | { title: string; - icon: 'mobile-phone' | 'laptop' | 'laptop-code'; + eyebrow: string; + visualization: 'mobile' | 'web' | 'webapp'; description: string; page: string | Page; + isHero?: boolean | null; id?: string | null; }[] | null; @@ -682,7 +684,7 @@ export interface AboutBlock { * via the `definition` "PhilosophyBlock". */ export interface PhilosophyBlock { - title: string; + title?: string | null; text: { root: { type: string; @@ -1366,9 +1368,11 @@ export interface ServicesBlockSelect { | T | { title?: T; - icon?: T; + eyebrow?: T; + visualization?: T; description?: T; page?: T; + isHero?: T; id?: T; }; id?: T; @@ -2010,14 +2014,31 @@ export interface Labels { app: string; seo: string; cms: string; + period: string; + tags: string; + customer: string; + 'testimonials-eyebrow': string; + 'testimonials-heading': string; }; articles: { 'written-by': string; 'last-updated-at': string; + 'published-at': string; + tags: string; + }; + authors: { + profession: string; + social: string; }; testimonials: { 'testimonials-title': string; }; + footer: { + site: string; + social: string; + contact: string; + 'write-us': string; + }; 'not-found-page': { title: string; description: string; @@ -2091,18 +2112,39 @@ export interface LabelsSelect { app?: T; seo?: T; cms?: T; + period?: T; + tags?: T; + customer?: T; + 'testimonials-eyebrow'?: T; + 'testimonials-heading'?: T; }; articles?: | T | { 'written-by'?: T; 'last-updated-at'?: T; + 'published-at'?: T; + tags?: T; + }; + authors?: + | T + | { + profession?: T; + social?: T; }; testimonials?: | T | { 'testimonials-title'?: T; }; + footer?: + | T + | { + site?: T; + social?: T; + contact?: T; + 'write-us'?: T; + }; 'not-found-page'?: | T | { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1727edd..0f9eeb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,9 +159,9 @@ importers: '@astrojs/vercel': specifier: ^8.2.0 version: 8.2.0(astro@5.18.1(@types/node@24.10.1)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.930.0))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.44.0)(sass@1.77.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(next@16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4))(react@19.2.3)(rollup@4.44.0) - '@fontsource-variable/montserrat': - specifier: ^5.2.6 - version: 5.2.6 + '@geist-ui/icons': + specifier: ^1.0.2 + version: 1.0.2(@geist-ui/core@2.3.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@jhb.software/astro-payload-richtext-lexical': specifier: 0.3.2 version: 0.3.2(astro@5.18.1(@types/node@24.10.1)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.930.0))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.44.0)(sass@1.77.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) @@ -183,9 +183,9 @@ importers: cms: specifier: workspace:* version: link:../cms - lucide-astro: - specifier: ^0.519.0 - version: 0.519.0(astro@5.18.1(@types/node@24.10.1)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.930.0))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.44.0)(sass@1.77.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) + geist: + specifier: ^1.7.0 + version: 1.7.0(next@16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4)) payload: specifier: 3.83.0 version: 3.83.0(graphql@16.12.0)(typescript@5.9.3) @@ -1130,8 +1130,17 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@fontsource-variable/montserrat@5.2.6': - resolution: {integrity: sha512-1eHefcz9ZXSMjZPckKCWSYd8rLjUpmzQIsNO8DuyU0IdPvsqletPvSIJmztr0sCU6sX4EFKsSHxJWawjmROp2A==} + '@geist-ui/core@2.3.8': + resolution: {integrity: sha512-OKwGgTA4+fBM41eQbqDoUj4XBycZbYH7Ynrn6LPO5yKX7zeWPu/R7HN3vB4/oHt34VTDQI5sDNb1SirHvNyB5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@geist-ui/icons@1.0.2': + resolution: {integrity: sha512-Npfa0NW6fQ31qw/+iMPWbs1hAcJ/3FqBjSLYgEfITDqy/3TJFpFKeVyK04AC/hTmYTsdNruVYczqPNcham5FOQ==} + peerDependencies: + '@geist-ui/core': '>=1.0.0' + react: '>=16.13.0' '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} @@ -3880,6 +3889,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + geist@1.7.0: + resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==} + peerDependencies: + next: '>=13.2.0' + generic-pool@3.9.0: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} engines: {node: '>= 4'} @@ -4651,12 +4665,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-astro@0.519.0: - resolution: {integrity: sha512-0nXr7K+vPxncPDuEhLJi6Ns+IhAZU4XgoZfQk6oEP5bVqJOMSarbej3j941A5xo01wjbOReMatstKzMCSEf6Ew==} - deprecated: 'Deprecated: Use `@lucide/astro`' - peerDependencies: - astro: '>=2.7.1' - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -7860,7 +7868,16 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@fontsource-variable/montserrat@5.2.6': {} + '@geist-ui/core@2.3.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@geist-ui/icons@1.0.2(@geist-ui/core@2.3.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@geist-ui/core': 2.3.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 '@hono/node-server@1.19.11(hono@4.12.8)': dependencies: @@ -9683,7 +9700,7 @@ snapshots: '@vercel/analytics@1.5.0(next@16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4))(react@19.2.3)': optionalDependencies: - next: 16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4) + next: 16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4) react: 19.2.3 '@vercel/edge@1.2.2': {} @@ -10696,8 +10713,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.2 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1)) @@ -10723,7 +10740,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -10734,18 +10751,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10763,7 +10780,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10774,7 +10791,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11191,6 +11208,10 @@ snapshots: functions-have-names@1.2.3: {} + geist@1.7.0(next@16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4)): + dependencies: + next: 16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4) + generic-pool@3.9.0: {} gensync@1.0.0-beta.2: {} @@ -11954,10 +11975,6 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-astro@0.519.0(astro@5.18.1(@types/node@24.10.1)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.930.0))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.44.0)(sass@1.77.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)): - dependencies: - astro: 5.18.1(@types/node@24.10.1)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.930.0))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.44.0)(sass@1.77.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -12521,6 +12538,32 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@16.2.2(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.4): + dependencies: + '@next/env': 16.2.2 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001786 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.2 + '@next/swc-darwin-x64': 16.2.2 + '@next/swc-linux-arm64-gnu': 16.2.2 + '@next/swc-linux-arm64-musl': 16.2.2 + '@next/swc-linux-x64-gnu': 16.2.2 + '@next/swc-linux-x64-musl': 16.2.2 + '@next/swc-win32-arm64-msvc': 16.2.2 + '@next/swc-win32-x64-msvc': 16.2.2 + '@opentelemetry/api': 1.9.0 + sass: 1.77.4 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -13606,6 +13649,11 @@ snapshots: optionalDependencies: '@babel/core': 7.28.6 + styled-jsx@5.1.6(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + stylis@4.2.0: {} suf-log@2.5.3: diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..982962d --- /dev/null +++ b/roadmap.md @@ -0,0 +1,22 @@ +# May 2026 Website Rework - Roadmap + +Phases are strictly sequential — Phase 2 starts only after Phase 1 ships. No fixed launch date. + +## Phase 1 — Visual redesign + +New corporate design and identity. See `design.md` for the full visual spec. + +- [x] Migrate all web components to the new design +- [x] Write `design.md` +- [x] Audit every page at sm/md/lg breakpoints and fix broken layouts +- [x] Design new logo, then ship the SVG and replace all usages in `web/` and `cms/` +- [x] Address existing `TODO` comments for CMS collections and blocks + - [x] While doing this, deduplicate and extract shared abstractions in files touched by the redesign (Heading, MetaStrip, Eyebrow are precedents) +- [ ] Create a system for dynamic OG images based on the pages hero frame + the company logo +- [ ] Specify a subtle background pattern/texture for outside of the frame boxes in `design.md`, then implement across the site + +## Phase 2 — Writing + +- [ ] Write a tonality guide for humans and the CMS agent chat (rules, examples, do/don't pairs); expose it as context in the agent chat +- [ ] Rewrite copy on every existing page to match the tonality guide (existing pages only, no new pages) +- [ ] Rewrite all existing articles (including German) — AI drafts, manual edit pass diff --git a/web/design.md b/web/design.md new file mode 100644 index 0000000..48fcb21 --- /dev/null +++ b/web/design.md @@ -0,0 +1,210 @@ +--- +version: alpha +name: JHB Software +description: > + Deep primary for gravitas, a single tertiary accent for every interactive + job, heavy mono for labels and code, hairline framing instead of cards. + Token names are role-based; underlying values can change without renaming. +colors: + primary: "#0a2540" + primary-deep: "#050e1f" + primary-hover: "#14365c" + primary-soft: "#e8eef7" + tertiary: "#0f766e" + tertiary-soft: "#a8d4c8" + neutral: "#0a0a0a" + neutral-muted: "#404040" + neutral-subtle: "#737373" + neutral-faint: "#a3a3a3" + canvas: "#fafafa" + surface: "#ffffff" + border: "#ececec" + border-strong: "#d4d4d4" +typography: + hero: + fontFamily: Geist + fontSize: 72px + fontWeight: 600 + lineHeight: 1.05 + letterSpacing: -0.025em + h1: + fontFamily: Geist + fontSize: 56px + fontWeight: 600 + lineHeight: 1.05 + letterSpacing: -0.025em + h2: + fontFamily: Geist + fontSize: 44px + fontWeight: 600 + lineHeight: 1.05 + letterSpacing: -0.025em + h3: + fontFamily: Geist + fontSize: 32px + fontWeight: 600 + lineHeight: 1.1 + letterSpacing: -0.025em + h4: + fontFamily: Geist + fontSize: 24px + fontWeight: 600 + lineHeight: 1.2 + body-lg: + fontFamily: Geist + fontSize: 18px + fontWeight: 400 + lineHeight: 1.55 + body-md: + fontFamily: Geist + fontSize: 16px + fontWeight: 400 + lineHeight: 1.55 + body-sm: + fontFamily: Geist + fontSize: 14px + fontWeight: 400 + lineHeight: 1.55 + eyebrow: + fontFamily: Geist Mono + fontSize: 12px + fontWeight: 500 + lineHeight: 1.4 + letterSpacing: 0.12em +rounded: + none: 0 + xs: 2px + sm: 4px + md: 6px + lg: 12px +spacing: + page-max-width: 1200px +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.surface}" + rounded: "{rounded.none}" + padding: 12px 20px + button-primary-hover: + backgroundColor: "{colors.primary-hover}" + button-ghost: + backgroundColor: transparent + textColor: "{colors.neutral}" + rounded: "{rounded.none}" + padding: 12px 20px + link-prose: + textColor: "{colors.neutral}" + link-prose-hover: + textColor: "{colors.tertiary}" + corner-mark: + textColor: "{colors.tertiary}" + eyebrow: + textColor: "{colors.neutral-subtle}" + typography: "{typography.eyebrow}" +--- + +# JHB Software — Design + +Tokens above are normative. Prose below is rationale. + +## Overview + +Modern engineering craft for CEOs, CTOs, and product owners needing custom +software. Reads as **considered engineering**, not "agency template". + +## Colors + +- `primary` — brand anchor: H1, primary buttons, mark, footer. Never body. +- `tertiary` — interactive only: focus, link hover, corner marks, `\`. Never + body. Never a fill larger than ~40px. Currently teal; orange `#ff5c00` + kept commented in `styles.css` for A/B. +- `neutral` scale — text & icons on light surfaces. +- `canvas` (page), `surface` (raised), `border` (default), `border-strong` + (rules, underlines). + +## Typography + +Geist Sans + Geist Mono, self-hosted. **Mono** for eyebrows, labels, code +chips, breadcrumb separators, file paths, stats numerals. **Sans** for +everything else. + +## Layout + +Site-wide max width `1200px` (`--container-page`). Sections set their own +internal grid. Generous vertical rhythm. + +## Elevation & Depth + +Flat color + hairlines. No shadows, no gradients. Depth comes from borders, +frames, and corner marks. + +## Shapes + +Sharp edges. `none` is the only widely used radius. Avatars are true circles. + +## Components + +### `` + `` + +One outer hairline frame with shared interior dividers — not floating cards +with gaps. Use for any 2+ related cells. Hover a cell → cell bg +`tertiary-soft`; adjacent outer edge `tertiary`. No translate, no scale. + +### `` + +Four `+` registration marks (14px, 1px, `tertiary`) at the inner corners of +hero, key sections, footer top. One pair per block. + +### Mono eyebrow + +`\ NN / LABEL` above every major heading. Mono, `text-xs`, uppercase, +tracking `0.12em`, `text-neutral-subtle`. Leading `\` is `tertiary`. Heading: +sans 600, `primary`. + +### The `\` thread + +Always mono + `tertiary`. Locations: breadcrumbs, language switcher (`DE \ EN`), +code chip prefix (`\ typescript`), eyebrows, nav-hover indicator. Never in +body prose. Never decorative. + +### Buttons & links + +- **Primary button** — filled `primary`, `surface` text. Hover → `primary-hover`. +- **Ghost button** — transparent, 1px `border-strong`, `neutral` text. Hover + → `tertiary` text. +- **In-prose link** — underlined `decoration-border-strong`. Hover → + `tertiary` text + decoration. +- **Tile / card** — the whole tile is the link. Affordance: Geist `arrow-right`, + `tertiary` on hover. No "Read more" text. + +### Imagery + +16:9 with hairline frame for project screenshots and article covers; 1:1 for +headshots; schematic SVGs are strokes-only in `primary` + `tertiary`. All via +the `Img` component, `loading="lazy"` below the fold. + +### Iconography + +One library: **Geist Icons**. Thin outlined strokes, `currentColor`, never +filled, never two-tone. If a concept isn't in Geist, pick the closest glyph. + +- Tile / CTA affordance → `arrow-right` +- Disclosure → `chevron-*` +- Contact / messaging → `messageSquare` (also WhatsApp fallback) +- Service categories → literal Geist glyphs (`smartphone`, `monitor`, `code`) + +### Motion + +≤200ms, ease `cubic-bezier(0.4, 0, 0.2, 1)`. Respect `prefers-reduced-motion`. +Catalog (don't add motion outside this list): `header-collapse`, `nav-hover`, +`link-underline`, `card-border`, `cta-arrow-nudge`, `eyebrow-counter`, +`corner-mark-draw`. + +## Do's and Don'ts + +- No new color values, font sizes, or arbitrary Tailwind values. Missing + scale = the design is wrong, not the scale. +- No rounded corners (except avatar circles). No shadows. No gradients. +- No second accent color — use the `neutral` scale. +- No emoji, stock photography, hero illustrations, or icons outside Geist. +- No "Read more" / "Mehr erfahren" / "Click here" — whole tile is the link. diff --git a/web/package.json b/web/package.json index eb787b0..21c999c 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/vercel": "^8.2.0", - "@fontsource-variable/montserrat": "^5.2.6", + "@geist-ui/icons": "^1.0.2", "@jhb.software/astro-payload-richtext-lexical": "0.3.2", "@payloadcms/live-preview": "3.83.0", "@payloadcms/sdk": "3.83.0", @@ -22,7 +22,7 @@ "astro": "^5.10.0", "astro-seo-schema": "^5.0.0", "cms": "workspace:*", - "lucide-astro": "^0.519.0", + "geist": "^1.7.0", "payload": "3.83.0", "schema-dts": "^1.1.5" }, diff --git a/web/public/favicon.ico b/web/public/favicon.ico index 042c91f..b126315 100644 Binary files a/web/public/favicon.ico and b/web/public/favicon.ico differ diff --git a/web/public/fonts/Geist-Variable.woff2 b/web/public/fonts/Geist-Variable.woff2 new file mode 100644 index 0000000..b2f0121 Binary files /dev/null and b/web/public/fonts/Geist-Variable.woff2 differ diff --git a/web/public/fonts/GeistMono-Variable.woff2 b/web/public/fonts/GeistMono-Variable.woff2 new file mode 100644 index 0000000..dbdb8c2 Binary files /dev/null and b/web/public/fonts/GeistMono-Variable.woff2 differ diff --git a/web/public/icon.png b/web/public/icon.png index d14ecd6..c64b93d 100644 Binary files a/web/public/icon.png and b/web/public/icon.png differ diff --git a/web/public/icon.svg b/web/public/icon.svg index 8451875..8750c79 100644 --- a/web/public/icon.svg +++ b/web/public/icon.svg @@ -1,21 +1,6 @@ - - - - - - - - - - - - - - - - - - - - + + + JHB diff --git a/web/public/logo.webp b/web/public/logo.webp index 307b221..d1edea3 100644 Binary files a/web/public/logo.webp and b/web/public/logo.webp differ diff --git a/web/public/og_de.png b/web/public/og_de.png index cba1083..ba89bc4 100644 Binary files a/web/public/og_de.png and b/web/public/og_de.png differ diff --git a/web/public/og_en.png b/web/public/og_en.png index ddc68a1..e926c8b 100644 Binary files a/web/public/og_en.png and b/web/public/og_en.png differ diff --git a/web/src/components/ArticleTagsList.astro b/web/src/components/ArticleTagsList.astro index ea18145..a75183d 100644 --- a/web/src/components/ArticleTagsList.astro +++ b/web/src/components/ArticleTagsList.astro @@ -1,5 +1,6 @@ --- import type { Article } from 'cms/src/payload-types' +import TagsList from './TagsList.astro' type Props = { tags: Article['tags'] @@ -8,15 +9,7 @@ type Props = { const { tags, class: className } = Astro.props -const articleTags = tags.filter((tag) => typeof tag === 'object') +const resolved = tags.filter((tag) => typeof tag === 'object').map((tag) => tag.name) --- -
- { - articleTags?.map((tag) => ( - - {tag.name} - - )) - } -
+ diff --git a/web/src/components/Breadcrumbs.astro b/web/src/components/Breadcrumbs.astro index 8628e1d..a3b392e 100644 --- a/web/src/components/Breadcrumbs.astro +++ b/web/src/components/Breadcrumbs.astro @@ -2,7 +2,6 @@ import { breadcrumbsSchema } from '@/schema/breadcrumbs' import { Schema } from 'astro-seo-schema' import type { Breadcrumbs } from 'cms/src/payload-types' -import Icon from './icons/Icon.astro' import Link from './Link.astro' type Props = { @@ -19,14 +18,14 @@ const schema = breadcrumbsSchema(crumbs) { displayMode === 'all' && (