Redesign: new editorial design language, drop tailwind for UnoCSS#71
Redesign: new editorial design language, drop tailwind for UnoCSS#71felipebalbi wants to merge 13 commits into
Conversation
Wires up the unocss-classes Rust crate (compile-time uno! macro)
together with the @unocss/cli Node package and a Trunk pre-build
hook so we can author class strings inline in view! blocks while
emitting a single tree-shaken stylesheet.
Pipeline:
* Cargo.toml: add unocss-classes = "2".
* package.json + package-lock.json: pin @unocss/cli, unocss,
@unocss/preset-wind3 (Tailwind-compatible utilities, eases
migration), @unocss/preset-icons (lucide collection), the
variant-group + directives transformers, and the lucide icon
set. CLI version family is 66.x.
* unocss.config.ts: theme references the design tokens that will
land in style/base.css (color, type, spacing, radius, shadow as
var(--...)) so palette swaps don't require a CSS regenerate.
* Trunk.toml [[hooks]]: pre-build step runs `npm run build:css`,
which scans src/**/*.rs + index.html and emits
style/uno.generated.css.
* .gitignore: ignore /node_modules and the generated stylesheet.
Smoke: `npm run build:css` extracts 257 utilities from the existing
Tailwind classes already present in the source. wasm cargo check +
clippy are clean.
This commit only adds tooling -- no Rust source changes, no asset
graph changes. Tailwind is still wired up in index.html and will be
removed in a later commit once the new design language is in place.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sets up the new design language as CSS custom properties on :root so the rest of the redesign can reference them via either UnoCSS utilities (which resolve `bg-surface-page` to var(--color-surface- page)) or raw CSS in repo_graph.css and similar legacy stylesheets. Layout: 1. Small opinionated reset (only what we actually need). 2. @font-face for self-hosted Geist (already shipped in t48) and Fraunces variable serif (latin subset; r05 will commit the woff2 file itself). 3. :root tokens for the light theme. 4. [data-theme="dark"] and prefers-color-scheme: dark fallback for the no-flash first paint. 5. prefers-reduced-motion override clamps animations + scroll. 6. ::selection styling. Design language (autonomous decisions, see plan.md): * Surfaces: warm bone (#fbf8f4), white raised, blush sunken, ink-blue inverse. * Ink: deep ink-blue (#161922) primary, calm secondary, soft muted, accent-ink for inline links. * Accent: sunset amber (#d6651f) for CTAs, focus rings, and brand mark. Softer halo for hover/focus states. * Trust: deep blue-grey (#2d4a63) for stability/security messaging (footer brand, project status badges). * Project tags: Patina green moves here (project-tag colour only), joined by warm copper (EC) and dusty violet (Services). * Typography: Geist for UI/body, Fraunces for editorial display. Fluid clamp() type scale across all heading sizes. * Spacing: 4px base; section/gutter use clamp() so the layout breathes from 320px up to 4K. * Radius: sm 6, md 10, lg 16, xl 24, pill. * Elevation: warm shadows with low alpha; dark theme uses higher alpha pure black. * Motion: fast/base/slow timings + emphasized/standard/decel easings; everything goes flat under reduced-motion. This commit only adds the file -- nothing references it yet. r05 will preload Fraunces and r06 will swap the index.html link from tailwind to base.css + uno.generated.css. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This squashes the planned per-task commits (r04 through r24, plus the
final cleanup r34) into a single change because the components and
pages share so much surface area that splitting them per-task would
have produced a long sequence of intermediate states that did not
build. The redesign is opinionated and self-contained:
* Drop tailwind entirely; ship a hand-tuned `style/base.css` (design
tokens, reset, type scale, dark mode, reduced motion) plus the
unocss-generated utility sheet (`style/uno.generated.css`).
* New design system under `src/components/`:
- `theme.rs` -- `ThemeProvider`, `ThemeToggle`, persisted to
`localStorage`, syncs to `<html data-theme>`.
- `layout.rs` -- `Container`, `Section`, `Stack`, `Cluster`,
`Grid`, `Surface` primitives with capped widths so the layout
breathes from 320px up to 4K.
- `typography.rs` -- `Display`, `Heading`, `Lead`, `Body`,
`Eyebrow`, `Mono`. Display uses a system serif fallback stack
(Iowan Old Style -> Palatino -> Georgia) -- no extra font file
shipped.
- `controls.rs` -- `Button`, `LinkButton`, `Tag`, `ArrowLink`,
`InlineLink`.
- `media.rs` -- theme-aware `Logo`, lucide-backed `Icon`,
`BrandIcon` for github/zulip/discord.
- `nav.rs` -- slim sticky `NavBar`, mobile drawer, `Footer` with
trust strip + 4-column link grid + copyright row.
- `cards.rs` -- `ProjectCard`, `ValueCard`, `TeamCard`, `DocCard`,
`TrustStrip`, `AnnouncementDetail`.
- `project_layout.rs` -- shared layout for the three project pages.
* New pages under `src/pages/`: `home`, `projects`,
`boot_firmware`, `embedded_controller`, `unified_ec_services`,
`community` (merges contributing + docs/training + governance with
anchored sections), `getting_started`, `announcements`,
`not_found`, plus `team_page` shared layout used by `team_patina`,
`team_ec`, `team_ec_services`.
* `lib.rs`: single `App` with `ThemeProvider` -> `Router`, flat
routes wrapped by one `NavBar`/`Footer` chrome.
* Trim partners section to a slim trust strip in the footer.
* Drop the camera icon next to the YouTube embed and the embed
itself for now -- to be re-added as a quiet framed media block if
the new design lands.
* `data/teams.rs`: move `TeamMember` struct here (was in the
deleted `team_grid.rs`).
* `Cargo.toml`: enable `Storage` + `MediaQueryList` features on
`web-sys` for the theme provider.
* `Trunk.toml`: pre-build hook now invokes `cmd /C npm` so it works
on Windows without `npm.cmd` shenanigans.
* `index.html`: drop tailwind link, add `base.css` +
`uno.generated.css`.
* `tests/wasm.rs`: rewrite around the new component surface.
Final cleanup pass (r34): repo-wide `dos2unix`, `leptosfmt src tests`,
`cargo fmt`. Clippy and tests are green:
* `cargo check --target wasm32-unknown-unknown` -- ok
* `cargo clippy --target wasm32-unknown-unknown --all-targets -- -D warnings` -- ok
* `cargo test --lib` -- 12 passed
* `trunk build` -- ok
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… native cargo test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ght bg * Mobile hamburger drawer now uses backdrop-blur only, no dark fill overlay. * Added darker -ink variants for project tag colours so the tag label meets AA contrast against the soft 15%-alpha tag background in light mode. Dark mode keeps the original (already-bright) hue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mode-only icon The small project icons (P, EC, ES) were white-on-transparent assets intended for dark mode; on the new light-mode landing page they were unreadable on the soft thumbnail background. Replace the <img> with a CSS-rendered monogram (P / EC / ES) coloured with the project's -ink token on a 12%-alpha tint of the project hue. This works in both themes without needing a light-mode icon set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The redesign moved CSS generation from a Tailwind binary to @unocss/cli (invoked via npm by the Trunk pre_build hook). CI runners did not have npm dependencies installed, so trunk build failed with `unocss: not found`. Add a setup-node step + `npm ci` to both the CI build job and the Cloudflare Pages deploy job, and update the asset size budget to track `base-*.css` and `uno.generated-*.css` instead of the now-deleted `tailwind-*.css`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The pre-build hook writes style/uno.generated.css. Trunk's watcher sees the write and triggers another build, which runs the hook again, looping forever during `trunk serve`. Add the generated CSS file (and node_modules + dist for good measure) to [watch].ignore so the loop breaks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
williampMSFT
left a comment
There was a problem hiding this comment.
Seems fine to me, although I have basically no experience with web development so it's unclear how much my assessment is worth :) Would definitely recommend clearing it with Karan, not sure if there's any sort of branding/theming stuff we're going for that I'm not tracking
Yes, we definitely need Karan's and Massimo's review. Let's wait until they're back in town |
There was a problem hiding this comment.
Pull request overview
Ground-up redesign of the ODP marketing site, replacing the legacy Tailwind component set with a token-driven design system built on UnoCSS (generated via a Trunk pre-build hook) and introducing a persistent light/dark theme toggle.
Changes:
- Added UnoCSS pipeline (config + Node tooling + Trunk hook) and new tokenized base stylesheet.
- Replaced legacy page/components with new editorial layouts and primitives (layout/typography/controls/cards/nav/theme).
- Refactored projects + teams into shared layouts and updated wasm integration tests accordingly.
Reviewed changes
Copilot reviewed 75 out of 79 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
unocss.config.ts |
Adds UnoCSS configuration mapping semantic utilities to CSS variables. |
Trunk.toml |
Adds pre-build hook to generate UnoCSS bundle and ignores generated output to prevent rebuild loops. |
tests/wasm.rs |
Rewrites browser-side wasm tests to cover new primitives/components. |
tailwind.config.js |
Removes legacy Tailwind configuration. |
style/tailwind.css |
Removes legacy Tailwind entry stylesheet and custom utility layer. |
style/base.css |
Introduces CSS reset + design tokens + dark-mode tokens + reduced-motion handling. |
src/pages/unified_ec_services.rs |
Switches EC Services page to shared ProjectLayout. |
src/pages/team_patina.rs |
Switches Patina team page to shared TeamPage. |
src/pages/team_page.rs |
Adds shared layout for team pages. |
src/pages/team_ec.rs |
Switches Secure EC team page to shared TeamPage. |
src/pages/team_ec_services.rs |
Switches EC Services team page to shared TeamPage. |
src/pages/projects.rs |
Rebuilds /projects as an editorial page using new primitives/cards. |
src/pages/not_found.rs |
Replaces simple 404 with redesigned Not Found page using new primitives. |
src/pages/mod.rs |
Updates pages module exports to include new pages and ordering. |
src/pages/home.rs |
Rebuilds / as editorial multi-section home page with new primitives. |
src/pages/getting_started.rs |
Adds redesigned /getting-started page and “Step” cards. |
src/pages/embedded_controller.rs |
Switches Embedded Controller page to shared ProjectLayout. |
src/pages/community.rs |
Replaces legacy community content with anchored editorial sections. |
src/pages/boot_firmware.rs |
Switches Boot Firmware page to shared ProjectLayout. |
src/pages/announcements.rs |
Redesigns announcements into list + detail layout using new cards/primitives. |
src/lib.rs |
Updates app chrome to new NavBar/Footer + theme provider and routes. |
src/data/teams.rs |
Moves TeamMember struct ownership into data::teams. |
src/data/projects.rs |
Adds monogram field for project cards and populates it for each project. |
src/components/ui/value_prop_card.rs |
Removes legacy Tailwind-era ValuePropCard. |
src/components/ui/two_column_intro.rs |
Removes legacy Tailwind-era TwoColumnIntro. |
src/components/ui/text.rs |
Removes legacy Tailwind-era text primitive. |
src/components/ui/stack.rs |
Removes legacy Tailwind-era stack primitive. |
src/components/ui/project_tabs.rs |
Removes legacy Tailwind-era project tabs. |
src/components/ui/mono.rs |
Removes legacy Tailwind-era mono primitive. |
src/components/ui/mod.rs |
Removes legacy components::ui design system module. |
src/components/ui/link.rs |
Removes legacy Tailwind-era link primitive. |
src/components/ui/labeled_section.rs |
Removes legacy Tailwind-era labeled section helper. |
src/components/ui/icon_block.rs |
Removes legacy Tailwind-era icon block primitive. |
src/components/ui/heading.rs |
Removes legacy Tailwind-era heading primitive. |
src/components/ui/grid.rs |
Removes legacy Tailwind-era grid primitive. |
src/components/ui/doc_link_item.rs |
Removes legacy Tailwind-era doc link item. |
src/components/ui/container.rs |
Removes legacy Tailwind-era container primitive. |
src/components/ui/button.rs |
Removes legacy Tailwind-era button primitives. |
src/components/ui/arrow_link.rs |
Removes legacy Tailwind-era arrow link implementation. |
src/components/ui/announcement_card.rs |
Removes legacy announcements card wrapper. |
src/components/typography.rs |
Adds new typography primitives backed by UnoCSS tokens. |
src/components/themed_icon.rs |
Removes ThemedIcon picture-based theme icon helper. |
src/components/theme.rs |
Adds theme provider/state + toggle + localStorage persistence. |
src/components/team_hero.rs |
Removes legacy team hero component. |
src/components/team_grid.rs |
Removes legacy team grid component. |
src/components/site_shell.rs |
Removes legacy router-level chrome shells. |
src/components/section.rs |
Removes legacy section/surface wrapper tied to Tailwind classes. |
src/components/projects_component.rs |
Removes legacy projects page component implementation. |
src/components/project_layout.rs |
Adds shared editorial layout for project pages (hero, facts, graph, other projects). |
src/components/project_introduction.rs |
Removes legacy project introduction component. |
src/components/partners_grid.rs |
Removes legacy partners grid from body layout. |
src/components/partner.rs |
Removes legacy partner component. |
src/components/nav.rs |
Adds new sticky nav + mobile drawer + redesigned footer. |
src/components/mod.rs |
Replaces components module tree with new primitives-focused structure. |
src/components/media.rs |
Adds theme-aware logo and icon helpers (lucide + brand icons). |
src/components/main.rs |
Removes legacy home page main content (incl. YouTube embed). |
src/components/layout.rs |
Adds layout primitives (Container/Section/Stack/Cluster/Grid/Surface). |
src/components/landing/value_proposition_section.rs |
Removes legacy landing section implementation. |
src/components/landing/projects_section.rs |
Removes legacy landing section implementation. |
src/components/landing/mod.rs |
Removes legacy landing module. |
src/components/landing/hero_section.rs |
Removes legacy landing section implementation. |
src/components/landing/closing_columns_section.rs |
Removes legacy landing section implementation. |
src/components/landing_page.rs |
Removes legacy landing page shell. |
src/components/image_button.rs |
Removes legacy image-button component. |
src/components/header.rs |
Removes legacy header implementation. |
src/components/footer.rs |
Removes legacy footer implementation. |
src/components/documentation_training.rs |
Removes legacy documentation/training footer block. |
src/components/controls.rs |
Adds new button/link/tag primitives for the redesign. |
src/components/community_teams.rs |
Removes legacy community page component. |
src/components/cards.rs |
Adds new card components (Project/Value/Team/Doc/TrustStrip/AnnouncementDetail). |
src/components/announce_banner.rs |
Removes legacy announcement banner. |
package.json |
Adds Node devDependencies and scripts for UnoCSS generation. |
index.html |
Switches from Tailwind CSS link to base + generated UnoCSS stylesheets. |
Cargo.toml |
Adds unocss-classes and enables required web-sys features. |
Cargo.lock |
Locks new Rust dependencies (unocss-classes and transitive crates). |
.gitignore |
Ignores UnoCSS generated CSS and Node node_modules. |
.github/workflows/cloudflare-pages-deploy.yaml |
Installs Node deps in deploy workflow so UnoCSS hook can run. |
.github/workflows/ci.yaml |
Installs Node deps in CI and updates bundle-size checks for new CSS outputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// `<a>` styled like a `Button`. Used for CTAs that navigate. | ||
| #[component] | ||
| pub fn LinkButton( | ||
| #[prop(into)] href: String, | ||
| #[prop(optional)] variant: ButtonVariant, | ||
| #[prop(optional)] size: ButtonSize, | ||
| #[prop(optional, default = false)] external: bool, | ||
| #[prop(into, optional)] class: String, | ||
| children: Children, | ||
| ) -> impl IntoView { | ||
| let final_class = format!("{} {class}", button_classes(variant, size)); | ||
| let target = if external { Some("_blank") } else { None }; | ||
| let rel = if external { Some("noopener noreferrer") } else { None }; | ||
| view! { | ||
| <a href=href class=final_class target=target rel=rel> | ||
| {children()} | ||
| </a> | ||
| } | ||
| } |
| /// Inline link with an arrow affordance and underline-on-hover. | ||
| #[component] | ||
| pub fn ArrowLink( | ||
| #[prop(into)] href: String, | ||
| #[prop(optional, default = false)] external: bool, | ||
| #[prop(into, optional)] class: String, | ||
| children: Children, | ||
| ) -> impl IntoView { | ||
| let target = if external { Some("_blank") } else { None }; | ||
| let rel = if external { Some("noopener noreferrer") } else { None }; | ||
| let final_class = format!( | ||
| "{} {class}", | ||
| uno!( | ||
| "group inline-flex items-baseline gap-2 text-ink-primary", | ||
| "border-b border-transparent hover:border-current", | ||
| "transition-colors duration-200" | ||
| ) | ||
| ); | ||
| view! { | ||
| <a href=href class=final_class target=target rel=rel> | ||
| <span>{children()}</span> | ||
| <span | ||
| class=uno![ | ||
| "i-lucide-arrow-up-right w-4 h-4 text-ink-muted", | ||
| "group-hover:(text-accent translate-x-0.5 translate-y--0.5)", | ||
| "transition-transform duration-200" | ||
| ] | ||
| aria-hidden="true" | ||
| ></span> | ||
| </a> | ||
| } | ||
| } |
| /// Plain inline link (underline on hover). | ||
| #[component] | ||
| pub fn InlineLink( | ||
| #[prop(into)] href: String, | ||
| #[prop(optional, default = false)] external: bool, | ||
| #[prop(into, optional)] class: String, | ||
| children: Children, | ||
| ) -> impl IntoView { | ||
| let target = if external { Some("_blank") } else { None }; | ||
| let rel = if external { Some("noopener noreferrer") } else { None }; | ||
| let final_class = format!( | ||
| "{} {class}", | ||
| uno!("text-ink-accent underline decoration-from-font underline-offset-4 hover:decoration-2") | ||
| ); | ||
| view! { | ||
| <a href=href class=final_class target=target rel=rel> | ||
| {children()} | ||
| </a> | ||
| } | ||
| } |
| <a | ||
| href=href | ||
| target=if external { Some("_blank") } else { None } | ||
| rel=if external { Some("noopener noreferrer") } else { None } | ||
| class="group block" | ||
| > | ||
| <Surface | ||
| tone=SurfaceTone::Outline | ||
| elevation=SurfaceElevation::Flat | ||
| class="h-full flex flex-col gap-3 group-hover:(border-border-accent bg-surface-raised) transition-colors duration-200" | ||
| > | ||
| <div class=uno!("flex items-start justify-between gap-3")> | ||
| <Heading level=HeadingLevel::H3 class="!text-h3"> | ||
| {title} | ||
| </Heading> | ||
| <span | ||
| class=uno![ | ||
| "i-lucide-arrow-up-right w-5 h-5 text-ink-muted flex-shrink-0", | ||
| "group-hover:(text-accent translate-x-0.5 translate-y--0.5) transition-transform duration-200" | ||
| ] | ||
| aria-hidden="true" | ||
| ></span> | ||
| </div> | ||
| <Body tone=BodyTone::Secondary class="!text-small"> | ||
| {description} | ||
| </Body> | ||
| </Surface> | ||
| </a> |
| /// Theme-aware ODP logo. Uses `<picture>` so the browser swaps | ||
| /// between the light and dark SVG without a JS round trip. | ||
| #[component] | ||
| pub fn Logo(#[prop(into, optional)] class: String) -> impl IntoView { | ||
| let class = if class.is_empty() { | ||
| uno!("h-8 md:h-10 w-auto").to_string() | ||
| } else { | ||
| class | ||
| }; | ||
| view! { | ||
| <picture> | ||
| <source srcset="/images/dark/odplogo.svg" media="(prefers-color-scheme: dark)" /> | ||
| <img src="/images/light/odplogo.svg" alt="Open Device Partnership" class=class /> | ||
| </picture> | ||
| } |
| /// Direct DOM helper used by `App` to apply the *initial* theme as | ||
| /// early as possible -- before Leptos hydration -- to avoid a | ||
| /// "theme flash" on first paint. Safe to call from any context. | ||
| pub fn paint_initial_theme() { | ||
| let theme = read_initial_theme(); | ||
| apply_theme(theme); | ||
| } |
| //! Community page (`/`) -- merges governance, contributing, docs & | ||
| //! training, and team directory into one editorial page with | ||
| //! anchored sections. |






Ground-up redesign of the marketing site. Drops the legacy Tailwind-based component zoo in favor of a small, opinionated design system built on tokens + UnoCSS (via the unocss-classes Rust crate). The voice is editorial and warm, the layout breathes from 320px up to 4K, and the chrome stays out of the way of the content.
Screenshots attached below.
What's new
Design language
Information architecture
Component primitives (src/components/)
Build pipeline
style/base.css.
Migration notes
Verified