Component Specifications
Trace typed sizing presets to their source and inspect actual rendered parts, content-driven dimensions and scoped overrides.
Choose a specification and inspect its part
Choose a component preset and field. The live specimen uses the exported component, while the inspector names the exact selector and source declaration behind that field. Open a popup with its own trigger when its selected part is absent. Keyboard interaction and dismissal remain the component’s normal behavior.
The authored value, browser-resolved property and host rectangle answer different questions. A var() expression describes a dependency; a resolved value reflects this instance; a rectangle describes the rendered border box. For a pseudo-element, the rectangle belongs to its host, while the resolved property belongs to the pseudo-element.
Modal behavior makes the background inspector unavailable while a dialog is open. Close normally to read the last captured observation for that field. Its timestamp, viewport, theme and local options remain explicit; the live status still reports that the closed part is not rendered. Captures are local page-memory observations, not persisted benchmarks.
Loading the component inspector…
- Authored specification
0.5rem- Source binding
.mt-input-sm → padding-inline
packages/ui/src/components/components.css:158- Browser resolved padding-inline
- Reading selected specimen…
- Host bounding rectangle, CSS pixels
- Unavailable
Reading selected specimen…
Last captured observation for this field
No observation yet. Open the selected part to capture it.
Open menus and surfaces with the specimen’s own controls; dismiss with their normal keyboard actions. Portals use the page viewport and do not inherit the stage override. A pseudo-element measurement reports its computed property and the host element’s rectangle separately.
Preset contract and source ownership
The nineteen existing component preset exports are descriptive specifications, not component props or complete React style objects. Their original field names remain exported; values that contradicted the actual CSS have been corrected. Additions name previously hidden constraints such as minimum height, list bounds, the painted Slider marker and reserved Toast close space.
components.css owns authored styling. component-bindings.json binds each public field to an explicit selector/property, including a shorthand extraction where needed. The generator takes the last matching direct declaration in an unlayered rule. Nested selectors and at-rules are excluded from this source contract. This is a source-binding contract, not an implementation of selector specificity, inheritance or the full cascade. The live browser is the authority for a particular rendered instance.
When a default has no component declaration, the binding records its initial value or the Tailwind preflight inheritance contract explicitly. It does not fabricate a source line. A missing required declaration or an unsupported asymmetric shorthand makes generation fail rather than silently retaining an older number.
Understand contextual dimensions
| Specification | What it describes | What to verify |
|---|---|---|
| Button sm/md/lg height | Minimum height with content-driven growth; legacy height token keys retained | Labels may wrap at enlarged text sizes. The recorded CSS property is min-height, while the measured geometry remains actual height. Icon geometry stays fixed. |
| Select triggerHeight: auto | Content-driven control; triggerMinHeight is 44px | Long selected labels may grow; measure the trigger and keep popup bounds inside the real viewport. |
| Tabs listHeight: auto | A list sized by its triggers, borders and orientation | Horizontal height includes the dividing border; vertical tabs have a different layout. |
| Slider thumbSize / visibleThumbSize | 44px target and 20px painted marker | Inspect the native input’s host and the ::after marker separately; test dragging and keyboard changes. |
| Scroll Area scrollbarSize | 24px authored vertical track | Horizontal track is separate; proportional thumb length comes from actual overflow. |
| Toast padding / paddingInlineEnd | Base padding and logical end space for its close action | The resolved shorthand includes the end override; inspect both directions and actual notification stacking. |
| Popup maxWidth / maxHeight | Viewport- and available-space expressions | Open near viewport edges, change the actual window size and check internal scrolling. |
| Accordion contentPadding: 0px | No default panel padding | Spacing supplied by nested content is separate from the panel’s own padding. |
Defaults corrected by this audit
These corrections change exported metadata, while preserving the component CSS behavior. Review consumers that use presets to draw external assets or manually style unrelated elements. A descriptive preset cannot be applied wholesale as though its keys were CSS properties.
| Older preset | Current contract |
|---|---|
| Input sm paddingX: 0.625rem | 0.5rem, matching the size class |
| Select fixed 2.75rem height and 16rem popup | auto height, 44px minimum and the actual available-height expression |
| Select item padding: 0.5rem / 0.375rem | 12px horizontally and 8px vertically |
| Slider thumbSize: 1.25rem | 44px interactive target; separate 20px painted marker |
| Scroll Area 0.5rem track / full radius | 24px track / radius-sm thumb |
| Tabs fixed list height / rounded list | Content-driven height / unrounded list |
| Accordion 1rem panel padding / rounded root | Unpadded panel / unrounded root |
| Unbounded Tooltip/Popover width presets | Actual min() viewport bounds |
| Icon button fontSize: 1rem | Inherited text size from the consumer’s preflight/style context |
Complete public preset register
| Export and field | Authored value | Source binding |
|---|---|---|
| buttonTokens.sm.height | 2.25rem | .mt-btn-sm { min-height } — CSS line 53; Minimum height; the control grows with wrapped content. The legacy height token key is retained. |
| buttonTokens.sm.paddingX | 0.75rem | .mt-btn-sm { padding-inline } — CSS line 53 |
| buttonTokens.sm.fontSize | 0.875rem | .mt-btn-sm { font-size } — CSS line 53 |
| buttonTokens.md.height | 2.75rem | .mt-btn-md { min-height } — CSS line 54; Minimum height; the control grows with wrapped content. The legacy height token key is retained. |
| buttonTokens.md.paddingX | 1rem | .mt-btn-md { padding-inline } — CSS line 54 |
| buttonTokens.md.fontSize | 1rem | .mt-btn-md { font-size } — CSS line 54 |
| buttonTokens.lg.height | 3rem | .mt-btn-lg { min-height } — CSS line 55; Minimum height; the control grows with wrapped content. The legacy height token key is retained. |
| buttonTokens.lg.paddingX | 1.5rem | .mt-btn-lg { padding-inline } — CSS line 55 |
| buttonTokens.lg.fontSize | 1.125rem | .mt-btn-lg { font-size } — CSS line 55 |
| buttonTokens.icon.height | 2.75rem | .mt-btn-icon { height } — CSS line 56 |
| buttonTokens.icon.width | 2.75rem | .mt-btn-icon { width } — CSS line 56 |
| buttonTokens.icon.fontSize | inherit | .mt-btn-icon { font-size } — preflight; Tailwind preflight inherits font size for native buttons; this is not a fixed text size. |
| buttonTokens.radius | var(--radius-md) | .mt-btn { border-radius } — CSS line 35 |
| inputTokens.sm.height | 2.25rem | .mt-input-sm { height } — CSS line 158 |
| inputTokens.sm.paddingX | 0.5rem | .mt-input-sm { padding-inline } — CSS line 158 |
| inputTokens.sm.fontSize | 0.875rem | .mt-input-sm { font-size } — CSS line 158 |
| inputTokens.md.height | 2.75rem | .mt-input-md { height } — CSS line 159 |
| inputTokens.md.paddingX | 0.75rem | .mt-input-md { padding-inline } — CSS line 159 |
| inputTokens.md.fontSize | 1rem | .mt-input-md { font-size } — CSS line 159 |
| inputTokens.lg.height | 3rem | .mt-input-lg { height } — CSS line 160 |
| inputTokens.lg.paddingX | 1rem | .mt-input-lg { padding-inline } — CSS line 160 |
| inputTokens.lg.fontSize | 1.125rem | .mt-input-lg { font-size } — CSS line 160 |
| inputTokens.borderWidth | 2px | .mt-input { border } — CSS line 148 |
| inputTokens.radius | var(--radius-md) | .mt-input { border-radius } — CSS line 149 |
| dialogTokens.sm.maxWidth | 24rem | .mt-dialog-sm { max-width } — CSS line 360 |
| dialogTokens.md.maxWidth | 32rem | .mt-dialog-md { max-width } — CSS line 361 |
| dialogTokens.lg.maxWidth | 42rem | .mt-dialog-lg { max-width } — CSS line 362 |
| dialogTokens.xl.maxWidth | 56rem | .mt-dialog-xl { max-width } — CSS line 363 |
| dialogTokens.padding | 1.5rem | .mt-dialog-popup { padding } — CSS line 354 |
| dialogTokens.backdropOpacity | 0.5 | .mt-dialog-backdrop { background } — CSS line 341 |
| dialogTokens.radius | var(--radius-lg) | .mt-dialog-popup { border-radius } — CSS line 353 |
| dialogTokens.maxHeight | calc(100dvh - 2rem) | .mt-dialog-popup { max-height } — CSS line 1358 |
| selectTokens.triggerHeight | auto | .mt-select-trigger { height } — CSS line 1553; Content-driven height; use triggerMinHeight for the target minimum. |
| selectTokens.triggerMinHeight | 44px | .mt-select-trigger { min-height } — CSS line 1553 |
| selectTokens.contentMaxHeight | min(320px, var(--available-height, 80dvh)) | .mt-select-popup { max-height } — CSS line 1557 |
| selectTokens.listMaxHeight | min(304px, calc(var(--available-height, 80dvh) - 12px)) | .mt-select-list { max-height } — CSS line 1559 |
| selectTokens.itemPaddingX | 12px | .mt-select-item { padding } — CSS line 1560 |
| selectTokens.itemPaddingY | 8px | .mt-select-item { padding } — CSS line 1560 |
| selectTokens.radius | var(--radius-md) | .mt-select-popup { border-radius } — CSS line 280 |
| tabsTokens.listHeight | auto | .mt-tabs-list { height } — initial; CSS initial height is auto; a horizontal list includes its border and sized triggers. |
| tabsTokens.triggerMinHeight | 44px | .mt-tabs-trigger { min-height } — CSS line 1489 |
| tabsTokens.triggerPaddingX | 1rem | .mt-tabs-trigger { padding } — CSS line 314 |
| tabsTokens.radius | 0px | .mt-tabs-list { border-radius } — initial; The list has no rounded surface. Focus styling on triggers is separate. |
| tooltipTokens.paddingX | 0.75rem | .mt-tooltip-popup { padding } — CSS line 372 |
| tooltipTokens.paddingY | 0.375rem | .mt-tooltip-popup { padding } — CSS line 372 |
| tooltipTokens.fontSize | 0.875rem | .mt-tooltip-popup { font-size } — CSS line 373 |
| tooltipTokens.radius | var(--radius-md) | .mt-tooltip-popup { border-radius } — CSS line 374 |
| tooltipTokens.maxWidth | min(20rem, calc(100vw - 16px)) | .mt-tooltip-popup { max-width } — CSS line 1614 |
| popoverTokens.padding | 1rem | .mt-popover-popup { padding } — CSS line 456 |
| popoverTokens.radius | var(--radius-lg) | .mt-popover-popup { border-radius } — CSS line 455 |
| popoverTokens.maxWidth | min(24rem, calc(100vw - 16px)) | .mt-popover-popup { max-width } — CSS line 1613 |
| toastTokens.padding | 1rem | .mt-toast { padding } — CSS line 537; Base padding, inspected through the block start. The logical end override is a separate field. |
| toastTokens.paddingInlineEnd | 3.5rem | .mt-toast { padding-inline-end } — CSS line 544; Reserves space for the close action; differs from the base padding. |
| toastTokens.radius | var(--radius-lg) | .mt-toast { border-radius } — CSS line 536 |
| toastTokens.maxWidth | 26rem | .mt-toast-viewport { max-width } — CSS line 527; The viewport bounds the toast stack; individual toasts fill that context. |
| checkboxTokens.size | 1.25rem | .mt-checkbox { width } — CSS line 179 |
| checkboxTokens.radius | var(--radius-sm) | .mt-checkbox { border-radius } — CSS line 182 |
| switchTokens.width | 2.75rem | .mt-switch { width } — CSS line 200 |
| switchTokens.height | 1.5rem | .mt-switch { height } — CSS line 201 |
| switchTokens.thumbSize | 1.25rem | .mt-switch-thumb { width } — CSS line 214 |
| switchTokens.radius | var(--radius-full) | .mt-switch { border-radius } — CSS line 202 |
| radioTokens.size | 1.25rem | .mt-radio { width } — CSS line 232 |
| radioTokens.dotSize | 0.5rem | .mt-radio-indicator { width } — CSS line 247 |
| sliderTokens.trackHeight | 0.375rem | .mt-slider-track { height } — CSS line 626 |
| sliderTokens.thumbSize | 44px | .mt-slider-thumb { width } — CSS line 1385; Interactive target dimension, not the smaller painted marker. |
| sliderTokens.visibleThumbSize | 20px | .mt-slider-thumb::after { width } — CSS line 1386; Painted marker; its border is included under border-box sizing. |
| sliderTokens.radius | var(--radius-full) | .mt-slider-track { border-radius } — CSS line 628 |
| progressTokens.height | 0.5rem | .mt-progress-track { height } — CSS line 583 |
| progressTokens.radius | var(--radius-full) | .mt-progress-track { border-radius } — CSS line 585 |
| menuTokens.itemPaddingX | 0.5rem | .mt-menu-item { padding } — CSS line 480 |
| menuTokens.itemPaddingY | 0.375rem | .mt-menu-item { padding } — CSS line 480 |
| menuTokens.radius | var(--radius-md) | .mt-menu-popup { border-radius } — CSS line 471 |
| menuTokens.minWidth | min(12rem, calc(100vw - 2rem)) | .mt-menu-popup { min-width } — CSS line 1458 |
| accordionTokens.headerPadding | 1rem | .mt-accordion-trigger { padding } — CSS line 402 |
| accordionTokens.contentPadding | 0px | .mt-accordion-panel { padding } — initial; Panel has no default padding. Consuming content may supply its own spacing. |
| accordionTokens.radius | 0px | .mt-accordion { border-radius } — initial; Root has no rounded surface; items use dividing lines. |
| avatarTokens.sm.size | 2rem | .mt-avatar-sm { width } — CSS line 722 |
| avatarTokens.sm.fontSize | 0.75rem | .mt-avatar-sm { font-size } — CSS line 722 |
| avatarTokens.md.size | 2.5rem | .mt-avatar-md { width } — CSS line 723 |
| avatarTokens.md.fontSize | 0.875rem | .mt-avatar-md { font-size } — CSS line 723 |
| avatarTokens.lg.size | 3rem | .mt-avatar-lg { width } — CSS line 724 |
| avatarTokens.lg.fontSize | 1rem | .mt-avatar-lg { font-size } — CSS line 724 |
| avatarTokens.xl.size | 4rem | .mt-avatar-xl { width } — CSS line 725 |
| avatarTokens.xl.fontSize | 1.25rem | .mt-avatar-xl { font-size } — CSS line 725 |
| separatorTokens.thickness | 1px | .mt-separator { height } — CSS line 384; Horizontal thickness. Vertical orientation uses width instead. |
| meterTokens.height | 0.5rem | .mt-meter-track { height } — CSS line 747 |
| meterTokens.radius | var(--radius-full) | .mt-meter-track { border-radius } — CSS line 749 |
| scrollAreaTokens.scrollbarSize | 24px | .mt-scroll-area-scrollbar[data-orientation="vertical"] { width } — CSS line 1648 |
| scrollAreaTokens.horizontalScrollbarSize | 24px | .mt-scroll-area-scrollbar-horizontal { height } — CSS line 1649 |
| scrollAreaTokens.scrollbarRadius | var(--radius-sm) | .mt-scroll-area-thumb { border-radius } — CSS line 1650; Thumb corner radius; the outer track is rectangular. |
Generate and check the contract
Edit component CSS deliberately, then regenerate the named presets. Bind new public fields to their real parts rather than copying dimensional literals into TypeScript. The shared tokens:generate/check commands cover both the CSS token exports and this component register. UI build/typecheck and styleguide dev/build reject stale output.
The source binding is explicit API schema for this library. It is not a synonym map or a classifier for arbitrary content. State/media overrides, consuming classes and portals must still be verified in their actual browser context.
pnpm --filter @mobtranslate/ui component-tokens:generate
pnpm --filter @mobtranslate/ui component-tokens:check
pnpm --filter styleguide tokens:check
pnpm --filter @mobtranslate/ui typecheck
pnpm --filter web typecheck
pnpm --filter styleguide build
# Start the production preview, then:
pnpm --filter styleguide test:component-specificationsChecked inspector implementation
This exact compiled source powers the live inspector. Resize/mutation observation updates the selected part; active animations are sampled with animation frames, and animation/transition completion or cancellation triggers a final read. Unmount and selection changes cancel scheduled reads and remove listeners. Hidden or unmounted popups have explicit unavailable status. Import componentPresets to obtain the typed grouped values and componentTokenSpecifications to inspect each field’s provenance.
'use client';
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import * as UI from '@mobtranslate/ui';
type Preset = keyof typeof UI.componentPresets;
const names = Object.keys(UI.componentPresets) as Preset[];
function ToastPreview() {
const manager = UI.useToastManager();
return <>
<UI.Button type="button" onClick={() => manager.add({ title: 'Local specification notice', description: 'Inspect the real notification and its viewport.', timeout: 0 })}>Show specification notice</UI.Button>
<UI.ToastPortal><UI.ToastViewport>{manager.toasts.map((toast) => <UI.Toast key={toast.id} toast={toast}><UI.ToastContent><UI.ToastTitle /><UI.ToastDescription /></UI.ToastContent><UI.ToastClose /></UI.Toast>)}</UI.ToastViewport></UI.ToastPortal>
</>;
}
function Fixtures({ preset, long }: { preset: Preset; long: boolean }) {
const [action, setAction] = useState('');
const label = long ? 'A longer English interface label demonstrates wrapping and content-driven geometry.' : 'First local choice';
const choices = [{ value: 'first', label }, { value: 'second', label: 'Second local choice' }];
const previews: Record<Preset, ReactNode> = {
buttonTokens: <div className="flex flex-wrap gap-3">{(['sm', 'md', 'lg', 'icon'] as const).map(size => <UI.Button key={size} type="button" size={size} aria-label={`Inspect ${size} button`} onClick={() => setAction(`Activated the ${size} local specimen.`)}>{size === 'icon' ? <span aria-hidden="true">+</span> : size}</UI.Button>)}</div>,
inputTokens: <div className="space-y-3">{(['sm', 'md', 'lg'] as const).map(size => <label key={size} className="block">{size} input<UI.Input size={size} defaultValue="Local editable text" /></label>)}</div>,
dialogTokens: <div className="flex flex-wrap gap-3">{(['sm', 'md', 'lg', 'xl'] as const).map(size => <UI.Dialog key={size}><UI.DialogTrigger render={<UI.Button type="button" />}>Open {size} specimen</UI.DialogTrigger><UI.DialogPortal><UI.DialogBackdrop /><UI.DialogPopup size={size}><UI.DialogTitle>{size} local dialog</UI.DialogTitle><UI.DialogDescription>Viewport bounds and content can constrain this surface.</UI.DialogDescription><UI.DialogClose render={<UI.Button type="button" />}>Close specimen</UI.DialogClose></UI.DialogPopup></UI.DialogPortal></UI.Dialog>)}</div>,
selectTokens: <UI.Select defaultValue="first" items={choices}><UI.SelectTrigger aria-label="Specification choice"><UI.SelectValue /></UI.SelectTrigger><UI.SelectPortal><UI.SelectPositioner sideOffset={8} collisionPadding={8} alignItemWithTrigger={false}><UI.SelectPopup><UI.SelectList>{choices.map(choice => <UI.SelectItem key={choice.value} value={choice.value}>{choice.label}</UI.SelectItem>)}</UI.SelectList></UI.SelectPopup></UI.SelectPositioner></UI.SelectPortal></UI.Select>,
tabsTokens: <UI.Tabs defaultValue="first"><UI.TabsList aria-label="Specification sections"><UI.TabsTrigger value="first">{label}</UI.TabsTrigger><UI.TabsTrigger value="second">Second section</UI.TabsTrigger></UI.TabsList><UI.TabsContent value="first">First local panel</UI.TabsContent><UI.TabsContent value="second">Second local panel</UI.TabsContent></UI.Tabs>,
tooltipTokens: <UI.TooltipProvider delay={0}><UI.Tooltip><UI.TooltipTrigger render={<UI.Button type="button" />}>Focus for size hint</UI.TooltipTrigger><UI.TooltipPortal><UI.TooltipPositioner sideOffset={8} collisionPadding={8}><UI.TooltipPopup>Supplemental English size hint. Move focus or press Escape to dismiss.</UI.TooltipPopup></UI.TooltipPositioner></UI.TooltipPortal></UI.Tooltip></UI.TooltipProvider>,
popoverTokens: <UI.Popover><UI.PopoverTrigger render={<UI.Button type="button" />}>Open size surface</UI.PopoverTrigger><UI.PopoverPortal><UI.PopoverPositioner sideOffset={8} collisionPadding={8}><UI.PopoverPopup><UI.PopoverTitle>Local size surface</UI.PopoverTitle><UI.PopoverDescription>{label}</UI.PopoverDescription><UI.PopoverClose render={<UI.Button type="button" />}>Close size surface</UI.PopoverClose></UI.PopoverPopup></UI.PopoverPositioner></UI.PopoverPortal></UI.Popover>,
toastTokens: <UI.ToastProvider><ToastPreview /></UI.ToastProvider>,
checkboxTokens: <UI.Checkbox label={label} defaultChecked />,
switchTokens: <UI.Switch label={label} defaultChecked />,
radioTokens: <UI.RadioGroup defaultValue="first" aria-label="Specification radio choices"><UI.RadioItem value="first" label={label} /><UI.RadioItem value="second" label="Second local choice" /></UI.RadioGroup>,
sliderTokens: <UI.Slider defaultValue={50} aria-label="Specification range" />,
progressTokens: <UI.Progress value={50} aria-label="Local specification progress" />,
menuTokens: <UI.Menu><UI.MenuTrigger render={<UI.Button type="button" />}>Open size menu</UI.MenuTrigger><UI.MenuPortal><UI.MenuPositioner sideOffset={8} collisionPadding={8}><UI.MenuPopup><UI.MenuItem onClick={() => setAction('Activated the first local command.')}>{label}</UI.MenuItem><UI.MenuItem onClick={() => setAction('Activated the second local command.')}>Second local command</UI.MenuItem></UI.MenuPopup></UI.MenuPositioner></UI.MenuPortal></UI.Menu>,
accordionTokens: <UI.Accordion defaultValue={['first']}><UI.AccordionItem value="first"><UI.AccordionHeader><UI.AccordionTrigger>{label}</UI.AccordionTrigger></UI.AccordionHeader><UI.AccordionPanel><p>This panel has no default padding. Its content controls its own spacing.</p></UI.AccordionPanel></UI.AccordionItem></UI.Accordion>,
avatarTokens: <div className="flex flex-wrap gap-3">{(['sm', 'md', 'lg', 'xl'] as const).map(size => <UI.Avatar key={size} size={size} alt={`Local ${size} avatar placeholder`} fallback="A" />)}</div>,
separatorTokens: <UI.Separator aria-hidden="true" />,
meterTokens: <UI.Meter value={50} min={0} max={100} aria-label="Local specification quantity" />,
scrollAreaTokens: <UI.ScrollArea style={{ height: 180 }} viewportProps={{ 'aria-label': 'Specification scrolling records', tabIndex: 0 }}><div style={{ width: 'max(1600px, 200vw)' }}>{Array.from({ length: 20 }, (_, index) => <p key={index}>Local English record {index + 1}. Native scrolling exposes both authored tracks.</p>)}</div></UI.ScrollArea>,
};
return <>{previews[preset]}{action && <p role="status">{action}</p>}</>;
}
interface Measurement { binding: string; available: boolean; computed: string; width: number; height: number }
interface Capture { measurement: Measurement; theme: string; viewport: number; scoped: boolean; long: boolean; capturedAt: string }
export function ComponentSpecificationsExample() {
const [ready, setReady] = useState(false);
const [preset, setPreset] = useState<Preset>('inputTokens');
const [path, setPath] = useState('sm.paddingX');
const [long, setLong] = useState(false);
const [scoped, setScoped] = useState(false);
const [measurement, setMeasurement] = useState<Measurement>({ binding: '', available: false, computed: '', width: 0, height: 0 });
const [captures, setCaptures] = useState<Record<string, Capture>>({});
const stage = useRef<HTMLDivElement>(null);
const rows = UI.componentTokenSpecifications.filter(row => row.exportName === preset);
const specification = rows.find(row => row.path === path) ?? rows[0];
const binding = `${specification.exportName}.${specification.path}`;
const available = measurement.binding === binding && measurement.available;
const capture = captures[binding];
useEffect(() => { setReady(true); }, []);
useEffect(() => {
let frame = 0;
let observed: Element | null = null;
const sizes = new ResizeObserver(schedule);
function read() {
const pseudo = specification.selector.endsWith('::after') ? '::after' : null;
const selector = pseudo ? specification.selector.slice(0, -pseudo.length) : specification.selector;
// Only one fixture family is mounted. Portals intentionally live outside the stage.
const candidates = [...(stage.current?.querySelectorAll(selector) ?? []), ...document.querySelectorAll(selector)];
const target = candidates.find(element => element.getClientRects().length && getComputedStyle(element).visibility !== 'hidden') ?? null;
if (target !== observed) { sizes.disconnect(); if (target) sizes.observe(target); observed = target; }
const bounds = target?.getBoundingClientRect();
const next = target && bounds ? { binding, available: true, computed: getComputedStyle(target, pseudo).getPropertyValue(specification.computedProperty).trim(), width: bounds.width, height: bounds.height } : { binding, available: false, computed: '', width: 0, height: 0 };
setMeasurement(previous => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
if (next.available) {
const observation = { measurement: next, theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light', viewport: innerWidth, scoped, long };
setCaptures(previous => {
const old = previous[binding];
if (old && JSON.stringify({ measurement: old.measurement, theme: old.theme, viewport: old.viewport, scoped: old.scoped, long: old.long }) === JSON.stringify(observation)) return previous;
return { ...previous, [binding]: { ...observation, capturedAt: new Date().toISOString() } };
});
}
if (target?.getAnimations().some(animation => animation.playState === 'running')) schedule();
}
function schedule() { cancelAnimationFrame(frame); frame = requestAnimationFrame(read); }
const changes = new MutationObserver(schedule);
changes.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['class', 'style', 'hidden', 'data-open', 'data-closed', 'data-checked', 'data-starting-style', 'data-ending-style'] });
changes.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
const animationEvents = ['animationstart', 'animationend', 'animationcancel', 'transitionrun', 'transitionend', 'transitioncancel'];
for (const event of animationEvents) document.addEventListener(event, schedule, true);
window.addEventListener('resize', schedule); schedule();
return () => { changes.disconnect(); sizes.disconnect(); cancelAnimationFrame(frame); window.removeEventListener('resize', schedule); for (const event of animationEvents) document.removeEventListener(event, schedule, true); };
}, [specification, binding, preset, scoped, long]);
const scopeStyle = scoped ? { '--radius-md': '20px', fontSize: '24px' } as CSSProperties : undefined;
return <div data-component-specifications data-current-specification={binding} data-inspector-ready={ready} className="space-y-4">
{!ready && <p role="status">Loading the component inspector…</p>}
<label className="block" htmlFor="component-preset">Component preset</label>
<select id="component-preset" className="mt-input w-full" disabled={!ready} value={preset} onChange={event => { const next = event.target.value as Preset; setPreset(next); setPath(UI.componentTokenSpecifications.find(row => row.exportName === next)!.path); }}>
{names.map(name => <option key={name} value={name}>{name}</option>)}
</select>
<label className="block" htmlFor="component-field">Specification field</label>
<select id="component-field" className="mt-input w-full" disabled={!ready} value={specification.path} onChange={event => setPath(event.target.value)}>{rows.map(row => <option key={row.path} value={row.path} data-specification={JSON.stringify(row)}>{row.path}</option>)}</select>
<div className="space-y-2"><label className="flex items-center gap-2 min-h-11"><input type="checkbox" disabled={!ready} checked={long} onChange={event => setLong(event.target.checked)} />Use longer English labels</label><label className="flex items-center gap-2 min-h-11"><input type="checkbox" disabled={!ready} checked={scoped} onChange={event => setScoped(event.target.checked)} />Override stage radius and inherited font size</label></div>
<div key={preset} ref={stage} data-component-spec-stage className="p-4 border border-[var(--color-border)] rounded-lg" style={scopeStyle}><Fixtures preset={preset} long={long} /></div>
<dl lang="en" dir="auto" className="space-y-2" style={{ overflowWrap: 'anywhere' }}>
<div><dt>Authored specification</dt><dd><code data-component-authored>{specification.value}</code></dd></div>
<div><dt>Source binding</dt><dd><code>{specification.selector} → {specification.property}</code><br />{specification.origin === 'declaration' ? `${specification.source}:${specification.line}` : `${specification.origin} value; no component declaration`}</dd></div>
<div><dt>Browser resolved {specification.computedProperty}</dt><dd data-component-computed>{available ? measurement.computed : measurement.binding !== binding ? 'Reading selected specimen…' : 'Not rendered; open this specimen’s surface to inspect it.'}</dd></div>
<div><dt>Host bounding rectangle, CSS pixels</dt><dd>{available ? `${measurement.width.toFixed(2)} × ${measurement.height.toFixed(2)}` : 'Unavailable'}</dd></div>
</dl>
{specification.note && <p>{specification.note}</p>}
<p role="status" lang="en" dir="auto" data-component-measurement data-measurement={JSON.stringify(measurement)}>{available ? 'Rendered specimen measured.' : measurement.binding !== binding ? 'Reading selected specimen…' : 'No visible matching part. Open the selected surface or choose a rendered field.'}</p>
<div data-component-capture lang="en" dir="auto" data-capture={JSON.stringify(capture ?? null)} style={{ overflowWrap: 'anywhere' }}><p className="font-semibold">Last captured observation for this field</p>{capture ? <p>{capture.measurement.computed}; host {capture.measurement.width.toFixed(2)} × {capture.measurement.height.toFixed(2)} CSS px. {capture.theme} theme, {capture.viewport}px viewport, stage override {capture.scoped ? 'on' : 'off'}, long labels {capture.long ? 'on' : 'off'}. Captured {capture.capturedAt}. This remains readable after closing a modal; it is an observation from that context, not a current measurement of a closed part.</p> : <p>No observation yet. Open the selected part to capture it.</p>}</div>
<p lang="en" dir="auto">Open menus and surfaces with the specimen’s own controls; dismiss with their normal keyboard actions. Portals use the page viewport and do not inherit the stage override. A pseudo-element measurement reports its computed property and the host element’s rectangle separately.</p>
</div>;
}
Failures and verification
- A generated file passing its drift check proves source agreement, not every selector combination or consuming style.
- Do not compare authored rem, var(), auto or min() strings directly with a pixel result. Inspect the constraint, actual instance and expected layout behavior.
- Do not treat a small painted checkbox/radio/Slider marker as its complete interaction target. Verify associated labels and keyboard alternatives.
- Test real overlays while open, confirm closing clears their measurement and preserve normal Escape/focus behavior.
- Test all preset families, both themes, long labels, scoped variables, narrow reflow and direction changes where dimensions are logical.
- A copied size does not prove contrast, accessible names, announcements, real task persistence or language authority. Continue the component’s behavior and accessibility checks.