@stackone/malachite 2.73.0 → 2.75.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Surface.js","sources":["../../src/components/Surface.tsx"],"sourcesContent":["import React from 'react';\nimport { paintOnChild } from './asChild';\nimport DisclosureArrow from './DisclosureArrow';\nimport { FOCUS_RING_INSET } from './focusRing';\nimport { resolveRel } from './linkRel';\nimport { safeHref } from './safeHref';\nimport { isRenderableNode } from './renderableNode';\nimport {\n FOREGROUND_RESET,\n SELECTED_ROW_ACCENT,\n SELECTED_ROW_BACKGROUND,\n SELECTED_ROW_FOREGROUND,\n SELECTED_TEXT_CONTEXT,\n} from './selectedRow';\n\nexport interface SurfaceProps {\n children?: React.ReactNode;\n padding?: boolean;\n /**\n * Content overflow. Defaults to `hidden` so children are clipped to the rounded\n * corners. Use `clip` to keep the rounded clipping WITHOUT creating a scroll\n * container — so a `position: sticky` descendant still pins (unlike `hidden`,\n * which traps it). `visible` disables clipping entirely.\n */\n overflow?: 'hidden' | 'visible' | 'clip';\n role?: React.AriaRole;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\ninterface SurfaceRowChromeProps {\n children?: React.ReactNode;\n /** Renders a bottom divider — omit on the last row in a Surface. */\n divider?: boolean;\n /** Removes the hover affordance / disables the control (e.g. while loading/busy). */\n disabled?: boolean;\n /** Announces the row as selected — `aria-checked` under a `radio`/`checkbox` role,\n * `aria-current` otherwise — and paints `Table`'s selected treatment unless `selection`\n * says not to. `'mixed'` is for a `checkbox` row that is partly selected: it announces\n * `aria-checked=\"mixed\"` there, never paints and never marks the row current, and counts\n * as not selected under any other role. */\n selected?: boolean | 'mixed';\n /** How a `selected` row shows it. `highlight` paints the treatment; `announce` leaves the\n * visual to a control inside the row and only sets the ARIA state. Defaults to `announce`\n * under `role=\"checkbox\"` and `highlight` otherwise. */\n selection?: 'highlight' | 'announce';\n /**\n * Tints the whole row by what it reports. `muted` is a row whose subject is inactive —\n * not disabled: it stays interactive. `success` and `error` are for a row that carries\n * an outcome, where the result belongs to the whole line rather than to one cell in it.\n */\n tone?: 'default' | 'muted' | 'success' | 'error';\n /** `compact` is an 8px by 16px inset the row owns. */\n inset?: 'none' | 'compact';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst INSET_CLASSES: Record<NonNullable<SurfaceRowChromeProps['inset']>, string> = {\n none: '',\n compact: 'px-[16px] py-[8px]',\n};\n\n/** Stays an interface under its original name: a consumer may `extends` it. */\nexport interface SurfaceRowProps extends SurfaceRowChromeProps {\n asChild?: false;\n /**\n * When provided, the whole row becomes clickable: it renders as a `<button>`\n * (or `<a>` when `href` is set), so the entire row is the click target with\n * native focus + Enter/Space — no manual tabIndex/key handlers. For a\n * selectable row, put a presentational `<Radio>`/`<Checkbox>` inside as the\n * visual indicator; do NOT nest an interactive control (invalid + double tab stop).\n */\n onClick?: (event: React.MouseEvent) => void;\n /** Renders the clickable row as an anchor instead of a button. */\n href?: string;\n target?: React.HTMLAttributeAnchorTarget;\n rel?: string;\n /** Content pinned right, outside the row button, so it may hold its own control. */\n trailing?: React.ReactNode;\n /**\n * Shows `trailing` only while the row is hovered or holds focus, and while a menu\n * it opened is still open. Hidden by opacity, so it stays in the tab order; a touch screen,\n * which has no hover, always shows it.\n */\n revealTrailing?: boolean;\n /** ARIA role for a clickable row, e.g. `radio`/`checkbox` (wrap the list in a `radiogroup`). */\n role?: React.AriaRole;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n/**\n * The outcome tones reuse the tag tokens rather than minting row ones, so a row saying \"this\n * passed\" and a tag saying it are the same green wherever the two sit together.\n */\nconst TONE_CLASSES: Record<NonNullable<SurfaceRowProps['tone']>, string> = {\n default: '',\n muted: 'bg-[var(--malachite-card-dimmed-background)]',\n success: 'bg-[var(--malachite-tag-success-background)]',\n error: 'bg-[var(--malachite-tag-error-background)]',\n};\n\n/**\n * The text an outcome tone carries with it, through the same context channel a selected row\n * uses, so `Typography` inside the row follows the tint instead of staying near-black on it.\n * `color` as well, for row content that is not Typography.\n *\n * Only the outcome tones. `muted` dims its subject rather than recolouring it, and its\n * foreground is the ordinary one.\n */\nconst TONE_FOREGROUNDS: Partial<Record<NonNullable<SurfaceRowProps['tone']>, string>> = {\n success: 'var(--malachite-tag-success-foreground)',\n error: 'var(--malachite-tag-error-foreground)',\n};\n\nexport interface SurfaceRowAsChildProps extends SurfaceRowChromeProps {\n asChild: true;\n children: React.ReactElement;\n}\n\nexport type AnySurfaceRowProps = SurfaceRowProps | SurfaceRowAsChildProps;\n\n// Not `group-hover` or `group-has-[:focus-visible]`: WebKit never applies the first (its\n// `@media (hover: hover)` nested in the group selector) nor the second inside a shadow root,\n// so the control stayed hidden in Safari. An open menu takes focus out of the row into a\n// portal, and would otherwise lose its trigger.\nconst REVEALED_ON_INTERACTION =\n 'opacity-0 transition-opacity group-[:hover]/row:opacity-100 group-focus-within/row:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100';\n\nconst TRIPLE_SHADOW =\n '0px 0px 4px 0px rgba(0,0,0,0.08), 0px 4px 4px 0px rgba(0,0,0,0.08), 0px 4px 10px 0px rgba(0,0,0,0.05)';\n\n/**\n * A single full-bleed row inside a <Surface>. Owns the row chrome — hover\n * highlight, colour transition and an optional bottom divider — so consumers\n * compose their own content inside without re-implementing design-system styling.\n * Pass `onClick`/`href` to make the entire row clickable (rendered as a real\n * button/anchor, so keyboard navigation works natively).\n */\nconst SurfaceRow: React.FC<AnySurfaceRowProps> = (props) => {\n const {\n children,\n divider = false,\n disabled = false,\n selected,\n selection,\n tone = 'default',\n inset = 'none',\n className = '',\n style,\n } = props;\n const asChild = props.asChild === true;\n const {\n onClick,\n href: rawHref,\n target,\n rel,\n trailing,\n revealTrailing,\n role,\n } = asChild ? ({} as SurfaceRowProps) : (props as SurfaceRowProps);\n // A rejected href (e.g. `javascript:`) leaves the row a button or a plain row, never an anchor.\n const href = safeHref(rawHref);\n const ariaLabel = asChild ? undefined : (props as SurfaceRowProps)['aria-label'];\n const ariaLabelledBy = asChild ? undefined : (props as SurfaceRowProps)['aria-labelledby'];\n const interactive = Boolean(onClick || href);\n // `asChild` is interactive for the hover affordance only, not the focus/reset classes.\n const hover =\n disabled && (interactive || asChild)\n ? ''\n : 'transition-colors hover:bg-[var(--malachite-buttonlist-hover-background)]';\n\n // A class, not an inline colour, which would outrank the hover utility. It replaces the\n // button reset's `bg-transparent` rather than joining it: same specificity, so the winner\n // would be whichever Tailwind emitted last.\n const tinted = tone !== 'default';\n const toneClass = TONE_CLASSES[tone];\n\n const hasTrailing = !asChild && isRenderableNode(trailing);\n const insetClass = INSET_CLASSES[inset];\n const controlPadding = insetClass && !hasTrailing ? '' : 'p-0';\n\n const chromeClasses = [\n 'w-full text-left border-0',\n toneClass,\n divider ? 'border-b border-solid border-[var(--malachite-card-border)]' : '',\n hover,\n ]\n .filter(Boolean)\n .join(' ');\n\n const controlClasses = [\n interactive\n ? `appearance-none ${tinted ? '' : 'bg-transparent'} ${controlPadding} ${FOCUS_RING_INSET}`\n : '',\n interactive && !disabled ? 'cursor-pointer' : '',\n ]\n .filter(Boolean)\n .join(' ');\n\n const classes = [chromeClasses, controlClasses, hasTrailing ? '' : insetClass, className]\n .filter(Boolean)\n .join(' ');\n\n const paints = selection ? selection === 'highlight' : role !== 'checkbox';\n\n // A gradient layer so the tint composites over hover, and an inset shadow because\n // `ring-*` silently doesn't paint in a shadow root.\n const selectedStyle: React.CSSProperties =\n selected === true && paints\n ? {\n backgroundImage: `linear-gradient(${SELECTED_ROW_BACKGROUND}, ${SELECTED_ROW_BACKGROUND})`,\n color: SELECTED_ROW_FOREGROUND,\n boxShadow: `inset 2px 0 0 ${SELECTED_ROW_ACCENT}`,\n ...SELECTED_TEXT_CONTEXT,\n }\n : {};\n\n const toneForeground = TONE_FOREGROUNDS[tone];\n const toneStyle: React.CSSProperties = toneForeground\n ? ({\n color: toneForeground,\n '--malachite-ctx-text-primary': toneForeground,\n } as React.CSSProperties)\n : {};\n\n // `selected` last of the two: a row can be both, and selection is the state the reader is\n // acting on, so it wins the colour.\n const rowStyle = { ...toneStyle, ...selectedStyle, ...style };\n\n // Only a checkbox has a partly-checked state; a radio is chosen or not.\n const ariaChecked =\n role === 'checkbox' && selected === 'mixed'\n ? ('mixed' as const)\n : role === 'radio' || role === 'checkbox'\n ? selected === true\n : undefined;\n // `aria-current`, not `aria-pressed`: a selected row is one of many choices, not a toggle\n // (same call as `Dropdown`'s options). A role that carries its own state uses that instead.\n const ariaCurrent = ariaChecked === undefined && selected === true ? true : undefined;\n\n // Everything a control would have carried, for the branches that render none: the row\n // element is then the only thing assistive tech sees. `aria-disabled` because there is no\n // control to be natively disabled — and it is inert without a role, like the name.\n const rowFallbackAria = {\n role,\n 'aria-checked': ariaChecked,\n 'aria-current': ariaCurrent,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-disabled': role && disabled ? true : undefined,\n };\n\n if (asChild) {\n return paintOnChild(children, classes, rowStyle);\n }\n\n if (hasTrailing) {\n const control =\n href && !disabled ? (\n <a\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n onClick={onClick}\n className={`${controlClasses} border-0 flex-1 min-w-0 text-left no-underline text-inherit`}\n role={role}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </a>\n ) : onClick ? (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={`${controlClasses} border-0 flex-1 min-w-0 text-left`}\n role={role}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </button>\n ) : (\n // The set goes here, not on the wrapper: `radio` and `checkbox` make their\n // descendants presentational, which would bury a control in the trailing slot.\n <div className=\"flex-1 min-w-0\" {...rowFallbackAria}>\n {children}\n </div>\n );\n\n return (\n <div\n className={[\n chromeClasses,\n 'flex items-center',\n insetClass,\n revealTrailing ? 'group/row' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n style={rowStyle}\n >\n {control}\n <div\n className={`shrink-0 flex items-center ${revealTrailing ? REVEALED_ON_INTERACTION : ''}`}\n >\n {trailing}\n </div>\n </div>\n );\n }\n\n if (href && !disabled) {\n return (\n <a\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n onClick={onClick}\n className={`${classes} no-underline text-inherit`}\n style={rowStyle}\n role={role}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </a>\n );\n }\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={classes}\n style={rowStyle}\n role={role}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </button>\n );\n }\n\n return (\n <div className={classes} style={rowStyle} {...rowFallbackAria}>\n {children}\n </div>\n );\n};\n\nexport interface SurfaceExpandableRowProps {\n /** The row itself, laid out across the line. May hold its own controls. */\n header: React.ReactNode;\n /** The body, rendered beneath the row while it is open. */\n children?: React.ReactNode;\n expanded: boolean;\n /** Omit for a row that cannot open. The arrow's space is still reserved, so a list of\n * rows stays in one column whether or not each one has a body. */\n onToggle?: (expanded: boolean) => void;\n /**\n * Pins the row while its body scrolls, so a long body never leaves the reader without\n * the line it belongs to. Offset by whatever is already pinned above it.\n */\n sticky?: boolean;\n stickyTopOffset?: number;\n divider?: boolean;\n tone?: SurfaceRowProps['tone'];\n /** Names the disclosure control, which is otherwise only an arrow. */\n 'aria-label'?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Stops a click inside it from toggling the row, for the cells that hold controls.\n *\n * Explicit rather than sniffing the event's target for an interactive ancestor: a control\n * rendering into a shadow root retargets the event to its host, so the sniff would miss\n * exactly the components most likely to sit in a row.\n */\nconst ExpandableRowControls: React.FC<{\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}> = ({ children, className = '', style }) => (\n <div className={className} style={style} onClick={(event) => event.stopPropagation()}>\n {children}\n </div>\n);\n\n/**\n * A row that opens to reveal a body, for a line that carries its own controls.\n *\n * Not `Accordion`: that puts its header inside the toggle button, so a header holding a\n * dropdown or a second button is invalid. Here the row surface takes the click and a cell\n * opts out with `ExpandableRow.Controls`, while the arrow stays a real button so the\n * keyboard reaches it.\n */\n/**\n * The space before the header: the arrow's own inset, its glyph and the gap after it.\n * Exported so a consumer aligning a column header above these rows offsets by the same\n * amount instead of guessing at it.\n */\nexport const EXPANDABLE_ROW_LEAD_PX = 39;\n\nconst SurfaceExpandableRow: React.FC<SurfaceExpandableRowProps> & {\n Controls: typeof ExpandableRowControls;\n} = Object.assign(\n ({\n header,\n children,\n expanded,\n onToggle,\n sticky = false,\n stickyTopOffset = 0,\n divider = false,\n tone = 'default',\n 'aria-label': ariaLabel,\n className = '',\n style,\n }: SurfaceExpandableRowProps) => {\n const bodyId = React.useId();\n const toggle = onToggle ? () => onToggle(!expanded) : undefined;\n\n return (\n <SurfaceRow divider={divider} tone={tone} className={className} style={style}>\n {/* `group` so the arrow takes its hover state from the whole row. */}\n <div\n className={`group flex items-center w-full ${toggle ? 'cursor-pointer' : ''} ${\n sticky && expanded\n ? 'sticky z-[1] bg-[var(--malachite-card-background)]'\n : ''\n }`}\n style={sticky && expanded ? { top: stickyTopOffset } : undefined}\n // Presentational: the arrow below is the keyboard control, and this only\n // widens the pointer target.\n onClick={toggle}\n role=\"presentation\"\n >\n {/* Reserved whether or not this row opens, so a list stays in one column.\n The inset and gap match Accordion's header. */}\n <span className=\"shrink-0 flex items-center pl-[16px] pr-[12px]\">\n {toggle && (\n <button\n type=\"button\"\n className={`appearance-none bg-transparent border-0 p-0 flex items-center cursor-pointer ${FOCUS_RING_INSET}`}\n aria-expanded={expanded}\n aria-controls={bodyId}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation();\n toggle();\n }}\n >\n <DisclosureArrow expanded={expanded} />\n </button>\n )}\n </span>\n {header}\n </div>\n {expanded && <div id={bodyId}>{children}</div>}\n </SurfaceRow>\n );\n },\n { Controls: ExpandableRowControls },\n);\n\ninterface SurfaceComponent extends React.FC<SurfaceProps> {\n Row: React.FC<AnySurfaceRowProps>;\n ExpandableRow: typeof SurfaceExpandableRow;\n}\n\nconst Surface = (({\n children,\n padding = false,\n overflow = 'hidden',\n role,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n className = '',\n style,\n}: SurfaceProps) => {\n // Literal class names (not interpolated) so Tailwind's JIT emits them.\n const overflowClass =\n overflow === 'clip'\n ? 'overflow-clip'\n : overflow === 'visible'\n ? 'overflow-visible'\n : 'overflow-hidden';\n return (\n <div\n className={`w-full rounded-[var(--malachite-radius-xxl,14px)] ${overflowClass} bg-[var(--malachite-card-background)] border border-solid border-[var(--malachite-card-border)] ${padding ? 'py-3 px-4' : ''} ${className}`}\n style={{ boxShadow: TRIPLE_SHADOW, ...FOREGROUND_RESET, ...style }}\n role={role}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </div>\n );\n}) as SurfaceComponent;\n\nSurface.Row = SurfaceRow;\nSurface.ExpandableRow = SurfaceExpandableRow;\n\nexport default Surface;\nexport { Surface, SurfaceRow, SurfaceExpandableRow };\n"],"names":["INSET_CLASSES","TONE_CLASSES","TONE_FOREGROUNDS","REVEALED_ON_INTERACTION","TRIPLE_SHADOW","SurfaceRow","props","children","divider","disabled","selected","selection","tone","inset","className","style","asChild","onClick","rawHref","target","rel","trailing","revealTrailing","role","href","safeHref","ariaLabel","ariaLabelledBy","interactive","hover","tinted","toneClass","hasTrailing","isRenderableNode","insetClass","controlPadding","chromeClasses","controlClasses","FOCUS_RING_INSET","classes","selectedStyle","SELECTED_ROW_BACKGROUND","SELECTED_ROW_FOREGROUND","SELECTED_ROW_ACCENT","SELECTED_TEXT_CONTEXT","toneForeground","rowStyle","ariaChecked","ariaCurrent","rowFallbackAria","paintOnChild","control","jsx","resolveRel","jsxs","ExpandableRowControls","event","EXPANDABLE_ROW_LEAD_PX","SurfaceExpandableRow","header","expanded","onToggle","sticky","stickyTopOffset","bodyId","React","toggle","DisclosureArrow","Surface","padding","overflow","FOREGROUND_RESET"],"mappings":"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"}
1
+ {"version":3,"file":"Surface.js","sources":["../../src/components/Surface.tsx"],"sourcesContent":["import React from 'react';\nimport DisclosureArrow from './DisclosureArrow';\nimport { paintOnChild } from './asChild';\nimport { FOCUS_RING_INSET } from './focusRing';\nimport { resolveRel } from './linkRel';\nimport { isRenderableNode } from './renderableNode';\nimport { safeHref } from './safeHref';\nimport {\n FOREGROUND_RESET,\n SELECTED_ROW_ACCENT,\n SELECTED_ROW_BACKGROUND,\n SELECTED_ROW_FOREGROUND,\n SELECTED_TEXT_CONTEXT,\n} from './selectedRow';\n\nexport interface SurfaceProps {\n children?: React.ReactNode;\n padding?: boolean;\n /**\n * Content overflow. Defaults to `hidden` so children are clipped to the rounded\n * corners. Use `clip` to keep the rounded clipping WITHOUT creating a scroll\n * container — so a `position: sticky` descendant still pins (unlike `hidden`,\n * which traps it). `visible` disables clipping entirely.\n */\n overflow?: 'hidden' | 'visible' | 'clip';\n role?: React.AriaRole;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\ninterface SurfaceRowChromeProps {\n children?: React.ReactNode;\n /** Renders a bottom divider — omit on the last row in a Surface. */\n divider?: boolean;\n /** Removes the hover affordance / disables the control (e.g. while loading/busy). */\n disabled?: boolean;\n /** Announces the row as selected — `aria-checked` under a `radio`/`checkbox` role,\n * `aria-current` otherwise — and paints `Table`'s selected treatment unless `selection`\n * says not to. `'mixed'` is for a `checkbox` row that is partly selected: it announces\n * `aria-checked=\"mixed\"` there, never paints and never marks the row current, and counts\n * as not selected under any other role. */\n selected?: boolean | 'mixed';\n /** How a `selected` row shows it. `highlight` paints the treatment; `tint` paints it\n * without the accent bar, for a menu whose choice is already marked inside the row;\n * `announce` leaves the visual to a control inside the row and only sets the ARIA state.\n * Defaults to `announce` under `role=\"checkbox\"` and `highlight` otherwise. */\n selection?: 'highlight' | 'tint' | 'announce';\n /**\n * Tints the whole row by what it reports. `muted` is a row whose subject is inactive —\n * not disabled: it stays interactive. `success` and `error` are for a row that carries\n * an outcome, where the result belongs to the whole line rather than to one cell in it.\n */\n tone?: 'default' | 'muted' | 'success' | 'error';\n /** `compact` is an 8px by 16px inset the row owns. */\n inset?: 'none' | 'compact';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst INSET_CLASSES: Record<NonNullable<SurfaceRowChromeProps['inset']>, string> = {\n none: '',\n compact: 'px-[16px] py-[8px]',\n};\n\n/** Stays an interface under its original name: a consumer may `extends` it. */\nexport interface SurfaceRowProps extends SurfaceRowChromeProps {\n asChild?: false;\n /**\n * When provided, the whole row becomes clickable: it renders as a `<button>`\n * (or `<a>` when `href` is set), so the entire row is the click target with\n * native focus + Enter/Space — no manual tabIndex/key handlers. For a\n * selectable row, put a presentational `<Radio>`/`<Checkbox>` inside as the\n * visual indicator; do NOT nest an interactive control (invalid + double tab stop).\n */\n onClick?: (event: React.MouseEvent) => void;\n /** Renders the clickable row as an anchor instead of a button. */\n href?: string;\n target?: React.HTMLAttributeAnchorTarget;\n rel?: string;\n /** Content pinned right, outside the row button, so it may hold its own control. */\n trailing?: React.ReactNode;\n /**\n * Shows `trailing` only while the row is hovered or holds focus, and while a menu\n * it opened is still open. Hidden by opacity, so it stays in the tab order; a touch screen,\n * which has no hover, always shows it.\n */\n revealTrailing?: boolean;\n /** ARIA role for a clickable row, e.g. `radio`/`checkbox` (wrap the list in a `radiogroup`). */\n role?: React.AriaRole;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n /** `-1` takes the row out of the Tab order, for a roving radio group. */\n tabIndex?: number;\n}\n\n/**\n * The outcome tones reuse the tag tokens rather than minting row ones, so a row saying \"this\n * passed\" and a tag saying it are the same green wherever the two sit together.\n */\nconst TONE_CLASSES: Record<NonNullable<SurfaceRowProps['tone']>, string> = {\n default: '',\n muted: 'bg-[var(--malachite-card-dimmed-background)]',\n success: 'bg-[var(--malachite-tag-success-background)]',\n error: 'bg-[var(--malachite-tag-error-background)]',\n};\n\n/**\n * The text an outcome tone carries with it, through the same context channel a selected row\n * uses, so `Typography` inside the row follows the tint instead of staying near-black on it.\n * `color` as well, for row content that is not Typography.\n *\n * Only the outcome tones. `muted` dims its subject rather than recolouring it, and its\n * foreground is the ordinary one.\n */\nconst TONE_FOREGROUNDS: Partial<Record<NonNullable<SurfaceRowProps['tone']>, string>> = {\n success: 'var(--malachite-tag-success-foreground)',\n error: 'var(--malachite-tag-error-foreground)',\n};\n\nexport interface SurfaceRowAsChildProps extends SurfaceRowChromeProps {\n asChild: true;\n children: React.ReactElement;\n}\n\nexport type AnySurfaceRowProps = SurfaceRowProps | SurfaceRowAsChildProps;\n\n// Not `group-hover` or `group-has-[:focus-visible]`: WebKit never applies the first (its\n// `@media (hover: hover)` nested in the group selector) nor the second inside a shadow root,\n// so the control stayed hidden in Safari. An open menu takes focus out of the row into a\n// portal, and would otherwise lose its trigger.\nconst REVEALED_ON_INTERACTION =\n 'opacity-0 transition-opacity group-[:hover]/row:opacity-100 group-focus-within/row:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100';\n\nconst TRIPLE_SHADOW =\n '0px 0px 4px 0px rgba(0,0,0,0.08), 0px 4px 4px 0px rgba(0,0,0,0.08), 0px 4px 10px 0px rgba(0,0,0,0.05)';\n\n/**\n * A single full-bleed row inside a <Surface>. Owns the row chrome — hover\n * highlight, colour transition and an optional bottom divider — so consumers\n * compose their own content inside without re-implementing design-system styling.\n * Pass `onClick`/`href` to make the entire row clickable (rendered as a real\n * button/anchor, so keyboard navigation works natively).\n */\nconst SurfaceRow: React.FC<AnySurfaceRowProps> = (props) => {\n const {\n children,\n divider = false,\n disabled = false,\n selected,\n selection,\n tone = 'default',\n inset = 'none',\n className = '',\n style,\n } = props;\n const asChild = props.asChild === true;\n const {\n onClick,\n href: rawHref,\n target,\n rel,\n trailing,\n revealTrailing,\n role,\n } = asChild ? ({} as SurfaceRowProps) : (props as SurfaceRowProps);\n // A rejected href (e.g. `javascript:`) leaves the row a button or a plain row, never an anchor.\n const href = safeHref(rawHref);\n const ariaLabel = asChild ? undefined : (props as SurfaceRowProps)['aria-label'];\n const ariaLabelledBy = asChild ? undefined : (props as SurfaceRowProps)['aria-labelledby'];\n const tabIndex = asChild ? undefined : (props as SurfaceRowProps).tabIndex;\n const interactive = Boolean(onClick || href);\n // `asChild` is interactive for the hover affordance only, not the focus/reset classes.\n const hover =\n disabled && (interactive || asChild)\n ? ''\n : 'transition-colors hover:bg-[var(--malachite-buttonlist-hover-background)]';\n\n // A class, not an inline colour, which would outrank the hover utility. It replaces the\n // button reset's `bg-transparent` rather than joining it: same specificity, so the winner\n // would be whichever Tailwind emitted last.\n const tinted = tone !== 'default';\n const toneClass = TONE_CLASSES[tone];\n\n const hasTrailing = !asChild && isRenderableNode(trailing);\n const insetClass = INSET_CLASSES[inset];\n const controlPadding = insetClass && !hasTrailing ? '' : 'p-0';\n\n const chromeClasses = [\n 'w-full text-left border-0',\n toneClass,\n divider ? 'border-b border-solid border-[var(--malachite-card-border)]' : '',\n hover,\n ]\n .filter(Boolean)\n .join(' ');\n\n const controlClasses = [\n interactive\n ? `appearance-none ${tinted ? '' : 'bg-transparent'} ${controlPadding} ${FOCUS_RING_INSET}`\n : '',\n interactive && !disabled ? 'cursor-pointer' : '',\n ]\n .filter(Boolean)\n .join(' ');\n\n const classes = [chromeClasses, controlClasses, hasTrailing ? '' : insetClass, className]\n .filter(Boolean)\n .join(' ');\n\n const paints = selection ? selection !== 'announce' : role !== 'checkbox';\n const accent = selection !== 'tint';\n\n // A gradient layer so the tint composites over hover, and an inset shadow because\n // `ring-*` silently doesn't paint in a shadow root.\n const selectedStyle: React.CSSProperties =\n selected === true && paints\n ? {\n backgroundImage: `linear-gradient(${SELECTED_ROW_BACKGROUND}, ${SELECTED_ROW_BACKGROUND})`,\n color: SELECTED_ROW_FOREGROUND,\n ...(accent ? { boxShadow: `inset 2px 0 0 ${SELECTED_ROW_ACCENT}` } : {}),\n ...SELECTED_TEXT_CONTEXT,\n }\n : {};\n\n const toneForeground = TONE_FOREGROUNDS[tone];\n const toneStyle: React.CSSProperties = toneForeground\n ? ({\n color: toneForeground,\n '--malachite-ctx-text-primary': toneForeground,\n } as React.CSSProperties)\n : {};\n\n // `selected` last of the two: a row can be both, and selection is the state the reader is\n // acting on, so it wins the colour.\n const rowStyle = { ...toneStyle, ...selectedStyle, ...style };\n\n // Only a checkbox has a partly-checked state; a radio is chosen or not.\n const ariaChecked =\n role === 'checkbox' && selected === 'mixed'\n ? ('mixed' as const)\n : role === 'radio' || role === 'checkbox'\n ? selected === true\n : undefined;\n // `aria-current`, not `aria-pressed`: a selected row is one of many choices, not a toggle\n // (same call as `Dropdown`'s options). A role that carries its own state uses that instead.\n const ariaCurrent = ariaChecked === undefined && selected === true ? true : undefined;\n\n // Everything a control would have carried, for the branches that render none: the row\n // element is then the only thing assistive tech sees. `aria-disabled` because there is no\n // control to be natively disabled — and it is inert without a role, like the name.\n const rowFallbackAria = {\n role,\n 'aria-checked': ariaChecked,\n 'aria-current': ariaCurrent,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-disabled': role && disabled ? true : undefined,\n };\n\n if (asChild) {\n return paintOnChild(children, classes, rowStyle);\n }\n\n if (hasTrailing) {\n const control =\n href && !disabled ? (\n <a\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n onClick={onClick}\n className={`${controlClasses} border-0 flex-1 min-w-0 text-left no-underline text-inherit`}\n role={role}\n tabIndex={tabIndex}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </a>\n ) : onClick ? (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={`${controlClasses} border-0 flex-1 min-w-0 text-left`}\n role={role}\n tabIndex={tabIndex}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </button>\n ) : (\n // The set goes here, not on the wrapper: `radio` and `checkbox` make their\n // descendants presentational, which would bury a control in the trailing slot.\n <div className=\"flex-1 min-w-0\" {...rowFallbackAria}>\n {children}\n </div>\n );\n\n return (\n <div\n className={[\n chromeClasses,\n 'flex items-center',\n insetClass,\n revealTrailing ? 'group/row' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n style={rowStyle}\n >\n {control}\n <div\n className={`shrink-0 flex items-center ${revealTrailing ? REVEALED_ON_INTERACTION : ''}`}\n >\n {trailing}\n </div>\n </div>\n );\n }\n\n if (href && !disabled) {\n return (\n <a\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n onClick={onClick}\n className={`${classes} no-underline text-inherit`}\n style={rowStyle}\n role={role}\n tabIndex={tabIndex}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </a>\n );\n }\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={classes}\n style={rowStyle}\n role={role}\n tabIndex={tabIndex}\n aria-checked={ariaChecked}\n aria-current={ariaCurrent}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </button>\n );\n }\n\n return (\n <div className={classes} style={rowStyle} {...rowFallbackAria}>\n {children}\n </div>\n );\n};\n\nexport interface SurfaceExpandableRowProps {\n /** The row itself, laid out across the line. May hold its own controls. */\n header: React.ReactNode;\n /** The body, rendered beneath the row while it is open. */\n children?: React.ReactNode;\n expanded: boolean;\n /** Omit for a row that cannot open. The arrow's space is still reserved, so a list of\n * rows stays in one column whether or not each one has a body. */\n onToggle?: (expanded: boolean) => void;\n /**\n * Pins the row while its body scrolls, so a long body never leaves the reader without\n * the line it belongs to. Offset by whatever is already pinned above it.\n */\n sticky?: boolean;\n stickyTopOffset?: number;\n divider?: boolean;\n tone?: SurfaceRowProps['tone'];\n /** Names the disclosure control, which is otherwise only an arrow. */\n 'aria-label'?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Stops a click inside it from toggling the row, for the cells that hold controls.\n *\n * Explicit rather than sniffing the event's target for an interactive ancestor: a control\n * rendering into a shadow root retargets the event to its host, so the sniff would miss\n * exactly the components most likely to sit in a row.\n */\nconst ExpandableRowControls: React.FC<{\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}> = ({ children, className = '', style }) => (\n <div className={className} style={style} onClick={(event) => event.stopPropagation()}>\n {children}\n </div>\n);\n\n/**\n * A row that opens to reveal a body, for a line that carries its own controls.\n *\n * Not `Accordion`: that puts its header inside the toggle button, so a header holding a\n * dropdown or a second button is invalid. Here the row surface takes the click and a cell\n * opts out with `ExpandableRow.Controls`, while the arrow stays a real button so the\n * keyboard reaches it.\n */\n/**\n * The space before the header: the arrow's own inset, its glyph and the gap after it.\n * Exported so a consumer aligning a column header above these rows offsets by the same\n * amount instead of guessing at it.\n */\nexport const EXPANDABLE_ROW_LEAD_PX = 39;\n\nconst SurfaceExpandableRow: React.FC<SurfaceExpandableRowProps> & {\n Controls: typeof ExpandableRowControls;\n} = Object.assign(\n ({\n header,\n children,\n expanded,\n onToggle,\n sticky = false,\n stickyTopOffset = 0,\n divider = false,\n tone = 'default',\n 'aria-label': ariaLabel,\n className = '',\n style,\n }: SurfaceExpandableRowProps) => {\n const bodyId = React.useId();\n const toggle = onToggle ? () => onToggle(!expanded) : undefined;\n\n return (\n <SurfaceRow divider={divider} tone={tone} className={className} style={style}>\n {/* `group` so the arrow takes its hover state from the whole row. */}\n <div\n className={`group flex items-center w-full ${toggle ? 'cursor-pointer' : ''} ${\n sticky && expanded\n ? 'sticky z-[1] bg-[var(--malachite-card-background)]'\n : ''\n }`}\n style={sticky && expanded ? { top: stickyTopOffset } : undefined}\n // Presentational: the arrow below is the keyboard control, and this only\n // widens the pointer target.\n onClick={toggle}\n role=\"presentation\"\n >\n {/* Reserved whether or not this row opens, so a list stays in one column.\n The inset and gap match Accordion's header. */}\n <span className=\"shrink-0 flex items-center pl-[16px] pr-[12px]\">\n {toggle && (\n <button\n type=\"button\"\n className={`appearance-none bg-transparent border-0 p-0 flex items-center cursor-pointer ${FOCUS_RING_INSET}`}\n aria-expanded={expanded}\n aria-controls={bodyId}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation();\n toggle();\n }}\n >\n <DisclosureArrow expanded={expanded} />\n </button>\n )}\n </span>\n {header}\n </div>\n {expanded && <div id={bodyId}>{children}</div>}\n </SurfaceRow>\n );\n },\n { Controls: ExpandableRowControls },\n);\n\ninterface SurfaceComponent extends React.FC<SurfaceProps> {\n Row: React.FC<AnySurfaceRowProps>;\n ExpandableRow: typeof SurfaceExpandableRow;\n}\n\nconst Surface = (({\n children,\n padding = false,\n overflow = 'hidden',\n role,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n className = '',\n style,\n}: SurfaceProps) => {\n // Literal class names (not interpolated) so Tailwind's JIT emits them.\n const overflowClass =\n overflow === 'clip'\n ? 'overflow-clip'\n : overflow === 'visible'\n ? 'overflow-visible'\n : 'overflow-hidden';\n return (\n <div\n className={`w-full rounded-[var(--malachite-radius-xxl,14px)] ${overflowClass} bg-[var(--malachite-card-background)] border border-solid border-[var(--malachite-card-border)] ${padding ? 'py-3 px-4' : ''} ${className}`}\n style={{ boxShadow: TRIPLE_SHADOW, ...FOREGROUND_RESET, ...style }}\n role={role}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n >\n {children}\n </div>\n );\n}) as SurfaceComponent;\n\nSurface.Row = SurfaceRow;\nSurface.ExpandableRow = SurfaceExpandableRow;\n\nexport default Surface;\nexport { Surface, SurfaceRow, SurfaceExpandableRow };\n"],"names":["INSET_CLASSES","TONE_CLASSES","TONE_FOREGROUNDS","REVEALED_ON_INTERACTION","TRIPLE_SHADOW","SurfaceRow","props","children","divider","disabled","selected","selection","tone","inset","className","style","asChild","onClick","rawHref","target","rel","trailing","revealTrailing","role","href","safeHref","ariaLabel","ariaLabelledBy","tabIndex","interactive","hover","tinted","toneClass","hasTrailing","isRenderableNode","insetClass","controlPadding","chromeClasses","controlClasses","FOCUS_RING_INSET","classes","paints","accent","selectedStyle","SELECTED_ROW_BACKGROUND","SELECTED_ROW_FOREGROUND","SELECTED_ROW_ACCENT","SELECTED_TEXT_CONTEXT","toneForeground","rowStyle","ariaChecked","ariaCurrent","rowFallbackAria","paintOnChild","control","jsx","resolveRel","jsxs","ExpandableRowControls","event","EXPANDABLE_ROW_LEAD_PX","SurfaceExpandableRow","header","expanded","onToggle","sticky","stickyTopOffset","bodyId","React","toggle","DisclosureArrow","Surface","padding","overflow","FOREGROUND_RESET"],"mappings":"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"}
@@ -1,39 +1,41 @@
1
1
  import { jsx as n } from "react/jsx-runtime";
2
- import { useRef as u } from "react";
3
- import x from "../Tooltip.js";
4
- import { getFontWeight as S, getFontSize as g, getTextColor as w } from "./textStyles.js";
5
- const b = ({
2
+ import { useRef as x } from "react";
3
+ import S from "../Tooltip.js";
4
+ import { getFontWeight as g, getFontSize as w, getTextColor as v } from "./textStyles.js";
5
+ const k = ({
6
6
  children: s,
7
7
  tooltip: t,
8
8
  maxWidth: p = "100%",
9
9
  color: c = "primary",
10
10
  size: o,
11
- fontWeight: r,
11
+ fontWeight: l,
12
12
  tooltipWhenTruncated: f = !1,
13
13
  tooltipPlacement: a,
14
- className: m = "",
15
- style: d = {}
14
+ inline: m = !1,
15
+ className: d = "",
16
+ style: h = {}
16
17
  }) => {
17
- const h = {
18
- color: w(c),
19
- ...o ? { fontSize: g(o) } : {},
20
- ...r ? { fontWeight: S(r) } : {},
18
+ const u = {
19
+ color: v(c),
20
+ ...o ? { fontSize: w(o) } : {},
21
+ ...l ? { fontWeight: g(l) } : {},
21
22
  maxWidth: p,
22
23
  overflow: "hidden",
23
24
  whiteSpace: "nowrap",
24
25
  textOverflow: "ellipsis",
25
26
  display: "inline-block",
26
27
  verticalAlign: "top",
27
- ...d
28
- }, l = u(null), i = /* @__PURE__ */ n("span", { ref: l, className: m, style: h, children: s });
28
+ ...h
29
+ }, r = x(null), i = /* @__PURE__ */ n("span", { ref: r, className: d, style: u, children: s });
29
30
  return t ? /* @__PURE__ */ n(
30
- x,
31
+ S,
31
32
  {
32
33
  text: t,
33
34
  placement: a,
35
+ inline: m,
34
36
  isShrinkable: !0,
35
37
  shouldShow: f ? () => {
36
- const e = l.current;
38
+ const e = r.current;
37
39
  return !!e && e.scrollWidth > e.clientWidth;
38
40
  } : void 0,
39
41
  children: i
@@ -41,6 +43,6 @@ const b = ({
41
43
  ) : i;
42
44
  };
43
45
  export {
44
- b as default
46
+ k as default
45
47
  };
46
48
  //# sourceMappingURL=EllipsisText.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EllipsisText.js","sources":["../../../src/components/Typography/EllipsisText.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport Tooltip from '../Tooltip';\nimport type { TextColor } from './Text';\nimport type { FontSize, FontWeight } from './textStyles';\nimport { getFontSize, getFontWeight, getTextColor } from './textStyles';\n\nexport interface EllipsisTextProps {\n children: React.ReactNode;\n tooltip?: React.ReactNode | string;\n /**\n * Defaults to filling the container. Pass a fixed width only when the text should\n * truncate before its container runs out of room (e.g. a column with a hard cap).\n */\n maxWidth?: string;\n color?: TextColor;\n size?: FontSize;\n fontWeight?: FontWeight;\n /**\n * Show `tooltip` only when the text is actually cut off. Opt-in, because `tooltip` is any\n * node: a caller repeating the label wants this, and one supplying something the label does\n * not say — a full path, a timestamp — would lose it.\n */\n tooltipWhenTruncated?: boolean;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst EllipsisText: React.FC<EllipsisTextProps> = ({\n children,\n tooltip,\n maxWidth = '100%',\n color = 'primary',\n size,\n fontWeight,\n tooltipWhenTruncated = false,\n tooltipPlacement,\n className = '',\n style = {},\n}) => {\n const ellipsisStyles: React.CSSProperties = {\n color: getTextColor(color),\n ...(size ? { fontSize: getFontSize(size) } : {}),\n ...(fontWeight ? { fontWeight: getFontWeight(fontWeight) } : {}),\n maxWidth,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n display: 'inline-block',\n verticalAlign: 'top',\n ...style,\n };\n\n const textRef = useRef<HTMLSpanElement>(null);\n\n const textElement = (\n <span ref={textRef} className={className} style={ellipsisStyles}>\n {children}\n </span>\n );\n\n if (tooltip) {\n // Measured on hover, not at mount: what fits depends on the container and on webfonts\n // that land after `DOMContentLoaded`, so a value read once is wrong for the rest of the\n // page's life.\n // Without `isShrinkable` the tooltip wrapper is floored at min-content and a percentage `maxWidth` never truncates.\n return (\n <Tooltip\n text={tooltip}\n placement={tooltipPlacement}\n isShrinkable\n shouldShow={\n tooltipWhenTruncated\n ? () => {\n const node = textRef.current;\n return !!node && node.scrollWidth > node.clientWidth;\n }\n : undefined\n }\n >\n {textElement}\n </Tooltip>\n );\n }\n\n return textElement;\n};\n\nexport default EllipsisText;\n"],"names":["EllipsisText","children","tooltip","maxWidth","color","size","fontWeight","tooltipWhenTruncated","tooltipPlacement","className","style","ellipsisStyles","getTextColor","getFontSize","getFontWeight","textRef","useRef","textElement","jsx","Tooltip","node"],"mappings":";;;;AA4BA,MAAMA,IAA4C,CAAC;AAAA,EAC/C,UAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,OAAAC,IAAQ;AAAA,EACR,MAAAC;AAAA,EACA,YAAAC;AAAA,EACA,sBAAAC,IAAuB;AAAA,EACvB,kBAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,OAAAC,IAAQ,CAAA;AACZ,MAAM;AACF,QAAMC,IAAsC;AAAA,IACxC,OAAOC,EAAaR,CAAK;AAAA,IACzB,GAAIC,IAAO,EAAE,UAAUQ,EAAYR,CAAI,EAAA,IAAM,CAAA;AAAA,IAC7C,GAAIC,IAAa,EAAE,YAAYQ,EAAcR,CAAU,EAAA,IAAM,CAAA;AAAA,IAC7D,UAAAH;AAAA,IACA,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,SAAS;AAAA,IACT,eAAe;AAAA,IACf,GAAGO;AAAA,EAAA,GAGDK,IAAUC,EAAwB,IAAI,GAEtCC,sBACD,QAAA,EAAK,KAAKF,GAAS,WAAAN,GAAsB,OAAOE,GAC5C,UAAAV,GACL;AAGJ,SAAIC,IAMI,gBAAAgB;AAAA,IAACC;AAAA,IAAA;AAAA,MACG,MAAMjB;AAAA,MACN,WAAWM;AAAA,MACX,cAAY;AAAA,MACZ,YACID,IACM,MAAM;AACF,cAAMa,IAAOL,EAAQ;AACrB,eAAO,CAAC,CAACK,KAAQA,EAAK,cAAcA,EAAK;AAAA,MAC7C,IACA;AAAA,MAGT,UAAAH;AAAA,IAAA;AAAA,EAAA,IAKNA;AACX;"}
1
+ {"version":3,"file":"EllipsisText.js","sources":["../../../src/components/Typography/EllipsisText.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport Tooltip from '../Tooltip';\nimport type { TextColor } from './Text';\nimport type { FontSize, FontWeight } from './textStyles';\nimport { getFontSize, getFontWeight, getTextColor } from './textStyles';\n\nexport interface EllipsisTextProps {\n children: React.ReactNode;\n tooltip?: React.ReactNode | string;\n /**\n * Defaults to filling the container. Pass a fixed width only when the text should\n * truncate before its container runs out of room (e.g. a column with a hard cap).\n */\n maxWidth?: string;\n color?: TextColor;\n size?: FontSize;\n fontWeight?: FontWeight;\n /**\n * Show `tooltip` only when the text is actually cut off. Opt-in, because `tooltip` is any\n * node: a caller repeating the label wants this, and one supplying something the label does\n * not say — a full path, a timestamp — would lose it.\n */\n tooltipWhenTruncated?: boolean;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n /** A span around the text, for a place a div may not go, such as inside a clickable `Surface.Row`. */\n inline?: boolean;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst EllipsisText: React.FC<EllipsisTextProps> = ({\n children,\n tooltip,\n maxWidth = '100%',\n color = 'primary',\n size,\n fontWeight,\n tooltipWhenTruncated = false,\n tooltipPlacement,\n inline = false,\n className = '',\n style = {},\n}) => {\n const ellipsisStyles: React.CSSProperties = {\n color: getTextColor(color),\n ...(size ? { fontSize: getFontSize(size) } : {}),\n ...(fontWeight ? { fontWeight: getFontWeight(fontWeight) } : {}),\n maxWidth,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n display: 'inline-block',\n verticalAlign: 'top',\n ...style,\n };\n\n const textRef = useRef<HTMLSpanElement>(null);\n\n const textElement = (\n <span ref={textRef} className={className} style={ellipsisStyles}>\n {children}\n </span>\n );\n\n if (tooltip) {\n // Measured on hover, not at mount: what fits depends on the container and on webfonts\n // that land after `DOMContentLoaded`, so a value read once is wrong for the rest of the\n // page's life.\n // Without `isShrinkable` the tooltip wrapper is floored at min-content and a percentage `maxWidth` never truncates.\n return (\n <Tooltip\n text={tooltip}\n placement={tooltipPlacement}\n inline={inline}\n isShrinkable\n shouldShow={\n tooltipWhenTruncated\n ? () => {\n const node = textRef.current;\n return !!node && node.scrollWidth > node.clientWidth;\n }\n : undefined\n }\n >\n {textElement}\n </Tooltip>\n );\n }\n\n return textElement;\n};\n\nexport default EllipsisText;\n"],"names":["EllipsisText","children","tooltip","maxWidth","color","size","fontWeight","tooltipWhenTruncated","tooltipPlacement","inline","className","style","ellipsisStyles","getTextColor","getFontSize","getFontWeight","textRef","useRef","textElement","jsx","Tooltip","node"],"mappings":";;;;AA8BA,MAAMA,IAA4C,CAAC;AAAA,EAC/C,UAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,OAAAC,IAAQ;AAAA,EACR,MAAAC;AAAA,EACA,YAAAC;AAAA,EACA,sBAAAC,IAAuB;AAAA,EACvB,kBAAAC;AAAA,EACA,QAAAC,IAAS;AAAA,EACT,WAAAC,IAAY;AAAA,EACZ,OAAAC,IAAQ,CAAA;AACZ,MAAM;AACF,QAAMC,IAAsC;AAAA,IACxC,OAAOC,EAAaT,CAAK;AAAA,IACzB,GAAIC,IAAO,EAAE,UAAUS,EAAYT,CAAI,EAAA,IAAM,CAAA;AAAA,IAC7C,GAAIC,IAAa,EAAE,YAAYS,EAAcT,CAAU,EAAA,IAAM,CAAA;AAAA,IAC7D,UAAAH;AAAA,IACA,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,SAAS;AAAA,IACT,eAAe;AAAA,IACf,GAAGQ;AAAA,EAAA,GAGDK,IAAUC,EAAwB,IAAI,GAEtCC,sBACD,QAAA,EAAK,KAAKF,GAAS,WAAAN,GAAsB,OAAOE,GAC5C,UAAAX,GACL;AAGJ,SAAIC,IAMI,gBAAAiB;AAAA,IAACC;AAAA,IAAA;AAAA,MACG,MAAMlB;AAAA,MACN,WAAWM;AAAA,MACX,QAAAC;AAAA,MACA,cAAY;AAAA,MACZ,YACIF,IACM,MAAM;AACF,cAAMc,IAAOL,EAAQ;AACrB,eAAO,CAAC,CAACK,KAAQA,EAAK,cAAcA,EAAK;AAAA,MAC7C,IACA;AAAA,MAGT,UAAAH;AAAA,IAAA;AAAA,EAAA,IAKNA;AACX;"}
@@ -0,0 +1,93 @@
1
+ const s = (a) => {
2
+ switch (a) {
3
+ // Figma has no text X-Small button, only the icon-only square — nothing to align to.
4
+ case "xsmall":
5
+ return "[--btn-px:calc(var(--spacing)*2)] px-[var(--btn-px)] py-1 text-xs";
6
+ case "small":
7
+ return "min-h-[var(--malachite-control-height-small,28px)] [--btn-px:10px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-small,13px)] leading-[16px] whitespace-nowrap";
8
+ case "medium":
9
+ return "min-h-[var(--malachite-control-height-medium,32px)] [--btn-px:10px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-small,13px)] leading-[16px] whitespace-nowrap";
10
+ case "large":
11
+ return "min-h-[var(--malachite-control-height-large,42px)] [--btn-px:12px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap";
12
+ default:
13
+ return "min-h-[var(--malachite-control-height-large,42px)] [--btn-px:12px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap";
14
+ }
15
+ }, n = (a, r) => {
16
+ switch (a) {
17
+ // Only the square rounds at 8px — text buttons at this size keep their 6px corner.
18
+ case "xsmall":
19
+ return r ? "rounded-lg" : "rounded-md";
20
+ case "small":
21
+ return "rounded-[var(--malachite-radius-10,10px)]";
22
+ case "medium":
23
+ return "rounded-xl";
24
+ case "large":
25
+ return "rounded-[var(--malachite-radius-xxl,14px)]";
26
+ default:
27
+ return "rounded-[var(--malachite-radius-xxl,14px)]";
28
+ }
29
+ }, l = (a, r) => {
30
+ const e = r === "start";
31
+ switch (a) {
32
+ case "small":
33
+ case "medium":
34
+ return e ? "mr-[6px]" : "ml-[6px]";
35
+ default:
36
+ return e ? "mr-2" : "ml-2";
37
+ }
38
+ }, i = (a) => {
39
+ switch (a) {
40
+ case "md":
41
+ return "rounded-md";
42
+ case "lg":
43
+ return "rounded-lg";
44
+ case "xl":
45
+ return "rounded-xl";
46
+ default:
47
+ return "rounded-xl";
48
+ }
49
+ }, t = {
50
+ neutral: "focus-visible:[outline:2px_solid_var(--malachite-text-primary)]",
51
+ destructive: "focus-visible:[outline:2px_solid_var(--malachite-text-destructive)]",
52
+ success: "focus-visible:[outline:2px_solid_var(--malachite-text-success)]",
53
+ warning: "focus-visible:[outline:2px_solid_var(--malachite-text-warning)]",
54
+ error: "focus-visible:[outline:2px_solid_var(--malachite-text-error)]",
55
+ assistant: "focus-visible:[outline:2px_solid_var(--malachite-assistant-foreground)]",
56
+ accent: "focus-visible:[outline:2px_solid_var(--malachite-accent-foreground)]"
57
+ }, u = (a, r) => {
58
+ const e = `font-medium transition-[color,background-color,border-color,opacity,box-shadow] duration-200 ease-in-out outline-none focus-visible:outline-offset-2 border border-solid ${r} cursor-pointer`;
59
+ switch (a) {
60
+ case "filled":
61
+ return `${e} ${t.neutral} btn-filled shadow-default`;
62
+ case "outline":
63
+ return `${e} ${t.neutral} btn-outline shadow-default`;
64
+ case "surface":
65
+ return `${e} ${t.neutral} btn-surface shadow-default`;
66
+ case "assistant":
67
+ return `${e} ${t.assistant} btn-assistant shadow-default`;
68
+ case "destructive":
69
+ return `${e} ${t.destructive} btn-destructive`;
70
+ case "success":
71
+ return `${e} ${t.success} btn-glass-success`;
72
+ case "warning":
73
+ return `${e} ${t.warning} btn-glass-warning`;
74
+ case "error":
75
+ return `${e} ${t.error} btn-glass-error`;
76
+ case "ghost":
77
+ return `${e} ${t.neutral} btn-ghost`;
78
+ case "quiet":
79
+ return `${e} ${t.accent} btn-quiet`;
80
+ case "no-style":
81
+ return `${e} ${t.neutral} btn-no-style border-none`;
82
+ default:
83
+ return `${e} ${t.neutral} btn-filled shadow-default`;
84
+ }
85
+ };
86
+ export {
87
+ n as getDefaultRadiusClass,
88
+ l as getIconGapClass,
89
+ i as getRadiusClass,
90
+ s as getSizeClasses,
91
+ u as getVariantClasses
92
+ };
93
+ //# sourceMappingURL=buttonStyles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buttonStyles.js","sources":["../../src/components/buttonStyles.ts"],"sourcesContent":["import type { ButtonRadius, ButtonSize, ButtonVariant } from './Button';\n\n// Geometry from the Figma component library, which is variant-independent.\n// `min-h` not `h`: Tailwind emits `h-*` after `min-h-*`, so a hard height would beat a\n// consumer's own `h-*`. Paired with `whitespace-nowrap` (as Figma has it) so a long label\n// cannot wrap into a line the height would clip.\nexport const getSizeClasses = (size: ButtonSize) => {\n switch (size) {\n // Figma has no text X-Small button, only the icon-only square — nothing to align to.\n case 'xsmall':\n return '[--btn-px:calc(var(--spacing)*2)] px-[var(--btn-px)] py-1 text-xs';\n case 'small':\n return 'min-h-[var(--malachite-control-height-small,28px)] [--btn-px:10px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-small,13px)] leading-[16px] whitespace-nowrap';\n case 'medium':\n return 'min-h-[var(--malachite-control-height-medium,32px)] [--btn-px:10px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-small,13px)] leading-[16px] whitespace-nowrap';\n case 'large':\n return 'min-h-[var(--malachite-control-height-large,42px)] [--btn-px:12px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap';\n default:\n return 'min-h-[var(--malachite-control-height-large,42px)] [--btn-px:12px] px-[var(--btn-px)] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap';\n }\n};\n\n// `rounded-*` reads the malachite radius tokens via `@theme`, so these follow the theme.\n// An explicit `radius` prop still wins, via getRadiusClass.\nexport const getDefaultRadiusClass = (size: ButtonSize, iconOnly: boolean): string => {\n switch (size) {\n // Only the square rounds at 8px — text buttons at this size keep their 6px corner.\n case 'xsmall':\n return iconOnly ? 'rounded-lg' : 'rounded-md';\n case 'small':\n return 'rounded-[var(--malachite-radius-10,10px)]';\n case 'medium':\n return 'rounded-xl';\n case 'large':\n return 'rounded-[var(--malachite-radius-xxl,14px)]';\n default:\n return 'rounded-[var(--malachite-radius-xxl,14px)]';\n }\n};\n\n// Figma pairs a 6px icon gap with the 13px tiers and 8px with the 15px tier.\nexport const getIconGapClass = (size: ButtonSize, position: 'start' | 'end'): string => {\n const start = position === 'start';\n switch (size) {\n case 'small':\n case 'medium':\n return start ? 'mr-[6px]' : 'ml-[6px]';\n default:\n return start ? 'mr-2' : 'ml-2';\n }\n};\n\nexport const getRadiusClass = (radius: ButtonRadius): string => {\n switch (radius) {\n case 'md':\n return 'rounded-md';\n case 'lg':\n return 'rounded-lg';\n case 'xl':\n return 'rounded-xl';\n default:\n return 'rounded-xl';\n }\n};\n\n// Focus ring: a `focus-visible` outline (keyboard only), coloured to match the variant.\n// Uses `outline` rather than Tailwind's `ring-*` because ring composes `--tw-ring-*`\n// custom properties via box-shadow that rely on document-level @property registration\n// and silently don't paint inside a shadow root (same class of issue as transform\n// utilities). The classes are full literals so Tailwind's JIT can generate them.\nconst FOCUS_OUTLINE = {\n neutral: 'focus-visible:[outline:2px_solid_var(--malachite-text-primary)]',\n destructive: 'focus-visible:[outline:2px_solid_var(--malachite-text-destructive)]',\n success: 'focus-visible:[outline:2px_solid_var(--malachite-text-success)]',\n warning: 'focus-visible:[outline:2px_solid_var(--malachite-text-warning)]',\n error: 'focus-visible:[outline:2px_solid_var(--malachite-text-error)]',\n assistant: 'focus-visible:[outline:2px_solid_var(--malachite-assistant-foreground)]',\n accent: 'focus-visible:[outline:2px_solid_var(--malachite-accent-foreground)]',\n};\n\nexport const getVariantClasses = (variant: ButtonVariant, radiusClass: string): string => {\n const baseClasses = `font-medium transition-[color,background-color,border-color,opacity,box-shadow] duration-200 ease-in-out outline-none focus-visible:outline-offset-2 border border-solid ${radiusClass} cursor-pointer`;\n\n switch (variant) {\n case 'filled':\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-filled shadow-default`;\n\n case 'outline':\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-outline shadow-default`;\n\n case 'surface':\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-surface shadow-default`;\n\n case 'assistant':\n return `${baseClasses} ${FOCUS_OUTLINE.assistant} btn-assistant shadow-default`;\n\n case 'destructive':\n return `${baseClasses} ${FOCUS_OUTLINE.destructive} btn-destructive`;\n\n case 'success':\n return `${baseClasses} ${FOCUS_OUTLINE.success} btn-glass-success`;\n\n case 'warning':\n return `${baseClasses} ${FOCUS_OUTLINE.warning} btn-glass-warning`;\n\n case 'error':\n return `${baseClasses} ${FOCUS_OUTLINE.error} btn-glass-error`;\n\n case 'ghost':\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-ghost`;\n\n case 'quiet':\n return `${baseClasses} ${FOCUS_OUTLINE.accent} btn-quiet`;\n\n case 'no-style':\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-no-style border-none`;\n\n default:\n return `${baseClasses} ${FOCUS_OUTLINE.neutral} btn-filled shadow-default`;\n }\n};\n"],"names":["getSizeClasses","size","getDefaultRadiusClass","iconOnly","getIconGapClass","position","start","getRadiusClass","radius","FOCUS_OUTLINE","getVariantClasses","variant","radiusClass","baseClasses"],"mappings":"AAMO,MAAMA,IAAiB,CAACC,MAAqB;AAChD,UAAQA,GAAA;AAAA;AAAA,IAEJ,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX;AACI,aAAO;AAAA,EAAA;AAEnB,GAIaC,IAAwB,CAACD,GAAkBE,MAA8B;AAClF,UAAQF,GAAA;AAAA;AAAA,IAEJ,KAAK;AACD,aAAOE,IAAW,eAAe;AAAA,IACrC,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX;AACI,aAAO;AAAA,EAAA;AAEnB,GAGaC,IAAkB,CAACH,GAAkBI,MAAsC;AACpF,QAAMC,IAAQD,MAAa;AAC3B,UAAQJ,GAAA;AAAA,IACJ,KAAK;AAAA,IACL,KAAK;AACD,aAAOK,IAAQ,aAAa;AAAA,IAChC;AACI,aAAOA,IAAQ,SAAS;AAAA,EAAA;AAEpC,GAEaC,IAAiB,CAACC,MAAiC;AAC5D,UAAQA,GAAA;AAAA,IACJ,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX;AACI,aAAO;AAAA,EAAA;AAEnB,GAOMC,IAAgB;AAAA,EAClB,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AACZ,GAEaC,IAAoB,CAACC,GAAwBC,MAAgC;AACtF,QAAMC,IAAc,4KAA4KD,CAAW;AAE3M,UAAQD,GAAA;AAAA,IACJ,KAAK;AACD,aAAO,GAAGE,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,SAAS;AAAA,IAEpD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,WAAW;AAAA,IAEtD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,KAAK;AAAA,IAEhD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,MAAM;AAAA,IAEjD,KAAK;AACD,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,IAElD;AACI,aAAO,GAAGI,CAAW,IAAIJ,EAAc,OAAO;AAAA,EAAA;AAE1D;"}
@@ -1 +1 @@
1
- {"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** Which trigger edge the menu prefers to share; defaults to the preset's. */\n align?: 'left' | 'right';\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n align: alignOverride,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const align = alignOverride ?? spec.align;\n const preferredLeft = align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted. It is rounded to a whole pixel, so a label\n // 144.2px wide reports 144 and ellipsises at that width; the extra pixel covers the fraction.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + 1 + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","alignOverride","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","align","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
1
+ {"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** Which trigger edge the menu prefers to share; defaults to the preset's. */\n align?: 'left' | 'right';\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n align: alignOverride,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const align = alignOverride ?? spec.align;\n const preferredLeft = align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted. It is rounded to a whole pixel, so a label\n // 144.2px wide reports 144 and ellipsises at that width; the extra pixel covers the fraction.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + 1 + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-end'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","alignOverride","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","align","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -292,6 +292,30 @@ export declare const Avatar: default_2.FC<AvatarProps>;
292
292
  /** `initial` suits a named entity (a person, a model); `placeholder` says "no logo available". */
293
293
  export declare type AvatarFallback = 'initial' | 'placeholder';
294
294
 
295
+ /** Overlapping `Avatar`s, first on top; one `img` to assistive tech, named by its items. */
296
+ export declare const AvatarGroup: default_2.FC<AvatarGroupProps>;
297
+
298
+ export declare interface AvatarGroupItem {
299
+ src?: string;
300
+ name: string;
301
+ fallback?: AvatarFallback;
302
+ }
303
+
304
+ export declare interface AvatarGroupProps {
305
+ items: AvatarGroupItem[];
306
+ size?: number;
307
+ shape?: AvatarShape;
308
+ /** Tiles shown before a `+N` tile takes the rest. */
309
+ max?: number;
310
+ /** Fraction of `size` (0 to 0.5) each tile sits under the one before it. */
311
+ overlap?: number;
312
+ /** Names the group; otherwise it is named by the item names joined with ", ". */
313
+ 'aria-label'?: string;
314
+ 'aria-labelledby'?: string;
315
+ className?: string;
316
+ style?: default_2.CSSProperties;
317
+ }
318
+
295
319
  export declare interface AvatarProps {
296
320
  /** Image URL (logo/photo). Falls back to `fallback` when missing or it fails to load. */
297
321
  src?: string;
@@ -1677,6 +1701,8 @@ declare interface EllipsisTextProps {
1677
1701
  */
1678
1702
  tooltipWhenTruncated?: boolean;
1679
1703
  tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';
1704
+ /** A span around the text, for a place a div may not go, such as inside a clickable `Surface.Row`. */
1705
+ inline?: boolean;
1680
1706
  className?: string;
1681
1707
  style?: default_2.CSSProperties;
1682
1708
  }
@@ -1886,6 +1912,8 @@ declare interface FlexProps {
1886
1912
  wrap?: boolean;
1887
1913
  width?: string;
1888
1914
  id?: string;
1915
+ /** `span` for content inside a button, such as a clickable `Surface.Row`. */
1916
+ as?: 'div' | 'span';
1889
1917
  children?: default_2.ReactNode;
1890
1918
  className?: string;
1891
1919
  }
@@ -1898,7 +1926,7 @@ declare const FLOATING_OFFSET_PX: {
1898
1926
 
1899
1927
  declare type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;
1900
1928
 
1901
- declare type FloatingPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-center' | 'right-start' | 'right-end' | 'left-start';
1929
+ declare type FloatingPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'top-center' | 'right-start' | 'right-end' | 'left-start';
1902
1930
 
1903
1931
  /** Which theme typeface to render in. Defaults to the sans body face. */
1904
1932
  declare type FontFamily = 'sans' | 'heading' | 'body' | 'mono';
@@ -4803,6 +4831,36 @@ declare interface SpinnerProps {
4803
4831
  style?: default_2.CSSProperties;
4804
4832
  }
4805
4833
 
4834
+ /** A primary action and a menu trigger in one pill; each segment is its own button. */
4835
+ export declare const SplitButton: default_2.ForwardRefExoticComponent<SplitButtonProps & default_2.RefAttributes<HTMLButtonElement>>;
4836
+
4837
+ export declare interface SplitButtonProps {
4838
+ variant?: ButtonVariant;
4839
+ size?: ButtonSize;
4840
+ children?: default_2.ReactNode;
4841
+ leading?: default_2.ReactNode;
4842
+ trailing?: default_2.ReactNode;
4843
+ onClick?: default_2.MouseEventHandler<HTMLButtonElement>;
4844
+ /** The trigger segment is always `type="button"`. */
4845
+ type?: 'button' | 'submit' | 'reset';
4846
+ loading?: boolean;
4847
+ disabled?: boolean;
4848
+ flexGrow?: boolean;
4849
+ /** Accessible name of the chevron segment. */
4850
+ menuLabel: string;
4851
+ /** Drives `aria-expanded`; the chevron points up while open. */
4852
+ menuOpen?: boolean;
4853
+ onMenuClick?: default_2.MouseEventHandler<HTMLButtonElement>;
4854
+ menuIcon?: default_2.ReactNode;
4855
+ /** `false` for a trigger that opens something other than a menu. */
4856
+ menuHasPopup?: boolean;
4857
+ menuRef?: default_2.Ref<HTMLButtonElement>;
4858
+ /** Wraps the trigger segment, e.g. as a `Popover`'s `trigger`. */
4859
+ menuTrigger?: (segment: default_2.ReactElement) => default_2.ReactNode;
4860
+ className?: string;
4861
+ style?: default_2.CSSProperties;
4862
+ }
4863
+
4806
4864
  export declare const SplitScreenLayout: default_2.FC<SplitScreenLayoutProps>;
4807
4865
 
4808
4866
  declare interface SplitScreenLayoutProps {
@@ -5027,10 +5085,11 @@ declare interface SurfaceRowChromeProps {
5027
5085
  * `aria-checked="mixed"` there, never paints and never marks the row current, and counts
5028
5086
  * as not selected under any other role. */
5029
5087
  selected?: boolean | 'mixed';
5030
- /** How a `selected` row shows it. `highlight` paints the treatment; `announce` leaves the
5031
- * visual to a control inside the row and only sets the ARIA state. Defaults to `announce`
5032
- * under `role="checkbox"` and `highlight` otherwise. */
5033
- selection?: 'highlight' | 'announce';
5088
+ /** How a `selected` row shows it. `highlight` paints the treatment; `tint` paints it
5089
+ * without the accent bar, for a menu whose choice is already marked inside the row;
5090
+ * `announce` leaves the visual to a control inside the row and only sets the ARIA state.
5091
+ * Defaults to `announce` under `role="checkbox"` and `highlight` otherwise. */
5092
+ selection?: 'highlight' | 'tint' | 'announce';
5034
5093
  /**
5035
5094
  * Tints the whole row by what it reports. `muted` is a row whose subject is inactive —
5036
5095
  * not disabled: it stays interactive. `success` and `error` are for a row that carries
@@ -5070,6 +5129,8 @@ export declare interface SurfaceRowProps extends SurfaceRowChromeProps {
5070
5129
  role?: default_2.AriaRole;
5071
5130
  'aria-label'?: string;
5072
5131
  'aria-labelledby'?: string;
5132
+ /** `-1` takes the row out of the Tab order, for a roving radio group. */
5133
+ tabIndex?: number;
5073
5134
  }
5074
5135
 
5075
5136
  export declare const Swatch: default_2.FC<SwatchProps>;