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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useRef, useState, type FormEvent } from "react";
import { RotateCcw } from "lucide-react";
import { UiButton, UiCopyAction, UiDisclosure, UiDownloadAction, UiQuantityField, UiRepeatableList, UiSelect, UiTextField, type UiOption } from "@/app/ui/controls";
import { UiButton, UiCopyAction, UiDisclosure, UiDownloadAction, UiQuantityField, UiRepeatableList, UiSelect, UiShareAction, UiTextField, uiSetSummary, type UiOption } from "@/app/ui/controls";
import { UiActionRow, UiStatus } from "@/app/ui/display";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";
Expand All @@ -23,6 +23,9 @@ type Drive = { id: string; capacity: number; unit: string };
export function ActionsAndFormsExamples() {
const [hostname, setHostname] = useState("");
const [recordSize, setRecordSize] = useState("128K");
const [windowDays, setWindowDays] = useState("");
const [annualShare, setAnnualShare] = useState("");
const advancedSet = [windowDays, annualShare].filter((value) => value.trim()).length;
const [hostnameError, setHostnameError] = useState<string | null>(null);
const [status, setStatus] = useState("Enter a hostname, then check it.");
const hostnameRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -112,8 +115,17 @@ export function ActionsAndFormsExamples() {
/>
</Example>

<Example components={["UiDisclosure"]} note="Hide settings most people never change." source="app/ui/controls.tsx" title="Hidden settings">
<UiDisclosure label="Advanced settings" summary={`${recordSizeOptions.find((option) => option.id === recordSize)?.label} record size`}>
<Example components={["UiDisclosure", "uiSetSummary"]} note="Advanced holds optional refinements, closed by default. Name it for what's inside. While closed, the summary says what's set. Empty fields change nothing." source="app/ui/controls.tsx" title="Advanced settings">
<UiDisclosure label="Cohorts and billing" summary={uiSetSummary(advancedSet)}>
<div className={styles.twoColumns}>
<UiTextField hint="Days each trial gets to pay, counted from its start." inputMode="numeric" label="Follow-up window" onChange={setWindowDays} placeholder="30" suffix="days" value={windowDays} />
<UiTextField hint="Share of new customers who pick a yearly plan." inputMode="decimal" label="Annual-plan share" onChange={setAnnualShare} placeholder="20" suffix="%" value={annualShare} />
</div>
</UiDisclosure>
</Example>

<Example components={["UiDisclosure"]} note="A disclosure can also hold evidence or diagnostics, such as raw responses." source="app/ui/controls.tsx" title="Diagnostics">
<UiDisclosure label="Pool settings" summary={`${recordSizeOptions.find((option) => option.id === recordSize)?.label} record size`}>
<div className={styles.narrow}>
<UiSelect
label="Record size"
Expand All @@ -124,6 +136,12 @@ export function ActionsAndFormsExamples() {
</div>
</UiDisclosure>
</Example>

<Example components={["UiShareAction"]} note="Copies a link that reopens the tool with the same inputs. Empty values stay out of the link." source="app/ui/controls.tsx" title="Share link">
<UiActionRow>
<UiShareAction params={{ trials: 200, paid: 22, price: 29, window: "" }} />
</UiActionRow>
</Example>
</>
);
}
117 changes: 117 additions & 0 deletions app/(site)/(components-ui)/components-ui/examples/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ const visitors: UiChartSpec = {
xFormat: "date",
};

// A paperback that costs $4.60 to print: 50% below $9.99, 60% from $9.99, so the line steps up there.
const royaltyCurve: UiChartSpec = (() => {
const royalty = (cents: number) => (Math.round(cents * (cents >= 999 ? 0.6 : 0.5)) - 460) / 100;
const prices = [920, 998, 999, ...Array.from({ length: 21 }, (_, index) => 1000 + index * 100)];
return {
format: "currency",
kind: "line",
marks: [{ label: "Your price", x: 14.99, y: royalty(1499) }],
series: [{ id: "royalty", label: "Royalty per sale", points: prices.map((cents): UiChartPoint => [cents / 100, royalty(cents)]) }],
title: "Royalty by list price",
xFormat: "currency",
};
})();

const minute = 60_000;
const rawLatency = (() => {
const random = seeded(9);
Expand Down Expand Up @@ -161,6 +175,79 @@ const signups: UiChartSpec = {
xFormat: "date",
};

const trialFunnel: UiChartSpec = {
format: "number",
height: 280,
kind: "funnel",
stages: [
{ id: "visitors", label: "Visitors", value: 4800 },
{ id: "signups", label: "Signups", value: 640 },
{ id: "trials", label: "Trials", value: 200 },
{ id: "paid", label: "Paid", value: 22 },
],
title: "Visitors to paid customers",
};

const goal: UiChartSpec = { format: "currency", height: 240, kind: "gauge", label: "of $10,000 MRR goal", max: 10_000, title: "MRR goal", value: 6_400 };

const costs: UiChartSpec = {
format: "currency",
height: 280,
kind: "treemap",
parts: [
{ id: "hosting", label: "Hosting", value: 420 },
{ id: "database", label: "Database", value: 260 },
{ id: "email", label: "Email", value: 90 },
{ id: "analytics", label: "Analytics", value: 60 },
{ id: "monitoring", label: "Monitoring", value: 40 },
{ id: "backups", label: "Backups", value: 30 },
{ id: "domains", label: "Domains", value: 25 },
],
title: "Monthly costs",
};

const mrrFlow: UiChartSpec = {
format: "currency",
height: 300,
kind: "sankey",
links: [
{ source: "start", target: "kept", value: 8200 },
{ source: "start", target: "churned", value: 600 },
{ source: "start", target: "contracted", value: 200 },
{ source: "kept", target: "end", value: 8200 },
{ source: "new", target: "end", value: 1400 },
{ source: "expansion", target: "end", value: 500 },
],
nodes: [
{ id: "start", label: "Starting MRR" },
{ id: "new", label: "New" },
{ id: "expansion", label: "Expansion" },
{ id: "kept", label: "Kept" },
{ id: "churned", label: "Churned" },
{ id: "contracted", label: "Contraction" },
{ id: "end", label: "Ending MRR" },
],
title: "Where this month's MRR came from and went",
};

const plans: UiChartSpec = {
axes: [
{ label: "Price", max: 10 },
{ label: "Storage", max: 10 },
{ label: "Seats", max: 10 },
{ label: "Support", max: 10 },
{ label: "Integrations", max: 10 },
],
format: "number",
height: 320,
kind: "radar",
series: [
{ id: "starter", label: "Starter", values: [9, 4, 3, 5, 6] },
{ id: "team", label: "Team", values: [5, 8, 9, 8, 7] },
],
title: "Two plans compared",
};

const kilobytes = (text: string) => `${(new TextEncoder().encode(text).length / 1000).toFixed(1)} KB`;

/** What a chart adds to the page HTML: the server drawing, and the data handed to the browser. */
Expand Down Expand Up @@ -196,6 +283,11 @@ export function ChartsExamples() {
<LiveChartExample />
</Example>

<Example components={["UiChart"]} note="Marks put labeled dots on a line, such as the person's own value on a curve." source="app/ui/charts/chart-option.ts" title="Line with a mark">
<UiChart spec={royaltyCurve} />
<Weight spec={royaltyCurve} />
</Example>

<Example components={["UiChart"]} note="Stacked areas add up to the total." source="app/ui/charts/chart.tsx" title="Stacked area">
<UiChart spec={storage} />
<Weight spec={storage} />
Expand Down Expand Up @@ -226,6 +318,31 @@ export function ChartsExamples() {
<Weight spec={activity} />
</Example>

<Example components={["UiChart"]} note="Stages that narrow, in order. Hover a stage for its share of the first and the previous stage." source="app/ui/charts/chart.tsx" title="Funnel">
<UiChart spec={trialFunnel} />
<Weight note="funnel, gauge, treemap, sankey, and radar load their script only on pages that use them" spec={trialFunnel} />
</Example>

<Example components={["UiChart"]} note="One value against a scale, such as progress toward a goal." source="app/ui/charts/chart.tsx" title="Gauge">
<UiChart spec={goal} />
<Weight spec={goal} />
</Example>

<Example components={["UiChart"]} note="Parts of a total as blocks. Reads better than a donut when there are many parts." source="app/ui/charts/chart.tsx" title="Treemap">
<UiChart spec={costs} />
<Weight spec={costs} />
</Example>

<Example components={["UiChart"]} note="How amounts flow from one group to the next." source="app/ui/charts/chart.tsx" title="Flow">
<UiChart spec={mrrFlow} />
<Weight spec={mrrFlow} />
</Example>

<Example components={["UiChart"]} note="Several measures compared as shapes. Click a name to hide or show it." source="app/ui/charts/chart.tsx" title="Radar">
<UiChart spec={plans} />
<Weight spec={plans} />
</Example>

<Example components={["UiStaticChart"]} note="Drawn on the server only, with no chart script and no hover. For articles and reference pages." source="app/ui/charts/chart-static.tsx" title="Static chart">
<UiStaticChart spec={signups} />
<Weight interactive={false} spec={signups} />
Expand Down
22 changes: 22 additions & 0 deletions app/(site)/(components-ui)/components-ui/examples/page-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { PageHeader } from "@/app/ui/site/page-header";
import { PageMeta } from "@/app/ui/site/page-meta";
import { PageTitleIcon } from "@/app/ui/site/page-title-icon";
import { ThemeToggle } from "@/app/ui/site/theme-toggle";
import { UiButton, UiTextField } from "@/app/ui/controls";
import { UiActionRow, UiPrimaryResult } from "@/app/ui/display";
import { ToolFieldRow, ToolResult, ToolWorkbench } from "@/app/ui/tools/tool-workbench";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";

Expand Down Expand Up @@ -60,6 +63,25 @@ export function PageLayoutExamples() {
</div>
</Example>

<Example components={["ToolWorkbench", "ToolFieldRow", "ToolResult"]} note="Every tool sits in this band: the shared working width, inputs in rows that fill it, the action row, and the result below one divider. A result whose inputs changed steps back with stale." source="app/ui/tools/tool-workbench.tsx" title="Tool workbench">
<ToolWorkbench label="Example tool">
<ToolFieldRow>
<UiTextField label="Drives" suffix="drives" value="4" />
<UiTextField label="Drive size" suffix="TB" value="8" />
</ToolFieldRow>
<UiActionRow>
<UiButton type="button" variant="primary">Calculate capacity</UiButton>
<UiButton type="button">Reset</UiButton>
</UiActionRow>
<ToolResult label="Example result">
<UiPrimaryResult detail="Example" label="Usable capacity" unit="TB" value="24" />
</ToolResult>
<ToolResult label="Stale example result" stale>
<UiPrimaryResult detail="Inputs changed" label="Usable capacity, stale" unit="TB" value="24" />
</ToolResult>
</ToolWorkbench>
</Example>

<Example components={["AppShell", "PageCanvas"]} note="Every page uses the shared shell with a sidebar, and PageCanvas for each content section. This gallery uses both. See full layouts on their live pages." source="app/ui/site/app-shell.tsx, app/ui/site/page-canvas.tsx" title="Full page layouts">
<ul className={styles.layoutLinks}>
{fullLayouts.map((layout) => (
Expand Down
52 changes: 50 additions & 2 deletions app/(site)/(components-ui)/components-ui/examples/results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from "react";
import { UiButton, UiCopyAction, UiRawOutput } from "@/app/ui/controls";
import { UiActionRow, UiAllocationBar, UiCodeOutput, UiKeyValueList, UiMetricGroup, UiPrimaryResult, UiProgressBar, UiResultLead, UiStatusWord, UiSteps, UiTimeline } from "@/app/ui/display";
import { UiActionRow, UiAllocationBar, UiCodeOutput, UiRangeMarker, UiKeyValueList, UiMetricGroup, UiPrimaryResult, UiProgressBar, UiResultLead, UiStatusWord, UiSteps, UiTimeline, UiUnitGrid } from "@/app/ui/display";
import { Example } from "../gallery";
import styles from "../components-ui.module.css";

Expand All @@ -21,12 +21,26 @@ export function ResultsExamples() {
items={[
{ id: "raw", label: "Raw", value: "32", unit: "TiB" },
{ id: "protection", label: "Protection", value: "8", unit: "TiB" },
{ id: "reserve", label: "Reserve", value: "2.2", unit: "TiB" },
{ id: "reserve", label: "Reserve", value: "2.2", unit: "TiB", hint: "Free space kept back so the pool stays fast." },
]}
/>
</div>
</Example>

<Example components={["UiMetricGroup"]} note="The list layout gives one row per number, label left and value right, for numbers people read down a column." source="app/ui/display.tsx" title="Supporting numbers as a list">
<div className={styles.stack}>
<UiMetricGroup
ariaLabel="Trial numbers"
items={[
{ id: "not-paid", label: "Didn't pay", value: "460" },
{ id: "per-paid", label: "Trials per paying customer", value: "12.5", hint: "How many trial starts it took for each one that paid." },
{ id: "mrr", label: "New MRR", value: "$1,960.00" },
]}
layout="list"
/>
</div>
</Example>

<Example components={["UiPrimaryResult"]} note="Tones mark a good, risky, or failing answer. Neutral is the default." source="app/ui/display.tsx" title="Result tones">
<div className={styles.twoColumns}>
<UiPrimaryResult label="Break-even ACoS" tone="success" unit="%" value="42" />
Expand Down Expand Up @@ -64,6 +78,40 @@ export function ResultsExamples() {
</div>
</Example>

<Example components={["UiRangeMarker"]} note="Where a value sits next to published ranges. Position only, never a grade. The hint names each range's source. An optional range under the track shows where the person's own value probably sits, such as at a small sample size." source="app/ui/display.tsx" title="Range marker">
<div className={styles.stack}>
<UiRangeMarker
ariaLabel="Your trial conversion rate next to published ranges"
bands={[
{ id: "median", from: 4, to: 6, label: "Median, no-card trials", valueText: "4 to 6%", hint: "ChartMogul survey of 200 software products, January 2026." },
{ id: "top", from: 10, to: 15, label: "Top quarter, no-card trials", valueText: "10 to 15%", hint: "ChartMogul survey of 200 software products, January 2026." },
]}
max={30}
maxText="30%"
minText="0%"
range={{ from: 5.92, to: 10.72, label: "Likely range at 500 starts", valueText: "5.92 to 10.72%", hint: "The true rate is probably in this range. Fewer starts make it wider." }}
value={8}
valueText="8%"
/>
<UiRangeMarker
ariaLabel="A value near the start of the scale"
bands={[{ id: "median", from: 4, to: 6, label: "Median", valueText: "4 to 6%" }]}
max={30}
maxText="30%"
minText="0%"
value={1.5}
valueText="1.5%"
/>
</div>
</Example>

<Example components={["UiUnitGrid"]} note="A share of a group, as filled units out of 100. The last unit fills partly. Use it for rates of people or items. An optional target outlines the units up to a goal." source="app/ui/display.tsx" title="Unit grid">
<div className={styles.twoColumns}>
<UiUnitGrid ariaLabel="Paid out of every 100 who started" emptyLabel="Didn't pay" filled={13.22} filledLabel="Paid" target={{ amount: 20, label: "At 20%", valueText: "20 of every 100" }} valueText="13.22 of every 100" />
<UiUnitGrid ariaLabel="Drives that failed out of every 100" emptyLabel="Still running" filled={4.5} filledLabel="Failed" tone="neutral" valueText="4.5 of every 100" />
</div>
</Example>

<Example components={["UiAllocationBar"]} note="Parts of one total. Pick the neutral, accent, or categorical palette." source="app/ui/display.tsx" title="Allocation bar">
<div className={styles.stack}>
<UiAllocationBar ariaLabel="Neutral allocation" formatValue={formatTiB} palette="neutral" segments={[{ id: "used", label: "Used", value: 21.8 }, { id: "free", label: "Free", value: 10.2 }]} total={32} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function TextAndChoicesExamples() {
const [hourCycle, setHourCycle] = useState<12 | 24>(12);
const [layout, setLayout] = useState("raidz1");
const [format, setFormat] = useState("paperback");
const [trialType, setTrialType] = useState("no-card");
const [marketplaces, setMarketplaces] = useState(["us", "uk"]);
const [recordTypes, setRecordTypes] = useState(["a"]);
const [keepFree, setKeepFree] = useState(false);
Expand All @@ -43,12 +44,29 @@ export function TextAndChoicesExamples() {
<Example components={["UiTextField"]} source="app/ui/controls.tsx" title="Text field">
<div className={styles.twoColumns}>
<UiTextField label="DNS name" placeholder="example.com" required spellCheck={false} />
<UiTextField description="Shown under the field when it helps." label="Book title" placeholder="My first cookbook" />
<UiTextField description="Outside tools only. Tools use a hint." label="Book title" placeholder="My first cookbook" />
<UiTextField error={domainError} label="Field with an error" onChange={setDomain} required spellCheck={false} value={domain} />
<UiTextField label="Field with a suffix" placeholder="80" suffix="pages" />
</div>
</Example>

<Example components={["UiHint"]} note="A label most people wouldn't understand gets one sentence behind a ?. It opens on hover, keyboard focus, and tap. Every field takes a hint prop." source="app/ui/controls.tsx" title="Field hint">
<div className={styles.twoColumns}>
<UiTextField hint="Everyone who started a trial in the same month." inputMode="numeric" label="Trials started" placeholder="200" required />
<UiChoiceGroup
hint="A card upfront means they enter a card before the trial starts."
label="Trial type"
onChange={setTrialType}
options={[
{ id: "no-card", label: "No card" },
{ id: "card", label: "Card upfront" },
{ id: "free-plan", label: "Free plan" },
]}
value={trialType}
/>
</div>
</Example>

<Example components={["UiSearchField"]} source="app/ui/controls.tsx" title="Search field">
<div className={styles.narrow}>
<UiSearchField label="Filter records" onChange={setSearch} placeholder="Type to filter…" value={search} />
Expand Down Expand Up @@ -77,7 +95,7 @@ export function TextAndChoicesExamples() {
</div>
</Example>

<Example components={["UiChoiceGroup"]} note="Pick exactly one. Options can carry a short description." source="app/ui/controls.tsx" title="One choice">
<Example components={["UiChoiceGroup"]} note="Pick exactly one. Tools keep options to names. Outside tools, options can carry a short description." source="app/ui/controls.tsx" title="One choice">
<div className={styles.twoColumns}>
<UiChoiceGroup
label="Layout"
Expand Down
Loading
Loading