@eifi1/ui-kit 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -16
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -0
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +1 -0
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/list.d.ts +14 -1
- package/dist/components/list.js +7 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/menu-item.d.ts +11 -1
- package/dist/components/menu-item.js +6 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +40 -8
- package/dist/components/table.js +41 -7
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toggle-group.d.ts +15 -0
- package/dist/components/toggle-group.js +23 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +55 -4
- package/dist/components/ui.js +84 -7
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/choice-card.tsx +97 -1
- package/src/components/data-table.tsx +7 -0
- package/src/components/field.tsx +137 -0
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/list.tsx +28 -2
- package/src/components/menu-item.tsx +22 -1
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart.tsx +21 -0
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +85 -11
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +48 -3
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +149 -11
- package/src/components/user-avatar.tsx +57 -3
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +9 -0
- package/src/i18n/locales/es.ts +9 -0
- package/src/i18n/locales/fr.ts +9 -0
- package/src/i18n/locales/hu.ts +9 -0
- package/src/i18n/locales/it.ts +9 -0
- package/src/i18n/locales/zh.ts +9 -0
- package/src/index.ts +5 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/wizard-context.tsx +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/tooltip.tsx"],"sourcesContent":["import {\n cloneElement,\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { useAnchoredRect, type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { hasClippingAncestor } from \"../lib/clipping\";\n\n/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the\n * right in LTR and the left in RTL — and are what a layout that mirrors should ask\n * for. `left` / `right` stay physical, for a bubble tied to something that does not\n * mirror (a chart axis, a map). */\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\" | \"start\" | \"end\";\n\n/** The placement the maths works in: a logical side resolved against the trigger. */\ntype PhysicalSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nfunction physicalSide(side: TooltipSide, dir: Direction): PhysicalSide {\n if (side === \"start\") return dir === \"rtl\" ? \"right\" : \"left\";\n if (side === \"end\") return dir === \"rtl\" ? \"left\" : \"right\";\n return side;\n}\n\n/** The floating bubble itself. Uses the shared surface/border/text tokens so it\n * reads as part of the app's chrome (like the top bar and cards) rather than the\n * cold slate pill it used to be.\n *\n * `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that\n * too, but it also let a sentence-length label grow without bound, and a bubble\n * wider than the space beside its trigger gets clipped by whatever overflow\n * container it sits in. So cap it and let long text wrap instead. The cap tracks\n * the viewport as well, for narrow screens where 20rem is already most of it.\n * `side` is a preference rather than an instruction for the PORTALLED variant,\n * which measures the bubble and turns it round when it would not fit\n * (Steering Design feedback #126). The CSS-only one never learns its own size,\n * so there `side` is still the whole of the placement. */\nconst TOOLTIP_SURFACE =\n \"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-lg\";\n\nconst sidePositionClass: Record<TooltipSide, string> = {\n top: \"bottom-full left-1/2 -translate-x-1/2 mb-1\",\n bottom: \"top-full left-1/2 -translate-x-1/2 mt-1\",\n left: \"right-full top-1/2 -translate-y-1/2 mr-1\",\n right: \"left-full top-1/2 -translate-y-1/2 ml-1\",\n // Logical insets, so CSS resolves the side from the inherited direction with no\n // JavaScript: `end-full` pins the bubble's END edge to the trigger's start.\n start: \"end-full top-1/2 -translate-y-1/2 me-1\",\n end: \"start-full top-1/2 -translate-y-1/2 ms-1\",\n};\n\n/**\n * `extends ComponentPropsWithoutRef<\"span\">` because the wrapper this renders IS a span,\n * and a tooltip is the component a caller most often needs to reach past: it sits\n * between the layout and the control, so a `data-tour` anchor, a test id or an\n * `aria-label` aimed at the trigger used to be swallowed by it.\n *\n * ⚠️ On the empty-label branch there is no wrapper at all, and therefore nothing for\n * those attributes to land on — see the note in the body.\n */\nexport interface TooltipProps extends ComponentPropsWithoutRef<\"span\"> {\n label: ReactNode;\n side?: TooltipSide;\n className?: string;\n /**\n * Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for\n * itself: the bubble stays next to the trigger unless an ancestor clips or scrolls\n * (`overflow` other than `visible`), in which case it is portalled — see \"Inside a\n * scroll container\" below. `true` always portals, `false` never does; both are exactly\n * what they were before the default existed.\n */\n portal?: boolean;\n /** Tag the bubble `data-private`, for a label that repeats the user's own data. */\n redact?: boolean;\n children: ReactNode;\n}\n\n/** What each variant below takes: the resolved `side`, and every span attribute the\n * caller handed {@link Tooltip}, forwarded to that variant's own wrapper. */\ntype TooltipVariantProps = Omit<TooltipProps, \"side\" | \"portal\"> & { side: TooltipSide };\n\n/**\n * Hover/focus label for a control.\n *\n * Two placements, and the choice matters more than it looks. In place, the bubble is\n * always mounted next to the trigger and fades in on `:hover`, which costs no state and\n * works in a plain render test. Portalled, the bubble is mounted in `document.body`\n * only while it is up, positioned by measurement.\n *\n * ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs\n * `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is\n * mounted on `document.body`, which is INSIDE that class, so the rule reaches it as\n * long as the bubble is tagged. It is not tagged by default, because most labels are\n * UI strings; pass `redact` on the ones that repeat the user's own data (a truncated\n * payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,\n * the bubble spells the value out on hover.\n *\n * ⚠️ **An empty label renders nothing at all.** `title={payee ?? \"\"}` is an ordinary\n * shape at a call site that reveals truncated text, and the native attribute answers\n * it by showing no tooltip. A component that faithfully rendered an empty bubble\n * would be a worse `title`, so the emptiness check is here rather than at every call\n * site that could forget it.\n *\n * ⚠️ **The bubble describes its trigger, which means cloning it.** `role=\"tooltip\"` is\n * a name for a box, not a relationship — so for as long as nothing referenced the\n * bubble, the label reached the pointer and nobody else. That is worst on the call\n * sites that need it most: the kit's icon-only buttons, where the tooltip IS the\n * label. `aria-describedby` has to sit on the focusable element, which is the\n * caller's child and not this component's wrapper, so the child is CLONED to carry\n * it. A description the caller already set is appended to, never replaced — a field's\n * error text and its hint bubble both describe it. Children that cannot take props (a\n * fragment, a bare string, several elements) are left exactly as they were.\n *\n * ⚠️ **Escape dismisses it (WCAG 1.4.13).** Anything that appears on hover or focus has\n * to be dismissible without moving the pointer, and a bubble is opaque: it lands over\n * the row, field or figure you were reading, and the only way out of it used to be to\n * point somewhere else — which is precisely what you cannot do when what you need to\n * read is underneath it. The listener is the document's rather than the wrapper's\n * because the pointer opens this with the keyboard focus somewhere else entirely, and\n * it is subscribed only while a bubble is actually up: the in-place bubble is always\n * mounted, and a table of forty tooltips must not mean forty keydown listeners.\n *\n * ⚠️ **Inside a scroll container, the bubble has to be portalled — and by default it\n * now is.** An always-mounted bubble is absolutely positioned, but an absolutely\n * positioned descendant still counts towards its scroll-container ancestor's\n * scrollable overflow — so an invisible bubble on a control near the right edge makes\n * the container scroll sideways with nothing to reveal. That is what Keksdose feedback\n * dev#488 reported on the admin roster: 66px of horizontal scroll on a table that fit,\n * 44px of it owed to tooltips nobody could see. And a visible one is clipped by the\n * container's edge. The portalled bubble is `position: fixed` and absent until hovered,\n * so it adds no width and cannot be clipped.\n *\n * Until 0.10.0 the cure was `portal` at the call site, and kastlan asked for it to stop\n * being one: every tooltip in a table, a drawer or a scrolling card had to remember it,\n * and the ones that forgot were only found by someone scrolling sideways. So with\n * `portal` left out, the tooltip looks for a clipping ancestor itself — any element\n * between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible`.\n * It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble\n * nobody opened, so a check that waited for the hover would find the damage already\n * done. It looks again on every open, for a container that started scrolling after\n * the tooltip mounted (a table that grew). Only the bubble changes place; the trigger\n * and the caller's child stay mounted, so a switch never costs a focused button its\n * focus. Outside any such container the in-place bubble is kept, which is still the\n * cheaper one and the one a plain render test can find without a hover.\n *\n * Why not simply portal everything? Because the in-place bubble is the one existing\n * app tests rely on (it is in the DOM without a hover), and because it follows its\n * trigger through a scroll or an animation for free — the portalled one re-measures.\n */\nexport function Tooltip({\n label,\n side = \"top\",\n className,\n portal,\n redact = false,\n children,\n ...rest\n}: TooltipProps) {\n // No label, no bubble — and no wrapper either, so a conditional tooltip costs the\n // layout nothing on the branch where it does not apply. `...rest` goes with the\n // wrapper on this branch, which is the documented limit of the pass-through: there is\n // no element left to put an attribute on.\n if (isEmptyLabel(label)) return <>{children}</>;\n if (portal === true) {\n return (\n <PortalTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n {...rest}\n >\n {children}\n </PortalTooltip>\n );\n }\n // Both variants are separate components so that `Tooltip` itself can keep calling NO\n // hooks: the empty-label branch above returns before either of them, and a hook after\n // a conditional return is a hooks-order bug rather than a style violation.\n return (\n <InPlaceTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n detect={portal === undefined}\n {...rest}\n >\n {children}\n </InPlaceTooltip>\n );\n}\n\n\n/** The variant that lives next to its trigger, and — when `detect` is on, which is the\n * default — moves its bubble to `<body>` when that turns out to be inside a clipping\n * container (see \"Inside a scroll container\" on {@link Tooltip}).\n *\n * In place it holds the little state it does for the two things CSS cannot express —\n * which element to point `aria-describedby` at, and Escape — and not for the fade,\n * which is still `group-hover`/`group-focus-within` and still costs a render nothing.\n *\n * ONE component for both placements rather than a switch between the two variants: a\n * switch would be a different component at the same place in the tree, and React\n * would remount the caller's child with it — a button that loses focus the moment\n * its own tooltip decided where to go. Here the wrapper and the child stay put and\n * only the bubble's slot changes. */\nfunction InPlaceTooltip({\n label,\n side,\n className,\n redact,\n detect,\n children,\n ...rest\n}: TooltipVariantProps & { detect: boolean }) {\n const id = useId();\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const [clipped, setClipped] = useState(false);\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const open = (hovered || focused) && !dismissed;\n useEscapeKey(() => setDismissed(true), open);\n // At mount, before the first paint: the in-place bubble inside a scroller is the\n // phantom-scroll bug whether or not anyone opens it.\n useLayoutEffect(() => {\n if (detect) setClipped(hasClippingAncestor(triggerRef.current));\n }, [detect]);\n // Re-armed by the next hover or focus rather than by an effect watching those flags:\n // coming back to a trigger is a fresh request for its label, and an effect would also\n // re-show the bubble under a pointer that never left. The clipping check is repeated\n // here for a container that began to scroll after mount.\n const arm = (el: Element) => {\n setDismissed(false);\n if (!detect) return;\n setDir(dirOf(el));\n setClipped(hasClippingAncestor(el));\n };\n return (\n <span\n // `...rest` first: the four handlers below are what decides whether a bubble is\n // up, and a caller passing an `onFocus` of its own must not replace them.\n {...rest}\n ref={triggerRef}\n className={cn(\"relative inline-flex\", !clipped && \"group/tooltip\", className)}\n // These four track WHETHER A BUBBLE IS UP. They activate nothing — the only thing\n // here that can be activated is the caller's child, which keeps every handler it\n // arrived with — so this wrapper needs no role and no key handling of its own.\n // `jsx-a11y/no-static-element-interactions` warns about it all the same, as it\n // already does about the portal variant's identical trigger below; both are left\n // visible rather than silenced, because a rule this package ratchets should be\n // argued with in the backlog and not in a disable comment.\n onMouseEnter={(e) => {\n setHovered(true);\n arm(e.currentTarget);\n }}\n onMouseLeave={() => setHovered(false)}\n onFocus={(e) => {\n setFocused(true);\n arm(e.currentTarget);\n }}\n onBlur={() => setFocused(false)}\n >\n {/* In place the bubble is always there to point at; portalled, only while up. */}\n {describedBy(children, clipped ? (open ? id : undefined) : dismissed ? undefined : id)}\n {clipped ? (\n open && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )\n ) : (\n <span\n id={id}\n role=\"tooltip\"\n // The `hidden` ATTRIBUTE, not an opacity class: dismissing has to take the\n // bubble out of the accessibility tree as well as off the screen, or a screen\n // reader still reads out the description of a bubble the user just closed.\n hidden={dismissed || undefined}\n data-private={redact ? \"\" : undefined}\n className={cn(\n TOOLTIP_SURFACE,\n \"pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100\",\n sidePositionClass[side],\n )}\n >\n {label}\n </span>\n )}\n </span>\n );\n}\n\n/** Hand `children` the bubble's id as an `aria-describedby`, if it is an element that\n * can hold one. `id` is undefined while there is no bubble to point at — a dangling\n * reference describes the trigger as nothing at all, which is worse than silence. */\nfunction describedBy(children: ReactNode, id: string | undefined): ReactNode {\n if (id === undefined || !isValidElement(children)) return children;\n const child = children as ReactElement<{ \"aria-describedby\"?: string }>;\n // Fragments, Suspense and friends are symbol-typed and take no DOM props; cloning one\n // with an aria attribute warns in development and drops it in production.\n if (typeof child.type === \"symbol\") return children;\n const own = child.props[\"aria-describedby\"];\n return cloneElement(child, { \"aria-describedby\": own ? `${own} ${id}` : id });\n}\n\n/** \"Would this bubble be blank.\" Only the values a call site actually produces when\n * it has nothing to say — `\"\"`, `null`, `undefined`, `false` from a `&&` guard. A\n * numeric `0` is a real label and stays one. */\nfunction isEmptyLabel(label: ReactNode): boolean {\n return (\n label == null ||\n label === false ||\n (typeof label === \"string\" && label.trim() === \"\")\n );\n}\n\nconst TOOLTIP_GAP = 4;\n\n/** How close to the viewport edge a bubble may sit. Not zero: a label flush\n * against the glass reads as clipped even when every character is on screen. */\nconst TOOLTIP_MARGIN = 4;\n\nconst portalTransformBySide: Record<PhysicalSide, string> = {\n right: \"translate(0, -50%)\",\n left: \"translate(-100%, -50%)\",\n top: \"translate(-50%, -100%)\",\n bottom: \"translate(-50%, 0)\",\n};\n\n/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired\n * with {@link portalTransformBySide}, which shifts the box onto that point. */\nfunction tooltipAnchor(\n r: AnchorRect,\n side: PhysicalSide,\n): { left: number; top: number } {\n switch (side) {\n case \"right\":\n return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"left\":\n return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"top\":\n return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };\n case \"bottom\":\n return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };\n }\n}\n\nexport interface TooltipSize {\n width: number;\n height: number;\n}\n\nexport interface TooltipViewport {\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n left: number;\n top: number;\n /** Which side it ended up on, which need not be the one that was asked for. */\n side: PhysicalSide;\n}\n\nconst opposite: Record<PhysicalSide, PhysicalSide> = {\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n bottom: \"top\",\n};\n\n/** Whether the bubble clears the viewport edge on `side` of the trigger. */\nfunction roomOn(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): boolean {\n switch (side) {\n case \"left\":\n return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;\n case \"right\":\n return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;\n case \"top\":\n return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;\n case \"bottom\":\n return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;\n }\n}\n\nfunction sameRoom(\n a: { size: TooltipSize; viewport: TooltipViewport },\n b: { size: TooltipSize; viewport: TooltipViewport },\n): boolean {\n return (\n a.size.width === b.size.width &&\n a.size.height === b.size.height &&\n a.viewport.width === b.viewport.width &&\n a.viewport.height === b.viewport.height\n );\n}\n\nfunction clamp(value: number, low: number, high: number): number {\n // `high` first, so a bubble taller or wider than the viewport is pinned to the\n // top-left corner rather than to the bottom-right one — the start of a label\n // is the half worth keeping.\n return Math.max(low, Math.min(value, high));\n}\n\n/**\n * Where the bubble actually goes, given how big it turned out to be.\n *\n * Two rules, and they are separate because they fix separate failures.\n *\n * **Turn round when the preferred side has no room.** `side` says which side of\n * the trigger the label reads best on, and on a form near the left edge of the\n * window that side is off the screen — the capped bubble can only wrap, not\n * move, so what the reader gets is a sentence with its first half outside the\n * glass. Flipped only when the *other* side is genuinely better: a trigger in a\n * viewport too narrow for the bubble either way keeps the side it asked for, and\n * the clamp below does what it can.\n *\n * **Then clamp both axes.** The cross axis is the one that needs it — a `top`\n * bubble is centred on the trigger, so a trigger near the left edge pushes half\n * the label off even though the side it is on is right — and clamping the main\n * axis too costs nothing and covers the flip having nowhere to land.\n *\n * Pure, and measured in viewport pixels throughout, so it can be tested without\n * a layout: the caller supplies the trigger's rect, the bubble's own size and\n * the window.\n */\nexport function placeTooltip(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): TooltipPlacement {\n const chosen =\n roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport)\n ? side\n : opposite[side];\n const point = tooltipAnchor(r, chosen);\n const box =\n chosen === \"left\"\n ? { left: point.left - size.width, top: point.top - size.height / 2 }\n : chosen === \"right\"\n ? { left: point.left, top: point.top - size.height / 2 }\n : chosen === \"top\"\n ? { left: point.left - size.width / 2, top: point.top - size.height }\n : { left: point.left - size.width / 2, top: point.top };\n return {\n left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),\n top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),\n side: chosen,\n };\n}\n\nfunction PortalTooltip({\n label,\n side,\n className,\n redact,\n children,\n ...rest\n}: TooltipVariantProps) {\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const [visible, setVisible] = useState(false);\n // The trigger's reading direction, read when the bubble is asked for (an event, not a\n // render): it resolves `start` / `end`, and the portalled bubble — which has left the\n // subtree it would have inherited `dir` from — carries it too.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const show = (el: Element) => {\n setDir(dirOf(el));\n setVisible(true);\n };\n const id = useId();\n // Escape closes it outright, since this variant's bubble only exists while it is\n // shown. The next mouseenter/focus brings it back, which is the behaviour WCAG\n // 1.4.13 asks for: dismissible now, still available when you ask again.\n useEscapeKey(() => setVisible(false), visible);\n\n return (\n <>\n <span\n // As in `InPlaceTooltip`: the caller's attributes first, the four handlers that\n // run this component after them. The BUBBLE is deliberately not given them — it\n // is portalled to `<body>`, and an id or a tour anchor duplicated onto a node\n // that only exists while hovered would match twice or match nothing.\n {...rest}\n ref={triggerRef}\n className={cn(\"relative inline-flex\", className)}\n onMouseEnter={(e) => show(e.currentTarget)}\n onMouseLeave={() => setVisible(false)}\n onFocus={(e) => show(e.currentTarget)}\n onBlur={() => setVisible(false)}\n >\n {describedBy(children, visible ? id : undefined)}\n </span>\n {visible && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )}\n </>\n );\n}\n\n/** The measured, `position: fixed` bubble on `<body>`, mounted only while it is up.\n * Shared by {@link PortalTooltip} and the in-place variant's clipped mode, so the two\n * cannot place a bubble differently. */\nfunction PortalBubble({\n triggerRef,\n id,\n label,\n side,\n dir,\n redact,\n}: {\n triggerRef: RefObject<HTMLSpanElement | null>;\n id: string;\n label: ReactNode;\n side: TooltipSide;\n dir: Direction;\n redact: boolean | undefined;\n}) {\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n // The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;\n // here we only map the rect to a side-specific anchor point.\n const rect = useAnchoredRect(triggerRef, true);\n // The bubble's own size and the window it has to fit in — neither of which is\n // knowable in render: the width is whatever the label wrapped to inside the\n // cap, and reading `window` while rendering is not a pure thing to do. Both\n // are taken in a LAYOUT effect, so the correction lands before the browser\n // paints and there is no frame in which the label sits off the screen.\n const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);\n useLayoutEffect(() => {\n const measured = bubbleRef.current?.getBoundingClientRect();\n setRoom((previous) => {\n if (!measured) return null;\n const next = {\n size: { width: measured.width, height: measured.height },\n viewport: { width: window.innerWidth, height: window.innerHeight },\n };\n // Only publish what actually CHANGED: every re-measure allocates a fresh\n // object, and a new object on every scroll event would re-render the\n // bubble forever.\n return previous && sameRoom(previous, next) ? previous : next;\n });\n }, [rect, label]);\n\n const physical = physicalSide(side, dir);\n const point = rect ? tooltipAnchor(rect, physical) : null;\n // Unmeasured on the very first pass, where the anchor point plus the side's\n // own transform is exactly what this always did. One layout effect later the\n // size is known and the placement is decided properly.\n const placed = rect && room ? placeTooltip(rect, physical, room.size, room.viewport) : null;\n\n if (!point || typeof document === \"undefined\") return null;\n return createPortal(\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n dir={dir}\n data-private={redact ? \"\" : undefined}\n style={\n placed\n ? { position: \"fixed\", left: placed.left, top: placed.top }\n : {\n position: \"fixed\",\n left: point.left,\n top: point.top,\n transform: portalTransformBySide[physical],\n }\n }\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none z-50\")}\n >\n {label}\n </span>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/tooltip.tsx"],"sourcesContent":["import {\n cloneElement,\n isValidElement,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type ReactElement,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { useAnchoredRect, type AnchorRect } from \"../hooks/use-anchored-rect\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { hasClippingAncestor } from \"../lib/clipping\";\n\n/** The attribute that marks an element as a clipping container for {@link Tooltip}'s\n * auto-portal, whatever its computed `overflow` — `<div data-clips>` or\n * `<div {...{ [CLIPS_ATTRIBUTE]: \"\" }}>`. Put it on an app's own scroller so a test\n * environment without stylesheets (jsdom) portals the same tooltips the browser\n * does. DataTable's body and Table's wrapper already carry it. */\nexport { CLIPS_ATTRIBUTE } from \"../lib/clipping\";\n\n/** Where the bubble sits. `start` / `end` follow the reading direction — `end` is the\n * right in LTR and the left in RTL — and are what a layout that mirrors should ask\n * for. `left` / `right` stay physical, for a bubble tied to something that does not\n * mirror (a chart axis, a map). */\nexport type TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\" | \"start\" | \"end\";\n\n/** The placement the maths works in: a logical side resolved against the trigger. */\ntype PhysicalSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nfunction physicalSide(side: TooltipSide, dir: Direction): PhysicalSide {\n if (side === \"start\") return dir === \"rtl\" ? \"right\" : \"left\";\n if (side === \"end\") return dir === \"rtl\" ? \"left\" : \"right\";\n return side;\n}\n\n/** The floating bubble itself. Uses the shared surface/border/text tokens so it\n * reads as part of the app's chrome (like the top bar and cards) rather than the\n * cold slate pill it used to be.\n *\n * `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that\n * too, but it also let a sentence-length label grow without bound, and a bubble\n * wider than the space beside its trigger gets clipped by whatever overflow\n * container it sits in. So cap it and let long text wrap instead. The cap tracks\n * the viewport as well, for narrow screens where 20rem is already most of it.\n * `side` is a preference rather than an instruction for the PORTALLED variant,\n * which measures the bubble and turns it round when it would not fit\n * (Steering Design feedback #126). The CSS-only one never learns its own size,\n * so there `side` is still the whole of the placement. */\nconst TOOLTIP_SURFACE =\n \"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-lg\";\n\nconst sidePositionClass: Record<TooltipSide, string> = {\n top: \"bottom-full left-1/2 -translate-x-1/2 mb-1\",\n bottom: \"top-full left-1/2 -translate-x-1/2 mt-1\",\n left: \"right-full top-1/2 -translate-y-1/2 mr-1\",\n right: \"left-full top-1/2 -translate-y-1/2 ml-1\",\n // Logical insets, so CSS resolves the side from the inherited direction with no\n // JavaScript: `end-full` pins the bubble's END edge to the trigger's start.\n start: \"end-full top-1/2 -translate-y-1/2 me-1\",\n end: \"start-full top-1/2 -translate-y-1/2 ms-1\",\n};\n\n/**\n * `extends ComponentPropsWithoutRef<\"span\">` because the wrapper this renders IS a span,\n * and a tooltip is the component a caller most often needs to reach past: it sits\n * between the layout and the control, so a `data-tour` anchor, a test id or an\n * `aria-label` aimed at the trigger used to be swallowed by it.\n *\n * ⚠️ On the empty-label branch there is no wrapper at all, and therefore nothing for\n * those attributes to land on — see the note in the body.\n */\nexport interface TooltipProps extends ComponentPropsWithoutRef<\"span\"> {\n label: ReactNode;\n side?: TooltipSide;\n className?: string;\n /**\n * Where the bubble lives. Left out (the default since 0.10.0), the tooltip decides for\n * itself: the bubble stays next to the trigger unless an ancestor clips or scrolls\n * (`overflow` other than `visible`, or the {@link CLIPS_ATTRIBUTE} marker), in which\n * case it is portalled — see \"Inside a scroll container\" below. `true` always portals, `false` never does; both are exactly\n * what they were before the default existed.\n */\n portal?: boolean;\n /** Tag the bubble `data-private`, for a label that repeats the user's own data. */\n redact?: boolean;\n children: ReactNode;\n}\n\n/** What each variant below takes: the resolved `side`, and every span attribute the\n * caller handed {@link Tooltip}, forwarded to that variant's own wrapper. */\ntype TooltipVariantProps = Omit<TooltipProps, \"side\" | \"portal\"> & { side: TooltipSide };\n\n/**\n * Hover/focus label for a control.\n *\n * Two placements, and the choice matters more than it looks. In place, the bubble is\n * always mounted next to the trigger and fades in on `:hover`, which costs no state and\n * works in a plain render test. Portalled, the bubble is mounted in `document.body`\n * only while it is up, positioned by measurement.\n *\n * ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs\n * `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is\n * mounted on `document.body`, which is INSIDE that class, so the rule reaches it as\n * long as the bubble is tagged. It is not tagged by default, because most labels are\n * UI strings; pass `redact` on the ones that repeat the user's own data (a truncated\n * payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,\n * the bubble spells the value out on hover.\n *\n * ⚠️ **An empty label renders nothing at all.** `title={payee ?? \"\"}` is an ordinary\n * shape at a call site that reveals truncated text, and the native attribute answers\n * it by showing no tooltip. A component that faithfully rendered an empty bubble\n * would be a worse `title`, so the emptiness check is here rather than at every call\n * site that could forget it.\n *\n * ⚠️ **The bubble describes its trigger, which means cloning it.** `role=\"tooltip\"` is\n * a name for a box, not a relationship — so for as long as nothing referenced the\n * bubble, the label reached the pointer and nobody else. That is worst on the call\n * sites that need it most: the kit's icon-only buttons, where the tooltip IS the\n * label. `aria-describedby` has to sit on the focusable element, which is the\n * caller's child and not this component's wrapper, so the child is CLONED to carry\n * it. A description the caller already set is appended to, never replaced — a field's\n * error text and its hint bubble both describe it. Children that cannot take props (a\n * fragment, a bare string, several elements) are left exactly as they were.\n *\n * ⚠️ **Escape dismisses it (WCAG 1.4.13).** Anything that appears on hover or focus has\n * to be dismissible without moving the pointer, and a bubble is opaque: it lands over\n * the row, field or figure you were reading, and the only way out of it used to be to\n * point somewhere else — which is precisely what you cannot do when what you need to\n * read is underneath it. The listener is the document's rather than the wrapper's\n * because the pointer opens this with the keyboard focus somewhere else entirely, and\n * it is subscribed only while a bubble is actually up: the in-place bubble is always\n * mounted, and a table of forty tooltips must not mean forty keydown listeners.\n *\n * ⚠️ **Inside a scroll container, the bubble has to be portalled — and by default it\n * now is.** An always-mounted bubble is absolutely positioned, but an absolutely\n * positioned descendant still counts towards its scroll-container ancestor's\n * scrollable overflow — so an invisible bubble on a control near the right edge makes\n * the container scroll sideways with nothing to reveal. That is what Keksdose feedback\n * dev#488 reported on the admin roster: 66px of horizontal scroll on a table that fit,\n * 44px of it owed to tooltips nobody could see. And a visible one is clipped by the\n * container's edge. The portalled bubble is `position: fixed` and absent until hovered,\n * so it adds no width and cannot be clipped.\n *\n * Until 0.10.0 the cure was `portal` at the call site, and kastlan asked for it to stop\n * being one: every tooltip in a table, a drawer or a scrolling card had to remember it,\n * and the ones that forgot were only found by someone scrolling sideways. So with\n * `portal` left out, the tooltip looks for a clipping ancestor itself — any element\n * between it and `<body>` whose computed `overflow-x` / `overflow-y` is not `visible`,\n * or that carries {@link CLIPS_ATTRIBUTE} (`data-clips`). The marker is what makes the\n * answer the same under test: jsdom computes no Tailwind, so there every scroller\n * reads `visible` and a table-cell tooltip used to stay in place — its always-mounted\n * bubble then repeated the label in the cell's accessible name and `textContent`\n * (\"CheckingChecking\"), and apps pinned `portal` to stop it. The kit's own scrollers\n * are marked, so a tooltip in a DataTable or Table cell portals in jsdom exactly as it\n * does in the browser.\n * It looks at MOUNT, not only on open: dev#488's phantom scroll is caused by a bubble\n * nobody opened, so a check that waited for the hover would find the damage already\n * done. It looks again on every open, for a container that started scrolling after\n * the tooltip mounted (a table that grew). Only the bubble changes place; the trigger\n * and the caller's child stay mounted, so a switch never costs a focused button its\n * focus. Outside any such container the in-place bubble is kept, which is still the\n * cheaper one and the one a plain render test can find without a hover.\n *\n * Why not simply portal everything? Because the in-place bubble is the one existing\n * app tests rely on (it is in the DOM without a hover), and because it follows its\n * trigger through a scroll or an animation for free — the portalled one re-measures.\n */\nexport function Tooltip({\n label,\n side = \"top\",\n className,\n portal,\n redact = false,\n children,\n ...rest\n}: TooltipProps) {\n // No label, no bubble — and no wrapper either, so a conditional tooltip costs the\n // layout nothing on the branch where it does not apply. `...rest` goes with the\n // wrapper on this branch, which is the documented limit of the pass-through: there is\n // no element left to put an attribute on.\n if (isEmptyLabel(label)) return <>{children}</>;\n if (portal === true) {\n return (\n <PortalTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n {...rest}\n >\n {children}\n </PortalTooltip>\n );\n }\n // Both variants are separate components so that `Tooltip` itself can keep calling NO\n // hooks: the empty-label branch above returns before either of them, and a hook after\n // a conditional return is a hooks-order bug rather than a style violation.\n return (\n <InPlaceTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n detect={portal === undefined}\n {...rest}\n >\n {children}\n </InPlaceTooltip>\n );\n}\n\n\n/** The variant that lives next to its trigger, and — when `detect` is on, which is the\n * default — moves its bubble to `<body>` when that turns out to be inside a clipping\n * container (see \"Inside a scroll container\" on {@link Tooltip}).\n *\n * In place it holds the little state it does for the two things CSS cannot express —\n * which element to point `aria-describedby` at, and Escape — and not for the fade,\n * which is still `group-hover`/`group-focus-within` and still costs a render nothing.\n *\n * ONE component for both placements rather than a switch between the two variants: a\n * switch would be a different component at the same place in the tree, and React\n * would remount the caller's child with it — a button that loses focus the moment\n * its own tooltip decided where to go. Here the wrapper and the child stay put and\n * only the bubble's slot changes. */\nfunction InPlaceTooltip({\n label,\n side,\n className,\n redact,\n detect,\n children,\n ...rest\n}: TooltipVariantProps & { detect: boolean }) {\n const id = useId();\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const [clipped, setClipped] = useState(false);\n const [hovered, setHovered] = useState(false);\n const [focused, setFocused] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const open = (hovered || focused) && !dismissed;\n useEscapeKey(() => setDismissed(true), open);\n // At mount, before the first paint: the in-place bubble inside a scroller is the\n // phantom-scroll bug whether or not anyone opens it.\n useLayoutEffect(() => {\n if (detect) setClipped(hasClippingAncestor(triggerRef.current));\n }, [detect]);\n // Re-armed by the next hover or focus rather than by an effect watching those flags:\n // coming back to a trigger is a fresh request for its label, and an effect would also\n // re-show the bubble under a pointer that never left. The clipping check is repeated\n // here for a container that began to scroll after mount.\n const arm = (el: Element) => {\n setDismissed(false);\n if (!detect) return;\n setDir(dirOf(el));\n setClipped(hasClippingAncestor(el));\n };\n return (\n <span\n // `...rest` first: the four handlers below are what decides whether a bubble is\n // up, and a caller passing an `onFocus` of its own must not replace them.\n {...rest}\n ref={triggerRef}\n className={cn(\"relative inline-flex\", !clipped && \"group/tooltip\", className)}\n // These four track WHETHER A BUBBLE IS UP. They activate nothing — the only thing\n // here that can be activated is the caller's child, which keeps every handler it\n // arrived with — so this wrapper needs no role and no key handling of its own.\n // `jsx-a11y/no-static-element-interactions` warns about it all the same, as it\n // already does about the portal variant's identical trigger below; both are left\n // visible rather than silenced, because a rule this package ratchets should be\n // argued with in the backlog and not in a disable comment.\n onMouseEnter={(e) => {\n setHovered(true);\n arm(e.currentTarget);\n }}\n onMouseLeave={() => setHovered(false)}\n onFocus={(e) => {\n setFocused(true);\n arm(e.currentTarget);\n }}\n onBlur={() => setFocused(false)}\n >\n {/* In place the bubble is always there to point at; portalled, only while up. */}\n {describedBy(children, clipped ? (open ? id : undefined) : dismissed ? undefined : id)}\n {clipped ? (\n open && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )\n ) : (\n <span\n id={id}\n role=\"tooltip\"\n // The `hidden` ATTRIBUTE, not an opacity class: dismissing has to take the\n // bubble out of the accessibility tree as well as off the screen, or a screen\n // reader still reads out the description of a bubble the user just closed.\n hidden={dismissed || undefined}\n data-private={redact ? \"\" : undefined}\n className={cn(\n TOOLTIP_SURFACE,\n \"pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100\",\n sidePositionClass[side],\n )}\n >\n {label}\n </span>\n )}\n </span>\n );\n}\n\n/** Hand `children` the bubble's id as an `aria-describedby`, if it is an element that\n * can hold one. `id` is undefined while there is no bubble to point at — a dangling\n * reference describes the trigger as nothing at all, which is worse than silence. */\nfunction describedBy(children: ReactNode, id: string | undefined): ReactNode {\n if (id === undefined || !isValidElement(children)) return children;\n const child = children as ReactElement<{ \"aria-describedby\"?: string }>;\n // Fragments, Suspense and friends are symbol-typed and take no DOM props; cloning one\n // with an aria attribute warns in development and drops it in production.\n if (typeof child.type === \"symbol\") return children;\n const own = child.props[\"aria-describedby\"];\n return cloneElement(child, { \"aria-describedby\": own ? `${own} ${id}` : id });\n}\n\n/** \"Would this bubble be blank.\" Only the values a call site actually produces when\n * it has nothing to say — `\"\"`, `null`, `undefined`, `false` from a `&&` guard. A\n * numeric `0` is a real label and stays one. */\nfunction isEmptyLabel(label: ReactNode): boolean {\n return (\n label == null ||\n label === false ||\n (typeof label === \"string\" && label.trim() === \"\")\n );\n}\n\nconst TOOLTIP_GAP = 4;\n\n/** How close to the viewport edge a bubble may sit. Not zero: a label flush\n * against the glass reads as clipped even when every character is on screen. */\nconst TOOLTIP_MARGIN = 4;\n\nconst portalTransformBySide: Record<PhysicalSide, string> = {\n right: \"translate(0, -50%)\",\n left: \"translate(-100%, -50%)\",\n top: \"translate(-50%, -100%)\",\n bottom: \"translate(-50%, 0)\",\n};\n\n/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired\n * with {@link portalTransformBySide}, which shifts the box onto that point. */\nfunction tooltipAnchor(\n r: AnchorRect,\n side: PhysicalSide,\n): { left: number; top: number } {\n switch (side) {\n case \"right\":\n return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"left\":\n return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"top\":\n return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };\n case \"bottom\":\n return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };\n }\n}\n\nexport interface TooltipSize {\n width: number;\n height: number;\n}\n\nexport interface TooltipViewport {\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n left: number;\n top: number;\n /** Which side it ended up on, which need not be the one that was asked for. */\n side: PhysicalSide;\n}\n\nconst opposite: Record<PhysicalSide, PhysicalSide> = {\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n bottom: \"top\",\n};\n\n/** Whether the bubble clears the viewport edge on `side` of the trigger. */\nfunction roomOn(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): boolean {\n switch (side) {\n case \"left\":\n return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;\n case \"right\":\n return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;\n case \"top\":\n return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;\n case \"bottom\":\n return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;\n }\n}\n\nfunction sameRoom(\n a: { size: TooltipSize; viewport: TooltipViewport },\n b: { size: TooltipSize; viewport: TooltipViewport },\n): boolean {\n return (\n a.size.width === b.size.width &&\n a.size.height === b.size.height &&\n a.viewport.width === b.viewport.width &&\n a.viewport.height === b.viewport.height\n );\n}\n\nfunction clamp(value: number, low: number, high: number): number {\n // `high` first, so a bubble taller or wider than the viewport is pinned to the\n // top-left corner rather than to the bottom-right one — the start of a label\n // is the half worth keeping.\n return Math.max(low, Math.min(value, high));\n}\n\n/**\n * Where the bubble actually goes, given how big it turned out to be.\n *\n * Two rules, and they are separate because they fix separate failures.\n *\n * **Turn round when the preferred side has no room.** `side` says which side of\n * the trigger the label reads best on, and on a form near the left edge of the\n * window that side is off the screen — the capped bubble can only wrap, not\n * move, so what the reader gets is a sentence with its first half outside the\n * glass. Flipped only when the *other* side is genuinely better: a trigger in a\n * viewport too narrow for the bubble either way keeps the side it asked for, and\n * the clamp below does what it can.\n *\n * **Then clamp both axes.** The cross axis is the one that needs it — a `top`\n * bubble is centred on the trigger, so a trigger near the left edge pushes half\n * the label off even though the side it is on is right — and clamping the main\n * axis too costs nothing and covers the flip having nowhere to land.\n *\n * Pure, and measured in viewport pixels throughout, so it can be tested without\n * a layout: the caller supplies the trigger's rect, the bubble's own size and\n * the window.\n */\nexport function placeTooltip(\n r: AnchorRect,\n side: PhysicalSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): TooltipPlacement {\n const chosen =\n roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport)\n ? side\n : opposite[side];\n const point = tooltipAnchor(r, chosen);\n const box =\n chosen === \"left\"\n ? { left: point.left - size.width, top: point.top - size.height / 2 }\n : chosen === \"right\"\n ? { left: point.left, top: point.top - size.height / 2 }\n : chosen === \"top\"\n ? { left: point.left - size.width / 2, top: point.top - size.height }\n : { left: point.left - size.width / 2, top: point.top };\n return {\n left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),\n top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),\n side: chosen,\n };\n}\n\nfunction PortalTooltip({\n label,\n side,\n className,\n redact,\n children,\n ...rest\n}: TooltipVariantProps) {\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const [visible, setVisible] = useState(false);\n // The trigger's reading direction, read when the bubble is asked for (an event, not a\n // render): it resolves `start` / `end`, and the portalled bubble — which has left the\n // subtree it would have inherited `dir` from — carries it too.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const show = (el: Element) => {\n setDir(dirOf(el));\n setVisible(true);\n };\n const id = useId();\n // Escape closes it outright, since this variant's bubble only exists while it is\n // shown. The next mouseenter/focus brings it back, which is the behaviour WCAG\n // 1.4.13 asks for: dismissible now, still available when you ask again.\n useEscapeKey(() => setVisible(false), visible);\n\n return (\n <>\n <span\n // As in `InPlaceTooltip`: the caller's attributes first, the four handlers that\n // run this component after them. The BUBBLE is deliberately not given them — it\n // is portalled to `<body>`, and an id or a tour anchor duplicated onto a node\n // that only exists while hovered would match twice or match nothing.\n {...rest}\n ref={triggerRef}\n className={cn(\"relative inline-flex\", className)}\n onMouseEnter={(e) => show(e.currentTarget)}\n onMouseLeave={() => setVisible(false)}\n onFocus={(e) => show(e.currentTarget)}\n onBlur={() => setVisible(false)}\n >\n {describedBy(children, visible ? id : undefined)}\n </span>\n {visible && (\n <PortalBubble triggerRef={triggerRef} id={id} label={label} side={side} dir={dir} redact={redact} />\n )}\n </>\n );\n}\n\n/** The measured, `position: fixed` bubble on `<body>`, mounted only while it is up.\n * Shared by {@link PortalTooltip} and the in-place variant's clipped mode, so the two\n * cannot place a bubble differently. */\nfunction PortalBubble({\n triggerRef,\n id,\n label,\n side,\n dir,\n redact,\n}: {\n triggerRef: RefObject<HTMLSpanElement | null>;\n id: string;\n label: ReactNode;\n side: TooltipSide;\n dir: Direction;\n redact: boolean | undefined;\n}) {\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n // The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;\n // here we only map the rect to a side-specific anchor point.\n const rect = useAnchoredRect(triggerRef, true);\n // The bubble's own size and the window it has to fit in — neither of which is\n // knowable in render: the width is whatever the label wrapped to inside the\n // cap, and reading `window` while rendering is not a pure thing to do. Both\n // are taken in a LAYOUT effect, so the correction lands before the browser\n // paints and there is no frame in which the label sits off the screen.\n const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);\n useLayoutEffect(() => {\n const measured = bubbleRef.current?.getBoundingClientRect();\n setRoom((previous) => {\n if (!measured) return null;\n const next = {\n size: { width: measured.width, height: measured.height },\n viewport: { width: window.innerWidth, height: window.innerHeight },\n };\n // Only publish what actually CHANGED: every re-measure allocates a fresh\n // object, and a new object on every scroll event would re-render the\n // bubble forever.\n return previous && sameRoom(previous, next) ? previous : next;\n });\n }, [rect, label]);\n\n const physical = physicalSide(side, dir);\n const point = rect ? tooltipAnchor(rect, physical) : null;\n // Unmeasured on the very first pass, where the anchor point plus the side's\n // own transform is exactly what this always did. One layout effect later the\n // size is known and the placement is decided properly.\n const placed = rect && room ? placeTooltip(rect, physical, room.size, room.viewport) : null;\n\n if (!point || typeof document === \"undefined\") return null;\n return createPortal(\n <span\n ref={bubbleRef}\n id={id}\n role=\"tooltip\"\n dir={dir}\n data-private={redact ? \"\" : undefined}\n style={\n placed\n ? { position: \"fixed\", left: placed.left, top: placed.top }\n : {\n position: \"fixed\",\n left: point.left,\n top: point.top,\n transform: portalTransformBySide[physical],\n }\n }\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none z-50\")}\n >\n {label}\n </span>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/components/ui.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ButtonHTMLAttributes, Ref, ReactNode, SelectHTMLAttributes, InputHTMLAttributes, ComponentPropsWithoutRef, TextareaHTMLAttributes } from 'react';
|
|
3
3
|
import { TooltipSide } from './tooltip.js';
|
|
4
4
|
import '../hooks/use-anchored-rect.js';
|
|
5
|
+
import '../lib/clipping.js';
|
|
5
6
|
|
|
6
7
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
|
|
7
8
|
type ButtonSize = "sm" | "md";
|
|
@@ -96,8 +97,8 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
96
97
|
disabledReason?: ReactNode;
|
|
97
98
|
}
|
|
98
99
|
declare function Button({ variant, size, stretch, tone, pressed, disabledReason, className, onClick, ...rest }: ButtonProps): react.JSX.Element;
|
|
99
|
-
type IconButtonSize = "lg" | "md" | "sm" | "xs" | "2xs";
|
|
100
|
-
type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info";
|
|
100
|
+
type IconButtonSize = "xl" | "lg" | "md" | "sm" | "xs" | "2xs";
|
|
101
|
+
type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info" | "success" | "custom";
|
|
101
102
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
102
103
|
/** Any {@link ButtonVariant}, or `overlay` — a round translucent disc for use over
|
|
103
104
|
* an image (see `ICON_BUTTON_OVERLAY`). */
|
|
@@ -107,11 +108,31 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
107
108
|
* xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
|
|
108
109
|
* icon (an action on a chip or a tab). */
|
|
109
110
|
size?: IconButtonSize;
|
|
111
|
+
/**
|
|
112
|
+
* Fill the height of the flex row it stands in, keeping its width — the delete at
|
|
113
|
+
* the end of keksdose's split line (transaction-fields:162), level with the labelled
|
|
114
|
+
* fields beside it rather than floating at half their height. {@link Button}'s
|
|
115
|
+
* `stretch`; no effect outside a flex row. With a `label`, the tooltip's wrapper is
|
|
116
|
+
* what stretches.
|
|
117
|
+
*/
|
|
118
|
+
stretch?: boolean;
|
|
119
|
+
/** `round`: a circle instead of the rounded square — a compact status chip in a top
|
|
120
|
+
* bar (keksdose's sync indicator). `overlay` is always round. */
|
|
121
|
+
shape?: "square" | "round";
|
|
122
|
+
/**
|
|
123
|
+
* The colour of `tone="custom"`, as any CSS colour — a token (`var(--warning)`),
|
|
124
|
+
* ideally. Sets `--icon-button-tone` on the button, so passing a different one per
|
|
125
|
+
* state re-colours the glyph, its hover fill and its focus ring together. Passing it
|
|
126
|
+
* implies `tone="custom"`. Or leave it out and set the variable yourself from a class
|
|
127
|
+
* (`className="[--icon-button-tone:var(--success)]"`).
|
|
128
|
+
*/
|
|
129
|
+
toneColor?: string;
|
|
110
130
|
/** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
|
|
111
131
|
* hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
|
|
112
132
|
* remove/delete that repeats down a list. `warning`: amber at rest — a flag that
|
|
113
133
|
* wants attention. `info`: sky at rest — a notice-worthy but harmless action
|
|
114
|
-
* (keksdose's reconcile).
|
|
134
|
+
* (keksdose's reconcile). `success`: green at rest — a good state worth seeing.
|
|
135
|
+
* `custom`: the colour of `toneColor` (see there). Default: the variant's own colours. See `quiet` for
|
|
115
136
|
* turning a coloured tone's resting look the other way. */
|
|
116
137
|
tone?: IconButtonTone;
|
|
117
138
|
/**
|
|
@@ -193,6 +214,26 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
193
214
|
tooltipSide?: TooltipSide;
|
|
194
215
|
/** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */
|
|
195
216
|
tooltipPortal?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* Why the action is not available — {@link Button}'s `disabledReason`, on the icon
|
|
219
|
+
* button: keksdose's accounts page cannot hide an account with a balance or delete
|
|
220
|
+
* one with bookings (accounts-page:879/913 — "hide requires zero", "delete blocked"),
|
|
221
|
+
* nor its budgets page delete the only budget (budgets-page:330), and each wrapped a
|
|
222
|
+
* natively disabled IconButton in a Tooltip that swaps its label for the reason — a
|
|
223
|
+
* bubble a keyboard never opens, since `disabled` leaves the tab order, and a reason
|
|
224
|
+
* a screen reader never hears.
|
|
225
|
+
*
|
|
226
|
+
* The same contract as Button's: `aria-disabled` instead of `disabled`, so it stays
|
|
227
|
+
* focusable and hoverable; clicks (and the Enter/Space they stand for) are swallowed
|
|
228
|
+
* — `stopPropagation` still applies, so a locked action in a clickable row does not
|
|
229
|
+
* open the row either; the reason shows in the kit {@link Tooltip} and is attached
|
|
230
|
+
* through `aria-describedby` as a `hidden` copy. The NAME stays `aria-label` /
|
|
231
|
+
* `label` — "Delete", described by "The active budget cannot be deleted" — and the
|
|
232
|
+
* bubble shows the reason in place of the label, since the glyph already says what
|
|
233
|
+
* the button is and the reason is the news. Shown even with `tooltip={false}`: a
|
|
234
|
+
* reason nobody can see is not one. Wins over `disabled`, as on Button.
|
|
235
|
+
*/
|
|
236
|
+
disabledReason?: ReactNode;
|
|
196
237
|
}
|
|
197
238
|
declare const IconButton: react.ForwardRefExoticComponent<IconButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
198
239
|
declare const FIELD_BASE: string;
|
|
@@ -541,8 +582,18 @@ interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "childre
|
|
|
541
582
|
* verdict.
|
|
542
583
|
*/
|
|
543
584
|
tone?: "danger" | "success";
|
|
585
|
+
/**
|
|
586
|
+
* `inline` only (0.11.0). `sm`: 12px, start-aligned and tight (`py-1`) — the empty
|
|
587
|
+
* line INSIDE a detail panel, under a panel heading and among `text-xs` rows, where
|
|
588
|
+
* the default's 14px centred line with `py-4` read as a section of its own.
|
|
589
|
+
* keksdose's holdings panel (holdings-panel:563), asset-loan panel
|
|
590
|
+
* (asset-loan-panel:108) and loan-payment panel (loan-payment-panel:191) each wrote
|
|
591
|
+
* `className="justify-start py-1"` over it and still got body-size text. Default `md`,
|
|
592
|
+
* the look it had. Ignored by the box.
|
|
593
|
+
*/
|
|
594
|
+
size?: "sm" | "md";
|
|
544
595
|
}
|
|
545
|
-
declare function EmptyState({ title, hint, icon, action, headingAs, variant, tone, className, ...rest }: EmptyStateProps): react.JSX.Element;
|
|
596
|
+
declare function EmptyState({ title, hint, icon, action, headingAs, variant, tone, size, className, ...rest }: EmptyStateProps): react.JSX.Element;
|
|
546
597
|
/** One tab of a {@link Tabs} strip. */
|
|
547
598
|
interface TabItem<T extends string> {
|
|
548
599
|
id: T;
|
package/dist/components/ui.js
CHANGED
|
@@ -99,6 +99,9 @@ function Button({
|
|
|
99
99
|
}
|
|
100
100
|
const ICON_BUTTON_BASE = "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
101
101
|
const ICON_BUTTON_SIZES = {
|
|
102
|
+
// 48px with a 24px glyph: the one primary action of a phone screen or sheet, a step
|
|
103
|
+
// past the 44px minimum rather than on it.
|
|
104
|
+
xl: "size-12 [&_svg]:size-6",
|
|
102
105
|
// The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's
|
|
103
106
|
// bulk-action bars write `size-11` by hand over an `md` button to get it on phones.
|
|
104
107
|
lg: "size-11 [&_svg]:size-5",
|
|
@@ -107,6 +110,14 @@ const ICON_BUTTON_SIZES = {
|
|
|
107
110
|
xs: "size-7 rounded [&_svg]:size-4",
|
|
108
111
|
"2xs": "size-6 rounded [&_svg]:size-3.5"
|
|
109
112
|
};
|
|
113
|
+
const ICON_BUTTON_STRETCH_SIZES = {
|
|
114
|
+
xl: "w-12 min-h-12 self-stretch [&_svg]:size-6",
|
|
115
|
+
lg: "w-11 min-h-11 self-stretch [&_svg]:size-5",
|
|
116
|
+
md: "w-9 min-h-9 self-stretch [&_svg]:size-5",
|
|
117
|
+
sm: "w-8 min-h-8 self-stretch [&_svg]:size-5",
|
|
118
|
+
xs: "w-7 min-h-7 self-stretch rounded [&_svg]:size-4",
|
|
119
|
+
"2xs": "w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5"
|
|
120
|
+
};
|
|
110
121
|
const ICON_BUTTON_TONES = {
|
|
111
122
|
danger: {
|
|
112
123
|
quiet: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
|
|
@@ -119,10 +130,27 @@ const ICON_BUTTON_TONES = {
|
|
|
119
130
|
info: {
|
|
120
131
|
quiet: "text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
|
|
121
132
|
toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]"
|
|
133
|
+
},
|
|
134
|
+
success: {
|
|
135
|
+
quiet: "text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus:ring-[var(--success-border)]",
|
|
136
|
+
toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus:ring-[var(--success-border)]"
|
|
137
|
+
},
|
|
138
|
+
// The hover fill and the ring are MIXED from the one colour, since a custom tone
|
|
139
|
+
// brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a
|
|
140
|
+
// `custom` tone with no colour set is the default look rather than invisible.
|
|
141
|
+
custom: {
|
|
142
|
+
quiet: "text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
143
|
+
toned: "text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]"
|
|
122
144
|
}
|
|
123
145
|
};
|
|
124
146
|
const ICON_BUTTON_MUTED = "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
|
|
125
|
-
const QUIET_BY_DEFAULT = {
|
|
147
|
+
const QUIET_BY_DEFAULT = {
|
|
148
|
+
danger: true,
|
|
149
|
+
warning: false,
|
|
150
|
+
info: false,
|
|
151
|
+
success: false,
|
|
152
|
+
custom: false
|
|
153
|
+
};
|
|
126
154
|
function iconButtonToneClass(tone, quiet) {
|
|
127
155
|
if (tone === "default") return "";
|
|
128
156
|
if (tone === "muted") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);
|
|
@@ -145,28 +173,45 @@ const ICON_BUTTON_OVERLAY = "rounded-full bg-[color-mix(in_srgb,var(--bg-inverse
|
|
|
145
173
|
const IconButton = forwardRef(function IconButton2({
|
|
146
174
|
variant = "ghost",
|
|
147
175
|
size = "md",
|
|
148
|
-
tone
|
|
176
|
+
tone: toneProp,
|
|
149
177
|
quiet,
|
|
150
178
|
pressed,
|
|
151
179
|
stopPropagation,
|
|
180
|
+
stretch,
|
|
181
|
+
shape = "square",
|
|
182
|
+
toneColor,
|
|
152
183
|
label,
|
|
153
184
|
tooltip = true,
|
|
154
185
|
tooltipSide,
|
|
155
186
|
tooltipPortal,
|
|
187
|
+
disabledReason,
|
|
156
188
|
className,
|
|
189
|
+
style,
|
|
157
190
|
onClick,
|
|
158
191
|
onKeyDown,
|
|
159
192
|
...rest
|
|
160
193
|
}, ref) {
|
|
194
|
+
const tone = toneProp ?? (toneColor !== void 0 ? "custom" : "default");
|
|
195
|
+
const reasonId = useId();
|
|
196
|
+
const locked = hasContent(disabledReason);
|
|
197
|
+
const ownDescribedBy = rest["aria-describedby"];
|
|
161
198
|
const button = /* @__PURE__ */ jsx(
|
|
162
199
|
"button",
|
|
163
200
|
{
|
|
164
201
|
ref,
|
|
165
202
|
...rest,
|
|
203
|
+
disabled: locked ? void 0 : rest.disabled,
|
|
204
|
+
"aria-disabled": locked || rest["aria-disabled"],
|
|
205
|
+
"aria-describedby": locked ? ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId : ownDescribedBy,
|
|
166
206
|
"aria-label": rest["aria-label"] ?? label,
|
|
167
207
|
"aria-pressed": pressed ?? rest["aria-pressed"],
|
|
208
|
+
style: toneColor !== void 0 ? { ...style, ["--icon-button-tone"]: toneColor } : style,
|
|
168
209
|
onClick: (e) => {
|
|
169
210
|
if (stopPropagation) e.stopPropagation();
|
|
211
|
+
if (locked) {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
170
215
|
onClick?.(e);
|
|
171
216
|
},
|
|
172
217
|
onKeyDown: (e) => {
|
|
@@ -175,21 +220,43 @@ const IconButton = forwardRef(function IconButton2({
|
|
|
175
220
|
},
|
|
176
221
|
className: cn(
|
|
177
222
|
ICON_BUTTON_BASE,
|
|
178
|
-
ICON_BUTTON_SIZES[size],
|
|
223
|
+
stretch ? ICON_BUTTON_STRETCH_SIZES[size] : ICON_BUTTON_SIZES[size],
|
|
179
224
|
// After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
|
|
180
225
|
variant === "overlay" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],
|
|
226
|
+
shape === "round" && "rounded-full",
|
|
181
227
|
ICON_BUTTON_DISABLED_REST[variant],
|
|
182
228
|
iconButtonToneClass(tone, quiet),
|
|
183
229
|
pressed && ICON_BUTTON_PRESSED,
|
|
230
|
+
// The disabled look for the focusable kind of disabled, as on Button.
|
|
231
|
+
locked && "cursor-not-allowed opacity-50",
|
|
184
232
|
className
|
|
185
233
|
)
|
|
186
234
|
}
|
|
187
235
|
);
|
|
236
|
+
if (locked) {
|
|
237
|
+
return (
|
|
238
|
+
// Button's shape: the button and the hidden reason in a FRAGMENT, so Tooltip does
|
|
239
|
+
// not add its bubble to the description as well.
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
Tooltip,
|
|
242
|
+
{
|
|
243
|
+
label: disabledReason,
|
|
244
|
+
side: tooltipSide,
|
|
245
|
+
portal: tooltipPortal,
|
|
246
|
+
className: stretch ? "self-stretch" : void 0,
|
|
247
|
+
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
248
|
+
button,
|
|
249
|
+
/* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
|
|
250
|
+
] })
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
188
255
|
if (!tooltip || label === void 0 || label === "") return button;
|
|
189
256
|
return (
|
|
190
257
|
// A fragment, so Tooltip leaves the button's description alone: the bubble says
|
|
191
258
|
// exactly what `aria-label` already does.
|
|
192
|
-
/* @__PURE__ */ jsx(Tooltip, { label, side: tooltipSide, portal: tooltipPortal, children: /* @__PURE__ */ jsx(Fragment, { children: button }) })
|
|
259
|
+
/* @__PURE__ */ jsx(Tooltip, { label, side: tooltipSide, portal: tooltipPortal, className: stretch ? "self-stretch" : void 0, children: /* @__PURE__ */ jsx(Fragment, { children: button }) })
|
|
193
260
|
);
|
|
194
261
|
});
|
|
195
262
|
IconButton.displayName = "IconButton";
|
|
@@ -663,6 +730,7 @@ function EmptyState({
|
|
|
663
730
|
headingAs,
|
|
664
731
|
variant = "box",
|
|
665
732
|
tone,
|
|
733
|
+
size = "md",
|
|
666
734
|
className,
|
|
667
735
|
...rest
|
|
668
736
|
}) {
|
|
@@ -670,17 +738,26 @@ function EmptyState({
|
|
|
670
738
|
const toneClass = tone ? EMPTY_STATE_TONE[tone] : void 0;
|
|
671
739
|
const hasHint = hint != null && hint !== false && hint !== "";
|
|
672
740
|
if (variant === "inline") {
|
|
741
|
+
const sm = size === "sm";
|
|
673
742
|
return /* @__PURE__ */ jsxs(
|
|
674
743
|
"div",
|
|
675
744
|
{
|
|
676
745
|
...rest,
|
|
677
746
|
className: cn(
|
|
678
|
-
"flex flex-wrap items-center
|
|
747
|
+
"flex flex-wrap items-center text-[var(--text-muted)]",
|
|
748
|
+
sm ? "justify-start gap-x-1.5 gap-y-0.5 py-1 text-start text-xs" : "justify-center gap-x-2 gap-y-1 py-4 text-center text-sm",
|
|
679
749
|
className
|
|
680
750
|
),
|
|
681
751
|
children: [
|
|
682
|
-
icon != null && /* @__PURE__ */ jsx(
|
|
683
|
-
|
|
752
|
+
icon != null && /* @__PURE__ */ jsx(
|
|
753
|
+
"span",
|
|
754
|
+
{
|
|
755
|
+
"aria-hidden": true,
|
|
756
|
+
className: cn("flex text-[var(--text-placeholder)]", sm ? "[&_svg]:size-3.5" : "[&_svg]:size-4", toneClass),
|
|
757
|
+
children: icon
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ jsx(Title, { className: cn(sm ? "text-xs" : "text-sm", toneClass), children: title }),
|
|
684
761
|
hasHint && /* @__PURE__ */ jsx("span", { className: "text-xs", children: hint }),
|
|
685
762
|
action != null && /* @__PURE__ */ jsx("span", { className: "flex flex-wrap items-center gap-2", children: action })
|
|
686
763
|
]
|