diff options
| author | Leitwolf | 2026-10-23 13:01:49 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-23 13:01:49 +0300 |
| commit | f22418235a0a87bc05b3bf9079fcdefba002d3b2 (patch) | |
| tree | 603c9323bbd1e7706f9d0b9dc0953f42b1809727 | |
| parent | ba89b398976f4429338673afc1b6e961e995089f (diff) | |
| parent | 85bdf1aba26641bf90da84fd6ce642ac41a6e73e (diff) | |
| download | progmaweb-f22418235a0a87bc05b3bf9079fcdefba002d3b2.tar.gz | |
Merge pull request #46 from Progmasoft/feature/progmasoft-site
Redesign progmasoft.com; add product.progmasoft.com, ides.progmasoft.com/xide and catch3.progmasoft.com
26 files changed, 2757 insertions, 454 deletions
diff --git a/apps/web/app/catch3/layout.tsx b/apps/web/app/catch3/layout.tsx new file mode 100644 index 0000000..de7e946 --- /dev/null +++ b/apps/web/app/catch3/layout.tsx @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { ReactNode } from "react"; +import "../site.css"; + +export default function Catch3Layout({ + children, +}: Readonly<{ children: ReactNode }>) { + return children; +} diff --git a/apps/web/app/catch3/page.tsx b/apps/web/app/catch3/page.tsx new file mode 100644 index 0000000..eec2772 --- /dev/null +++ b/apps/web/app/catch3/page.tsx @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { Metadata } from "next"; +import { Footer } from "@/components/Footer"; +import { SiteHeader } from "@/components/SiteHeader"; +import { getLocale } from "@/lib/locale.server"; +import { products } from "@/lib/products"; +import { getSitesMessages } from "@/lib/product-sites"; + +export async function generateMetadata(): Promise<Metadata> { + const { catch3 } = getSitesMessages(await getLocale()); + return { + title: `Catch3 · ${catch3.tagline}`, + description: catch3.description, + alternates: { canonical: "https://catch3.progmasoft.com/" }, + }; +} + +// The example of the Catch3 documentation (docs/progmasoft-properties.md). +const example = `#include <Progmasoft/Catch3.hpp> + +using namespace Progmasoft::Catch3; + +TEST_CASE( "reversing twice gives the original", "[property]" ) { + CATCH3_CHECK_FOR_ALL( + Gen::Vectors( Gen::Integers<int>() ), + []( const std::vector<int>& values ) { + auto copy = values; + std::reverse( copy.begin(), copy.end() ); + std::reverse( copy.begin(), copy.end() ); + return copy == values; + } ); +}`; + +/** catch3.progmasoft.com: the site of Catch3. */ +export default async function Catch3Page() { + const locale = await getLocale(); + const { catch3, common } = getSitesMessages(locale); + return ( + <div className="pm-page"> + <SiteHeader locale={locale} /> + <main> + <section className="pm-hero"> + <div className="pm-shell pm-hero-inner"> + <div> + <p className="eyebrow">{catch3.eyebrow}</p> + <h1> + Catch3 <span>{catch3.tagline}</span> + </h1> + <p className="pm-lede">{catch3.description}</p> + <div className="pm-actions"> + <a className="button" href={products.catch3.source}> + {common.source} + </a> + <a + className="button button-secondary" + href="https://product.progmasoft.com/catch3" + > + {common.productPage} + </a> + </div> + <p className="pm-note">{catch3.disclaimer}</p> + </div> + <div> + <p className="eyebrow">{catch3.exampleTitle}</p> + <pre className="pm-code pm-code-light" dir="ltr"> + <code>{example}</code> + </pre> + </div> + </div> + </section> + + <section className="pm-section"> + <div className="pm-shell"> + <div className="pm-heading"> + <h2>{catch3.featuresTitle}</h2> + </div> + <div className="pm-bento"> + {catch3.features.map(([title, description]) => ( + <article className="pm-card" key={title}> + <h3>{title}</h3> + <p>{description}</p> + </article> + ))} + </div> + </div> + </section> + + <section className="pm-section pm-section-muted"> + <div className="pm-shell"> + <div className="pm-heading"> + <h2>{catch3.roadmapTitle}</h2> + </div> + <ul className="pm-features pm-roadmap"> + {catch3.roadmap.map((item) => ( + <li key={item}>{item}</li> + ))} + </ul> + </div> + </section> + </main> + <Footer locale={locale} /> + </div> + ); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 3a27983..c405f30 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1415,3 +1415,11 @@ a { direction: ltr; unicode-bidi: isolate; } + +/* The theme changes in one frame; nothing animates its colours meanwhile. */ +.theme-switching, +.theme-switching *, +.theme-switching *::before, +.theme-switching *::after { + transition: none !important; +} diff --git a/apps/web/app/ides/layout.tsx b/apps/web/app/ides/layout.tsx new file mode 100644 index 0000000..8b1c6b7 --- /dev/null +++ b/apps/web/app/ides/layout.tsx @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { ReactNode } from "react"; +import "../site.css"; + +export default function IdesLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + return children; +} diff --git a/apps/web/app/ides/page.tsx b/apps/web/app/ides/page.tsx new file mode 100644 index 0000000..3a832a8 --- /dev/null +++ b/apps/web/app/ides/page.tsx @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { Metadata } from "next"; +import { Footer } from "@/components/Footer"; +import { SiteHeader } from "@/components/SiteHeader"; +import { getLocale } from "@/lib/locale.server"; +import { forwardArrow } from "@/lib/localization"; +import { getCatalogMessages, products } from "@/lib/products"; +import { getSitesMessages } from "@/lib/product-sites"; + +export async function generateMetadata(): Promise<Metadata> { + const { ides } = getSitesMessages(await getLocale()); + return { + title: ides.title, + description: ides.description, + alternates: { canonical: "https://ides.progmasoft.com/" }, + }; +} + +/** ides.progmasoft.com: the development environments of Progmasoft. */ +export default async function IdesPage() { + const locale = await getLocale(); + const { ides } = getSitesMessages(locale); + const xide = getCatalogMessages(locale).products.xide; + return ( + <div className="pm-page"> + <SiteHeader locale={locale} /> + <main> + <section className="pm-hero"> + <div className="pm-shell pm-product-hero"> + <h1>{ides.title}</h1> + <p className="pm-lede">{ides.description}</p> + </div> + </section> + <section className="pm-section"> + <div className="pm-shell pm-product-grid"> + <a className="pm-card pm-product-card" href="/xide"> + <span className={`pm-accent pm-accent-${products.xide.accent}`} /> + <span className="pm-tag">{xide.category}</span> + <h3>{products.xide.name}</h3> + <p>{xide.tagline}</p> + <span className="pm-more"> + {ides.open} {forwardArrow(locale)} + </span> + </a> + </div> + </section> + </main> + <Footer locale={locale} /> + </div> + ); +} diff --git a/apps/web/app/ides/xide/page.tsx b/apps/web/app/ides/xide/page.tsx new file mode 100644 index 0000000..86022e0 --- /dev/null +++ b/apps/web/app/ides/xide/page.tsx @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { Metadata } from "next"; +import { Footer } from "@/components/Footer"; +import { SiteHeader } from "@/components/SiteHeader"; +import { getLocale } from "@/lib/locale.server"; +import { products } from "@/lib/products"; +import { getSitesMessages } from "@/lib/product-sites"; + +export async function generateMetadata(): Promise<Metadata> { + const { xide } = getSitesMessages(await getLocale()); + return { + title: `Xide · ${xide.tagline}`, + description: xide.description, + alternates: { canonical: "https://ides.progmasoft.com/xide" }, + }; +} + +/** ides.progmasoft.com/xide: the site of Xide. */ +export default async function XidePage() { + const locale = await getLocale(); + const { xide, common } = getSitesMessages(locale); + return ( + <div className="pm-page"> + <SiteHeader locale={locale} /> + <main> + <section className="pm-hero"> + <div className="pm-shell pm-hero-inner"> + <div> + <p className="eyebrow">{xide.eyebrow}</p> + <h1> + Xide <span>{xide.tagline}</span> + </h1> + <p className="pm-lede">{xide.description}</p> + <div className="pm-actions"> + <a className="button" href={products.xide.source}> + {common.source} + </a> + <a + className="button button-secondary" + href="https://product.progmasoft.com/xide" + > + {common.productPage} + </a> + </div> + </div> + <aside className="pm-aside"> + <span className="pm-tag">{common.status}</span> + <p>{xide.status}</p> + </aside> + </div> + </section> + + <section className="pm-section"> + <div className="pm-shell"> + <div className="pm-heading"> + <h2>{xide.layoutTitle}</h2> + </div> + <div className="pm-bento"> + {xide.layout.map(([title, description]) => ( + <article className="pm-card" key={title}> + <h3>{title}</h3> + <p>{description}</p> + </article> + ))} + </div> + </div> + </section> + + <section className="pm-section pm-section-muted"> + <div className="pm-shell pm-detail"> + <div className="pm-heading"> + <h2>{xide.languagesTitle}</h2> + <p>{xide.languagesDescription}</p> + </div> + <div> + <div className="pm-heading"> + <h2>{xide.architectureTitle}</h2> + </div> + <ul className="pm-features"> + {xide.architecture.map((item) => ( + <li key={item}>{item}</li> + ))} + </ul> + </div> + </div> + </section> + </main> + <Footer locale={locale} /> + </div> + ); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index de04763..db5826b 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -46,7 +46,9 @@ export default async function RootLayout({ <html lang={locale} data-text-direction={textDirection(locale)} - data-theme="light" + // data-theme belongs to the bootstrap script and the theme control only. + // Rendered here, React would set it back to light whenever the server + // re-renders the layout (a language switch), and the page would flash. suppressHydrationWarning > <head> diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index ade7da8..ca8672e 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -2,11 +2,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 import type { Metadata } from "next"; -import { Fragment } from "react"; import { Footer } from "@/components/Footer"; import { SiteHeader } from "@/components/SiteHeader"; +import { AreaIcon, areaKinds } from "@/components/site/AreaIcon"; +import { getHomeMessages } from "@/lib/home-messages"; import { getLocale } from "@/lib/locale.server"; import { forwardArrow, getMessages } from "@/lib/localization"; +import "./site.css"; export async function generateMetadata(): Promise<Metadata> { const { metadata } = getMessages(await getLocale()); @@ -18,154 +20,135 @@ export async function generateMetadata(): Promise<Metadata> { }; } -const productLinks = [ - { - href: "https://tala.run/", - accent: "violet", - }, - { - href: "https://viget.progmasoft.com/", - accent: "blue", - }, - { - href: "https://github.com/Progmasoft", - accent: "green", - }, -] as const; - +/** + * progmasoft.com. Progmasoft is presented as a pool of technology areas; the + * products have their own catalog on product.progmasoft.com. + */ export default async function Homepage() { const locale = await getLocale(); - const { home } = getMessages(locale); + const { hero, pool, principles, developers } = getHomeMessages(locale); + const arrow = forwardArrow(locale); return ( - <> + <div className="pm-page"> <SiteHeader locale={locale} /> <main> - <section className="hero organization-hero"> - <div className="shell hero-grid"> - <div className="hero-copy"> - <p className="eyebrow">{home.heroEyebrow}</p> + <section className="pm-hero"> + <div className="pm-shell pm-hero-inner"> + <div> + <p className="eyebrow">{hero.eyebrow}</p> <h1> - {home.heroTitleStart} <span>{home.heroTitleAccent}</span> + {hero.titleStart} <span>{hero.titleAccent}</span> </h1> - <p className="hero-lede">{home.heroDescription}</p> - <div className="hero-actions"> - <a className="button" href="#products"> - {home.exploreProducts} + <p className="pm-lede">{hero.description}</p> + <div className="pm-actions"> + <a className="button" href="#technology"> + {hero.explore} </a> <a className="button button-secondary" - href="https://github.com/Progmasoft" + href="https://product.progmasoft.com/" > - {home.browseSource} + {hero.products} {arrow} </a> </div> - <dl className="hero-facts"> - {home.facts.map(([term, description]) => ( - <div key={term}> - <dt>{term}</dt> - <dd>{description}</dd> - </div> - ))} - </dl> </div> - <div className="hero-visual" aria-label={home.visualLabel}> - <div className="system-window"> - <div className="window-bar"> - <span></span> - <span></span> - <span></span> - <code>progmasoft/organization</code> - </div> - <div className="system-stack"> - {home.stack.map(([label, title, description], index) => ( - <Fragment key={label}> - {index > 0 && ( - <div className="stack-arrow" aria-hidden="true"> - ↓ - </div> - )} - <div - className={`stack-row ${["stack-language", "stack-project", "stack-native"][index]}`} - > - <span>{label}</span> - <strong>{title}</strong> - <small>{description}</small> - </div> - </Fragment> - ))} + <div className="pm-pool-visual" aria-label={hero.poolLabel}> + {pool.areas.map(([title], index) => ( + <div className="pm-pool-tile" key={title}> + <span className={`pm-icon pm-icon-${areaKinds[index]}`}> + <AreaIcon kind={areaKinds[index] ?? "languages"} /> + </span> + <strong>{title}</strong> </div> - </div> + ))} </div> </div> </section> - <section className="section" id="products"> - <div className="shell"> - <div className="section-heading"> - <div> - <p className="eyebrow">{home.productsEyebrow}</p> - <h2>{home.productsTitle}</h2> - </div> - <p>{home.productsDescription}</p> + <section className="pm-section" id="technology"> + <div className="pm-shell"> + <div className="pm-heading"> + <p className="eyebrow">{pool.eyebrow}</p> + <h2>{pool.title}</h2> + <p>{pool.description}</p> + </div> + <div className="pm-bento"> + {pool.areas.map(([title, description], index) => ( + <article + className={`pm-card${[0, 3, 4].includes(index) ? " pm-card-wide" : ""}`} + key={title} + > + <span className={`pm-icon pm-icon-${areaKinds[index]}`}> + <AreaIcon kind={areaKinds[index] ?? "languages"} /> + </span> + <h3>{title}</h3> + <p>{description}</p> + </article> + ))} </div> - <div className="product-grid"> - {home.products.map( - ([eyebrow, name, description, action], index) => ( - <article - className={`product-card accent-${productLinks[index]?.accent}`} - key={name} - > - <p className="eyebrow">{eyebrow}</p> - <h3>{name}</h3> - <p>{description}</p> - <a href={productLinks[index]?.href}> - {action} {forwardArrow(locale)} - </a> - </article> - ), - )} + <div className="pm-catalog"> + <p>{pool.catalog}</p> + <a className="pm-link" href="https://product.progmasoft.com/"> + {pool.catalogLink} {arrow} + </a> </div> </div> </section> - <section className="section section-muted" id="principles"> - <div className="shell principles-grid"> - <div className="section-heading vertical"> - <p className="eyebrow">{home.principlesEyebrow}</p> - <h2>{home.principlesTitle}</h2> - <p>{home.principlesDescription}</p> + <section className="pm-section pm-section-muted" id="principles"> + <div className="pm-shell"> + <div className="pm-heading"> + <p className="eyebrow">{principles.eyebrow}</p> + <h2>{principles.title}</h2> </div> - <div className="principle-list"> - {home.principles.map(([title, description], index) => ( + <div className="pm-principles"> + {principles.items.map(([title, description], index) => ( <article key={title}> <span>{String(index + 1).padStart(2, "0")}</span> - <div> - <h3>{title}</h3> - <p>{description}</p> - </div> + <h3>{title}</h3> + <p>{description}</p> </article> ))} </div> </div> </section> - <section className="section callout-section"> - <div className="shell callout"> + <section className="pm-section" id="developers"> + <div className="pm-shell pm-band"> <div> - <p className="eyebrow">{home.accountEyebrow}</p> - <h2>{home.accountTitle}</h2> - <p>{home.accountDescription}</p> + <p className="eyebrow">{developers.eyebrow}</p> + <h2>{developers.title}</h2> + <p>{developers.description}</p> + <div className="pm-actions"> + <a + className="button button-light" + href="https://console.progmasoft.com/" + > + {developers.console} + </a> + <a + className="button button-secondary" + href="https://account.progmasoft.com/" + > + {developers.account} + </a> + </div> </div> - <a - className="button button-light" - href="https://account.progmasoft.com/" - > - {home.openAccount} - </a> + <pre className="pm-code" dir="ltr"> + <code> + <b>GET</b> https://credentials.progmasoft.com + {"\n"} + {" "}/.well-known/openid-configuration + {"\n\n"} + <b>scope</b> openid profile email + {"\n"} + {" "}viget.packages.read + </code> + </pre> </div> </section> </main> <Footer locale={locale} /> - </> + </div> ); } diff --git a/apps/web/app/products/[slug]/page.tsx b/apps/web/app/products/[slug]/page.tsx new file mode 100644 index 0000000..4731eba --- /dev/null +++ b/apps/web/app/products/[slug]/page.tsx @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { Footer } from "@/components/Footer"; +import { SiteHeader } from "@/components/SiteHeader"; +import { getLocale } from "@/lib/locale.server"; +import { forwardArrow } from "@/lib/localization"; +import { getCatalogMessages, isProductSlug, products } from "@/lib/products"; + +interface ProductPageProps { + params: Promise<{ slug: string }>; +} + +export async function generateMetadata({ + params, +}: ProductPageProps): Promise<Metadata> { + const { slug } = await params; + if (!isProductSlug(slug)) { + return {}; + } + const text = getCatalogMessages(await getLocale()).products[slug]; + return { + title: `${products[slug].name} · ${text.category}`, + description: text.description, + alternates: { canonical: `https://product.progmasoft.com/${slug}` }, + }; +} + +/** One product: what it is, what it does, and where it lives. */ +export default async function ProductPage({ params }: ProductPageProps) { + const { slug } = await params; + if (!isProductSlug(slug)) { + notFound(); + } + const locale = await getLocale(); + const catalog = getCatalogMessages(locale); + const product = products[slug]; + const text = catalog.products[slug]; + const arrow = forwardArrow(locale); + return ( + <div className="pm-page"> + <SiteHeader locale={locale} /> + <main> + <section className="pm-hero"> + <div className="pm-shell pm-product-hero"> + <p className="eyebrow">{text.category}</p> + <h1> + {product.name} <span>{text.tagline}</span> + </h1> + <p className="pm-lede">{text.description}</p> + </div> + </section> + <section className="pm-section"> + <div className="pm-shell pm-detail"> + <div> + <div className="pm-heading"> + <h2>{catalog.features}</h2> + </div> + <ul className="pm-features"> + {text.features.map((feature) => ( + <li key={feature}>{feature}</li> + ))} + </ul> + </div> + <aside className="pm-aside"> + <span className={`pm-accent pm-accent-${product.accent}`} /> + <a className="button" href={product.site}> + {catalog.site} {arrow} + </a> + <a className="button button-secondary" href={product.source}> + {catalog.source} + </a> + <span className="pm-note" dir="ltr"> + {product.site.replace(/^https:\/\//, "").replace(/\/$/, "")} + </span> + <Link className="pm-link" href="/"> + {catalog.back} + </Link> + </aside> + </div> + </section> + </main> + <Footer locale={locale} /> + </div> + ); +} diff --git a/apps/web/app/products/layout.tsx b/apps/web/app/products/layout.tsx new file mode 100644 index 0000000..c57d103 --- /dev/null +++ b/apps/web/app/products/layout.tsx @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { ReactNode } from "react"; +import "../site.css"; + +export default function ProductsLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + return children; +} diff --git a/apps/web/app/products/page.tsx b/apps/web/app/products/page.tsx new file mode 100644 index 0000000..6b160ab --- /dev/null +++ b/apps/web/app/products/page.tsx @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +import type { Metadata } from "next"; +import { Footer } from "@/components/Footer"; +import { SiteHeader } from "@/components/SiteHeader"; +import { getLocale } from "@/lib/locale.server"; +import { forwardArrow } from "@/lib/localization"; +import { getCa |