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
101 changes: 78 additions & 23 deletions website/src/components/demo/AppControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
SelectTrigger,
SelectValue,
} from "../ui/select";
import { ControlInfo } from "./control-docs";

export const TIMEZONES = [
"America/New_York",
Expand Down Expand Up @@ -167,14 +168,21 @@ const toInputValue = (zdt: Temporal.ZonedDateTime) =>

function SingleCheckbox({
children,
info,
...props
}: ComponentProps<typeof Checkbox> & { children: React.ReactNode }) {
}: ComponentProps<typeof Checkbox> & {
children: React.ReactNode;
info?: React.ReactNode;
}) {
return (
<FieldPrimitive.Root>
<FieldPrimitive.Label className="flex w-full items-center gap-2">
<Checkbox {...props} />
{children}
</FieldPrimitive.Label>
<div className="flex w-full items-center gap-1.5">
<FieldPrimitive.Label className="flex items-center gap-2">
<Checkbox {...props} />
{children}
</FieldPrimitive.Label>
{info}
</div>
</FieldPrimitive.Root>
);
}
Expand Down Expand Up @@ -280,7 +288,10 @@ export function AppControls(props: AppControlsProps) {

{selectionMode === "range" && (
<Field>
<FieldLabel htmlFor="range-mode">rangeMode</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="range-mode">rangeMode</FieldLabel>
<ControlInfo control="rangeMode" />
</div>
<Select
id="range-mode"
value={rangeMode}
Expand Down Expand Up @@ -310,13 +321,17 @@ export function AppControls(props: AppControlsProps) {
onCheckedChange={(checked) =>
setPreventRangeReversal(checked)
}
info={<ControlInfo control="preventRangeReversal" />}
>
preventRangeReversal
</SingleCheckbox>
)}

<Field>
<FieldLabel htmlFor="timezone-select">timeZone</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="timezone-select">timeZone</FieldLabel>
<ControlInfo control="timeZone" />
</div>
<Select
id="timezone-select"
value={timeZone}
Expand All @@ -343,7 +358,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field>
<FieldLabel htmlFor="locale-select">locale</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="locale-select">locale</FieldLabel>
<ControlInfo control="locale" />
</div>
<Select
id="locale-select"
value={locale}
Expand All @@ -363,7 +381,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field>
<FieldLabel htmlFor="week-start-day">weekStartDay</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="week-start-day">weekStartDay</FieldLabel>
<ControlInfo control="weekStartDay" />
</div>
<Select
id="week-start-day"
value={String(weekStartDay)}
Expand Down Expand Up @@ -395,7 +416,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field className="min-w-0">
<FieldLabel htmlFor="min-date">min</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="min-date">min</FieldLabel>
<ControlInfo control="min" />
</div>
<input
id="min-date"
type="date"
Expand All @@ -406,7 +430,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field className="min-w-0">
<FieldLabel htmlFor="max-date">max</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="max-date">max</FieldLabel>
<ControlInfo control="max" />
</div>
<input
id="max-date"
type="date"
Expand All @@ -419,13 +446,15 @@ export function AppControls(props: AppControlsProps) {
<SingleCheckbox
checked={disabled}
onCheckedChange={(checked) => setDisabled(checked)}
info={<ControlInfo control="disabled" />}
>
disabled
</SingleCheckbox>

<SingleCheckbox
checked={readOnly}
onCheckedChange={(checked) => setReadOnly(checked)}
info={<ControlInfo control="readOnly" />}
>
readOnly
</SingleCheckbox>
Expand All @@ -439,7 +468,10 @@ export function AppControls(props: AppControlsProps) {
/>
}
>
<FieldsetLegend>isDateDisabled</FieldsetLegend>
<div className="flex items-center gap-1.5">
<FieldsetLegend>isDateDisabled</FieldsetLegend>
<ControlInfo control="isDateDisabled" />
</div>
<RadioGroupItem value="none">None (default)</RadioGroupItem>
<RadioGroupItem value="weekends">Weekends</RadioGroupItem>
<RadioGroupItem value="past">Past dates</RadioGroupItem>
Expand All @@ -458,9 +490,12 @@ export function AppControls(props: AppControlsProps) {
<AccordionContent>
<div className="flex flex-col gap-4">
<Field>
<FieldLabel htmlFor="number-of-months">
numberOfMonths
</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="number-of-months">
numberOfMonths
</FieldLabel>
<ControlInfo control="numberOfMonths" />
</div>
<input
id="number-of-months"
type="number"
Expand All @@ -479,7 +514,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field>
<FieldLabel>outsideDays</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel>outsideDays</FieldLabel>
<ControlInfo control="outsideDays" />
</div>
<RadioGroup<OutsideDays>
value={outsideDays}
onValueChange={(v) => setOutsideDays(v as OutsideDays)}
Expand All @@ -499,10 +537,14 @@ export function AppControls(props: AppControlsProps) {
onCheckedChange={(checked) => setFixedWeeks(checked)}
/>
<FieldLabel>fixedWeeks</FieldLabel>
<ControlInfo control="fixedWeeks" />
</Field>

<Field>
<FieldLabel>outOfRangeBehavior</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel>outOfRangeBehavior</FieldLabel>
<ControlInfo control="monthOutOfRangeBehavior" />
</div>
<RadioGroup
value={monthOutOfRangeBehavior}
onValueChange={(v) =>
Expand All @@ -526,7 +568,10 @@ export function AppControls(props: AppControlsProps) {
<AccordionContent>
<div className="flex flex-col gap-4">
<Field>
<FieldLabel htmlFor="week-count">weekCount</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="week-count">weekCount</FieldLabel>
<ControlInfo control="weekCount" />
</div>
<Select
id="week-count"
value={String(weekCount)}
Expand All @@ -549,7 +594,10 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field>
<FieldLabel>scrollBy</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel>scrollBy</FieldLabel>
<ControlInfo control="scrollBy" />
</div>
<RadioGroup
value={scrollBy}
onValueChange={(v) => setScrollBy(v as "row" | "page")}
Expand All @@ -560,9 +608,12 @@ export function AppControls(props: AppControlsProps) {
</Field>

<Field>
<FieldLabel htmlFor="overflow-behavior">
outOfRangeBehavior
</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel htmlFor="overflow-behavior">
outOfRangeBehavior
</FieldLabel>
<ControlInfo control="weeksOutOfRangeBehavior" />
</div>
<Select
id="overflow-behavior"
value={outOfRangeBehavior}
Expand Down Expand Up @@ -601,6 +652,7 @@ export function AppControls(props: AppControlsProps) {
onCheckedChange={(checked) => setAutoFocus(checked)}
/>
<FieldLabel>Grid.autoFocus</FieldLabel>
<ControlInfo control="autoFocus" />
</Field>

<Field orientation="horizontal">
Expand All @@ -626,7 +678,10 @@ export function AppControls(props: AppControlsProps) {
)}

<Field>
<FieldLabel>Grid.orientation</FieldLabel>
<div className="flex items-center gap-1.5">
<FieldLabel>Grid.orientation</FieldLabel>
<ControlInfo control="orientation" />
</div>
<RadioGroup
value={orientation}
onValueChange={(v) =>
Expand Down
142 changes: 142 additions & 0 deletions website/src/components/demo/control-docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { createContext, useContext, useMemo } from "react";
import InlineDescription from "#/components/InlineDescription";
import { Code } from "#/components/ui/code";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/ui/tooltip";
import type { ApiData } from "#/lib/api-data";

/**
* Maps each demo control to the library symbol + property whose TSDoc documents
* it. The tooltip text is pulled live from the generated API data
* (`api-data/symbols.gen.json`), so it always mirrors the source TSDoc.
*
* `rangeMode`/`preventRangeReversal` are only documented on the stable context
* value (they live on a selection-mode union, not the base provider props), so
* they reference `CalendarStableContextValue`.
*/
const CONTROL_DOC_MAP = {
rangeMode: { symbol: "CalendarStableContextValue", prop: "rangeMode" },
preventRangeReversal: {
symbol: "CalendarStableContextValue",
prop: "preventRangeReversal",
},
timeZone: { symbol: "CalendarProviderProps", prop: "timeZone" },
locale: { symbol: "CalendarProviderProps", prop: "locale" },
weekStartDay: { symbol: "CalendarProviderProps", prop: "weekStartDay" },
min: { symbol: "CalendarProviderProps", prop: "min" },
max: { symbol: "CalendarProviderProps", prop: "max" },
disabled: { symbol: "CalendarProviderProps", prop: "disabled" },
readOnly: { symbol: "CalendarProviderProps", prop: "readOnly" },
isDateDisabled: { symbol: "CalendarProviderProps", prop: "isDateDisabled" },
numberOfMonths: { symbol: "MonthViewRootProps", prop: "numberOfMonths" },
outsideDays: { symbol: "MonthViewRootProps", prop: "outsideDays" },
fixedWeeks: { symbol: "MonthViewRootProps", prop: "fixedWeeks" },
monthOutOfRangeBehavior: {
symbol: "MonthViewRootProps",
prop: "outOfRangeBehavior",
},
weekCount: { symbol: "WeeksViewRootProps", prop: "weekCount" },
scrollBy: { symbol: "WeeksViewRootProps", prop: "scrollBy" },
weeksOutOfRangeBehavior: {
symbol: "WeeksViewRootProps",
prop: "outOfRangeBehavior",
},
autoFocus: { symbol: "GridProps", prop: "autoFocus" },
orientation: { symbol: "GridProps", prop: "orientation" },
} as const satisfies Record<string, { symbol: string; prop: string }>;

export type ControlKey = keyof typeof CONTROL_DOC_MAP;

interface ResolvedDoc {
description: string;
defaultValue?: string;
}

const ApiDataContext = createContext<ApiData | null>(null);

/** Provides the generated API data to descendant {@link ControlInfo} icons. */
export function ControlDocsProvider({
apiData,
children,
}: {
apiData: ApiData;
children: React.ReactNode;
}) {
return (
<ApiDataContext.Provider value={apiData}>
{children}
</ApiDataContext.Provider>
);
}

function useControlDoc(control: ControlKey): ResolvedDoc | null {
const apiData = useContext(ApiDataContext);
return useMemo(() => {
if (!apiData) return null;
const ref = CONTROL_DOC_MAP[control];
const symbol = apiData.symbols.find((s) => s.name === ref.symbol);
const prop = symbol?.properties?.find((p) => p.name === ref.prop);
if (!prop?.description.trim()) return null;
return { description: prop.description, defaultValue: prop.defaultValue };
}, [apiData, control]);
}

const infoIcon = (
<svg
width="14"
height="14"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.25" />
<path
d="M8 7.4v3.1"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="8" cy="5.2" r="0.85" fill="currentColor" />
</svg>
);

/**
* Info icon rendered next to a demo control label. On hover/focus it reveals the
* library's TSDoc for the mapped prop. Renders nothing when the prop has no
* extracted documentation, so undocumented/demo-only controls stay icon-free.
*/
export function ControlInfo({ control }: { control: ControlKey }) {
const doc = useControlDoc(control);
if (!doc) return null;

const { prop } = CONTROL_DOC_MAP[control];

return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label={`What does ${prop} do?`}
className="inline-flex shrink-0 cursor-help items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:outline-none"
>
{infoIcon}
</button>
}
/>
<TooltipContent>
<div className="max-w-72 py-0.5 leading-relaxed whitespace-pre-line text-foreground">
<InlineDescription text={doc.description} />
{doc.defaultValue && (
<div className="mt-1.5 text-muted-foreground">
Default: <Code size={100}>{doc.defaultValue}</Code>
</div>
)}
</div>
</TooltipContent>
</Tooltip>
);
}
Loading
Loading