Active Foundation · Example
One presentation for every formatter
One presentation for every formatterClient
| formatDateOnly | |
| formatDateTime | |
| formatNumber | |
| formatCurrency | |
| formatFractionAsPercent | |
| formatByteSize | |
| formatList |
Full source examples/format/presentation-switcher.tsx
// apps/docs/examples/format/presentation-switcher.tsx'use client';
import {useId, useState} from 'react';
import { composePresentationContext, formatByteSize, formatCurrency, formatDateOnly, formatDateTime, formatFractionAsPercent, formatList, formatNumber, toIanaTimeZone, type DateNotationPreset, type NumberNotationPreset, type PresentationProfileConfig, type TimeNotationPreset,} from '@workspace/active-format';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import { NativeSelect, NativeSelectOption,} from '@workspace/active-ui/components/primitives/native-select';import { Table, TableBody, TableCell, TableRow,} from '@workspace/active-ui/components/primitives/table';
// Fixed values, so every reader sees the same input.const CALENDAR_DAY = '2026-03-09';const INSTANT = '2026-03-09T20:18:05Z';const LANGUAGES = ['en', 'nl', 'de', 'fr', 'es', 'ja', 'ar-EG'];const CURRENCIES = ['EUR', 'USD', 'GBP', 'JPY', 'KWD'];const TIME_ZONES = [ 'Europe/Amsterdam', 'America/New_York', 'Asia/Tokyo', 'UTC',];
type Choice<T> = T | 'language';
// What each notation choice shows. The value stays the preset name the// profile takes; the label says what it does, short enough to read in full// in a field on a narrow screen.const CHOICE_LABELS: Record<string, string> = { language: 'From the language', DMY_NUMERIC: 'Day, month, year', MDY_NUMERIC: 'Month, day, year', ISO_NUMERIC: 'Year, month, day (ISO)', H24: '24-hour clock', H12: '12-hour clock', DOT_DECIMAL_COMMA_GROUP: 'Decimal point, comma groups', COMMA_DECIMAL_DOT_GROUP: 'Decimal comma, point groups', COMMA_DECIMAL_SPACE_GROUP: 'Decimal comma, space groups',};
/** One presentation, composed from language, profile and zone, feeding every formatter. */export default function PresentationSwitcher() { const [language, setLanguage] = useState('en'); const [currency, setCurrency] = useState('EUR'); const [timeZone, setTimeZone] = useState('Europe/Amsterdam'); const [dateFormat, setDateFormat] = useState<Choice<DateNotationPreset>>('language'); const [timeFormat, setTimeFormat] = useState<Choice<TimeNotationPreset>>('language'); const [numberFormat, setNumberFormat] = useState<Choice<NumberNotationPreset>>('language');
// A profile field that is absent means "not configured": the language's own // convention applies. const profile: PresentationProfileConfig = { ...(dateFormat === 'language' ? {} : {dateFormat}), ...(timeFormat === 'language' ? {} : {timeFormat}), ...(numberFormat === 'language' ? {} : {numberFormat}), }; const presentation = composePresentationContext( language, profile, toIanaTimeZone(timeZone), );
const rows: [string, string][] = [ ['formatDateOnly', formatDateOnly(CALENDAR_DAY, 'long', presentation)], [ 'formatDateTime', formatDateTime(INSTANT, 'numeric-datetime', presentation), ], ['formatNumber', formatNumber(1234567.891, 'decimal-2', presentation)], [ 'formatCurrency', formatCurrency({amount: 1234.5, currency}, presentation), ], [ 'formatFractionAsPercent', formatFractionAsPercent(0.125, 'decimal-1', presentation), ], [ 'formatByteSize', formatByteSize(1_536_000, presentation, {system: 'decimal'}), ], ['formatList', formatList(['Amsterdam', 'Utrecht', 'Delft'], presentation)], ];
// The stage centres its content, which sizes a plain root to its widest // child: `w-full min-w-0` keeps the demo to the width there is, so the // fields wrap and only the result table scrolls, in its own named region. return ( <div className="flex w-full min-w-0 flex-col gap-4"> {/* Columns follow the width the demo has, not the screen: beside the sidebar a three-column row at `sm` left each field too narrow for its longest choice. */} <div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,15rem),1fr))] gap-3"> <Select label="Language" value={language} onChange={setLanguage} options={LANGUAGES} /> <Select label="Currency" value={currency} onChange={setCurrency} options={CURRENCIES} /> <Select label="Time zone" value={timeZone} onChange={setTimeZone} options={TIME_ZONES} /> <Select label="Date notation" value={dateFormat} onChange={(value) => setDateFormat(value as Choice<DateNotationPreset>) } options={['language', 'DMY_NUMERIC', 'MDY_NUMERIC', 'ISO_NUMERIC']} /> <Select label="Clock" value={timeFormat} onChange={(value) => setTimeFormat(value as Choice<TimeNotationPreset>) } options={['language', 'H24', 'H12']} /> <Select label="Number notation" value={numberFormat} onChange={(value) => setNumberFormat(value as Choice<NumberNotationPreset>) } options={[ 'language', 'DOT_DECIMAL_COMMA_GROUP', 'COMMA_DECIMAL_DOT_GROUP', 'COMMA_DECIMAL_SPACE_GROUP', ]} /> </div> <Table scrollLabel="Results, scrolls sideways"> <TableBody> {rows.map(([name, text]) => ( <TableRow key={name}> <TableCell className="text-muted-foreground font-mono text-xs"> {name} </TableCell> <TableCell> <output suppressHydrationWarning>{text}</output> </TableCell> </TableRow> ))} </TableBody> </Table> </div> );}
function Select({ label, value, onChange, options,}: { label: string; value: string; onChange: (value: string) => void; options: readonly string[];}) { const id = useId(); return ( <Field className="min-w-0"> <FieldLabel htmlFor={id}>{label}</FieldLabel> <NativeSelect id={id} className="w-full" value={value} onChange={(event) => onChange(event.target.value)} > {options.map((option) => ( <NativeSelectOption key={option} value={option}> {CHOICE_LABELS[option] ?? option} </NativeSelectOption> ))} </NativeSelect> </Field> );}