From 9de1ab37bb1ee02591d742773d1f591599692727 Mon Sep 17 00:00:00 2001 From: Engramar Bollas Date: Mon, 12 Oct 2026 08:11:32 +1100 Subject: [PATCH] Restyle the site in the Xhibiter Home 2 look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design tokens in globals.css now carry the Xhibiter palette — violet accent, jacarta navy text, Cal Sans and DM Sans — so every component follows from the token swap. On top of that: a shared centred Hero over the Home 2 gradient for Agent Exchange, Claude Practitioners and Meetups; a gradient faded in behind the top of every other page; volume-shadow pill buttons; bordered cards; an inline nav in the header on wide screens; and a footer in Xhibiter's column layout. The agent directory lists agents as cards. The practitioner directory keeps rows, styled as Xhibiter's rankings table, because the file records why rows win there: credentials line up in a column for comparison. The Verified badge stays with the credentials. The collaborators strip is now a quiet row of greyscale logos that link out, shown on all three tabs. --- public/img/xhibiter/gradient.jpg | Bin 0 -> 8611 bytes public/img/xhibiter/gradient_light.jpg | Bin 0 -> 6013 bytes src/app/admin/review-queue.tsx | 2 +- src/app/contact/contact-form.tsx | 2 +- src/app/globals.css | 83 ++-- src/app/layout.tsx | 30 +- src/app/meetups/page.tsx | 48 ++- src/app/metadata.test.ts | 4 +- src/app/p/_lib/profile-detail.tsx | 6 +- src/app/page.tsx | 40 +- src/app/practitioners/page.tsx | 40 +- src/app/prototype/page.tsx | 2 +- src/components/agent-directory.tsx | 356 ++++++++-------- src/components/collaborators.tsx | 34 +- src/components/meetup-events.tsx | 2 +- src/components/practitioner-directory.tsx | 403 +++++++++--------- .../profile-editor/profile-form.tsx | 10 +- src/components/profile-editor/row-preview.tsx | 2 +- src/components/site-footer.tsx | 130 +++--- src/components/site-header.tsx | 37 +- src/components/ui.tsx | 114 +++-- 21 files changed, 746 insertions(+), 599 deletions(-) create mode 100644 public/img/xhibiter/gradient.jpg create mode 100644 public/img/xhibiter/gradient_light.jpg diff --git a/public/img/xhibiter/gradient.jpg b/public/img/xhibiter/gradient.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ea99334d3a4e4bdd7a4cae05bfa871f4e1e22f4 GIT binary patch literal 8611 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 z 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 UeRUgdtUdbV?|!@Gvr8BL3x1knvj6}9 literal 0 HcmV?d00001 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" }`} >