@stackone/malachite 2.73.0 → 2.74.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/COMPONENTS.md CHANGED
@@ -19,13 +19,13 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
19
19
  - **`WizardLayout`** — Full-screen wizard scaffold: top bar (back button, right-aligned stepper, trailing slot), pinned `header` slot, centered scrolling content column, and a pinned footer (above sticky body content). `footerGlass` gives the footer a frosted translucent + backdrop-blur treatment. `banner` pins a page-wide notice under the top bar, above `header` and outside the content column, so a warning about the whole page is not squeezed to the column's width — pair it with `Alert layout="banner"`. `topBarSize="large"` draws a 60px top bar with 28px insets (52px and 16px below 640px). `footerAlign="content"` holds the footer to the content column, inside its gutter, so a full-width action lines up with the list above it. `reserveBack={false}` drops the square held for a hidden back button, so `leading` sits at the same inset as `trailing`; the default keeps it so `leading` does not move as the wizard steps. _Key props:_ `showBack`, `onBack`, `reserveBack`, `topBarSize` (`default` | `large`), `footerAlign` (`full` | `content`), `leading`, `stepper`, `trailing`, `banner`, `header`, `footer`, `footerGlass`, `center`, `contentWidth`, `minWidth`.
20
20
  - **`PageActions`** — A page's action buttons, collapsing into an overflow menu as the row narrows. `PageLayout.Regular` renders these from its `buttons` prop; import it directly when a page builds its own header block (e.g. one that scrolls the title away) so the collapse, the dividers and the href handling come with it rather than being hand-rolled. _Key props:_ `buttons` (`PageAction[]`).
21
21
  - **`Toolbar`** — Flat, opaque horizontal bar for a sub-header / filter row (e.g. a Search). Owns its chrome — an opaque ground (`card-background`, or the sunken page ground with `surface="page"` for a bar above a card), so it can pin over scrolling content, plus an optional bottom `divider` — so consumers don’t hand-roll background/border tokens. Its `TOOLBAR_FILTER_WIDTH_PX` / `TOOLBAR_FILTER_MIN_WIDTH_PX` / `TOOLBAR_FILL_BASIS_PX` / `TOOLBAR_FILL_MIN_WIDTH_PX` are exported so a control outside the bar can line up with one inside it without re-typing the number. `sticky` pins it to the top of its scroll container; any other positioning is left to the consumer, as is the gutter when `padding="none"` — use that when the bar must sit flush with the content it pins over. `layout="filters"` makes it own the layout too, which consumers were each re-deriving and getting wrong the same way: a `flex: 1` search with no floor collapses to nothing on a narrow viewport, and a row that cannot wrap runs off the page. Compose it as `Toolbar.Fill` for the search and `Toolbar.Item` per filter, as flat siblings; `grow={false}` stops a slot growing past its basis, for a row that already spaces its children, though it can still shrink toward its `minWidth`. The row packs greedily from its basis widths and each line's spare space is shared out by grow factor, which is deliberately lopsided: the search outweighs the filters a hundred to one, so a shared line leaves them at their 200px while a filter left alone on a wrapped line grows into the whole width instead of stranding beside a gap. A filter drops to the next line once its full 200px basis will not fit, and the filters sharing a line then compress toward a 160px floor. A search and a filter are laid out differently rather than uniformly, because a search field's useful width is "as much as you can spare" where a filter's is the width of a value. _Key props:_ `divider`, `padding` (`none` | `compact` | `default`), `layout` (`plain` | `filters`), `surface` (`card` | `page` — `page` paints the sunken page ground, for a bar that sits above a card rather than inside it), `sticky` (pins it to the top of its scroll container above the rows under it), `className`, `style`. `Fill`: `minWidth`. `Item`: `width`, `minWidth`, `grow`.
22
- - **`Flex`** — Flexbox layout helper with semantic direction/align/justify/gap props. _Key props:_ `FlexDirection`, `FlexAlign`, `FlexJustify`, `FlexGapSize`.
22
+ - **`Flex`** — Flexbox layout helper with semantic direction/align/justify/gap props. _Key props:_ `FlexDirection`, `FlexAlign`, `FlexJustify`, `FlexGapSize`. `as` (`div` | `span`, the latter for content inside a clickable `Surface.Row`)
23
23
  - **`Padded`** — Padding wrapper applying consistent design-system spacing.
24
24
  - **`Spacer`** — Empty element that adds fixed spacing between siblings.
25
25
  - **`Divider`** — Separator; supports a dashed line style. Horizontal by default and spanning its container; `orientation="vertical"` draws a rule between inline controls (e.g. groups in a toolbar) and holds its width in a tight flex row. `length` sizes it along its own axis on a fixed scale, and it never shrinks below 1px. _Key props:_ `orientation="horizontal" | "vertical"`, `length` (`small` | `medium` | `large`), `lineStyle="solid" | "dashed"`, `DividerVariant`, `SpacingSize`, `PaddingSize`.
26
26
  - **`OverflowRow`** — Horizontal row that collapses items that do not fit into an overflow affordance.
27
27
  - **`ActionBar`** — A row of actions that collapses into a single `ActionMenu` once its container drops below `collapseBelow`. Measures the nearest genuinely-wider ancestor rather than itself, since the row shrinks to its own content. `dividerBefore` renders a vertical rule in the row and a divider in the collapsed menu, so both forms group the actions the same way. _Key props:_ `buttons` (`label`, `onClick`, `icon`, `variant`, `size`, `radius`, `tooltip`, `disabled`, `dividerBefore`), `collapseBelow`, `menuLabel`, `className`.
28
- - **`Popover`** — An anchored, non-modal panel for arbitrary content — a workspace switcher, an org flyout. The panel renders in its own shadow root, so malachite components inside it are styled, and joins the top layer like the other overlays. It prefers its `placement` and flips to the opposite side when there is no room, clamping along the other axis. Click-opened panels take focus and hand it back to the trigger on close; `openOn="hover"` also opens on focus and stays open while the pointer or focus is in the panel. A nested Popover counts as inside its parent for outside-press and hover dismissal, and Escape closes the topmost only. A click panel also closes when focus moves somewhere outside it and its trigger, and on close it hands focus back to the trigger only if focus would otherwise fall to the page. _Key props:_ `trigger`, `children`, `open`, `defaultOpen`, `onOpenChange`, `placement` (`bottom-start` | `bottom-end` | `top-start` | `top-center` | `right-start` | `right-end` | `left-start`), `openOn` (`click` | `hover`), `width` (`content` | `trigger`), `padding` (`none` | `small` | `medium`), `anchorRef` (position against another element, e.g. the section a trigger row sits in), `offset` (`small` 8px | `medium` 12px | `large` 14px), `elevation` (`default` | `raised`), `initialFocus` (a ref, or a function read when the panel is shown, for what takes focus in place of the first focusable; it is read again as the panel's content changes, so focus follows content that mounts late or a focused element that is removed, but never leaves an element the user moved to; `initialFocusRef` is its deprecated form, read on open only), `openOnArrowKey` (a click panel also opens, focus inside, on the arrow key its placement points at — ArrowDown for a `bottom-*` placement; a text field trigger keeps its arrow keys), `role` (`dialog` | `menu` | `listbox`; `menu` brings no roving focus of its own), `aria-label` or `aria-labelledby` (one is required), `className`. The panel is capped to the viewport width, and only overlays opened from inside it (menus, dropdowns, modals) count as inside for outside-press dismissal. A side placement stays level with its anchor and scrolls within the room below, sliding up only when under 160px is left. Parts for the panel body: `Popover.Header` (`title`, `action`, `divider` — `false` drops the rule and sits it 4px over the list, as its label), `Popover.Action` (`icon`, `aria-label`, `onClick`, `href` (a link that opens in a new tab on Cmd/Ctrl-click or a middle click; a plain click runs `onClick` when one is given), `disabled`, `tooltip`), `Popover.List` (a scrolling list; `scrollToSelected` scrolls the list, never the page, to its selected row, one marked `aria-current`, `aria-selected` or a checked radio, each time the popover is shown, once that row mounts), `Popover.Section` (a bordered section), `Popover.Empty` (an empty-state line), `Popover.Search` (pads a `Search` above the list without a second rule), `Popover.Note` (a footnote section in small secondary text) and `Popover.Status` (`message`, `tone` (`default` | `error`), `action` (`label`, `onClick`, `pending`), `focusable`): one `status` live region for the panel's life, announcing each message it is given and staying mounted, visually hidden, while there is none, so clear `message` rather than unmounting it. Errors announce through the same region. `focusable` with a ref makes it an `initialFocus` target; resolve to it only while it has a message, since an empty one is invisible. Reach for `Status` when the message changes while the panel is open or carries an action (loading, failed, sign-in needed), and `Empty` for a static line in a list that simply has nothing in it.
28
+ - **`Popover`** — An anchored, non-modal panel for arbitrary content — a workspace switcher, an org flyout. The panel renders in its own shadow root, so malachite components inside it are styled, and joins the top layer like the other overlays. It prefers its `placement` and flips to the opposite side when there is no room, clamping along the other axis. Click-opened panels take focus and hand it back to the trigger on close; `openOn="hover"` also opens on focus and stays open while the pointer or focus is in the panel. A nested Popover counts as inside its parent for outside-press and hover dismissal, and Escape closes the topmost only. A click panel also closes when focus moves somewhere outside it and its trigger, and on close it hands focus back to the trigger only if focus would otherwise fall to the page. _Key props:_ `trigger`, `children`, `open`, `defaultOpen`, `onOpenChange`, `placement` (`bottom-start` | `bottom-end` | `top-start` | `top-end` | `top-center` | `right-start` | `right-end` | `left-start`), `openOn` (`click` | `hover`), `width` (`content` | `trigger`), `padding` (`none` | `small` | `medium`), `anchorRef` (position against another element, e.g. the section a trigger row sits in), `offset` (`small` 8px | `medium` 12px | `large` 14px), `elevation` (`default` | `raised`), `initialFocus` (a ref, or a function read when the panel is shown, for what takes focus in place of the first focusable; it is read again as the panel's content changes, so focus follows content that mounts late or a focused element that is removed, but never leaves an element the user moved to; `initialFocusRef` is its deprecated form, read on open only), `openOnArrowKey` (a click panel also opens, focus inside, on the arrow key its placement points at — ArrowDown for a `bottom-*` placement; a text field trigger keeps its arrow keys), `role` (`dialog` | `menu` | `listbox`; `menu` brings no roving focus of its own), `aria-label` or `aria-labelledby` (one is required), `className`. The panel is capped to the viewport width, and only overlays opened from inside it (menus, dropdowns, modals) count as inside for outside-press dismissal. A side placement stays level with its anchor and scrolls within the room below, sliding up only when under 160px is left. Parts for the panel body: `Popover.Header` (`title`, `action`, `divider` — `false` drops the rule and sits it 4px over the list, as its label), `Popover.Action` (`icon`, `aria-label`, `onClick`, `href` (a link that opens in a new tab on Cmd/Ctrl-click or a middle click; a plain click runs `onClick` when one is given), `disabled`, `tooltip`), `Popover.List` (a scrolling list; `scrollToSelected` scrolls the list, never the page, to its selected row, one marked `aria-current`, `aria-selected` or a checked radio, each time the popover is shown, once that row mounts), `Popover.Section` (a bordered section), `Popover.Empty` (an empty-state line), `Popover.Search` (pads a `Search` above the list without a second rule), `Popover.Note` (a footnote section in small secondary text) and `Popover.Status` (`message`, `tone` (`default` | `error`), `action` (`label`, `onClick`, `pending`), `focusable`): one `status` live region for the panel's life, announcing each message it is given and staying mounted, visually hidden, while there is none, so clear `message` rather than unmounting it. Errors announce through the same region. `focusable` with a ref makes it an `initialFocus` target; resolve to it only while it has a message, since an empty one is invisible. Reach for `Status` when the message changes while the panel is open or carries an action (loading, failed, sign-in needed), and `Empty` for a static line in a list that simply has nothing in it.
29
29
  - **`Modal`** — Centred dialog over a scrim, with header slots (icon/title/subtitle/extra), a scrolling body and a pinned footer. Locks background scroll and dismisses on ESC, the close button, or a scrim click unless `closeOnOverlayClick` is off; `busy` suppresses the two pointer routes while a request is in flight and sets `aria-busy`, so a stray click cannot tear the dialog down mid-write — but never ESC, which is the only guaranteed keyboard exit from a focus trap and so always reaches `onClose`. ESC, the Tab focus trap and `aria-modal` all apply to the topmost overlay only, so these may be stacked — a dialog opened from a panel keeps Tab to itself and the panel stops claiming to be modal. The Tab trap follows focus into a malachite popup portaled out of the dialog’s own tree — a `MultiSelect` listbox or a `Popover` opened from the body — and traps Tab within that popup while it holds focus, so Tab walks and wraps that popup’s own stops rather than being yanked back to the dialog’s first control or escaping the dialog altogether. A `Dropdown` list or an `ActionMenu` is dismissed by Tab rather than trapping it: it closes and focus returns to its trigger, so Tab carries on from there. A consumer’s own portaled popup is not known to the trap and Tab will pull focus out of it. Set `role="alertdialog"` for a consequential choice, which asks assistive tech to announce the body and not just the name, and point `aria-describedby` at whichever element carries it. Width comes from the `size` union (`small` 420px | `medium` 612px | `large` 760px) rather than a raw pixel prop. `titleAdornment` sits immediately after the title behind a hairline divider, for naming the thing being acted on — distinct from `headerExtra`, which pins to the far right. `tray` paints a tinted plate around the card and hangs a `caption` beneath it; the plate is the dialog element itself, so that caption stays inside the `aria-modal` subtree rather than being stranded outside it by the visual nesting, and it is added to `aria-describedby` automatically because it says what the dialog is asking. It takes a tone (`neutral` | `warning` | `error` | `success`) rather than a colour, so no consumer can paint it off-palette — and the tone sets the plate and its caption colour together, so the two cannot disagree. The caption is a `{ label?, message }` pair rather than a node, because its row height, type scale and colour belong to the plate. `size` names the card, and the plate’s 4px ring is added on top. Like `SidePanel` it sits in a z-index band from 1000 — one step per stacked overlay, so what paints on top is what holds the keyboard — and stays out of the browser top layer, so a host app’s own dialogs can still cover it. `callouts` stacks full-bleed `Alert` bands at the top of the body, as on `SidePanel`; they ignore `bodyPadding` and lead `aria-describedby`, ahead of the consumer’s own description and the tray caption. For a confirmation use `ConfirmationDialog`, which is a preset over this. _Key props:_ `open`, `onClose`, `title`, `subtitle`, `icon`, `headerExtra`, `titleAdornment`, `tray`, `callouts`, `renderLink`, `headerDivider`, `footer`, `size`, `showCloseButton`, `closeOnOverlayClick`, `bodyPadding`, `bodyHeight` (`auto` | `tall` — 600px, or the viewport less 160px when smaller, for an embedded iframe at `height: 100%`), `busy`, `role` (`dialog` | `alertdialog`), `aria-label`, `aria-labelledby`, `aria-describedby`.
30
30
  - **`SidePanel`** — Right-hand slide-in overlay panel (drawer) with header slots (back/icon/title/subtitle/extra), scrollable body and optional footer. `subtitlePlacement="below"` stacks the subtitle on its own line under the title row, so neither truncates the other in a narrow panel. `toolbar` pins content between the header and the scrolling body (16px sides, 16px above, 8px below, no divider), such as a Search over the list it filters. Locks background scroll and dismisses on ESC, an overlay click or close. ESC and the Tab focus trap apply to the topmost overlay only, so a `Modal` may be stacked over it. The Tab trap follows focus into a malachite popup portaled out of the panel’s own tree — a `MultiSelect` listbox or a `Popover` opened from the body — and traps Tab within that popup while it holds focus, so Tab walks and wraps that popup’s own stops rather than being yanked back to the panel’s first control or escaping the panel altogether. A `Dropdown` list or an `ActionMenu` is dismissed by Tab rather than trapping it: it closes and focus returns to its trigger, so Tab carries on from there. A consumer’s own portaled popup is not known to the trap and Tab will pull focus out of it. `callouts` stacks full-bleed `Alert` bands at the top of the body, above `children` and unaffected by `bodyPadding` — the same band `ConfirmationDialog` gives its `callout` — and `renderLink` routes their root-relative markdown links through the app’s router. The callouts are announced as the panel’s description, ahead of `description`.
31
31
  - **`useHasOpenOverlay`** — Hook reporting whether any modal-type overlay - `SidePanel` or `Modal` - is open anywhere in the app. For host chrome that has to get out of an overlay’s way, a floating support widget being the case it was added for: overlays render through a portal, so a host has no DOM signal of its own to read, and reading the library’s internal markers would couple it to something it does not own. Dropdowns and tooltips are deliberately not counted - they do not trap focus, and chrome does not need to move for them. _Key props:_ No arguments. Returns a boolean.
@@ -100,6 +100,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
100
100
  ## Buttons & actions
101
101
 
102
102
  - **`Button`** — Primary button. Variants: `filled`, `outline`, `surface` (outline's geometry on a ground of its own, for a control sitting on a tinted surface), `assistant` (the violet the assistant is named in — provenance, not a severity: it says who is being asked, not that anything is wrong), `destructive`, `success`, `warning`, `error`, `ghost`, `quiet` (text only in the accent colour, for a secondary action such as Reset beside a primary one — accent, not success, because it is something to press rather than a status), `no-style`. Supports `flush` (pulls a borderless button out by its padding so its label lines up with the text around it), `iconOnly`, `loading`, `tooltip`. `leading` puts something that is not an icon, such as an avatar, before the label at its own size; `contentAlign="start"` starts the label at the leading edge and pins an end `icon` to the far edge, which with `flexGrow` makes a full-width switcher trigger — a project pill whose width comes from its container (e.g. a box `SIDEBAR_POPOVER_WIDTH_PX` wide, so it lines up with the `Sidebar.Panel` it opens). _Key props:_ `variant`, `size`, `icon`, `iconPosition`, `leading`, `contentAlign` (`center` | `start`), `flexGrow` (fills the container), `width` (`auto` | `wide` — a 120px floor for a panel's or dialog's primary action), `iconOnly`, `loading`, `disabled`, `fill`, `tooltip`.
103
+ - **`SplitButton`** — One pill made of a primary action and a trailing menu trigger, sharing `Button`'s chrome: for an action with a default and a choice behind it, such as authorizing on every connected app or on one. The two segments are two real buttons, each with its own hover, active and keyboard focus, separated by a translucent seam that reads on every variant's ground; `disabled` and `loading` take both out together. The trigger shows a chevron pointing down while closed and up while `menuOpen`, which also sets `aria-expanded`, and is named by `menuLabel` since it shows no text. `trailing` sits after the label at its own size, for an `AvatarGroup` of what the action reaches. To open a `Popover` from the trigger, pass `menuTrigger`: it hands the segment to the Popover as its `trigger`, so the main segment stays a plain action rather than toggling the menu too. The menu itself is composed by the consumer from `Popover` and `Surface.Row`. _Key props:_ `variant` (every `Button` variant, default `filled`), `size` (default `medium`), `children`, `leading`, `trailing`, `onClick`, `type`, `loading`, `disabled`, `flexGrow`, `menuLabel`, `menuOpen`, `onMenuClick`, `menuIcon`, `menuHasPopup`, `menuRef`, `menuTrigger` (`(segment) => node`), `ref`, `className`, `style`.
103
104
  - **`CloseButton`** — The bare X that dismisses a screen, panel or wizard: secondary at rest, primary on hover and keyboard focus, with no box or background at either. Use it instead of a `ghost` `Button` (which draws a hover box) or `no-style` (which has no disabled look). `surface="inverse"` is for a panel that stays dark in every theme, such as `SplitScreenLayout`’s, and reads the `closeButton` theme tokens. `loading` swaps the X for a spinner and sets `aria-busy`. Disabled and loading both use `aria-disabled` rather than `disabled`, so they ignore clicks but stay focusable and can still trigger a `Tooltip` explaining why. Named "Close" unless `aria-label` says otherwise. _Key props:_ `onClick`, `aria-label`, `size` (`small` 24px | `medium` 32px, 12px glyph), `surface` (`default` | `inverse`), `disabled`, `loading`.
104
105
  - **`AccessPills`** — Three toggle pills, R / W / D, saying which effects an account grants (Read, Write, Delete) — on a linked-account row and in the header above the rows. Each pill is a button with `aria-pressed`, named in full by its state (`Write: some actions off`), and a tooltip on hover and focus spells that name out, since a letter alone is cryptic. On paints the effect’s tint (Read in `Tag`’s primary, Write in warning, Delete in error, the colours the dashboard already gives these effects), `"mixed"` (some, not all, of its actions on) an empty pill with a dashed border, off a neutral one. The colours come from the theme and there is no colour prop. It reports only which pill was pressed through `onToggle`: any cascade between effects (Read off turning Write and Delete off, say) and what pressing a mixed pill means belong to the caller. A pill in `disabled` stays focusable with `aria-disabled`, ignores clicks and keys, and its tooltip shows `disabledReason` instead, which is also its accessible description. `size="dense"` draws 24×20 pills in place of 26×24, for a narrow screen. _Key props:_ `value` (`{ read, write, delete }`, each `true` | `false` | `"mixed"`), `onToggle(effect)`, `disabled` (per effect), `disabledReason` (per effect, or one string), `size` (`default` | `dense`), `aria-label`, `aria-labelledby`.
105
106
  - **`PillButton`** — Pill-shaped button, often used for filters/toggleable chips. `shape="tab"` squares the top corners so a row of them reads as attached to whatever sits above — the controls under a chat composer. Pass `href` and an interactive, non-disabled pill renders a real anchor (cmd-click, copy-link) instead of navigating from `onClick`; a disabled or `interactive={false}` pill stays static, and an unsafe scheme such as `javascript:` is dropped. `target="_blank"` always carries `noopener noreferrer`. _Key props:_ `label`, `icon`, `trailingIcon`, `selected`, `disabled`, `interactive`, `size`, `variant`, `shape` (`pill` | `tab`), `onClick`, `href`, `target`, `rel`.
@@ -110,13 +111,14 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
110
111
  ## Typography
111
112
 
112
113
  - **`ClampText`** — Clamps text to N lines; expands to full text on hover/focus with a height animation. Wrapping markdown? Pass `variant="flow"` to the `Typography.MarkdownBlock` inside it, or the clamp counts blocks instead of lines.
113
- - **`Typography`** — Text primitives — use these instead of raw colour tokens on `<span>`s. Includes `Typography.Text`, `Typography.SecondaryText`, `Typography.EllipsisText`, etc., with size/weight/color props. `Typography.Text`'s `size` runs `xsmall` (11px), `12`, `small` (13px), `medium` (14px) and up; `12` is off the named ladder, like the theme's `15`, for a secondary line under a 14px label. `wrap={false}` keeps a `Typography.Text` on one line. `Typography.Text` takes `font="sans" | "heading" | "body" | "mono"`, and `as` (`p`, `h1`–`h6`) for a heading or paragraph that keeps its look; `Typography.CodeText` takes `variant="inline"` for a chip mid-sentence; `Typography.MarkdownBlock` takes `variant="flow"` for use inside `ClampText`, `variant="inline"` for one line of inline syntax set inside another component (no autolinks, images as alt text), `links="inert"` to draw links without an anchor inside a button, `renderLink` to send root-relative links (`/path`, never `//host`) through the app’s router in the same tab while every other link still opens in a new one — without it every link opens in a new tab, and `normalizeSource` for machine-generated descriptions that omit blank lines between blocks. `Typography.PathText` draws one line of path segments (`segments`, root first, each `{ text, tone, minChars }` with `tone` `default` | `secondary`) joined by a `separator` that is never cut: the segments give way in order, each down to `minChars` (2) characters, so the last stays whole the longest, and a cut never leaves a space before its ellipsis, which `text-overflow` does. The full path is always the accessible name and shows in a tooltip only when something is cut (`tooltip`, `tooltipPlacement`, `tooltipSuppressed` while the menu it labels is open). It measures with canvas text metrics against a shared ResizeObserver, so it costs no layout read per instance. `Typography.EllipsisText` takes a `size` and a `fontWeight` (Text's scale) so a truncating label can match the text beside it, and shows its `tooltip` on every hover unless `tooltipWhenTruncated` is set, which limits it to text that is actually cut off — for a tooltip that repeats the label, where a tooltip carrying something the label does not say should keep the default.
114
+ - **`Typography`** — Text primitives — use these instead of raw colour tokens on `<span>`s. Includes `Typography.Text`, `Typography.SecondaryText`, `Typography.EllipsisText`, etc., with size/weight/color props. `Typography.Text`'s `size` runs `xsmall` (11px), `12`, `small` (13px), `medium` (14px) and up; `12` is off the named ladder, like the theme's `15`, for a secondary line under a 14px label. `wrap={false}` keeps a `Typography.Text` on one line. `Typography.Text` takes `font="sans" | "heading" | "body" | "mono"`, and `as` (`p`, `h1`–`h6`) for a heading or paragraph that keeps its look; `Typography.CodeText` takes `variant="inline"` for a chip mid-sentence; `Typography.MarkdownBlock` takes `variant="flow"` for use inside `ClampText`, `variant="inline"` for one line of inline syntax set inside another component (no autolinks, images as alt text), `links="inert"` to draw links without an anchor inside a button, `renderLink` to send root-relative links (`/path`, never `//host`) through the app’s router in the same tab while every other link still opens in a new one — without it every link opens in a new tab, and `normalizeSource` for machine-generated descriptions that omit blank lines between blocks. `Typography.PathText` draws one line of path segments (`segments`, root first, each `{ text, tone, minChars }` with `tone` `default` | `secondary`) joined by a `separator` that is never cut: the segments give way in order, each down to `minChars` (2) characters, so the last stays whole the longest, and a cut never leaves a space before its ellipsis, which `text-overflow` does. The full path is always the accessible name and shows in a tooltip only when something is cut (`tooltip`, `tooltipPlacement`, `tooltipSuppressed` while the menu it labels is open). It measures with canvas text metrics against a shared ResizeObserver, so it costs no layout read per instance. `Typography.EllipsisText` takes a `size` and a `fontWeight` (Text's scale) so a truncating label can match the text beside it, and shows its `tooltip` on every hover unless `tooltipWhenTruncated` is set, which limits it to text that is actually cut off — for a tooltip that repeats the label, where a tooltip carrying something the label does not say should keep the default. `inline` wraps the text in a span rather than a div, for inside a clickable `Surface.Row`.
114
115
 
115
116
  ## Data display
116
117
 
117
118
  - **`Table`** — Data table with columns, sizing, pagination and row selection. `column.align` places a column's header as well as its cells. What lands on the aligned edge is the last thing in the header row rather than the title: the sort caret on a sortable column, the info icon on one carrying `info` without a sorter. `headerVariant` picks the header layout, and only `inline` lines header and body up exactly, because both are cells of one `<table>`. `detached` builds them from flex rows, where a sortable header cell is 32px narrower than a non-sortable one — its padding sits on the inner button rather than on the flex item — so its header cells sit 15–17px off their column at any alignment; reach for `inline` where exact alignment matters. Sorting is internal, so the array passed as `data` is not the order rendered once a column is sorted: `onVisibleOrderChange` reports the rows in render order — the whole of `data`, compared by `rowKey` so an equal order never re-fires — for a consumer stepping a master-detail view the way the user sees it. _Key props:_ `TableColumns` (incl. `align`), `TableSize`, `TableType`, `TableRowSelection`, `TablePaginationPosition`, `headerVariant`, `onVisibleOrderChange`.
118
119
  - **`Statistic`** — Displays a statistic value with a label; primary/secondary sizes. _Key props:_ `StatisticValueSize`.
119
120
  - **`Avatar`** — Logo/initial avatar. Renders the image when available, otherwise a coloured tile with the first letter of `name`, or with `fallback="placeholder"` the generic "no logo available" tile. Encapsulates the fallback colour. _Key props:_ `src`, `name`, `size`, `shape="square" | "circle"`, `fallback="initial" | "placeholder"`.
121
+ - **`AvatarGroup`** — A horizontal stack of overlapping `Avatar`s, first on top, for a "these apps" cluster beside a label or in a button. The gap between tiles is cut out of the tile beneath rather than painted, so it shows whatever ground the group sits on. Past `max` the rest fold into a `+N` tile. One image to assistive tech, named by its items joined with ", " unless `aria-label` says otherwise. _Key props:_ `items` (`{src?, name, fallback?}[]`), `size` (px, default 24), `shape` (`square` | `circle`), `max` (default 4), `overlap` (fraction of `size`, 0 to 0.5, default 0.3), `aria-label`, `className`, `style`.
120
122
  - **`AppLogo`** — A connector's mark, optionally with its name and version tag. Builds on `Avatar`, so a missing or broken logo falls back to the generic "no logo available" placeholder, a neutral grey tile with a darker square, the same mark stackone-logos.com serves. The gap tightens below 18px so the mark and label still read as one unit. _Key props:_ `src`, `name`, `showName`, `size`, `nameSize`, `nameWeight`, `version`, `truncate`, `className`.
121
123
  - **`ConnectionHeader`** — Two tilted logo tiles (from → to) showing a connection between apps. _Key props:_ `from`, `to` (`ConnectionEndpoint`: `icon`, `label`, `background`).
122
124
  - **`CodeBlock`** — Syntax-styled code block. _Key props:_ `code`.
@@ -0,0 +1,74 @@
1
+ import { jsxs as x, jsx as o } from "react/jsx-runtime";
2
+ import { Avatar as w } from "./Avatar.js";
3
+ const i = 2, p = (a, t, n) => {
4
+ const l = a + i * 2, c = `<svg xmlns="http://www.w3.org/2000/svg" width="${a}" height="${a}"><mask id="m"><rect width="100%" height="100%" fill="white"/><rect x="${-n - i}" y="${-i}" width="${l}" height="${l}" rx="${t + i}" fill="black"/></mask><rect width="${a}" height="${a}" fill="white" mask="url(#m)"/></svg>`;
5
+ return `url("data:image/svg+xml,${encodeURIComponent(c)}")`;
6
+ }, I = ({
7
+ items: a,
8
+ size: t = 24,
9
+ shape: n = "square",
10
+ max: l = 4,
11
+ overlap: c = 0.3,
12
+ "aria-label": $,
13
+ "aria-labelledby": h,
14
+ className: m = "",
15
+ style: v
16
+ }) => {
17
+ const r = a.slice(0, l), s = a.length - r.length, d = r.length + (s > 0 ? 1 : 0), g = t - Math.round(t * Math.min(Math.max(c, 0), 0.5)), u = n === "circle" ? t / 2 : Math.round(t * 0.25), f = p(t, u, g), b = (e) => e === 0 ? { zIndex: d } : {
18
+ zIndex: d - e,
19
+ marginInlineStart: g - t,
20
+ maskImage: f,
21
+ WebkitMaskImage: f
22
+ };
23
+ return /* @__PURE__ */ x(
24
+ "span",
25
+ {
26
+ role: "img",
27
+ "aria-label": h ? void 0 : $ ?? a.map((e) => e.name).join(", "),
28
+ "aria-labelledby": h,
29
+ className: `inline-flex items-center${m ? ` ${m}` : ""}`,
30
+ style: v,
31
+ children: [
32
+ r.map((e, k) => /* @__PURE__ */ o(
33
+ "span",
34
+ {
35
+ className: "relative inline-flex shrink-0",
36
+ style: b(k),
37
+ children: /* @__PURE__ */ o(
38
+ w,
39
+ {
40
+ src: e.src,
41
+ name: e.name,
42
+ size: t,
43
+ shape: n,
44
+ fallback: e.fallback
45
+ }
46
+ )
47
+ },
48
+ `${k}-${e.name}`
49
+ )),
50
+ s > 0 ? /* @__PURE__ */ o("span", { className: "relative inline-flex shrink-0", style: b(r.length), children: /* @__PURE__ */ x(
51
+ "span",
52
+ {
53
+ className: "inline-flex items-center justify-center bg-[var(--malachite-card-dimmed-background)] font-semibold text-[var(--malachite-text-secondary)] leading-none",
54
+ style: {
55
+ width: t,
56
+ height: t,
57
+ borderRadius: n === "circle" ? "50%" : u,
58
+ fontSize: Math.round(t * 0.4)
59
+ },
60
+ children: [
61
+ "+",
62
+ s
63
+ ]
64
+ }
65
+ ) }) : null
66
+ ]
67
+ }
68
+ );
69
+ };
70
+ export {
71
+ I as AvatarGroup,
72
+ I as default
73
+ };
74
+ //# sourceMappingURL=AvatarGroup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AvatarGroup.js","sources":["../../src/components/AvatarGroup.tsx"],"sourcesContent":["import type React from 'react';\nimport Avatar, { type AvatarFallback, type AvatarShape } from './Avatar';\n\nexport interface AvatarGroupItem {\n src?: string;\n name: string;\n fallback?: AvatarFallback;\n}\n\nexport interface AvatarGroupProps {\n items: AvatarGroupItem[];\n size?: number;\n shape?: AvatarShape;\n /** Tiles shown before a `+N` tile takes the rest. */\n max?: number;\n /** Fraction of `size` (0 to 0.5) each tile sits under the one before it. */\n overlap?: number;\n /** Names the group; otherwise it is named by the item names joined with \", \". */\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst GAP_PX = 2;\n\n// The gap is cut out of the lower tile, so the ground shows through; CSS masks an image by its\n// alpha, so the cut is transparent in the SVG itself.\nconst cutoutMask = (size: number, radius: number, shift: number): string => {\n const cut = size + GAP_PX * 2;\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">` +\n '<mask id=\"m\"><rect width=\"100%\" height=\"100%\" fill=\"white\"/>' +\n `<rect x=\"${-shift - GAP_PX}\" y=\"${-GAP_PX}\" width=\"${cut}\" height=\"${cut}\" rx=\"${radius + GAP_PX}\" fill=\"black\"/></mask>` +\n `<rect width=\"${size}\" height=\"${size}\" fill=\"white\" mask=\"url(#m)\"/>` +\n '</svg>';\n return `url(\"data:image/svg+xml,${encodeURIComponent(svg)}\")`;\n};\n\n/** Overlapping `Avatar`s, first on top; one `img` to assistive tech, named by its items. */\nconst AvatarGroup: React.FC<AvatarGroupProps> = ({\n items,\n size = 24,\n shape = 'square',\n max = 4,\n overlap = 0.3,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n className = '',\n style,\n}) => {\n const visible = items.slice(0, max);\n const hidden = items.length - visible.length;\n const tileCount = visible.length + (hidden > 0 ? 1 : 0);\n const shift = size - Math.round(size * Math.min(Math.max(overlap, 0), 0.5));\n const radius = shape === 'circle' ? size / 2 : Math.round(size * 0.25);\n const mask = cutoutMask(size, radius, shift);\n\n const tileStyle = (index: number): React.CSSProperties =>\n index === 0\n ? { zIndex: tileCount }\n : {\n zIndex: tileCount - index,\n marginInlineStart: shift - size,\n maskImage: mask,\n WebkitMaskImage: mask,\n };\n\n return (\n <span\n role=\"img\"\n aria-label={\n ariaLabelledBy\n ? undefined\n : (ariaLabel ?? items.map((item) => item.name).join(', '))\n }\n aria-labelledby={ariaLabelledBy}\n className={`inline-flex items-center${className ? ` ${className}` : ''}`}\n style={style}\n >\n {visible.map((item, index) => (\n <span\n key={`${index}-${item.name}`}\n className=\"relative inline-flex shrink-0\"\n style={tileStyle(index)}\n >\n <Avatar\n src={item.src}\n name={item.name}\n size={size}\n shape={shape}\n fallback={item.fallback}\n />\n </span>\n ))}\n {hidden > 0 ? (\n <span className=\"relative inline-flex shrink-0\" style={tileStyle(visible.length)}>\n <span\n className=\"inline-flex items-center justify-center bg-[var(--malachite-card-dimmed-background)] font-semibold text-[var(--malachite-text-secondary)] leading-none\"\n style={{\n width: size,\n height: size,\n borderRadius: shape === 'circle' ? '50%' : radius,\n fontSize: Math.round(size * 0.4),\n }}\n >\n +{hidden}\n </span>\n </span>\n ) : null}\n </span>\n );\n};\n\nexport default AvatarGroup;\nexport { AvatarGroup };\n"],"names":["GAP_PX","cutoutMask","size","radius","shift","cut","svg","AvatarGroup","items","shape","max","overlap","ariaLabel","ariaLabelledBy","className","style","visible","hidden","tileCount","mask","tileStyle","index","jsxs","item","jsx","Avatar"],"mappings":";;AAwBA,MAAMA,IAAS,GAITC,IAAa,CAACC,GAAcC,GAAgBC,MAA0B;AACxE,QAAMC,IAAMH,IAAOF,IAAS,GACtBM,IACF,kDAAkDJ,CAAI,aAAaA,CAAI,0EAE3D,CAACE,IAAQJ,CAAM,QAAQ,CAACA,CAAM,YAAYK,CAAG,aAAaA,CAAG,SAASF,IAASH,CAAM,uCACjFE,CAAI,aAAaA,CAAI;AAEzC,SAAO,2BAA2B,mBAAmBI,CAAG,CAAC;AAC7D,GAGMC,IAA0C,CAAC;AAAA,EAC7C,OAAAC;AAAA,EACA,MAAAN,IAAO;AAAA,EACP,OAAAO,IAAQ;AAAA,EACR,KAAAC,IAAM;AAAA,EACN,SAAAC,IAAU;AAAA,EACV,cAAcC;AAAA,EACd,mBAAmBC;AAAA,EACnB,WAAAC,IAAY;AAAA,EACZ,OAAAC;AACJ,MAAM;AACF,QAAMC,IAAUR,EAAM,MAAM,GAAGE,CAAG,GAC5BO,IAAST,EAAM,SAASQ,EAAQ,QAChCE,IAAYF,EAAQ,UAAUC,IAAS,IAAI,IAAI,IAC/Cb,IAAQF,IAAO,KAAK,MAAMA,IAAO,KAAK,IAAI,KAAK,IAAIS,GAAS,CAAC,GAAG,GAAG,CAAC,GACpER,IAASM,MAAU,WAAWP,IAAO,IAAI,KAAK,MAAMA,IAAO,IAAI,GAC/DiB,IAAOlB,EAAWC,GAAMC,GAAQC,CAAK,GAErCgB,IAAY,CAACC,MACfA,MAAU,IACJ,EAAE,QAAQH,MACV;AAAA,IACI,QAAQA,IAAYG;AAAA,IACpB,mBAAmBjB,IAAQF;AAAA,IAC3B,WAAWiB;AAAA,IACX,iBAAiBA;AAAA,EAAA;AAG/B,SACI,gBAAAG;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,MAAK;AAAA,MACL,cACIT,IACM,SACCD,KAAaJ,EAAM,IAAI,CAACe,MAASA,EAAK,IAAI,EAAE,KAAK,IAAI;AAAA,MAEhE,mBAAiBV;AAAA,MACjB,WAAW,2BAA2BC,IAAY,IAAIA,CAAS,KAAK,EAAE;AAAA,MACtE,OAAAC;AAAA,MAEC,UAAA;AAAA,QAAAC,EAAQ,IAAI,CAACO,GAAMF,MAChB,gBAAAG;AAAA,UAAC;AAAA,UAAA;AAAA,YAEG,WAAU;AAAA,YACV,OAAOJ,EAAUC,CAAK;AAAA,YAEtB,UAAA,gBAAAG;AAAA,cAACC;AAAA,cAAA;AAAA,gBACG,KAAKF,EAAK;AAAA,gBACV,MAAMA,EAAK;AAAA,gBACX,MAAArB;AAAA,gBACA,OAAAO;AAAA,gBACA,UAAUc,EAAK;AAAA,cAAA;AAAA,YAAA;AAAA,UACnB;AAAA,UAVK,GAAGF,CAAK,IAAIE,EAAK,IAAI;AAAA,QAAA,CAYjC;AAAA,QACAN,IAAS,IACN,gBAAAO,EAAC,QAAA,EAAK,WAAU,iCAAgC,OAAOJ,EAAUJ,EAAQ,MAAM,GAC3E,UAAA,gBAAAM;AAAA,UAAC;AAAA,UAAA;AAAA,YACG,WAAU;AAAA,YACV,OAAO;AAAA,cACH,OAAOpB;AAAA,cACP,QAAQA;AAAA,cACR,cAAcO,MAAU,WAAW,QAAQN;AAAA,cAC3C,UAAU,KAAK,MAAMD,IAAO,GAAG;AAAA,YAAA;AAAA,YAEtC,UAAA;AAAA,cAAA;AAAA,cACKe;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,GAEV,IACA;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGhB;"}
@@ -1,217 +1,106 @@
1
- import { jsx as r, jsxs as i, Fragment as d } from "react/jsx-runtime";
1
+ import { jsx as e, jsxs as i, Fragment as o } from "react/jsx-runtime";
2
2
  import { forwardRef as E } from "react";
3
- import H from "./Tooltip.js";
4
- import { getIconOnlySizeClasses as M, getIconSizeClass as q } from "./buttonSizing.js";
5
- import { resolveRel as D } from "./linkRel.js";
6
- const O = (t) => {
7
- switch (t) {
8
- // Figma has no text X-Small button, only the icon-only square — nothing to align to.
9
- case "xsmall":
10
- return "[--btn-px:calc(var(--spacing)*2)] px-[var(--btn-px)] py-1 text-xs";
11
- case "small":
12
- 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";
13
- case "medium":
14
- 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";
15
- case "large":
16
- 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";
17
- default:
18
- 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";
19
- }
20
- }, T = "-mx-[calc(var(--btn-px)+1px)]", V = "-mx-[var(--btn-px)]", W = (t, a) => {
21
- switch (t) {
22
- // Only the square rounds at 8px — text buttons at this size keep their 6px corner.
23
- case "xsmall":
24
- return a ? "rounded-lg" : "rounded-md";
25
- case "small":
26
- return "rounded-[var(--malachite-radius-10,10px)]";
27
- case "medium":
28
- return "rounded-xl";
29
- case "large":
30
- return "rounded-[var(--malachite-radius-xxl,14px)]";
31
- default:
32
- return "rounded-[var(--malachite-radius-xxl,14px)]";
33
- }
34
- }, R = (t, a) => {
35
- const e = a === "start";
36
- switch (t) {
37
- case "small":
38
- case "medium":
39
- return e ? "mr-[6px]" : "ml-[6px]";
40
- default:
41
- return e ? "mr-2" : "ml-2";
42
- }
43
- }, G = (t) => {
44
- switch (t) {
45
- case "md":
46
- return "rounded-md";
47
- case "lg":
48
- return "rounded-lg";
49
- case "xl":
50
- return "rounded-xl";
51
- default:
52
- return "rounded-xl";
53
- }
54
- }, s = {
55
- neutral: "focus-visible:[outline:2px_solid_var(--malachite-text-primary)]",
56
- destructive: "focus-visible:[outline:2px_solid_var(--malachite-text-destructive)]",
57
- success: "focus-visible:[outline:2px_solid_var(--malachite-text-success)]",
58
- warning: "focus-visible:[outline:2px_solid_var(--malachite-text-warning)]",
59
- error: "focus-visible:[outline:2px_solid_var(--malachite-text-error)]",
60
- assistant: "focus-visible:[outline:2px_solid_var(--malachite-assistant-foreground)]",
61
- accent: "focus-visible:[outline:2px_solid_var(--malachite-accent-foreground)]"
62
- }, J = (t, a) => {
63
- 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 ${a} cursor-pointer`;
64
- switch (t) {
65
- case "filled":
66
- return `${e} ${s.neutral} btn-filled shadow-default`;
67
- case "outline":
68
- return `${e} ${s.neutral} btn-outline shadow-default`;
69
- case "surface":
70
- return `${e} ${s.neutral} btn-surface shadow-default`;
71
- case "assistant":
72
- return `${e} ${s.assistant} btn-assistant shadow-default`;
73
- case "destructive":
74
- return `${e} ${s.destructive} btn-destructive`;
75
- case "success":
76
- return `${e} ${s.success} btn-glass-success`;
77
- case "warning":
78
- return `${e} ${s.warning} btn-glass-warning`;
79
- case "error":
80
- return `${e} ${s.error} btn-glass-error`;
81
- case "ghost":
82
- return `${e} ${s.neutral} btn-ghost`;
83
- case "quiet":
84
- return `${e} ${s.accent} btn-quiet`;
85
- case "no-style":
86
- return `${e} ${s.neutral} btn-no-style border-none`;
87
- default:
88
- return `${e} ${s.neutral} btn-filled shadow-default`;
89
- }
90
- }, K = () => /* @__PURE__ */ i(
91
- "svg",
92
- {
93
- className: "animate-spin -ml-1 mr-2 h-4 w-4",
94
- xmlns: "http://www.w3.org/2000/svg",
95
- fill: "none",
96
- viewBox: "0 0 24 24",
97
- children: [
98
- /* @__PURE__ */ r(
99
- "circle",
100
- {
101
- className: "opacity-25",
102
- cx: "12",
103
- cy: "12",
104
- r: "10",
105
- stroke: "currentColor",
106
- strokeWidth: "4"
107
- }
108
- ),
109
- /* @__PURE__ */ r(
110
- "path",
111
- {
112
- className: "opacity-75",
113
- fill: "currentColor",
114
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
115
- }
116
- )
117
- ]
118
- }
119
- ), Q = E(
3
+ import { ButtonSpinner as H } from "./ButtonSpinner.js";
4
+ import M from "./Tooltip.js";
5
+ import { getIconOnlySizeClasses as U, getIconSizeClass as G } from "./buttonSizing.js";
6
+ import { getSizeClasses as T, getVariantClasses as V, getIconGapClass as j, getRadiusClass as W, getDefaultRadiusClass as q } from "./buttonStyles.js";
7
+ import { resolveRel as J } from "./linkRel.js";
8
+ const K = "-mx-[calc(var(--btn-px)+1px)]", O = "-mx-[var(--btn-px)]", Q = E(
120
9
  ({
121
- variant: t,
122
- size: a = "large",
123
- radius: e,
10
+ variant: d,
11
+ size: t = "large",
12
+ radius: x,
124
13
  loading: g = !1,
125
- disabled: I = !1,
126
- icon: c,
127
- iconPosition: b = "start",
128
- leading: v,
129
- contentAlign: k = "center",
130
- flexGrow: m = !1,
131
- width: j = "auto",
132
- fill: B = !1,
133
- tooltip: x,
134
- tooltipPlacement: L = "top",
135
- iconOnly: p = !1,
136
- flush: z = !1,
137
- children: n,
138
- className: F = "",
139
- href: w,
140
- target: $,
141
- rel: U,
142
- ..._
143
- }, C) => {
144
- const A = e ? G(e) : W(a, p), l = I || g, N = k === "start", y = [
145
- N ? "flex items-center justify-start text-left" : "flex items-center justify-center",
146
- p ? `p-0 ${M(a)}` : O(a),
147
- J(t, A),
148
- m ? "w-full" : "",
149
- j === "wide" ? "min-w-[120px]" : "",
150
- B ? "max-w-full" : "",
151
- l ? "cursor-not-allowed" : "",
152
- x && l ? "pointer-events-none" : "",
153
- z && !p ? t === "no-style" ? V : T : "",
154
- F
155
- ].filter(Boolean).join(" "), o = N ? /* @__PURE__ */ r("span", { className: "flex min-w-0 flex-1 items-center", children: n }) : n, u = v ? /* @__PURE__ */ r(
14
+ disabled: B = !1,
15
+ icon: l,
16
+ iconPosition: C = "start",
17
+ leading: N,
18
+ contentAlign: v = "center",
19
+ flexGrow: c = !1,
20
+ width: k = "auto",
21
+ fill: y = !1,
22
+ tooltip: m,
23
+ tooltipPlacement: F = "top",
24
+ iconOnly: f = !1,
25
+ flush: L = !1,
26
+ children: s,
27
+ className: _ = "",
28
+ href: S,
29
+ target: b,
30
+ rel: A,
31
+ ...h
32
+ }, $) => {
33
+ const D = x ? W(x) : q(t, f), n = B || g, R = v === "start", w = [
34
+ R ? "flex items-center justify-start text-left" : "flex items-center justify-center",
35
+ f ? `p-0 ${U(t)}` : T(t),
36
+ V(d, D),
37
+ c ? "w-full" : "",
38
+ k === "wide" ? "min-w-[120px]" : "",
39
+ y ? "max-w-full" : "",
40
+ n ? "cursor-not-allowed" : "",
41
+ m && n ? "pointer-events-none" : "",
42
+ L && !f ? d === "no-style" ? O : K : "",
43
+ _
44
+ ].filter(Boolean).join(" "), a = R ? /* @__PURE__ */ e("span", { className: "flex min-w-0 flex-1 items-center", children: s }) : s, r = N ? /* @__PURE__ */ e(
156
45
  "span",
157
46
  {
158
- className: n ? "btn-leading inline-flex shrink-0 items-center me-2" : "btn-leading inline-flex shrink-0 items-center",
159
- children: v
47
+ className: s ? "btn-leading inline-flex shrink-0 items-center me-2" : "btn-leading inline-flex shrink-0 items-center",
48
+ children: N
160
49
  }
161
- ) : null, S = () => {
50
+ ) : null, I = () => {
162
51
  if (g)
163
- return /* @__PURE__ */ i(d, { children: [
164
- u,
165
- /* @__PURE__ */ r(K, {}),
166
- o
52
+ return /* @__PURE__ */ i(o, { children: [
53
+ r,
54
+ /* @__PURE__ */ e(H, {}),
55
+ a
167
56
  ] });
168
- const f = `inline-flex items-center shrink-0 ${q(a)}`;
169
- return c && b === "start" ? /* @__PURE__ */ i(d, { children: [
170
- u,
171
- /* @__PURE__ */ r(
57
+ const p = `inline-flex items-center shrink-0 ${G(t)}`;
58
+ return l && C === "start" ? /* @__PURE__ */ i(o, { children: [
59
+ r,
60
+ /* @__PURE__ */ e(
172
61
  "span",
173
62
  {
174
- className: `${f}${n ? ` ${R(a, "start")}` : ""}`,
175
- children: c
63
+ className: `${p}${s ? ` ${j(t, "start")}` : ""}`,
64
+ children: l
176
65
  }
177
66
  ),
178
- o
179
- ] }) : c && b === "end" ? /* @__PURE__ */ i(d, { children: [
180
- u,
181
- o,
182
- /* @__PURE__ */ r(
67
+ a
68
+ ] }) : l && C === "end" ? /* @__PURE__ */ i(o, { children: [
69
+ r,
70
+ a,
71
+ /* @__PURE__ */ e(
183
72
  "span",
184
73
  {
185
- className: `${f}${n ? ` ${R(a, "end")}` : ""}`,
186
- children: c
74
+ className: `${p}${s ? ` ${j(t, "end")}` : ""}`,
75
+ children: l
187
76
  }
188
77
  )
189
- ] }) : /* @__PURE__ */ i(d, { children: [
190
- u,
191
- o
78
+ ] }) : /* @__PURE__ */ i(o, { children: [
79
+ r,
80
+ a
192
81
  ] });
193
- }, h = w && !l ? /* @__PURE__ */ r(
82
+ }, u = S && !n ? /* @__PURE__ */ e(
194
83
  "a",
195
84
  {
196
- ref: C,
197
- href: w,
198
- target: $,
199
- rel: D(U, $),
200
- className: y,
201
- ..._,
202
- children: S()
85
+ ref: $,
86
+ href: S,
87
+ target: b,
88
+ rel: J(A, b),
89
+ className: w,
90
+ ...h,
91
+ children: I()
203
92
  }
204
- ) : /* @__PURE__ */ r(
93
+ ) : /* @__PURE__ */ e(
205
94
  "button",
206
95
  {
207
- ref: C,
208
- disabled: l,
209
- className: y,
210
- ..._,
211
- children: S()
96
+ ref: $,
97
+ disabled: n,
98
+ className: w,
99
+ ...h,
100
+ children: I()
212
101
  }
213
102
  );
214
- return x ? /* @__PURE__ */ r(H, { text: x, placement: L, fill: m, children: l ? /* @__PURE__ */ r("span", { className: `inline-flex cursor-not-allowed${m ? " w-full" : ""}`, children: h }) : h }) : h;
103
+ return m ? /* @__PURE__ */ e(M, { text: m, placement: F, fill: c, children: n ? /* @__PURE__ */ e("span", { className: `inline-flex cursor-not-allowed${c ? " w-full" : ""}`, children: u }) : u }) : u;
215
104
  }
216
105
  );
217
106
  Q.displayName = "Button";
@@ -1 +1 @@
1
- {"version":3,"file":"Button.js","sources":["../../src/components/Button.tsx"],"sourcesContent":["import React, { forwardRef } from 'react';\nimport Tooltip from './Tooltip';\nimport { getIconOnlySizeClasses, getIconSizeClass } from './buttonSizing';\nimport { resolveRel } from './linkRel';\n\n// Button variant types matching the old component\nexport type ButtonVariant =\n | 'filled'\n | 'outline'\n /**\n * `outline`'s geometry on a ground of its own. For a button sitting on a tinted or\n * patterned surface, where `outline`'s transparent background lets that surface through\n * and the control stops reading as a button.\n */\n | 'surface'\n /**\n * The violet the assistant is named in elsewhere — for a control that hands work to it, or\n * acts on what it produced. Provenance, not a severity: it says who is being asked, not\n * that anything is wrong.\n */\n | 'assistant'\n | 'destructive'\n | 'success'\n | 'warning'\n | 'error'\n | 'ghost'\n /** Text only, in the accent colour, for a secondary action beside a primary one. */\n | 'quiet'\n | 'no-style';\n\nexport type ButtonSize = 'xsmall' | 'small' | 'medium' | 'large';\n\nexport type ButtonRadius = 'md' | 'lg' | 'xl';\n\ninterface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n variant: ButtonVariant;\n size?: ButtonSize;\n radius?: ButtonRadius;\n loading?: boolean;\n disabled?: boolean;\n icon?: React.ReactNode;\n iconPosition?: 'start' | 'end';\n /** Before the label at its own size, never squeezed (an avatar); pairs with an end `icon`. */\n leading?: React.ReactNode;\n /** `start` aligns the label to the leading edge and pins an end `icon` to the far edge. */\n contentAlign?: 'center' | 'start';\n /** Fills the container's width. */\n flexGrow?: boolean;\n /** `wide` holds a 120px floor for a primary action. */\n width?: 'auto' | 'wide';\n fill?: boolean;\n tooltip?: string;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n iconOnly?: boolean;\n /** Pulls a borderless button out by its own padding, so its label lines up with the text around it. */\n flush?: boolean;\n children?: React.ReactNode;\n onClick?: React.MouseEventHandler<HTMLButtonElement & HTMLAnchorElement>;\n /** Renders the button as a real anchor, so a link action is never an <a> nested in a <button>. */\n href?: string;\n target?: React.HTMLAttributeAnchorTarget;\n rel?: string;\n}\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.\nconst 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// The 1px is the transparent border, which `no-style` drops.\nconst FLUSH_MARGIN = '-mx-[calc(var(--btn-px)+1px)]';\nconst FLUSH_MARGIN_BORDERLESS = '-mx-[var(--btn-px)]';\n\n// `rounded-*` reads the malachite radius tokens via `@theme`, so these follow the theme.\n// An explicit `radius` prop still wins, via getRadiusClass.\nconst 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.\nconst 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\nconst 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\nconst 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\nconst LoadingSpinner = () => (\n <svg\n className=\"animate-spin -ml-1 mr-2 h-4 w-4\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n);\n\nexport const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(\n (\n {\n variant,\n size = 'large',\n radius,\n loading = false,\n disabled = false,\n icon,\n iconPosition = 'start',\n leading,\n contentAlign = 'center',\n flexGrow = false,\n width = 'auto',\n fill = false,\n tooltip,\n tooltipPlacement = 'top',\n iconOnly = false,\n flush = false,\n children,\n className = '',\n href,\n target,\n rel,\n ...props\n },\n ref,\n ) => {\n const radiusClass = radius ? getRadiusClass(radius) : getDefaultRadiusClass(size, iconOnly);\n const isInactive = disabled || loading;\n const alignStart = contentAlign === 'start';\n const baseClasses = [\n alignStart\n ? 'flex items-center justify-start text-left'\n : 'flex items-center justify-center',\n iconOnly ? `p-0 ${getIconOnlySizeClasses(size)}` : getSizeClasses(size),\n getVariantClasses(variant, radiusClass),\n flexGrow ? 'w-full' : '',\n width === 'wide' ? 'min-w-[120px]' : '',\n fill ? 'max-w-full' : '',\n isInactive ? 'cursor-not-allowed' : '',\n tooltip && isInactive ? 'pointer-events-none' : '',\n flush && !iconOnly\n ? variant === 'no-style'\n ? FLUSH_MARGIN_BORDERLESS\n : FLUSH_MARGIN\n : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const label = alignStart ? (\n <span className=\"flex min-w-0 flex-1 items-center\">{children}</span>\n ) : (\n children\n );\n\n const leadingSlot = leading ? (\n <span\n className={\n children\n ? 'btn-leading inline-flex shrink-0 items-center me-2'\n : 'btn-leading inline-flex shrink-0 items-center'\n }\n >\n {leading}\n </span>\n ) : null;\n\n const renderContent = () => {\n if (loading) {\n return (\n <>\n {leadingSlot}\n <LoadingSpinner />\n {label}\n </>\n );\n }\n\n const iconWrapperClasses = `inline-flex items-center shrink-0 ${getIconSizeClass(size)}`;\n\n if (icon && iconPosition === 'start') {\n return (\n <>\n {leadingSlot}\n <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'start')}` : ''}`}\n >\n {icon}\n </span>\n {label}\n </>\n );\n }\n\n if (icon && iconPosition === 'end') {\n return (\n <>\n {leadingSlot}\n {label}\n <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'end')}` : ''}`}\n >\n {icon}\n </span>\n </>\n );\n }\n\n return (\n <>\n {leadingSlot}\n {label}\n </>\n );\n };\n\n // An anchor cannot be disabled, so an inactive link still renders as a <button>.\n const button =\n href && !isInactive ? (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n className={baseClasses}\n {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {renderContent()}\n </a>\n ) : (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n disabled={isInactive}\n className={baseClasses}\n {...props}\n >\n {renderContent()}\n </button>\n );\n\n if (tooltip) {\n const trigger = isInactive ? (\n <span className={`inline-flex cursor-not-allowed${flexGrow ? ' w-full' : ''}`}>\n {button}\n </span>\n ) : (\n button\n );\n return (\n <Tooltip text={tooltip} placement={tooltipPlacement} fill={flexGrow}>\n {trigger}\n </Tooltip>\n );\n }\n\n return button;\n },\n);\n\nButton.displayName = 'Button';\n\nexport default Button;\n"],"names":["getSizeClasses","size","FLUSH_MARGIN","FLUSH_MARGIN_BORDERLESS","getDefaultRadiusClass","iconOnly","getIconGapClass","position","start","getRadiusClass","radius","FOCUS_OUTLINE","getVariantClasses","variant","radiusClass","baseClasses","LoadingSpinner","jsxs","jsx","Button","forwardRef","loading","disabled","icon","iconPosition","leading","contentAlign","flexGrow","width","fill","tooltip","tooltipPlacement","flush","children","className","href","target","rel","props","ref","isInactive","alignStart","getIconOnlySizeClasses","label","leadingSlot","renderContent","Fragment","iconWrapperClasses","getIconSizeClass","button","resolveRel","Tooltip"],"mappings":"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"}
1
+ {"version":3,"file":"Button.js","sources":["../../src/components/Button.tsx"],"sourcesContent":["import React, { forwardRef } from 'react';\nimport ButtonSpinner from './ButtonSpinner';\nimport Tooltip from './Tooltip';\nimport { getIconOnlySizeClasses, getIconSizeClass } from './buttonSizing';\nimport {\n getDefaultRadiusClass,\n getIconGapClass,\n getRadiusClass,\n getSizeClasses,\n getVariantClasses,\n} from './buttonStyles';\nimport { resolveRel } from './linkRel';\n\n// Button variant types matching the old component\nexport type ButtonVariant =\n | 'filled'\n | 'outline'\n /**\n * `outline`'s geometry on a ground of its own. For a button sitting on a tinted or\n * patterned surface, where `outline`'s transparent background lets that surface through\n * and the control stops reading as a button.\n */\n | 'surface'\n /**\n * The violet the assistant is named in elsewhere — for a control that hands work to it, or\n * acts on what it produced. Provenance, not a severity: it says who is being asked, not\n * that anything is wrong.\n */\n | 'assistant'\n | 'destructive'\n | 'success'\n | 'warning'\n | 'error'\n | 'ghost'\n /** Text only, in the accent colour, for a secondary action beside a primary one. */\n | 'quiet'\n | 'no-style';\n\nexport type ButtonSize = 'xsmall' | 'small' | 'medium' | 'large';\n\nexport type ButtonRadius = 'md' | 'lg' | 'xl';\n\ninterface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n variant: ButtonVariant;\n size?: ButtonSize;\n radius?: ButtonRadius;\n loading?: boolean;\n disabled?: boolean;\n icon?: React.ReactNode;\n iconPosition?: 'start' | 'end';\n /** Before the label at its own size, never squeezed (an avatar); pairs with an end `icon`. */\n leading?: React.ReactNode;\n /** `start` aligns the label to the leading edge and pins an end `icon` to the far edge. */\n contentAlign?: 'center' | 'start';\n /** Fills the container's width. */\n flexGrow?: boolean;\n /** `wide` holds a 120px floor for a primary action. */\n width?: 'auto' | 'wide';\n fill?: boolean;\n tooltip?: string;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n iconOnly?: boolean;\n /** Pulls a borderless button out by its own padding, so its label lines up with the text around it. */\n flush?: boolean;\n children?: React.ReactNode;\n onClick?: React.MouseEventHandler<HTMLButtonElement & HTMLAnchorElement>;\n /** Renders the button as a real anchor, so a link action is never an <a> nested in a <button>. */\n href?: string;\n target?: React.HTMLAttributeAnchorTarget;\n rel?: string;\n}\n\n// The 1px is the transparent border, which `no-style` drops.\nconst FLUSH_MARGIN = '-mx-[calc(var(--btn-px)+1px)]';\nconst FLUSH_MARGIN_BORDERLESS = '-mx-[var(--btn-px)]';\n\nexport const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(\n (\n {\n variant,\n size = 'large',\n radius,\n loading = false,\n disabled = false,\n icon,\n iconPosition = 'start',\n leading,\n contentAlign = 'center',\n flexGrow = false,\n width = 'auto',\n fill = false,\n tooltip,\n tooltipPlacement = 'top',\n iconOnly = false,\n flush = false,\n children,\n className = '',\n href,\n target,\n rel,\n ...props\n },\n ref,\n ) => {\n const radiusClass = radius ? getRadiusClass(radius) : getDefaultRadiusClass(size, iconOnly);\n const isInactive = disabled || loading;\n const alignStart = contentAlign === 'start';\n const baseClasses = [\n alignStart\n ? 'flex items-center justify-start text-left'\n : 'flex items-center justify-center',\n iconOnly ? `p-0 ${getIconOnlySizeClasses(size)}` : getSizeClasses(size),\n getVariantClasses(variant, radiusClass),\n flexGrow ? 'w-full' : '',\n width === 'wide' ? 'min-w-[120px]' : '',\n fill ? 'max-w-full' : '',\n isInactive ? 'cursor-not-allowed' : '',\n tooltip && isInactive ? 'pointer-events-none' : '',\n flush && !iconOnly\n ? variant === 'no-style'\n ? FLUSH_MARGIN_BORDERLESS\n : FLUSH_MARGIN\n : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const label = alignStart ? (\n <span className=\"flex min-w-0 flex-1 items-center\">{children}</span>\n ) : (\n children\n );\n\n const leadingSlot = leading ? (\n <span\n className={\n children\n ? 'btn-leading inline-flex shrink-0 items-center me-2'\n : 'btn-leading inline-flex shrink-0 items-center'\n }\n >\n {leading}\n </span>\n ) : null;\n\n const renderContent = () => {\n if (loading) {\n return (\n <>\n {leadingSlot}\n <ButtonSpinner />\n {label}\n </>\n );\n }\n\n const iconWrapperClasses = `inline-flex items-center shrink-0 ${getIconSizeClass(size)}`;\n\n if (icon && iconPosition === 'start') {\n return (\n <>\n {leadingSlot}\n <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'start')}` : ''}`}\n >\n {icon}\n </span>\n {label}\n </>\n );\n }\n\n if (icon && iconPosition === 'end') {\n return (\n <>\n {leadingSlot}\n {label}\n <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'end')}` : ''}`}\n >\n {icon}\n </span>\n </>\n );\n }\n\n return (\n <>\n {leadingSlot}\n {label}\n </>\n );\n };\n\n // An anchor cannot be disabled, so an inactive link still renders as a <button>.\n const button =\n href && !isInactive ? (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n target={target}\n rel={resolveRel(rel, target)}\n className={baseClasses}\n {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {renderContent()}\n </a>\n ) : (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n disabled={isInactive}\n className={baseClasses}\n {...props}\n >\n {renderContent()}\n </button>\n );\n\n if (tooltip) {\n const trigger = isInactive ? (\n <span className={`inline-flex cursor-not-allowed${flexGrow ? ' w-full' : ''}`}>\n {button}\n </span>\n ) : (\n button\n );\n return (\n <Tooltip text={tooltip} placement={tooltipPlacement} fill={flexGrow}>\n {trigger}\n </Tooltip>\n );\n }\n\n return button;\n },\n);\n\nButton.displayName = 'Button';\n\nexport default Button;\n"],"names":["FLUSH_MARGIN","FLUSH_MARGIN_BORDERLESS","Button","forwardRef","variant","size","radius","loading","disabled","icon","iconPosition","leading","contentAlign","flexGrow","width","fill","tooltip","tooltipPlacement","iconOnly","flush","children","className","href","target","rel","props","ref","radiusClass","getRadiusClass","getDefaultRadiusClass","isInactive","alignStart","baseClasses","getIconOnlySizeClasses","getSizeClasses","getVariantClasses","label","jsx","leadingSlot","renderContent","jsxs","Fragment","ButtonSpinner","iconWrapperClasses","getIconSizeClass","getIconGapClass","button","resolveRel","Tooltip"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { jsxs as e, jsx as o } from "react/jsx-runtime";
2
+ const r = () => /* @__PURE__ */ e(
3
+ "svg",
4
+ {
5
+ className: "animate-spin motion-reduce:animate-none -ml-1 mr-2 h-4 w-4",
6
+ xmlns: "http://www.w3.org/2000/svg",
7
+ fill: "none",
8
+ viewBox: "0 0 24 24",
9
+ children: [
10
+ /* @__PURE__ */ o(
11
+ "circle",
12
+ {
13
+ className: "opacity-25 motion-reduce:opacity-100",
14
+ cx: "12",
15
+ cy: "12",
16
+ r: "10",
17
+ stroke: "currentColor",
18
+ strokeWidth: "4"
19
+ }
20
+ ),
21
+ /* @__PURE__ */ o(
22
+ "path",
23
+ {
24
+ className: "opacity-75 motion-reduce:opacity-0",
25
+ fill: "currentColor",
26
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
27
+ }
28
+ )
29
+ ]
30
+ }
31
+ );
32
+ export {
33
+ r as ButtonSpinner,
34
+ r as default
35
+ };
36
+ //# sourceMappingURL=ButtonSpinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ButtonSpinner.js","sources":["../../src/components/ButtonSpinner.tsx"],"sourcesContent":["export const ButtonSpinner = () => (\n <svg\n className=\"animate-spin motion-reduce:animate-none -ml-1 mr-2 h-4 w-4\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n className=\"opacity-25 motion-reduce:opacity-100\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75 motion-reduce:opacity-0\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n);\n\nexport default ButtonSpinner;\n"],"names":["ButtonSpinner","jsxs","jsx"],"mappings":";AAAO,MAAMA,IAAgB,MACzB,gBAAAC;AAAA,EAAC;AAAA,EAAA;AAAA,IACG,WAAU;AAAA,IACV,OAAM;AAAA,IACN,MAAK;AAAA,IACL,SAAQ;AAAA,IAER,UAAA;AAAA,MAAA,gBAAAC;AAAA,QAAC;AAAA,QAAA;AAAA,UACG,WAAU;AAAA,UACV,IAAG;AAAA,UACH,IAAG;AAAA,UACH,GAAE;AAAA,UACF,QAAO;AAAA,UACP,aAAY;AAAA,QAAA;AAAA,MAAA;AAAA,MAEhB,gBAAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACG,WAAU;AAAA,UACV,MAAK;AAAA,UACL,GAAE;AAAA,QAAA;AAAA,MAAA;AAAA,IACN;AAAA,EAAA;AACJ;"}