Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -936,14 +936,14 @@ function ClinicalCollapsedRail({
<div className="grid w-full shrink-0 justify-items-center gap-2 px-3">
{collapseLocked ? (
<span className={collapsedSidebarButton} aria-hidden="true">
<BrandMark tone="emphasis" className="h-7 w-7" />
<BrandMark tone="emphasis" optical="chrome" className="h-7 w-7" />
</span>
) : (
<>
{/* Tablet: the expanded panel does not exist below lg, so show a
static brand mark instead of a dead expand control. */}
<span className={cn("hidden md:grid lg:hidden", collapsedSidebarControl)} aria-hidden="true">
<BrandMark tone="emphasis" className="h-7 w-7" />
<BrandMark tone="emphasis" optical="chrome" className="h-7 w-7" />
</span>
<Button
variant="ghost"
Expand All @@ -952,7 +952,11 @@ function ClinicalCollapsedRail({
title="Expand sidebar"
onClick={() => onCollapsedChange(false)}
>
<BrandMark tone="emphasis" className="h-7 w-7 group-hover:hidden group-focus-visible:hidden" />
<BrandMark
tone="emphasis"
optical="chrome"
className="h-7 w-7 group-hover:hidden group-focus-visible:hidden"
/>
<PanelLeftOpen
aria-hidden="true"
className="hidden size-icon-lg group-hover:block group-focus-visible:block"
Expand Down Expand Up @@ -1188,7 +1192,7 @@ export function ClinicalMobileSidebar({
closeLabel="Close Clinical Guide menu"
placement="left"
contentClassName={hiddenFrom === "lg" ? "lg:hidden" : "md:hidden"}
headerLeading={<BrandMark tone="emphasis" className="h-7 w-7 sm:h-8 sm:w-8" />}
headerLeading={<BrandMark tone="emphasis" optical="chrome" className="h-7 w-7 sm:h-8 sm:w-8" />}
headerClassName={drawerHeader}
titleClassName={drawerHeaderTitle}
closeButtonClassName={drawerHeaderClose}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function AccountOrientationPanel() {
data-testid="account-workspace-mark"
className="grid size-tap place-items-center rounded-xl border border-[color:var(--border-lux)] bg-[color:var(--surface)] shadow-[var(--e2),var(--shadow-inset)]"
>
<BrandMark tone="emphasis" className="h-8 w-8" />
<BrandMark tone="emphasis" optical="chrome" className="h-8 w-8" />
</span>
<h3
id="account-workspace-benefits"
Expand Down
45 changes: 33 additions & 12 deletions src/components/clinical-dashboard/brand.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { cn } from "@/components/ui-primitives";
import {
BRAND_COUNTER_TRANSFORM,
BRAND_GLYPH_TRANSFORM_BARE,
BRAND_POINT,
BRAND_STROKE_PATH,
BRAND_VIEWBOX,
} from "@/lib/brand-mark";
import { BRAND_COUNTER_TRANSFORM, BRAND_VIEWBOX, brandMarkOptics } from "@/lib/brand-mark";

/**
* Site brand mark: the PsychSift S — two counter-turning strokes divided by one
Expand Down Expand Up @@ -47,7 +41,34 @@ const BRAND_MARK_INK: Record<BrandMarkTone, string> = {
contrast: "var(--brand-mark-contrast)",
};

export function BrandMark({ className, tone = "resting" }: { className?: string; tone?: BrandMarkTone }) {
/**
* Which optical cut of the glyph to draw. The mark is a construction, not a
* bitmap, so it does not simply get smaller — two pieces of its negative space
* close up before anything else does.
*
* - `display` — the primary construction. Correct above 32px.
* - `chrome` — the small-size cut, for 32px and below: a wider gap between the
* strokes and the point slid out of its cradle. Both are the brand's own
* committed geometry, not a redraw.
*
* Sized by className, so the component cannot measure itself and this cannot be
* inferred. State it at the call site, matched to the height you are setting.
*/
export type BrandMarkOptical = "display" | "chrome";

export function BrandMark({
className,
tone = "resting",
optical = "display",
}: {
className?: string;
tone?: BrandMarkTone;
optical?: BrandMarkOptical;
}) {
// One call, so the stroke, the point and the centring transform can only be
// taken as a set — the brand doc is explicit that mixing one variant's point
// with the other's placement puts the glyph off-centre in its box.
const { transform, stroke, point } = brandMarkOptics(optical);
return (
<svg
viewBox={BRAND_VIEWBOX}
Expand All @@ -56,10 +77,10 @@ export function BrandMark({ className, tone = "resting" }: { className?: string;
focusable="false"
className={cn("shrink-0", className)}
>
<g transform={BRAND_GLYPH_TRANSFORM_BARE} fill={BRAND_MARK_INK[tone]}>
<path d={BRAND_STROKE_PATH} />
<path d={BRAND_STROKE_PATH} transform={BRAND_COUNTER_TRANSFORM} />
<circle cx={BRAND_POINT.cx} cy={BRAND_POINT.cy} r={BRAND_POINT.r} />
<g transform={transform} fill={BRAND_MARK_INK[tone]}>
<path d={stroke} />
<path d={stroke} transform={BRAND_COUNTER_TRANSFORM} />
<circle cx={point.cx} cy={point.cy} r={point.r} />
</g>
</svg>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function ClinicalRail({ activeMode }: { activeMode?: WardMode } = {}) {
<>
<div className={sidebarStyles.phoneBar}>
<Link href="/mockups/ward-flow" className={sidebarStyles.phoneBrand}>
<BrandMark tone="emphasis" className={sidebarStyles.brandGlyph} />
<BrandMark tone="emphasis" optical="chrome" className={sidebarStyles.brandGlyph} />
<span className={sidebarStyles.phoneBrandName}>Ward Flow</span>
</Link>
<button
Expand Down Expand Up @@ -117,7 +117,7 @@ export function ClinicalRail({ activeMode }: { activeMode?: WardMode } = {}) {
closeLabel="Close Ward Flow menu"
placement="left"
contentClassName={sidebarStyles.drawerHidden}
headerLeading={<BrandMark tone="emphasis" className={sidebarStyles.brandGlyph} />}
headerLeading={<BrandMark tone="emphasis" optical="chrome" className={sidebarStyles.brandGlyph} />}
>
<div className={sidebarStyles.drawerBody}>
<WardSidebarContent activeMode={activeMode} showBrandRow={false} onNavigate={() => setMenuOpen(false)} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/ward-management/ward-sidebar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function WardSidebarNav({
{showBrandRow ? (
<div className={styles.brandRow}>
<Link href="/mockups/ward-flow" className={styles.brandLink} onClick={onNavigate}>
<BrandMark tone="emphasis" className={styles.brandGlyph} />
<BrandMark tone="emphasis" optical="chrome" className={styles.brandGlyph} />
<span className={styles.brandText}>
<span className={styles.brandName}>Ward Flow</span>
<span className={styles.brandTagline}>Synthetic patient-flow prototype</span>
Expand Down
50 changes: 50 additions & 0 deletions src/lib/brand-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,56 @@ export const BRAND_GLYPH_TRANSFORM_SMALL = "translate(122.7103 51.2) scale(4.080
* mark therefore occupies exactly the slot the tiled version used to. */
export const BRAND_GLYPH_TRANSFORM_BARE = "translate(114.8907 0) scale(5.1006)";

/** The bare glyph's small-size placement — the counterpart of
* BRAND_GLYPH_TRANSFORM_SMALL for the untiled, in-app mark.
*
* Not a new measurement. The brand doc fixes the two ink boxes: the primary
* glyph spans 0 to 55.33 horizontally, the small variant 0 to 65.33, and both
* span the same 0 to 100.38 vertically. Every placement here centres that box
* in the 512 viewBox, which is verifiable against the three transforms above —
* each lands its centre on 256.000 to within 0.003, the rounding of a
* two-decimal box figure.
*
* Two consequences, and both are why this is a derivation rather than a redraw:
*
* The vertical extent is identical between the variants (the point slides
* sideways; its cy and r do not move), so the scale is unchanged at 5.1006 —
* 100.3813 x 5.1006 = 512.0, the bare variant's whole point.
*
* The point moves exactly 10 units out and is the box's right edge, so the box
* widens by exactly 10 and its centre by exactly 5. The placement therefore
* shifts left by 5 glyph units at this scale: 114.8907 - 5 x 5.1006 = 89.3877.
* The same arithmetic reproduces the committed tiled pair (143.1125 - 5 x
* 4.0804 = 122.7103, as shipped), which is the check that the rule is the one
* actually used rather than one that merely fits.
*
* It travels with BRAND_STROKE_PATH_SMALL and BRAND_POINT_SMALL as a set. The
* doc is explicit that mixing one variant's point with the other's placement
* puts the glyph off-centre; `brandMarkOptics()` below is the only supported
* way to pick, so a caller cannot half-apply the variant. */
export const BRAND_GLYPH_TRANSFORM_BARE_SMALL = "translate(89.3877 0) scale(5.1006)";

/** The two optical variants of the bare glyph, as whole sets.
*
* `display` is the primary construction. `chrome` is the small-size cut: the
* 4.2-unit gap between the strokes opened to 7.2, and the point slid 10 units
* out of its cradle so the crescent opens from 7.26 to 11.55.
*
* The threshold is 32px, from the brand doc's own "at 32 px and below two
* things close up" - below it the cut and the crescent both fall under two
* pixels and the dot fuses into the S, which reads as a heavy blob at the top
* of a mark whose lower stroke is already the thinner one. That fusion is what
* made the mark look top-heavy in app chrome; the deeper `--brand-mark-emphasis`
* ink compensates for it, and this fixes it.
*
* Below about 20px the crescent is under two pixels whatever is done. That is
* the size, not the placement, and no variant rescues it. */
export function brandMarkOptics(variant: "display" | "chrome") {
return variant === "chrome"
? { transform: BRAND_GLYPH_TRANSFORM_BARE_SMALL, stroke: BRAND_STROKE_PATH_SMALL, point: BRAND_POINT_SMALL }
: { transform: BRAND_GLYPH_TRANSFORM_BARE, stroke: BRAND_STROKE_PATH, point: BRAND_POINT };
}

/** The upper stroke. Four arcs and the straight cut, meeting at two cusps. */
export const BRAND_STROKE_PATH =
"M41.3675 2.6554 A17.7232 17.7232 0 0 0 29.0679 28.0493 A13 13 0 0 1 28.8667 40.1963 " +
Expand Down
192 changes: 192 additions & 0 deletions tests/brand-mark-optics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";

import { describe, expect, it } from "vitest";

import {
BRAND_GLYPH_TRANSFORM,
BRAND_GLYPH_TRANSFORM_BARE,
BRAND_GLYPH_TRANSFORM_BARE_SMALL,
BRAND_GLYPH_TRANSFORM_SMALL,
BRAND_POINT,
BRAND_POINT_SMALL,
BRAND_STROKE_PATH,
BRAND_STROKE_PATH_SMALL,
brandMarkOptics,
} from "@/lib/brand-mark";

/**
* The mark is a construction, not a bitmap, and it does not simply get smaller.
* Two pieces of its negative space — the 4.2-unit cut between the strokes and
* the 7.26-unit crescent around the point — close up at 32px and below, which
* fuses the dot into the S and reads as a heavy blob above an already-thinner
* lower stroke. That is the top-heavy look; `docs/brand/psychsift-logo.md`
* §"Small sizes" specifies a separate optical cut for exactly that range.
*
* These tests guard the two ways that can go wrong: the placement arithmetic,
* and a call site drawing the wrong cut for the size it renders at.
*/

/** Ink bounding boxes, from docs/brand/psychsift-logo.md. */
const INK = { displayWidth: 55.33, chromeWidth: 65.33, height: 100.38 };
const VIEWBOX = 512;
/** Two-decimal box figures, so the centre lands within a rounding of 256. */
const CENTRING_TOLERANCE = 0.01;

function parseTransform(transform: string) {
const match = /^translate\((-?[\d.]+) (-?[\d.]+)\) scale\(([\d.]+)\)$/.exec(transform);
expect(match, `unparseable transform: ${transform}`).not.toBeNull();
const [, x, y, scale] = match as RegExpExecArray;
return { x: Number(x), y: Number(y), scale: Number(scale) };
}

describe("the glyph placements all centre their own ink box", () => {
it.each([
["BRAND_GLYPH_TRANSFORM (tiled, display)", BRAND_GLYPH_TRANSFORM, INK.displayWidth],
["BRAND_GLYPH_TRANSFORM_SMALL (tiled, chrome)", BRAND_GLYPH_TRANSFORM_SMALL, INK.chromeWidth],
["BRAND_GLYPH_TRANSFORM_BARE (bare, display)", BRAND_GLYPH_TRANSFORM_BARE, INK.displayWidth],
["BRAND_GLYPH_TRANSFORM_BARE_SMALL (bare, chrome)", BRAND_GLYPH_TRANSFORM_BARE_SMALL, INK.chromeWidth],
])("%s lands its centre on 256", (_name, transform, inkWidth) => {
const { x, scale } = parseTransform(transform);
expect(x + (inkWidth / 2) * scale).toBeCloseTo(VIEWBOX / 2, 1);
expect(Math.abs(x + (inkWidth / 2) * scale - VIEWBOX / 2)).toBeLessThan(CENTRING_TOLERANCE * VIEWBOX);
});
});

describe("the bare pair is one construction at two cuts", () => {
const display = parseTransform(BRAND_GLYPH_TRANSFORM_BARE);
const chrome = parseTransform(BRAND_GLYPH_TRANSFORM_BARE_SMALL);

it("shares a scale, because only the point's x moves between variants", () => {
// BRAND_POINT_SMALL keeps cy and r, so the vertical extent is identical and
// the bare scale — which exists to fill the box top to bottom — cannot change.
expect(BRAND_POINT_SMALL.cy).toBe(BRAND_POINT.cy);
expect(BRAND_POINT_SMALL.r).toBe(BRAND_POINT.r);
expect(chrome.scale).toBe(display.scale);
expect(chrome.y).toBe(display.y);
});

it("fills the box top to bottom at that scale", () => {
expect(INK.height * display.scale).toBeCloseTo(VIEWBOX, 0);
});

it("shifts left by exactly half the point's travel", () => {
// The point moves 10 units out and is the box's right edge, so the box
// widens by 10 and its centre by 5.
const travel = BRAND_POINT_SMALL.cx - BRAND_POINT.cx;
expect(travel).toBe(10);
expect(INK.chromeWidth - INK.displayWidth).toBeCloseTo(travel, 1);
expect(display.x - chrome.x).toBeCloseTo((travel / 2) * display.scale, 2);
});

it("uses the rule the committed tiled pair already used", () => {
// The check that this is the rule actually in use, not one that merely fits.
const tiledDisplay = parseTransform(BRAND_GLYPH_TRANSFORM);
const tiledChrome = parseTransform(BRAND_GLYPH_TRANSFORM_SMALL);
expect(tiledDisplay.x - tiledChrome.x).toBeCloseTo(5 * tiledDisplay.scale, 2);
});
});

describe("the variants are selected as whole sets", () => {
// Mixing one variant's point with the other's placement puts the glyph
// off-centre; the doc calls that out by name. One selector makes it unreachable.
it("pairs each stroke with its own point and placement", () => {
expect(brandMarkOptics("display")).toEqual({
transform: BRAND_GLYPH_TRANSFORM_BARE,
stroke: BRAND_STROKE_PATH,
point: BRAND_POINT,
});
expect(brandMarkOptics("chrome")).toEqual({
transform: BRAND_GLYPH_TRANSFORM_BARE_SMALL,
stroke: BRAND_STROKE_PATH_SMALL,
point: BRAND_POINT_SMALL,
});
});

it("keeps the cuts genuinely different", () => {
expect(BRAND_STROKE_PATH_SMALL).not.toBe(BRAND_STROKE_PATH);
expect(BRAND_POINT_SMALL.cx).toBeGreaterThan(BRAND_POINT.cx);
});
});

/** The brand doc's threshold: "at 32 px and below two things close up". */
const CHROME_CUT_MAX_PX = 32;
const REM_PX = 16;

/**
* Tailwind `h-N` is N/4 rem; a CSS-module class is resolved to its own rule.
*
* The module lookup must go through the *specific* import identifier, not every
* `.module.css` the file imports. `ward-management-navigation.tsx` imports two
* modules and BOTH define `.brandGlyph`, at 2.5rem and 2rem — an identifier-blind
* resolver merges them and reports every site there as spanning 32-40px, which
* is a defect in this helper that reads exactly like a defect in the source.
*/
function renderedHeightsPx(className: string, sourcePath: string): number[] {
const cssModule = /\{\s*(\w+)\.(\w+)\s*\}/.exec(className);
if (!cssModule) {
return [...className.matchAll(/(?:^|\s|:)h-(\d+)\b/g)].map(([, n]) => (Number(n) / 4) * REM_PX);
}
const [, identifier, cssClass] = cssModule;
const source = readFileSync(sourcePath, "utf8");
const importPath = new RegExp(String.raw`import\s+${identifier}\s+from\s+"([^"]*\.module\.css)"`).exec(source)?.[1];
if (!importPath) return [];
const css = readFileSync(path.resolve(path.dirname(sourcePath), importPath), "utf8");
const rule = new RegExp(String.raw`\.${cssClass}\s*\{[^}]*?height:\s*([\d.]+)rem`, "s").exec(css);
return rule ? [Number(rule[1]) * REM_PX] : [];
}

describe("every production call site draws the cut its size needs", () => {
const files = execFileSync("git", ["ls-files", "src"], { encoding: "utf8" })
.split("\n")
.filter((file) => file.endsWith(".tsx") && !/mockups?\b/.test(file));

const callSites = files.flatMap((file) => {
const source = readFileSync(file, "utf8");
return [...source.matchAll(/<BrandMark\b([^>]*?)\/>/gs)].map((match) => ({
file,
attributes: match[1],
optical: /optical="chrome"/.test(match[1]) ? "chrome" : "display",
heights: renderedHeightsPx(
/className=(?:"([^"]*)"|(\{[^}]*\}))/.exec(match[1])?.slice(1).find(Boolean) ?? "",
file,
),
}));
});

it("finds the production call sites at all", () => {
// Guards the scan itself: a regex that silently matches nothing would make
// every assertion below pass vacuously.
expect(callSites.length).toBeGreaterThanOrEqual(10);
expect(callSites.every((site) => site.heights.length > 0)).toBe(true);
});

it.each([
["never straddles the threshold, which would need a human decision", "straddle"],
["draws the chrome cut at 32px and below", "chrome"],
["draws the display cut above 32px", "display"],
])("%s", (_name, mode) => {
for (const site of callSites) {
const smallest = Math.min(...site.heights);
const largest = Math.max(...site.heights);
if (mode === "straddle") {
expect(
smallest > CHROME_CUT_MAX_PX || largest <= CHROME_CUT_MAX_PX,
`${site.file} renders ${smallest}-${largest}px, spanning the ${CHROME_CUT_MAX_PX}px cut ` +
"threshold. One element draws one cut, so pick deliberately rather than by rounding.",
).toBe(true);
continue;
}
if (mode === "chrome" && largest <= CHROME_CUT_MAX_PX) {
expect(site.optical, `${site.file} renders at ${largest}px and needs optical="chrome"`).toBe("chrome");
}
if (mode === "display" && smallest > CHROME_CUT_MAX_PX) {
expect(
site.optical,
`${site.file} renders at ${smallest}px, above the small-cut range; the widened gap is not for it`,
).toBe("display");
}
}
});
});
Loading