From 2571c1fd552aae81f7c817baacfa29c11dd150b7 Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:25:21 +0100 Subject: [PATCH] feat(toolbar): add description tooltips to field components Add a Tooltip UI primitive and InfoTooltip helper, then wire a `description` prop through TextField, SwitchField, and ObjectField so that `paramDescriptions` from widget configs are shown as info icons with hover tooltips next to field labels. Co-Authored-By: Claude Opus 4.6 --- .../toolbar/src/components/block-editor.tsx | 4 + .../src/components/fields/info-tooltip.tsx | 26 +++++ .../src/components/fields/object-field.tsx | 8 +- .../src/components/fields/switch-field.tsx | 14 ++- .../src/components/fields/text-field.tsx | 8 +- .../toolbar/src/components/ui/tooltip.tsx | 97 +++++++++++++++++++ 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/toolbar/src/components/fields/info-tooltip.tsx create mode 100644 packages/toolbar/src/components/ui/tooltip.tsx diff --git a/packages/toolbar/src/components/block-editor.tsx b/packages/toolbar/src/components/block-editor.tsx index e941d7a..137bb74 100644 --- a/packages/toolbar/src/components/block-editor.tsx +++ b/packages/toolbar/src/components/block-editor.tsx @@ -27,6 +27,7 @@ export function BlockEditor({ const widgetType = WIDGET_TYPES[type]; const overrides = widgetType?.fieldOverrides ?? {}; const paramLabels = widgetType?.paramLabels ?? {}; + const paramDescriptions = widgetType?.paramDescriptions ?? {}; const paramKeys = widgetType?.fieldOrder ? widgetType.fieldOrder.filter((key) => { @@ -84,6 +85,7 @@ export function BlockEditor({ { return onParameterChange(key, text); @@ -98,6 +100,7 @@ export function BlockEditor({ { return onParameterChange(key, checked); @@ -172,6 +175,7 @@ export function BlockEditor({ + + + + + + + ); +} diff --git a/packages/toolbar/src/components/fields/object-field.tsx b/packages/toolbar/src/components/fields/object-field.tsx index 2a26030..94b0a44 100644 --- a/packages/toolbar/src/components/fields/object-field.tsx +++ b/packages/toolbar/src/components/fields/object-field.tsx @@ -1,5 +1,6 @@ import { useId, useState } from 'preact/hooks'; +import { InfoTooltip } from './info-tooltip'; import { CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'; import { Label } from '../ui/label'; import { Switch } from '../ui/switch'; @@ -7,6 +8,7 @@ import { TextField } from './text-field'; type ObjectFieldProps = { label: string; + description?: string; enabled: boolean; value: Record; defaultValue: Record; @@ -18,6 +20,7 @@ type ObjectFieldProps = { export function ObjectField({ label, + description, enabled, value, defaultValue, @@ -32,7 +35,10 @@ export function ObjectField({ return (
- + void; }; -export function SwitchField({ label, checked, onToggle }: SwitchFieldProps) { +export function SwitchField({ + label, + description, + checked, + onToggle, +}: SwitchFieldProps) { const id = useId(); return (
- +
); diff --git a/packages/toolbar/src/components/fields/text-field.tsx b/packages/toolbar/src/components/fields/text-field.tsx index a934bd0..1b11794 100644 --- a/packages/toolbar/src/components/fields/text-field.tsx +++ b/packages/toolbar/src/components/fields/text-field.tsx @@ -1,10 +1,12 @@ import { useId } from 'preact/hooks'; +import { InfoTooltip } from './info-tooltip'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; type TextFieldProps = { label: string; + description?: string; value: string; placeholder?: string; onInput: (value: string) => void; @@ -13,6 +15,7 @@ type TextFieldProps = { export function TextField({ label, + description, value, placeholder, onInput, @@ -22,7 +25,10 @@ export function TextField({ return (
- + (null); + const triggerRef = useRef(null); + const tooltipRef = useRef(null); + + const reposition = useCallback(() => { + const trigger = triggerRef.current; + const tooltip = tooltipRef.current; + if (!trigger || !tooltip) return; + + // Use the Shadow DOM host as the boundary for clamping. Falls back to + // the viewport width if rendered outside a Shadow DOM (e.g., in tests). + const root = trigger.getRootNode(); + const host = root instanceof ShadowRoot ? (root.host as HTMLElement) : null; + const hostRect = host?.getBoundingClientRect(); + const triggerRect = trigger.getBoundingClientRect(); + const tooltipWidth = tooltip.offsetWidth; + const tooltipHeight = tooltip.offsetHeight; + + const gap = 6; + const padding = 8; + const minLeft = (hostRect?.left ?? 0) + padding; + const maxRight = (hostRect?.right ?? window.innerWidth) - padding; + + // Center horizontally on trigger, clamp within host + const idealLeft = + triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2; + const clampedLeft = Math.max( + minLeft, + Math.min(idealLeft, maxRight - tooltipWidth) + ); + + setPosition({ + top: triggerRect.top - tooltipHeight - gap, + left: clampedLeft, + }); + }, []); + + const show = useCallback(() => { + setOpen(true); + requestAnimationFrame(reposition); + }, [reposition]); + + const hide = useCallback(() => { + setOpen(false); + setPosition(null); + }, []); + + return ( + + + {open && ( + + {content} + + )} + + ); +} + +export { Tooltip };