diff --git a/public/img/xhibiter/gradient.jpg b/public/img/xhibiter/gradient.jpg new file mode 100644 index 0000000..9ea9933 Binary files /dev/null and b/public/img/xhibiter/gradient.jpg differ diff --git a/public/img/xhibiter/gradient_light.jpg b/public/img/xhibiter/gradient_light.jpg new file mode 100644 index 0000000..c742eac Binary files /dev/null and b/public/img/xhibiter/gradient_light.jpg differ diff --git a/src/app/admin/review-queue.tsx b/src/app/admin/review-queue.tsx index 09cb370..938621f 100644 --- a/src/app/admin/review-queue.tsx +++ b/src/app/admin/review-queue.tsx @@ -877,7 +877,7 @@ function QueueRow({ /* Three states that are actually distinguishable on a #faf7f6 page: page, white on hover, ink when selected. */ className={`w-full rounded-tight px-3 py-2.5 text-left transition-colors ${ - selected ? "bg-ink text-t-invert" : "hover:bg-surface" + selected ? "bg-ink text-t-invert" : "hover:bg-xh-jacarta-50" }`} >

{profile.name}

diff --git a/src/app/contact/contact-form.tsx b/src/app/contact/contact-form.tsx index 369e46c..475dc56 100644 --- a/src/app/contact/contact-form.tsx +++ b/src/app/contact/contact-form.tsx @@ -88,7 +88,7 @@ export function ContactForm({ email, about }: { email: string; about?: string }) return (
{about ? ( -

+

Enquiring about {about}. Bluehex passes it on — practitioners are not contacted directly.

diff --git a/src/app/globals.css b/src/app/globals.css index d0a0976..55fefa9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,48 +1,50 @@ @import "tailwindcss"; /* ------------------------------------------------------------------ - * Design tokens ported from the Code.Sydney site. + * Design tokens. Originally ported from the Code.Sydney site; the values are + * now the Xhibiter "Home 2" palette (violet accent, jacarta navy text, Cal Sans + * and DM Sans), swapped in at token level so every component follows. * * That stylesheet sets the root font-size to 10px and expresses every * dimension in rem against that. We keep the browser default 16px root here * and carry the values across in px, so a `1.6rem` radius over there reads as * `16px` here. * - * `ink` is the high-contrast fill — near-black here — and `t-invert` is the + * `ink` is the high-contrast fill — the violet accent here — and `t-invert` is the * text that sits on top of it. Every component reads these roles rather than * literal colours, so re-theming is a token-level swap. Single scheme by * design: the page stays light regardless of OS preference. * ------------------------------------------------------------------ */ @theme { /* surfaces */ - --color-page: #faf7f6; + --color-page: #ffffff; --color-surface: #ffffff; - --color-surface-shade: #e0dddb; - --color-ink: #161616; - --color-ink-tint: #1c1c1c; + --color-surface-shade: #e7e8ec; + --color-ink: #8358ff; + --color-ink-tint: #7444ff; /* brand accents */ - --color-accent: #9f8be7; - --color-lime: #ddf160; + --color-accent: #8358ff; + --color-lime: #b9a0ff; /* text ramp */ - --color-t-bright: #161616; - --color-t-medium: #303030; - --color-t-muted: #585858; - --color-t-faint: #b2aead; + --color-t-bright: #131740; + --color-t-medium: #363a5d; + --color-t-muted: #5a5d79; + --color-t-faint: #a1a2b3; --color-t-invert: #ffffff; - --color-t-invert-muted: #acacac; + --color-t-invert-muted: #e7e8ec; /* Feedback. One colour, used only where something is wrong: a form error, and nothing else. Dark enough on `--color-page` and on `--color-surface` to clear 4.5:1 at the 12px these messages are set in — and never the only signal, since every message that uses it is text beside the field it is about. */ - --color-danger: #a02724; + --color-danger: #b42318; /* strokes */ - --color-stroke: #e0dddb; - --color-stroke-strong: #161616; + --color-stroke: #e7e8ec; + --color-stroke-strong: #8358ff; /* Radii. Named by role, not by a size letter: Tailwind already owns the single-letter suffixes for sides, so a `--radius-l` token produces a @@ -51,13 +53,30 @@ as a half-rounded box. Same trap as naming a colour `base` and losing `text-base`. Keep these names multi-letter and unlike any built-in. */ --radius-tight: 10px; - --radius-card: 26px; - --radius-block: 38px; - --radius-huge: 80px; + --radius-card: 20px; + --radius-block: 24px; + --radius-huge: 40px; /* type */ - --font-display: var(--font-funnel-display), ui-sans-serif, system-ui, sans-serif; - --font-sans: var(--font-funnel-sans), ui-sans-serif, system-ui, sans-serif; + --font-display: var(--font-cal-sans), ui-sans-serif, system-ui, sans-serif; + --font-sans: var(--font-dm-sans), ui-sans-serif, system-ui, sans-serif; + + /* The raw Xhibiter scale, for the places that want a specific step rather + than a role — the home page and the gradient page titles. */ + --color-xh-jacarta-50: #f4f4f6; + --color-xh-jacarta-100: #e7e8ec; + --color-xh-jacarta-200: #c4c5cf; + --color-xh-jacarta-300: #a1a2b3; + --color-xh-jacarta-400: #7d7f96; + --color-xh-jacarta-500: #5a5d79; + --color-xh-jacarta-600: #363a5d; + --color-xh-jacarta-700: #131740; + --color-xh-green: #10b981; + + --shadow-xh-accent-volume: + 5px 5px 10px rgba(108, 106, 213, 0.25), inset 2px 2px 6px #a78df0, inset -5px -5px 10px #6336e4; + --shadow-xh-white-volume: + 5px 5px 10px rgba(108, 106, 212, 0.25), inset 2px 2px 6px #eef1f9, inset -5px -5px 10px #dfe3ef; } @layer base { @@ -79,8 +98,8 @@ h5, h6 { font-family: var(--font-display); - font-weight: 500; - letter-spacing: -0.02em; + font-weight: 400; + letter-spacing: -0.01em; line-height: 1.06; text-wrap: balance; } @@ -88,8 +107,8 @@ /* Tracking is tight at display sizes and normal at body sizes, so it is set per-heading rather than globally. */ ::selection { - background-color: var(--color-lime); - color: var(--color-ink); + background-color: var(--color-ink); + color: var(--color-t-invert); } :focus-visible { @@ -117,19 +136,19 @@ /* Display headline scale — fluid between the mobile and 1600px extremes so the giant section titles never need per-breakpoint overrides. */ .display-1 { - font-size: clamp(3.25rem, 11vw, 11.5rem); - line-height: 0.92; - letter-spacing: -0.04em; + font-size: clamp(2.75rem, 7vw, 5.25rem); + line-height: 1; + letter-spacing: -0.02em; } .display-2 { - font-size: clamp(2.5rem, 6.2vw, 5.5rem); - line-height: 1.02; - letter-spacing: -0.03em; + font-size: clamp(2.1rem, 4.4vw, 3.5rem); + line-height: 1.06; + letter-spacing: -0.015em; } .display-3 { - font-size: clamp(1.9rem, 3.4vw, 2.9rem); + font-size: clamp(1.5rem, 2.4vw, 2rem); line-height: 1.12; letter-spacing: -0.02em; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eda2d7c..e657195 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,18 +1,21 @@ import type { Metadata } from "next"; -import { Funnel_Display, Funnel_Sans } from "next/font/google"; +import { Cal_Sans, DM_Sans } from "next/font/google"; +import Image from "next/image"; import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; import { site } from "@/lib/site"; import "./globals.css"; -const funnelDisplay = Funnel_Display({ - variable: "--font-funnel-display", +/* Cal Sans for display, DM Sans for body — the Xhibiter pair. */ +const calSans = Cal_Sans({ + variable: "--font-cal-sans", + weight: "400", subsets: ["latin"], display: "swap", }); -const funnelSans = Funnel_Sans({ - variable: "--font-funnel-sans", +const dmSans = DM_Sans({ + variable: "--font-dm-sans", subsets: ["latin"], display: "swap", }); @@ -49,11 +52,24 @@ export default function RootLayout({ children }: LayoutProps<"/">) { return ( -
{children}
+
+ {/* The Home 2 gradient behind the top of every page, faded out so it + never ends on a hard edge. Pages that open with their own gradient + band simply cover it. */} + + {children} +
diff --git a/src/app/meetups/page.tsx b/src/app/meetups/page.tsx index f844f48..4634abf 100644 --- a/src/app/meetups/page.tsx +++ b/src/app/meetups/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; +import { Collaborators } from "@/components/collaborators"; import { MeetupEvents } from "@/components/meetup-events"; -import { Button, SectionLabel } from "@/components/ui"; +import { Button, Hero } from "@/components/ui"; import { listUpcomingEvents } from "@/lib/events"; import { site } from "@/lib/site"; @@ -27,27 +28,30 @@ export default async function MeetupsPage() { return ( <> -
- Meetups - -

Meetups.

- -

- The Code.Sydney Claude community meets weekly in Sydney — beginners and builders - both. See what’s next below, or join the group to get notified of every one. -

- -
- - -
-
- -
+ + The Code.Sydney Claude community meets weekly in Sydney — beginners and builders + both. See what’s next below, or join the group to get notified of every one. + + } + actions={ + <> + + + + } + > + + + +
diff --git a/src/app/metadata.test.ts b/src/app/metadata.test.ts index 800efc6..645cf7a 100644 --- a/src/app/metadata.test.ts +++ b/src/app/metadata.test.ts @@ -20,8 +20,8 @@ import { site } from "@/lib/site"; transform rather than a runtime module, so importing the layout under Vitest throws without this. Only `variable` is read by the layout. */ vi.mock("next/font/google", () => ({ - Funnel_Display: () => ({ variable: "--font-funnel-display" }), - Funnel_Sans: () => ({ variable: "--font-funnel-sans" }), + Cal_Sans: () => ({ variable: "--font-cal-sans" }), + DM_Sans: () => ({ variable: "--font-dm-sans" }), })); const { metadata } = await import("./layout"); diff --git a/src/app/p/_lib/profile-detail.tsx b/src/app/p/_lib/profile-detail.tsx index edc8058..19f37ee 100644 --- a/src/app/p/_lib/profile-detail.tsx +++ b/src/app/p/_lib/profile-detail.tsx @@ -120,9 +120,9 @@ export function ProfileDetail({ }; return ( - /* A white card, because `bg-page` is a warm off-white and body copy at - `text-t-muted` on it reads as grey on grey. */ -
+ /* A bordered white card, so the profile reads as one panel over the + gradient behind the top of the page. */ +
{badged ? (

diff --git a/src/app/page.tsx b/src/app/page.tsx index 4f873e5..3ce6316 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ import { AgentDirectory } from "@/components/agent-directory"; import { Collaborators } from "@/components/collaborators"; -import { Button, SectionLabel } from "@/components/ui"; +import { Button, Hero } from "@/components/ui"; import { agents } from "@/lib/agents"; import { site } from "@/lib/site"; @@ -16,31 +16,23 @@ import { site } from "@/lib/site"; export default function HomePage() { return ( <> - {/* ---------------------------------------------------------------- */} - {/* Hero */} - {/* ---------------------------------------------------------------- */} -

- Agent Exchange - -

Agent Exchange.

- -

{site.tagline}

- -
- - -
- + + + + + } + > -
+ - {/* ---------------------------------------------------------------- */} - {/* Agent directory — search, filters, listings */} - {/* ---------------------------------------------------------------- */} ); diff --git a/src/app/practitioners/page.tsx b/src/app/practitioners/page.tsx index 48ec2ae..ec03070 100644 --- a/src/app/practitioners/page.tsx +++ b/src/app/practitioners/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Collaborators } from "@/components/collaborators"; import { PractitionerDirectory } from "@/components/practitioner-directory"; -import { Button, SectionLabel } from "@/components/ui"; +import { Button, Hero } from "@/components/ui"; import { listCredentialCatalogue, listProfiles, listServiceOptions } from "@/lib/directory"; /** @@ -73,29 +73,23 @@ export default async function PractitionersPage() { return ( <> - {/* ---------------------------------------------------------------- */} - {/* Hero */} - {/* ---------------------------------------------------------------- */} -
- Claude and Anthropic specialists - -

Claude Specialists.

- -

- Claude practitioners from the Code.Sydney community. Find one, hire one. -

- -
- - -
- + + + + + } + > -
+ {/* ---------------------------------------------------------------- */} {/* Practitioner directory — search, filters, profiles */} diff --git a/src/app/prototype/page.tsx b/src/app/prototype/page.tsx index ef5ea72..2769c98 100644 --- a/src/app/prototype/page.tsx +++ b/src/app/prototype/page.tsx @@ -57,7 +57,7 @@ export default function DesignIndex() {
  • {design.title} diff --git a/src/components/agent-directory.tsx b/src/components/agent-directory.tsx index 2306742..1b0fb2d 100644 --- a/src/components/agent-directory.tsx +++ b/src/components/agent-directory.tsx @@ -2,7 +2,7 @@ import { useId, useMemo, useRef, useState } from "react"; import { ArrowUpRight, Close, Search, Sparkle } from "@/components/icons"; -import { Badge, Card } from "@/components/ui"; +import { GradientBand } from "@/components/ui"; import { categories as categoryOptions, primaryLink, type Agent, type Category } from "@/lib/agents"; import { countryName } from "@/lib/practitioners"; @@ -18,6 +18,9 @@ import { countryName } from "@/lib/practitioners"; * this one has no credential catalogue and no verification, because every row * here already cleared Bluehex's review before it was committed. See * `@/lib/agents` for what that review replaces. + * + * The listings are cards rather than table rows: that is how the Xhibiter + * look the site follows lays out a roster. */ /** Every field a query is matched against, flattened once per agent. Mirrors @@ -102,147 +105,134 @@ export function AgentDirectory({ agents }: { agents: Agent[] }) { }; return ( -

    -

    Find an agent.

    + +
    +

    + Find an agent. +

    + +

    + Every listing here was built by an indie developer and reviewed by Bluehex before it + went live — there is no self-service publishing yet, so what you see is the whole + roster. +

    -

    - Every listing here was built by an indie developer and reviewed by Bluehex before it - went live — there is no self-service publishing yet, so what you see is the whole - roster. -

    + {/* Search ----------------------------------------------------- */} +
    + +
    + + setQuery(event.target.value)} + placeholder="Search by name, category, developer or location" + className="w-full rounded-2xl border border-stroke bg-white py-3 pr-12 pl-11 text-t-bright placeholder:text-t-faint focus:border-ink focus:ring-2 focus:ring-ink focus:outline-none [&::-webkit-search-cancel-button]:appearance-none" + /> + {query ? ( + + ) : null} +
    +
    + + {/* Filters ---------------------------------------------------- */} +
    + {countries.length > 0 ? ( + + {countries.map((country) => ( + toggleCountry(country.code)} + > + {country.name} + + ))} + + ) : null} - {/* Search ------------------------------------------------------- */} -
    - -
    - - setQuery(event.target.value)} - placeholder="Search by name, category, developer or location" - className="w-full bg-transparent text-base outline-none placeholder:text-t-faint md:text-lg [&::-webkit-search-cancel-button]:appearance-none" - /> - {query ? ( + {offered.length > 0 ? ( + + {offered.map((category) => ( + toggle(setCategoryFilters)(category)} + > + {category} + + ))} + + ) : null} + + {filtering ? ( ) : null}
    -
    - {/* Filters ------------------------------------------------------ */} -
    - {countries.length > 0 ? ( - - {countries.map((country) => ( - toggleCountry(country.code)} - > - {country.name} - - ))} - - ) : null} + {/* Results ---------------------------------------------------- */} +

    + {agents.length === 0 + ? "The first agents are being reviewed now." + : `${results.length} ${results.length === 1 ? "agent" : "agents"}`} +

    - {offered.length > 0 ? ( - - {offered.map((category) => ( - toggle(setCategoryFilters)(category)} - > - {category} - + {filtering && results.length === 0 ? ( +
    +

    No agent matches that yet.

    +

    + {agents.length > 0 + ? "Try a broader term, or clear the filters to see everyone in the exchange." + : "No agents are listed yet, so there is nothing to search — clearing this will not turn any up. Yours could be the first."} +

    +
    + ) : ( +
      + {results.map((agent) => ( +
    • + +
    • ))} - - ) : null} - {filtering ? ( - - ) : null} + {!filtering ? ( +
    • + + + +

      Your agent here

      +

      + Built something on Claude? Tell us about it through{" "} + + Contact + {" "} + and, once it clears review, it lists here. +

      +
    • + ) : null} +
    + )}
    - - {/* Results ------------------------------------------------------ */} -

    - {agents.length === 0 - ? "The first agents are being reviewed now." - : `${results.length} ${results.length === 1 ? "agent" : "agents"}`} -

    - - {filtering && results.length === 0 ? ( - -

    No agent matches that yet.

    -

    - {agents.length > 0 - ? "Try a broader term, or clear the filters to see everyone in the exchange." - : "No agents are listed yet, so there is nothing to search — clearing this will not turn any up. Yours could be the first."} -

    -
    - ) : ( -
    - {results.length > 0 ? ( - <> - - -
      - {results.map((agent) => ( -
    • - -
    • - ))} -
    - - ) : null} - - {!filtering ? ( -
    - -

    Your agent here

    -

    - Built something on Claude? Tell us about it through{" "} - - Contact - {" "} - and, once it clears review, it lists here. -

    -
    - ) : null} -
    - )} -
    + ); } @@ -250,14 +240,11 @@ function FilterGroup({ label, children }: { label: string; children: React.React const labelId = useId(); return ( -
    - +
    + {label} -
    +
    {children}
    @@ -278,10 +265,10 @@ function FilterChip({ type="button" onClick={onClick} aria-pressed={pressed} - className={`inline-flex h-10 items-center rounded-full border px-4 text-sm font-medium transition-colors ${ + className={`inline-flex h-9 items-center rounded-xl border px-4 font-display text-sm transition-colors ${ pressed - ? "border-ink bg-ink text-t-invert" - : "border-stroke text-t-muted hover:border-stroke-strong hover:text-t-bright" + ? "border-transparent bg-ink text-white" + : "border-stroke bg-white text-t-muted hover:border-transparent hover:bg-ink hover:text-white" }`} > {children} @@ -289,54 +276,69 @@ function FilterChip({ ); } -function AgentRow({ agent }: { agent: Agent }) { +function AgentCard({ agent }: { agent: Agent }) { const link = primaryLink(agent); return ( - <> -
    -

    {agent.name}

    - {agent.tagline ? ( -

    {agent.tagline}

    - ) : null} -

    - By {agent.developerName} - {agent.location ? ` · ${agent.location}` : ""} -

    - {agent.pricing ? ( -

    {agent.pricing}

    - ) : null} +
    +
    + +
    +

    + {agent.name} +

    +

    + By {agent.developerName} + {agent.location ? ` · ${agent.location}` : ""} +

    +
    -
    - {agent.description ? ( -

    {agent.description}

    - ) : ( -

    No description yet.

    - )} -
    + {agent.tagline ? ( +

    {agent.tagline}

    + ) : null} + + {agent.description ? ( +

    {agent.description}

    + ) : ( +

    No description yet.

    + )} -
    +
    {agent.categories.map((item) => ( - {item} + + {item} + ))}
    - {/* One call to action per row, an external link rather than a `Link` — - there is no `/agent/` page yet, so the row sends a visitor - straight to the agent's own site. See `primaryLink`. */} - {link ? ( - - Visit - {agent.name}, opens in a new tab - - - ) : null} - +
    + {agent.pricing ?? ""} + + {/* One call to action per card, an external link rather than a `Link` — + there is no `/agent/` page yet, so the card sends a visitor + straight to the agent's own site. See `primaryLink`. */} + {link ? ( + + Visit + {agent.name}, opens in a new tab + + + ) : null} +
    +
    ); } diff --git a/src/components/collaborators.tsx b/src/components/collaborators.tsx index 40a38a4..026e3c4 100644 --- a/src/components/collaborators.tsx +++ b/src/components/collaborators.tsx @@ -1,5 +1,4 @@ import Image from "next/image"; -import { Card } from "@/components/ui"; /* Communities Bluehex runs alongside. Logos are the collaborators' own artwork, trimmed to their bounding box and given a transparent @@ -28,30 +27,39 @@ const collaborators = [ }, ]; -/** The "In collaboration with" strip that closes a page hero. */ +/** The "In collaboration with" strip that closes a page hero. Deliberately + quiet — a row of greyscale logos, like Xhibiter's partner strip — so it + credits the collaborators without competing with the page's own actions. + Each one still links out, and comes up to full strength on hover. */ export function Collaborators() { return ( - <> -

    +

    +

    In collaboration with

    -
    +
    - + +
    ); } diff --git a/src/components/meetup-events.tsx b/src/components/meetup-events.tsx index 6401193..1eb4462 100644 --- a/src/components/meetup-events.tsx +++ b/src/components/meetup-events.tsx @@ -76,7 +76,7 @@ export function MeetupEvents({ events }: { events: Event[] }) { {upcoming.length > 0 ? ( -
    +
      {upcoming.map((event) => (
    • -

      Find a Claude practitioner.

      - -

      - Anyone in the community can publish a profile.{" "} - Verified means Bluehex has - checked that credential against the certificate that issued it. -

      - - {/* Search ------------------------------------------------------- */} -
      - - {/* The focus ring lives on the pill rather than the input, so it traces - the rounded shape instead of boxing the text. Matches the global - :focus-visible treatment in globals.css. */} -
      - - setQuery(event.target.value)} - placeholder="Search by service, skill, credential or location" - /* The native search affordances duplicate the clear button and the - icon this box already draws. */ - className="w-full bg-transparent text-base outline-none placeholder:text-t-faint md:text-lg [&::-webkit-search-cancel-button]:appearance-none" - /> - {query ? ( - - ) : null} -
      -
      - - {/* Filters ------------------------------------------------------ */} - {/* Grouped rather than one flat row of chips: "Verified", a country and a - focus area are three different kinds of claim, and mixing them makes - the row read as one undifferentiated pile. Groups whose source data is - empty render nothing at all. */} -
      - {anyBadged ? ( - - setVerifiedOnly(!verifiedOnly)}> - Verified only - - - ) : null} - - {countries.length > 0 ? ( - - {countries.map((country) => ( - toggle(setCountryFilters)(country.code)} + +
      +

      Find a Claude practitioner.

      + +

      + Anyone in the community can publish a profile.{" "} + Verified means Bluehex has + checked that credential against the certificate that issued it. +

      + + {/* Search ------------------------------------------------------- */} +
      + + {/* The focus ring lives on the box rather than the input, so it traces + the rounded shape instead of boxing the text. */} +
      + + setQuery(event.target.value)} + placeholder="Search by service, skill, credential or location" + /* The native search affordances duplicate the clear button and the + icon this box already draws. */ + className="w-full bg-transparent text-base text-t-bright outline-none placeholder:text-t-faint [&::-webkit-search-cancel-button]:appearance-none" + /> + {query ? ( + + ) : null} +
      +
      - {certificationOptions.length > 0 ? ( - - {certificationOptions.map((entry) => ( - toggle(setCertificationFilters)(entry.id)} - > - {entry.label} + {/* Filters ------------------------------------------------------ */} + {/* Grouped rather than one flat row of chips: "Verified", a country and a + focus area are three different kinds of claim, and mixing them makes + the row read as one undifferentiated pile. Groups whose source data is + empty render nothing at all. */} +
      + {anyBadged ? ( + + setVerifiedOnly(!verifiedOnly)}> + Verified only - ))} - - ) : null} + + ) : null} - {filtering ? ( - - ) : null} -
      + {countries.length > 0 ? ( + + {countries.map((country) => ( + toggle(setCountryFilters)(country.code)} + > + {country.name} + + ))} + + ) : null} - {/* Results ------------------------------------------------------ */} -

      - {practitioners.length === 0 - ? "The first profiles are being verified now." - : `${results.length} ${results.length === 1 ? "practitioner" : "practitioners"}`} -

      - - {/* No `practitioners.length > 0` guard. The empty roster is what production - ships, and the guard meant the one state that most needs an explanation - got none: filtering emptied the results, the invitation below is - suppressed under an active filter, and the container has no padding, so - the page's only call to action was replaced by nothing at all. Gating - the Verification chip above removes the one-click version of that; the - search box is not gated and cannot be, so the card has to cover it. */} - {filtering && results.length === 0 ? ( - -

      No practitioner matches that yet.

      -

      - {practitioners.length > 0 - ? "Try a broader term, or clear the filters to see everyone in the directory." - : "No profiles are published yet, so there is nothing to search — clearing this will not turn any up. Yours would be the first: certified, or working towards it."} -

      -
      - ) : ( -
      - {results.length > 0 ? ( - <> - {/* Column headings. Suppressed below lg, where a row stacks and - headings would label nothing. */} - + {offered.length > 0 ? ( + + {offered.map((service) => ( + toggle(setServiceFilters)(service.label)} + > + {service.label} + + ))} + + ) : null} -
        - {results.map((person) => ( -
      • - -
      • - ))} -
      - + {certificationOptions.length > 0 ? ( + + {certificationOptions.map((entry) => ( + toggle(setCertificationFilters)(entry.id)} + > + {entry.label} + + ))} + ) : null} - {/* The invitation only belongs on an unfiltered view — under an active - search it would read as a result. */} - {!filtering ? ( -
      - -

      Your profile here

      - {/* "Working towards it" is a statement about people, not about a - row anybody can enter — there is no in-progress credential and - a profile with none is an ordinary profile. The invitation is - still true and is deliberately not narrowed to the credentialed: - a practitioner says what they are working through in their bio, - in their own words. */} -

      - Certified, or working towards it. Both belong in the directory. -

      -
      + {filtering ? ( + ) : null}
      - )} -
  • + + {/* Results ------------------------------------------------------ */} +

    + {practitioners.length === 0 + ? "The first profiles are being verified now." + : `${results.length} ${results.length === 1 ? "practitioner" : "practitioners"}`} +

    + + {/* No `practitioners.length > 0` guard. The empty roster is what production + ships, and the guard meant the one state that most needs an explanation + got none: filtering emptied the results, the invitation below is + suppressed under an active filter, and the container has no padding, so + the page's only call to action was replaced by nothing at all. Gating + the Verification chip above removes the one-click version of that; the + search box is not gated and cannot be, so the card has to cover it. */} + {filtering && results.length === 0 ? ( + +

    No practitioner matches that yet.

    +

    + {practitioners.length > 0 + ? "Try a broader term, or clear the filters to see everyone in the directory." + : "No profiles are published yet, so there is nothing to search — clearing this will not turn any up. Yours would be the first: certified, or working towards it."} +

    +
    + ) : ( +
    + {results.length > 0 ? ( + <> + {/* Column headings, set like Xhibiter's Rankings table. + Suppressed below lg, where a row stacks and headings would + label nothing. */} + + +
      + {results.map((person) => ( +
    • + +
    • + ))} +
    + + ) : null} + + {/* The invitation only belongs on an unfiltered view — under an active + search it would read as a result. */} + {!filtering ? ( +
    + + + +

    Your profile here

    + {/* "Working towards it" is a statement about people, not about a + row anybody can enter — there is no in-progress credential and + a profile with none is an ordinary profile. The invitation is + still true and is deliberately not narrowed to the credentialed: + a practitioner says what they are working through in their bio, + in their own words. */} +

    + Certified, or working towards it. Both belong in the directory. +

    +
    + ) : null} +
    + )} + + ); } @@ -440,14 +444,11 @@ function FilterGroup({ label, children }: { label: string; children: React.React const labelId = useId(); return ( -
    - +
    + {label} -
    +
    {children}
    @@ -468,10 +469,10 @@ function FilterChip({ type="button" onClick={onClick} aria-pressed={pressed} - className={`inline-flex h-10 items-center rounded-full border px-4 text-sm font-medium transition-colors ${ + className={`inline-flex h-9 items-center rounded-xl border px-4 font-display text-sm transition-colors ${ pressed - ? "border-ink bg-ink text-t-invert" - : "border-stroke text-t-muted hover:border-stroke-strong hover:text-t-bright" + ? "border-transparent bg-ink text-t-invert" + : "border-stroke bg-surface text-t-muted hover:border-transparent hover:bg-ink hover:text-t-invert" }`} > {children} @@ -486,20 +487,28 @@ function PractitionerRow({ person }: { person: Profile }) { the top of the file. `countryCode` drives the location filter and is what a flag would be drawn from; the flag asset itself is its own ticket, so nothing renders it here yet. */} -
    -

    {person.name}

    - {person.headline ? ( -

    {person.headline}

    - ) : null} - {person.location ? ( -

    {person.location}

    - ) : null} +
    + +
    +

    {person.name}

    + {person.headline ? ( +

    {person.headline}

    + ) : null} + {person.location ? ( +

    {person.location}

    + ) : null} +
    {/* Credentials, and the badge that belongs to them. */}
    {hasVerifiedBadge(person.credentials) ? ( -

    +

    Verified by Bluehex

    @@ -570,7 +579,7 @@ function PractitionerRow({ person }: { person: Profile }) { and cut for that reason among others. */} View profile for {person.name} diff --git a/src/components/profile-editor/profile-form.tsx b/src/components/profile-editor/profile-form.tsx index acd876b..9d6630e 100644 --- a/src/components/profile-editor/profile-form.tsx +++ b/src/components/profile-editor/profile-form.tsx @@ -251,7 +251,7 @@ export function ProfileForm({ className={`rounded-full px-3.5 py-1.5 text-sm font-medium transition-colors ${ index === step ? "bg-ink text-t-invert" - : "text-t-muted hover:bg-surface hover:text-t-bright" + : "text-t-muted hover:bg-xh-jacarta-50 hover:text-t-bright" }`} >