@stackone/malachite 2.64.0 → 2.66.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 +4 -4
- package/dist/components/Avatar.js +10 -10
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Button.js +103 -89
- package/dist/components/Button.js.map +1 -1
- package/dist/components/NavItem.js +140 -101
- package/dist/components/NavItem.js.map +1 -1
- package/dist/components/Popover.js +157 -149
- package/dist/components/Popover.js.map +1 -1
- package/dist/components/Typography/PathText.js +158 -0
- package/dist/components/Typography/PathText.js.map +1 -0
- package/dist/components/Typography/index.js +15 -13
- package/dist/components/Typography/index.js.map +1 -1
- package/dist/components/Typography/pathTextFit.js +42 -0
- package/dist/components/Typography/pathTextFit.js.map +1 -0
- package/dist/components/Typography/pathTextMeasure.js +67 -0
- package/dist/components/Typography/pathTextMeasure.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/tailwind.css.js +1 -1
- package/package.json +1 -1
package/COMPONENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
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`), `initialFocusRef` (what takes focus on open, in place of the first focusable), `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`), `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), `Popover.Section` (a bordered section) and `Popover.Empty` (an empty-state line).
|
|
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`), `initialFocusRef` (what takes focus on open, in place of the first focusable), `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`), `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), `Popover.Section` (a bordered section) and `Popover.Empty` (an empty-state line).
|
|
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`, `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. 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.
|
|
@@ -61,7 +61,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
61
61
|
- **`Tabs`** — Tabbed navigation switching between content panels. `trailing` puts content on the same row, settled against its end — a count, a scope picker, the state the tabs are showing — inside the rule this component already draws. Without it a consumer had to redraw that rule to carry it past the tabs, and the tab list, which cannot wrap, would not yield any width to what sat beside it. The tabs hold their width and the trailing content gives ground; the `tablist` role stays on the tabs alone, so the extra content is not announced as one. _Key props:_ `TabItem[]`, `activeKey`, `onChange`, `trailing`, `className`, `style`.
|
|
62
62
|
- **`Breadcrumbs`** — Breadcrumb trail with caret separators, an optional back button (shown only when there is more than one crumb AND the parent — the second-to-last — carries a `url` to target) and a right-aligned `actions` row. A lone crumb marked `isRoot` switches to title mode: plain emphasised text, no trail chrome, but the `actions` row still renders. Renders as a `nav`/`ol` with `aria-current="page"` on the last crumb. Pass `renderLink` to route crumbs through the app's own router instead of a plain `<a>`. Used by `PageLayout`, but standalone too. _Key props:_ `links[]` (`name`, `url`, `isRoot`), `actions[]`, `renderLink`, `className`, `style`.
|
|
63
63
|
- **`ActionMenu`** — Dropdown menu of actions (kebab/overflow style). The trigger is an icon-only `Button` by default, so `size` draws exactly the square a `Button` of that size draws beside it: `xsmall` is Figma's 20px "Button Element - Square (X-Small)", `small` the 28px rung above it. Both squares are fixed rather than read off the `controlHeights` tokens, so re-theming those does not resize the trigger. A supplied `triggerIcon` is held to the tier's glyph size — 11px at `xsmall`, 16px at `small`. At 20px the `xsmall` trigger meets WCAG 2.5.8 through the *Spacing* exception rather than the 24px minimum, so keep at least 4px between adjacent triggers. `showLabel` draws `label` beside the icon instead of leaving it to the accessible name — for a menu that is the point of its corner rather than an overflow for a row that already says what it acts on — and `variant` puts the trigger on any `Button` variant, so a menu handing work to the assistant can wear the assistant ground rather than the overflow-kebab outline. An item's `icon` leads its label: every caller's names the action itself — copy, open, delete — and an icon that is part of an item's identity belongs where the eye starts rather than right-aligned, where it reads as a status column. The menu is Figma's `Dropdown Component - Options List`: a fixed sized to its widest item between a 180px floor and the 280px Figma draws, by default under the trigger's right edge, 14px below it, flipping above or clamping sideways when the viewport has no room, rounding at the library's 12px and padding wider (8px) than the menu a `Dropdown` opens. Items read in the primary text colour, or the secondary one with `tone="muted"` (e.g. a sidebar's menus); destructive ones in red; `danger: 'hover'` keeps an item in its normal colour and turns it destructive (red text on a light red ground) only on hover or keyboard focus, for an action like signing out. A ceiling rather than a flat width: taken literally it gave a menu of four short verbs 130px of air and made every kebab in an app the same width whatever it held. `minWidth` only raises it above that ceiling, so it is the one way to go past 280px, and a label too long for the box truncates rather than wrapping. _Key props:_ `ActionMenuItem[]`, `size`, `label`, `triggerIcon`, `minWidth`, `showLabel`, `variant`, `tone` (`default` | `muted`), `trigger` (your own control in place of the kebab, cloned with the menu's ref and aria state), `placement` (`bottom-end` default | `bottom-start` | `right-start` | `top-start` | `top-center`), `offset` (`small` 8px | `medium` 12px | `large` 14px; applies to every placement; without it the default `bottom-end` keeps the options menu's 14px and the others use 8px). An item's `tag` (e.g. an "Early access" `Tag`) follows its label in a lazily loaded shadow host of its own, since the menu renders into `document.body`, and joins its accessible name; `current: true` marks the item for the page the user is on (`aria-current="page"` and the selected row paint; a `danger` item keeps its red text). An item with an `href` is a link: Cmd/Ctrl-click or a middle click opens it in a new tab and closes the menu, while a plain click runs `onClick` in its place when one is given (so a router can navigate in-app); Enter and Space both activate it. `target: '_blank'` opens it in a new tab, adds `noopener noreferrer` and tells screen readers it opens a new tab. An item with `type: 'text'` is context rather than an action (an account's email above its actions): secondary text the arrow keys skip. Opened from the keyboard, focus lands on the first item; Up/Down/Home/End move through enabled items, Tab closes the menu and moves on as usual; Escape or a picked item returns focus to the trigger.
|
|
64
|
-
- **`NavItem`** — A row in an app's side navigation. Idle rows read secondary and take the list hover; the `active` row wears the accent tint and sets `aria-current="page"` on a link. `collapsed` shrinks it to a 36px icon tile whose label becomes the accessible name and a right-hand tooltip, with any `badge` pinned to the tile's corner. `variant="subitem"` indents a child row behind a dot instead of an icon; `disclosure` makes a group header button with `aria-expanded` and a rotating chevron, and `hasActiveChild` colours its text while a child is current. Renders an `<a>` with `href`, the element passed with `asChild` (a router `Link`), or a `<button>`, and forwards its ref and extra attributes so it can be a `Popover` or `ActionMenu` trigger. _Key props:_ `label`, `icon`, `active`, `hasActiveChild`, `collapsed`, `variant` (`item` | `subitem`), `danger`, `trailing`, `badge`, `disclosure` (`expanded` | `collapsed`), `tooltip`, `accessibleLabel` (overrides the accessible name; collapsed it defaults to the tooltip text plus `badgeLabel`), `href`, `target`, `rel`, `onClick`, `asChild` + `children`, `className`, `size` (`medium` | `small`, the tighter row for lists inside a popover), `tag` (beside the label, e.g. early access), `badgeLabel` (read in place of the badge and added to the collapsed name)
|
|
64
|
+
- **`NavItem`** — A row in an app's side navigation. Idle rows read secondary and take the list hover; the `active` row wears the accent tint and sets `aria-current="page"` on a link. `collapsed` shrinks it to a 36px icon tile whose label becomes the accessible name and a right-hand tooltip, with any `badge` pinned to the tile's corner. `variant="subitem"` indents a child row behind a dot instead of an icon; `disclosure` makes a group header button with `aria-expanded` and a rotating chevron, and `hasActiveChild` colours its text while a child is current. Renders an `<a>` with `href`, the element passed with `asChild` (a router `Link`), or a `<button>`, and forwards its ref and extra attributes so it can be a `Popover` or `ActionMenu` trigger. `disabled` keeps the row a tab stop with `aria-disabled`, drops its hover and pointer, and ignores clicks, Enter and Space; a `Popover` or `ActionMenu` it triggers stays shut on press, hover, focus and arrow keys, while other keys still reach a passed `onKeyDown` so arrow navigation through a list keeps working; a disabled `href` row renders as a button so it cannot be followed. `disabledReason` explains it in a right-hand tooltip on hover and focus and is the row's accessible description from the first focus. Not with `asChild`, whose child would still navigate. _Key props:_ `label`, `icon`, `active`, `hasActiveChild`, `collapsed`, `variant` (`item` | `subitem`), `danger`, `trailing`, `badge`, `disclosure` (`expanded` | `collapsed`), `tooltip`, `accessibleLabel` (overrides the accessible name; collapsed it defaults to the tooltip text plus `badgeLabel`), `href`, `target`, `rel`, `onClick`, `asChild` + `children`, `className`, `size` (`medium` | `small`, the tighter row for lists inside a popover), `tag` (beside the label, e.g. early access), `badgeLabel` (read in place of the badge and added to the collapsed name), `disabled`, `disabledReason`. A passed `aria-expanded` (e.g. from a `Popover`) wins over `disclosure`, and a button row forwards a passed `aria-current`.
|
|
65
65
|
- **`Sidebar`** — An app's left navigation shell, pinned to the viewport's left edge: 260px wide, 60px `collapsed`, on the sunken page background. Composed from `Sidebar.Header` (a 56px band for `Sidebar.Switcher`, the logo + workspace + parent trigger), `Sidebar.Body` (the part that scrolls), `Sidebar.Notices`, `Sidebar.Search`, `Sidebar.Section` (rows of `NavItem`s at the gutter), `Sidebar.Group` + `Sidebar.GroupItems` (a disclosure header and its sub-items), `Sidebar.Divider`, `Sidebar.Usage` (a `UsageCard` placed at the sidebar's gutter) and `Sidebar.Footer` holding `Sidebar.Account`. Popovers opened from it (with `padding="none"`) wrap their body in `Sidebar.Panel`, sized so the popover comes to `SIDEBAR_POPOVER_WIDTH_PX`, and build it from the `Popover` parts. `onToggleCollapse` adds the edge toggle, shown in CSS while the pointer is over the sidebar or a keyboard focus is inside it, and always on a touch screen, which has no hover to reveal it. A workspace, organization or account name cut off in the expanded sidebar shows in full on hover. Notices, the divider and the usage card hide while collapsed. Render it inside `<MalachiteContext layer="sidebar">`, or the page paints over the edge toggle. _Key props:_ `collapsed`, `onToggleCollapse`, `expandLabel`, `collapseLabel`, `aria-label`; `Sidebar.Switcher`: `logo`, `title`, `subtitle`, `titleTooltip`/`subtitleTooltip` (shown when the name is cut off, the name by default; pass a node to wrap it, e.g. to keep it out of session recordings) + button attributes; `Sidebar.Account`: `avatar` or `initials` (a small chip when there is no photo), `name`, `tooltip` (on the collapsed rail, and expanded when the name is cut off; `name` by default; pass a node to wrap it) + button attributes; `Sidebar.Usage`: the `UsageCard` props; `Sidebar.Notices`: `aria-label` (required). `useSidebarCollapsed()` reads the state from inside; `SIDEBAR_WIDTH_PX` gives the page its offset beside the sidebar; `SIDEBAR_TOOLTIP_DELAY_S` is the 0.2s delay its name and rail tooltips use, for tooltips a consumer adds inside it.
|
|
66
66
|
- **`SearchTrigger`** — A button drawn as a filled search field, for opening a command palette or search dialog from app chrome such as a sidebar. It is not an input — typing happens in what it opens. `shortcut` draws flat `<kbd>` chips at the end (not `Keycap`, whose raised shadow reads as a second button inside a filled field), kept out of the accessible name; `iconOnly` makes a 36px icon tile matching a collapsed `NavItem`, named by `label`. _Key props:_ `label` (default `Search`), `shortcut` (`ReactNode[]`), `iconOnly`, `aria-label`, `className`, plus native button attributes (`onClick`, `aria-haspopup`, `aria-expanded`, `aria-keyshortcuts`).
|
|
67
67
|
- **`FooterNav`** — Footer navigation bar of links. _Key props:_ `FooterNavLink[]`.
|
|
@@ -97,7 +97,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
97
97
|
|
|
98
98
|
## Buttons & actions
|
|
99
99
|
|
|
100
|
-
- **`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`, `no-style`. Supports `iconOnly`, `loading`, `tooltip`. _Key props:_ `variant`, `size`, `icon`, `iconOnly`, `loading`, `disabled`, `fill`, `tooltip`.
|
|
100
|
+
- **`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`, `no-style`. Supports `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), `iconOnly`, `loading`, `disabled`, `fill`, `tooltip`.
|
|
101
101
|
- **`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`.
|
|
102
102
|
- **`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`.
|
|
103
103
|
- **`Link`** — Styled anchor/link text. _Key props:_ `LinkProps`.
|
|
@@ -107,7 +107,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
107
107
|
## Typography
|
|
108
108
|
|
|
109
109
|
- **`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.
|
|
110
|
-
- **`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` 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.EllipsisText` takes a `size` 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.
|
|
110
|
+
- **`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` 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` 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.
|
|
111
111
|
|
|
112
112
|
## Data display
|
|
113
113
|
|
|
@@ -12,19 +12,19 @@ const p = (e) => {
|
|
|
12
12
|
size: t = 24,
|
|
13
13
|
shape: l = "square",
|
|
14
14
|
fallback: h = "initial",
|
|
15
|
-
className:
|
|
15
|
+
className: i = ""
|
|
16
16
|
}) => {
|
|
17
|
-
const [c, f] = u(void 0),
|
|
17
|
+
const [c, f] = u(void 0), o = (r == null ? void 0 : r.trim()) ?? "", a = l === "circle" ? "50%" : `${Math.round(t * 0.25)}px`;
|
|
18
18
|
if (e && c !== e)
|
|
19
19
|
return /* @__PURE__ */ n(
|
|
20
20
|
"img",
|
|
21
21
|
{
|
|
22
22
|
src: e,
|
|
23
|
-
alt:
|
|
23
|
+
alt: o,
|
|
24
24
|
loading: "lazy",
|
|
25
25
|
decoding: "async",
|
|
26
26
|
onError: () => f(e),
|
|
27
|
-
className:
|
|
27
|
+
className: i,
|
|
28
28
|
style: {
|
|
29
29
|
width: `${t}px`,
|
|
30
30
|
height: `${t}px`,
|
|
@@ -34,12 +34,12 @@ const p = (e) => {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
);
|
|
37
|
-
const d =
|
|
37
|
+
const d = o ? { role: "img", "aria-label": o } : { "aria-hidden": !0 };
|
|
38
38
|
return h === "placeholder" ? /* @__PURE__ */ n(
|
|
39
39
|
g,
|
|
40
40
|
{
|
|
41
41
|
...d,
|
|
42
|
-
className:
|
|
42
|
+
className: i,
|
|
43
43
|
width: t,
|
|
44
44
|
height: t,
|
|
45
45
|
style: {
|
|
@@ -48,10 +48,10 @@ const p = (e) => {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
) : /* @__PURE__ */ n(
|
|
51
|
-
"
|
|
51
|
+
"span",
|
|
52
52
|
{
|
|
53
53
|
...d,
|
|
54
|
-
className:
|
|
54
|
+
className: i,
|
|
55
55
|
style: {
|
|
56
56
|
width: `${t}px`,
|
|
57
57
|
height: `${t}px`,
|
|
@@ -60,14 +60,14 @@ const p = (e) => {
|
|
|
60
60
|
display: "flex",
|
|
61
61
|
alignItems: "center",
|
|
62
62
|
justifyContent: "center",
|
|
63
|
-
background: p(
|
|
63
|
+
background: p(o),
|
|
64
64
|
color: "#ffffff",
|
|
65
65
|
fontWeight: 600,
|
|
66
66
|
fontSize: `${Math.round(t * 0.45)}px`,
|
|
67
67
|
lineHeight: 1,
|
|
68
68
|
textTransform: "uppercase"
|
|
69
69
|
},
|
|
70
|
-
children:
|
|
70
|
+
children: o.charAt(0) || "?"
|
|
71
71
|
}
|
|
72
72
|
);
|
|
73
73
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","sources":["../../src/components/Avatar.tsx"],"sourcesContent":["import React, { useState } from 'react';\n// The no-logo mark stackone-logos.com serves; the same in every theme, so its colours are fixed.\nimport EmptyLogoSvg from '../assets/empty_logo.svg?react';\n\nexport type AvatarShape = 'square' | 'circle';\n\n/** `initial` suits a named entity (a person, a model); `placeholder` says \"no logo available\". */\nexport type AvatarFallback = 'initial' | 'placeholder';\n\nexport interface AvatarProps {\n /** Image URL (logo/photo). Falls back to `fallback` when missing or it fails to load. */\n src?: string;\n /** Used for the initial fallback and as the image alt / accessible label. */\n name?: string;\n /** Pixel size of the (square) avatar. */\n size?: number;\n shape?: AvatarShape;\n /** What shows when `src` is missing or fails to load. Defaults to `initial`. */\n fallback?: AvatarFallback;\n className?: string;\n}\n\n// Deterministic, evenly-distributed hue from the label so the same entity always gets\n// the same colour. Saturation/lightness are fixed for a consistent, on-brand feel.\n// NOTE: avatar identity colours (this hue and the white text below) are intentionally\n// NOT theme tokens — they are a generated identity palette, an explicit exception to\n// the \"colours come from theme tokens\" rule (there is no avatar-colour token group).\nconst backgroundFor = (seed: string): string => {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = seed.charCodeAt(index) + ((hash << 5) - hash);\n }\n return `hsl(${Math.abs(hash) % 360}, 55%, 52%)`;\n};\n\n/**\n * A logo/initial avatar. Renders `src` when available, otherwise a coloured tile\n * with the first letter of `name`. Encapsulates the fallback colour so consumers\n * never hand-roll it.\n */\nconst Avatar: React.FC<AvatarProps> = ({\n src,\n name,\n size = 24,\n shape = 'square',\n fallback = 'initial',\n className = '',\n}) => {\n // Track which src failed (not a boolean) so switching `src` to a new URL retries the\n // image instead of staying stuck on the fallback.\n const [failedSrc, setFailedSrc] = useState<string | undefined>(undefined);\n const label = name?.trim() ?? '';\n const borderRadius = shape === 'circle' ? '50%' : `${Math.round(size * 0.25)}px`;\n\n if (src && failedSrc !== src) {\n return (\n <img\n src={src}\n alt={label}\n loading=\"lazy\"\n decoding=\"async\"\n onError={() => setFailedSrc(src)}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n borderRadius,\n objectFit: 'contain',\n flexShrink: 0,\n }}\n />\n );\n }\n\n // With a label the tile is an image with an accessible name; without one it carries\n // no information, so mark it decorative (aria-hidden, no role) to avoid an unnamed\n // image in the accessibility tree.\n const tileSemantics = label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true };\n\n if (fallback === 'placeholder') {\n return (\n <EmptyLogoSvg\n {...tileSemantics}\n className={className}\n width={size}\n height={size}\n style={{\n flexShrink: 0,\n borderRadius: shape === 'circle' ? '50%' : undefined,\n }}\n />\n );\n }\n\n return (\n <
|
|
1
|
+
{"version":3,"file":"Avatar.js","sources":["../../src/components/Avatar.tsx"],"sourcesContent":["import React, { useState } from 'react';\n// The no-logo mark stackone-logos.com serves; the same in every theme, so its colours are fixed.\nimport EmptyLogoSvg from '../assets/empty_logo.svg?react';\n\nexport type AvatarShape = 'square' | 'circle';\n\n/** `initial` suits a named entity (a person, a model); `placeholder` says \"no logo available\". */\nexport type AvatarFallback = 'initial' | 'placeholder';\n\nexport interface AvatarProps {\n /** Image URL (logo/photo). Falls back to `fallback` when missing or it fails to load. */\n src?: string;\n /** Used for the initial fallback and as the image alt / accessible label. */\n name?: string;\n /** Pixel size of the (square) avatar. */\n size?: number;\n shape?: AvatarShape;\n /** What shows when `src` is missing or fails to load. Defaults to `initial`. */\n fallback?: AvatarFallback;\n className?: string;\n}\n\n// Deterministic, evenly-distributed hue from the label so the same entity always gets\n// the same colour. Saturation/lightness are fixed for a consistent, on-brand feel.\n// NOTE: avatar identity colours (this hue and the white text below) are intentionally\n// NOT theme tokens — they are a generated identity palette, an explicit exception to\n// the \"colours come from theme tokens\" rule (there is no avatar-colour token group).\nconst backgroundFor = (seed: string): string => {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = seed.charCodeAt(index) + ((hash << 5) - hash);\n }\n return `hsl(${Math.abs(hash) % 360}, 55%, 52%)`;\n};\n\n/**\n * A logo/initial avatar. Renders `src` when available, otherwise a coloured tile\n * with the first letter of `name`. Encapsulates the fallback colour so consumers\n * never hand-roll it.\n */\nconst Avatar: React.FC<AvatarProps> = ({\n src,\n name,\n size = 24,\n shape = 'square',\n fallback = 'initial',\n className = '',\n}) => {\n // Track which src failed (not a boolean) so switching `src` to a new URL retries the\n // image instead of staying stuck on the fallback.\n const [failedSrc, setFailedSrc] = useState<string | undefined>(undefined);\n const label = name?.trim() ?? '';\n const borderRadius = shape === 'circle' ? '50%' : `${Math.round(size * 0.25)}px`;\n\n if (src && failedSrc !== src) {\n return (\n <img\n src={src}\n alt={label}\n loading=\"lazy\"\n decoding=\"async\"\n onError={() => setFailedSrc(src)}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n borderRadius,\n objectFit: 'contain',\n flexShrink: 0,\n }}\n />\n );\n }\n\n // With a label the tile is an image with an accessible name; without one it carries\n // no information, so mark it decorative (aria-hidden, no role) to avoid an unnamed\n // image in the accessibility tree.\n const tileSemantics = label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true };\n\n if (fallback === 'placeholder') {\n return (\n <EmptyLogoSvg\n {...tileSemantics}\n className={className}\n width={size}\n height={size}\n style={{\n flexShrink: 0,\n borderRadius: shape === 'circle' ? '50%' : undefined,\n }}\n />\n );\n }\n\n // span, not div: a <button> may only hold phrasing content (Button's `leading`).\n return (\n <span\n {...tileSemantics}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n flexShrink: 0,\n borderRadius,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: backgroundFor(label),\n color: '#ffffff',\n fontWeight: 600,\n fontSize: `${Math.round(size * 0.45)}px`,\n lineHeight: 1,\n textTransform: 'uppercase',\n }}\n >\n {label.charAt(0) || '?'}\n </span>\n );\n};\n\nexport default Avatar;\nexport { Avatar };\n"],"names":["backgroundFor","seed","hash","index","Avatar","src","name","size","shape","fallback","className","failedSrc","setFailedSrc","useState","label","borderRadius","jsx","tileSemantics","EmptyLogoSvg"],"mappings":";;;AA2BA,MAAMA,IAAgB,CAACC,MAAyB;AAC5C,MAAIC,IAAO;AACX,WAASC,IAAQ,GAAGA,IAAQF,EAAK,QAAQE,KAAS;AAC9C,IAAAD,IAAOD,EAAK,WAAWE,CAAK,MAAMD,KAAQ,KAAKA;AAEnD,SAAO,OAAO,KAAK,IAAIA,CAAI,IAAI,GAAG;AACtC,GAOME,IAAgC,CAAC;AAAA,EACnC,KAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC,IAAO;AAAA,EACP,OAAAC,IAAQ;AAAA,EACR,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AAGF,QAAM,CAACC,GAAWC,CAAY,IAAIC,EAA6B,MAAS,GAClEC,KAAQR,KAAA,gBAAAA,EAAM,WAAU,IACxBS,IAAeP,MAAU,WAAW,QAAQ,GAAG,KAAK,MAAMD,IAAO,IAAI,CAAC;AAE5E,MAAIF,KAAOM,MAAcN;AACrB,WACI,gBAAAW;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,KAAAX;AAAA,QACA,KAAKS;AAAA,QACL,SAAQ;AAAA,QACR,UAAS;AAAA,QACT,SAAS,MAAMF,EAAaP,CAAG;AAAA,QAC/B,WAAAK;AAAA,QACA,OAAO;AAAA,UACH,OAAO,GAAGH,CAAI;AAAA,UACd,QAAQ,GAAGA,CAAI;AAAA,UACf,cAAAQ;AAAA,UACA,WAAW;AAAA,UACX,YAAY;AAAA,QAAA;AAAA,MAChB;AAAA,IAAA;AAQZ,QAAME,IAAgBH,IAAQ,EAAE,MAAM,OAAO,cAAcA,EAAA,IAAU,EAAE,eAAe,GAAA;AAEtF,SAAIL,MAAa,gBAET,gBAAAO;AAAA,IAACE;AAAAA,IAAA;AAAA,MACI,GAAGD;AAAA,MACJ,WAAAP;AAAA,MACA,OAAOH;AAAA,MACP,QAAQA;AAAA,MACR,OAAO;AAAA,QACH,YAAY;AAAA,QACZ,cAAcC,MAAU,WAAW,QAAQ;AAAA,MAAA;AAAA,IAC/C;AAAA,EAAA,IAOR,gBAAAQ;AAAA,IAAC;AAAA,IAAA;AAAA,MACI,GAAGC;AAAA,MACJ,WAAAP;AAAA,MACA,OAAO;AAAA,QACH,OAAO,GAAGH,CAAI;AAAA,QACd,QAAQ,GAAGA,CAAI;AAAA,QACf,YAAY;AAAA,QACZ,cAAAQ;AAAA,QACA,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,YAAYf,EAAcc,CAAK;AAAA,QAC/B,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,UAAU,GAAG,KAAK,MAAMP,IAAO,IAAI,CAAC;AAAA,QACpC,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAGlB,UAAAO,EAAM,OAAO,CAAC,KAAK;AAAA,IAAA;AAAA,EAAA;AAGhC;"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { forwardRef as
|
|
3
|
-
import
|
|
4
|
-
import { getIconOnlySizeClasses as
|
|
5
|
-
import { resolveRel as
|
|
6
|
-
const
|
|
7
|
-
switch (
|
|
1
|
+
import { jsx as r, jsxs as i, Fragment as d } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as O } from "react";
|
|
3
|
+
import T from "./Tooltip.js";
|
|
4
|
+
import { getIconOnlySizeClasses as U, getIconSizeClass as V } from "./buttonSizing.js";
|
|
5
|
+
import { resolveRel as W } from "./linkRel.js";
|
|
6
|
+
const A = (s) => {
|
|
7
|
+
switch (s) {
|
|
8
8
|
// Figma has no text X-Small button, only the icon-only square — nothing to align to.
|
|
9
9
|
case "xsmall":
|
|
10
10
|
return "px-2 py-1 text-xs";
|
|
@@ -17,8 +17,8 @@ const O = (a) => {
|
|
|
17
17
|
default:
|
|
18
18
|
return "min-h-[var(--malachite-control-height-large,42px)] px-[12px] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap";
|
|
19
19
|
}
|
|
20
|
-
},
|
|
21
|
-
switch (
|
|
20
|
+
}, D = (s, t) => {
|
|
21
|
+
switch (s) {
|
|
22
22
|
// Only the square rounds at 8px — text buttons at this size keep their 6px corner.
|
|
23
23
|
case "xsmall":
|
|
24
24
|
return t ? "rounded-lg" : "rounded-md";
|
|
@@ -31,17 +31,17 @@ const O = (a) => {
|
|
|
31
31
|
default:
|
|
32
32
|
return "rounded-[var(--malachite-radius-xxl,14px)]";
|
|
33
33
|
}
|
|
34
|
-
},
|
|
34
|
+
}, k = (s, t) => {
|
|
35
35
|
const e = t === "start";
|
|
36
|
-
switch (
|
|
36
|
+
switch (s) {
|
|
37
37
|
case "small":
|
|
38
38
|
case "medium":
|
|
39
39
|
return e ? "mr-[6px]" : "ml-[6px]";
|
|
40
40
|
default:
|
|
41
41
|
return e ? "mr-2" : "ml-2";
|
|
42
42
|
}
|
|
43
|
-
},
|
|
44
|
-
switch (
|
|
43
|
+
}, E = (s) => {
|
|
44
|
+
switch (s) {
|
|
45
45
|
case "md":
|
|
46
46
|
return "rounded-md";
|
|
47
47
|
case "lg":
|
|
@@ -51,40 +51,40 @@ const O = (a) => {
|
|
|
51
51
|
default:
|
|
52
52
|
return "rounded-xl";
|
|
53
53
|
}
|
|
54
|
-
},
|
|
54
|
+
}, a = {
|
|
55
55
|
neutral: "focus-visible:[outline:2px_solid_var(--malachite-text-primary)]",
|
|
56
56
|
destructive: "focus-visible:[outline:2px_solid_var(--malachite-text-destructive)]",
|
|
57
57
|
success: "focus-visible:[outline:2px_solid_var(--malachite-text-success)]",
|
|
58
58
|
warning: "focus-visible:[outline:2px_solid_var(--malachite-text-warning)]",
|
|
59
59
|
error: "focus-visible:[outline:2px_solid_var(--malachite-text-error)]",
|
|
60
60
|
assistant: "focus-visible:[outline:2px_solid_var(--malachite-assistant-foreground)]"
|
|
61
|
-
},
|
|
61
|
+
}, H = (s, t) => {
|
|
62
62
|
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 ${t} cursor-pointer`;
|
|
63
|
-
switch (
|
|
63
|
+
switch (s) {
|
|
64
64
|
case "filled":
|
|
65
|
-
return `${e} ${
|
|
65
|
+
return `${e} ${a.neutral} btn-filled shadow-default`;
|
|
66
66
|
case "outline":
|
|
67
|
-
return `${e} ${
|
|
67
|
+
return `${e} ${a.neutral} btn-outline shadow-default`;
|
|
68
68
|
case "surface":
|
|
69
|
-
return `${e} ${
|
|
69
|
+
return `${e} ${a.neutral} btn-surface shadow-default`;
|
|
70
70
|
case "assistant":
|
|
71
|
-
return `${e} ${
|
|
71
|
+
return `${e} ${a.assistant} btn-assistant shadow-default`;
|
|
72
72
|
case "destructive":
|
|
73
|
-
return `${e} ${
|
|
73
|
+
return `${e} ${a.destructive} btn-destructive`;
|
|
74
74
|
case "success":
|
|
75
|
-
return `${e} ${
|
|
75
|
+
return `${e} ${a.success} btn-glass-success`;
|
|
76
76
|
case "warning":
|
|
77
|
-
return `${e} ${
|
|
77
|
+
return `${e} ${a.warning} btn-glass-warning`;
|
|
78
78
|
case "error":
|
|
79
|
-
return `${e} ${
|
|
79
|
+
return `${e} ${a.error} btn-glass-error`;
|
|
80
80
|
case "ghost":
|
|
81
|
-
return `${e} ${
|
|
81
|
+
return `${e} ${a.neutral} btn-ghost`;
|
|
82
82
|
case "no-style":
|
|
83
|
-
return `${e} ${
|
|
83
|
+
return `${e} ${a.neutral} btn-no-style border-none`;
|
|
84
84
|
default:
|
|
85
|
-
return `${e} ${
|
|
85
|
+
return `${e} ${a.neutral} btn-filled shadow-default`;
|
|
86
86
|
}
|
|
87
|
-
},
|
|
87
|
+
}, M = () => /* @__PURE__ */ i(
|
|
88
88
|
"svg",
|
|
89
89
|
{
|
|
90
90
|
className: "animate-spin -ml-1 mr-2 h-4 w-4",
|
|
@@ -92,7 +92,7 @@ const O = (a) => {
|
|
|
92
92
|
fill: "none",
|
|
93
93
|
viewBox: "0 0 24 24",
|
|
94
94
|
children: [
|
|
95
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ r(
|
|
96
96
|
"circle",
|
|
97
97
|
{
|
|
98
98
|
className: "opacity-25",
|
|
@@ -103,7 +103,7 @@ const O = (a) => {
|
|
|
103
103
|
strokeWidth: "4"
|
|
104
104
|
}
|
|
105
105
|
),
|
|
106
|
-
/* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ r(
|
|
107
107
|
"path",
|
|
108
108
|
{
|
|
109
109
|
className: "opacity-75",
|
|
@@ -113,89 +113,103 @@ const O = (a) => {
|
|
|
113
113
|
)
|
|
114
114
|
]
|
|
115
115
|
}
|
|
116
|
-
),
|
|
116
|
+
), q = O(
|
|
117
117
|
({
|
|
118
|
-
variant:
|
|
118
|
+
variant: s,
|
|
119
119
|
size: t = "large",
|
|
120
120
|
radius: e,
|
|
121
|
-
loading:
|
|
122
|
-
disabled:
|
|
123
|
-
icon:
|
|
124
|
-
iconPosition:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
loading: f = !1,
|
|
122
|
+
disabled: j = !1,
|
|
123
|
+
icon: o,
|
|
124
|
+
iconPosition: g = "start",
|
|
125
|
+
leading: v,
|
|
126
|
+
contentAlign: I = "center",
|
|
127
|
+
flexGrow: m = !1,
|
|
128
|
+
fill: z = !1,
|
|
129
|
+
tooltip: x,
|
|
130
|
+
tooltipPlacement: B = "top",
|
|
131
|
+
iconOnly: w = !1,
|
|
130
132
|
children: n,
|
|
131
|
-
className:
|
|
132
|
-
href:
|
|
133
|
-
target:
|
|
134
|
-
rel:
|
|
135
|
-
...
|
|
136
|
-
},
|
|
137
|
-
const
|
|
138
|
-
"flex items-center justify-center",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
className: R = "",
|
|
134
|
+
href: b,
|
|
135
|
+
target: $,
|
|
136
|
+
rel: F,
|
|
137
|
+
...C
|
|
138
|
+
}, _) => {
|
|
139
|
+
const L = e ? E(e) : D(t, w), l = j || f, N = I === "start", y = [
|
|
140
|
+
N ? "flex items-center justify-start text-left" : "flex items-center justify-center",
|
|
141
|
+
w ? `p-0 ${U(t)}` : A(t),
|
|
142
|
+
H(s, L),
|
|
143
|
+
m ? "w-full" : "",
|
|
144
|
+
z ? "max-w-full" : "",
|
|
143
145
|
l ? "cursor-not-allowed" : "",
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
].filter(Boolean).join(" "),
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
x && l ? "pointer-events-none" : "",
|
|
147
|
+
R
|
|
148
|
+
].filter(Boolean).join(" "), c = N ? /* @__PURE__ */ r("span", { className: "flex min-w-0 flex-1 items-center", children: n }) : n, u = v ? /* @__PURE__ */ r(
|
|
149
|
+
"span",
|
|
150
|
+
{
|
|
151
|
+
className: n ? "btn-leading inline-flex shrink-0 items-center me-2" : "btn-leading inline-flex shrink-0 items-center",
|
|
152
|
+
children: v
|
|
153
|
+
}
|
|
154
|
+
) : null, S = () => {
|
|
155
|
+
if (f)
|
|
156
|
+
return /* @__PURE__ */ i(d, { children: [
|
|
157
|
+
u,
|
|
158
|
+
/* @__PURE__ */ r(M, {}),
|
|
159
|
+
c
|
|
151
160
|
] });
|
|
152
|
-
const
|
|
153
|
-
return
|
|
154
|
-
|
|
161
|
+
const h = `inline-flex items-center shrink-0 ${V(t)}`;
|
|
162
|
+
return o && g === "start" ? /* @__PURE__ */ i(d, { children: [
|
|
163
|
+
u,
|
|
164
|
+
/* @__PURE__ */ r(
|
|
155
165
|
"span",
|
|
156
166
|
{
|
|
157
|
-
className: `${
|
|
158
|
-
children:
|
|
167
|
+
className: `${h}${n ? ` ${k(t, "start")}` : ""}`,
|
|
168
|
+
children: o
|
|
159
169
|
}
|
|
160
170
|
),
|
|
161
|
-
|
|
162
|
-
] }) :
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
c
|
|
172
|
+
] }) : o && g === "end" ? /* @__PURE__ */ i(d, { children: [
|
|
173
|
+
u,
|
|
174
|
+
c,
|
|
175
|
+
/* @__PURE__ */ r(
|
|
165
176
|
"span",
|
|
166
177
|
{
|
|
167
|
-
className: `${
|
|
168
|
-
children:
|
|
178
|
+
className: `${h}${n ? ` ${k(t, "end")}` : ""}`,
|
|
179
|
+
children: o
|
|
169
180
|
}
|
|
170
181
|
)
|
|
171
|
-
] }) :
|
|
172
|
-
|
|
182
|
+
] }) : /* @__PURE__ */ i(d, { children: [
|
|
183
|
+
u,
|
|
184
|
+
c
|
|
185
|
+
] });
|
|
186
|
+
}, p = b && !l ? /* @__PURE__ */ r(
|
|
173
187
|
"a",
|
|
174
188
|
{
|
|
175
|
-
ref:
|
|
176
|
-
href:
|
|
177
|
-
target:
|
|
178
|
-
rel:
|
|
179
|
-
className:
|
|
180
|
-
...
|
|
181
|
-
children:
|
|
189
|
+
ref: _,
|
|
190
|
+
href: b,
|
|
191
|
+
target: $,
|
|
192
|
+
rel: W(F, $),
|
|
193
|
+
className: y,
|
|
194
|
+
...C,
|
|
195
|
+
children: S()
|
|
182
196
|
}
|
|
183
|
-
) : /* @__PURE__ */
|
|
197
|
+
) : /* @__PURE__ */ r(
|
|
184
198
|
"button",
|
|
185
199
|
{
|
|
186
|
-
ref:
|
|
200
|
+
ref: _,
|
|
187
201
|
disabled: l,
|
|
188
|
-
className:
|
|
189
|
-
...
|
|
190
|
-
children:
|
|
202
|
+
className: y,
|
|
203
|
+
...C,
|
|
204
|
+
children: S()
|
|
191
205
|
}
|
|
192
206
|
);
|
|
193
|
-
return
|
|
207
|
+
return x ? /* @__PURE__ */ r(T, { text: x, placement: B, fill: m, children: l ? /* @__PURE__ */ r("span", { className: `inline-flex cursor-not-allowed${m ? " w-full" : ""}`, children: p }) : p }) : p;
|
|
194
208
|
}
|
|
195
209
|
);
|
|
196
|
-
|
|
210
|
+
q.displayName = "Button";
|
|
197
211
|
export {
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
q as Button,
|
|
213
|
+
q as default
|
|
200
214
|
};
|
|
201
215
|
//# sourceMappingURL=Button.js.map
|
|
@@ -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 | '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 flexGrow?: boolean;\n fill?: boolean;\n tooltip?: string;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n iconOnly?: 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 'px-2 py-1 text-xs';\n case 'small':\n return 'min-h-[var(--malachite-control-height-small,28px)] px-[10px] 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)] px-[10px] 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)] px-[12px] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap';\n default:\n return 'min-h-[var(--malachite-control-height-large,42px)] px-[12px] 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.\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};\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 '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 flexGrow = false,\n fill = false,\n tooltip,\n tooltipPlacement = 'top',\n iconOnly = 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 baseClasses = [\n 'flex items-center justify-center',\n iconOnly ? `p-0 ${getIconOnlySizeClasses(size)}` : getSizeClasses(size),\n getVariantClasses(variant, radiusClass),\n flexGrow ? 'w-full' : '',\n fill ? 'max-w-full' : '',\n isInactive ? 'cursor-not-allowed' : '',\n tooltip && isInactive ? 'pointer-events-none' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const renderContent = () => {\n if (loading) {\n return (\n <>\n <LoadingSpinner />\n {children}\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 <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'start')}` : ''}`}\n >\n {icon}\n </span>\n {children}\n </>\n );\n }\n\n if (icon && iconPosition === 'end') {\n return (\n <>\n {children}\n <span\n className={`${iconWrapperClasses}${children ? ` ${getIconGapClass(size, 'end')}` : ''}`}\n >\n {icon}\n </span>\n </>\n );\n }\n\n return children;\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","getDefaultRadiusClass","iconOnly","getIconGapClass","position","start","getRadiusClass","radius","FOCUS_OUTLINE","getVariantClasses","variant","radiusClass","baseClasses","LoadingSpinner","jsxs","jsx","Button","forwardRef","loading","disabled","icon","iconPosition","flexGrow","fill","tooltip","tooltipPlacement","children","className","href","target","rel","props","ref","isInactive","getIconOnlySizeClasses","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 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 | '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 fill?: boolean;\n tooltip?: string;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n iconOnly?: 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 'px-2 py-1 text-xs';\n case 'small':\n return 'min-h-[var(--malachite-control-height-small,28px)] px-[10px] 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)] px-[10px] 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)] px-[12px] text-[length:var(--malachite-font-size-15,15px)] leading-[18px] whitespace-nowrap';\n default:\n return 'min-h-[var(--malachite-control-height-large,42px)] px-[12px] 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.\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};\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 '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 fill = false,\n tooltip,\n tooltipPlacement = 'top',\n iconOnly = 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 fill ? 'max-w-full' : '',\n isInactive ? 'cursor-not-allowed' : '',\n tooltip && isInactive ? 'pointer-events-none' : '',\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","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","fill","tooltip","tooltipPlacement","children","className","href","target","rel","props","ref","isInactive","alignStart","getIconOnlySizeClasses","label","leadingSlot","renderContent","Fragment","iconWrapperClasses","getIconSizeClass","button","resolveRel","Tooltip"],"mappings":"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"}
|