@stackone/malachite 2.63.0 → 2.65.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 -3
- 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/CloseButton.js +76 -0
- package/dist/components/CloseButton.js.map +1 -0
- package/dist/components/Popover.js +157 -149
- package/dist/components/Popover.js.map +1 -1
- package/dist/components/SplitScreenLayout.js +21 -38
- package/dist/components/SplitScreenLayout.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/components/focusRing.js +4 -3
- package/dist/components/focusRing.js.map +1 -1
- package/dist/index.d.ts +62 -0
- package/dist/index.js +284 -282
- package/dist/index.js.map +1 -1
- package/dist/tailwind.css.js +1 -1
- package/dist/theme.js +175 -151
- package/dist/theme.js.map +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.
|
|
@@ -97,7 +97,8 @@ 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
|
+
- **`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`.
|
|
101
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`.
|
|
102
103
|
- **`Link`** — Styled anchor/link text. _Key props:_ `LinkProps`.
|
|
103
104
|
- **`InlineConfirm`** — A confirmation asked on the control that triggered it rather than in a modal, for a reversible bulk edit where a dialog would be heavier than the change it guards. Drawn to the design's chip: 33px tall, 12px radius, its confirm cell running to the pill's own edge between two rules. Reads its own `inlineConfirm` theme tokens — a shade off the `SelectionBar` it appears beside, by design. Carries a spinning glyph by default — its own, so it takes the pill's colour rather than the sheet's dimmed grey — standing for a change waiting on an answer; pass `icon={null}` to drop it or a node to replace it. Anchored placement is in viewport coordinates, so the chip must not sit inside an ancestor carrying `transform`, `filter` or `contain`. It takes focus on open so `Enter` confirms, dismisses on `Escape` (through the shared escape-layer stack, so the topmost overlay answers) or a click outside, and returns focus to whatever opened it. `role="alertdialog"` named by the message itself, so the confirm button is never announced bare. Only one can be open at a time: opening a second dismisses the first, so it is never ambiguous what Yes answers. Pass `anchorTo` (a ref to the control it answers for) and it pins itself to that control's left, follows it while the page or a sticky header scrolls, dismisses once it scrolls out of view, and treats clicks on it as inside — without which the same click would dismiss and reopen. Left unanchored it renders in flow for the consumer to place, and must then not sit in a container that clips overflow (`Table` header cells among them). Nothing renders while `open` is false, and its listeners are attached only while it is open. _Key props:_ `open`, `message`, `confirmLabel`, `onConfirm`, `onDismiss`, `anchorTo`, `icon`, `className`, `style`.
|
|
@@ -106,7 +107,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
106
107
|
## Typography
|
|
107
108
|
|
|
108
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.
|
|
109
|
-
- **`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.
|
|
110
111
|
|
|
111
112
|
## Data display
|
|
112
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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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as l } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as f } from "react";
|
|
3
|
+
import h from "./CustomIcons/DismissIcon.js";
|
|
4
|
+
import { FOCUS_RING_CLOSE_BUTTON_INVERSE as v, FOCUS_RING as p } from "./focusRing.js";
|
|
5
|
+
const x = {
|
|
6
|
+
small: { target: 24, glyph: 12 },
|
|
7
|
+
medium: { target: 32, glyph: 12 }
|
|
8
|
+
}, y = {
|
|
9
|
+
default: {
|
|
10
|
+
idle: "text-[var(--malachite-text-secondary,rgb(123,123,123))]",
|
|
11
|
+
interactive: "hover:text-[var(--malachite-text-primary,rgb(10,10,10))] focus-visible:text-[var(--malachite-text-primary,rgb(10,10,10))]",
|
|
12
|
+
disabled: "text-[var(--malachite-text-dimmed,rgb(161,161,161))]",
|
|
13
|
+
focusRing: p
|
|
14
|
+
},
|
|
15
|
+
inverse: {
|
|
16
|
+
idle: "text-[var(--malachite-close-button-inverse-foreground,rgba(255,255,255,0.7))]",
|
|
17
|
+
interactive: "hover:text-[var(--malachite-close-button-inverse-hover-foreground,rgb(255,255,255))] focus-visible:text-[var(--malachite-close-button-inverse-hover-foreground,rgb(255,255,255))]",
|
|
18
|
+
disabled: "text-[var(--malachite-close-button-inverse-disabled-foreground,rgba(255,255,255,0.35))]",
|
|
19
|
+
focusRing: v
|
|
20
|
+
}
|
|
21
|
+
}, S = (e, a) => {
|
|
22
|
+
const { idle: r, interactive: o, disabled: t, focusRing: i } = y[e];
|
|
23
|
+
return `${{
|
|
24
|
+
enabled: `${r} ${o} cursor-pointer`,
|
|
25
|
+
disabled: `${t} cursor-not-allowed`,
|
|
26
|
+
loading: `${r} cursor-progress`
|
|
27
|
+
}[a]} ${i}`;
|
|
28
|
+
}, C = "flex shrink-0 items-center justify-center rounded-md border-0 bg-transparent p-0 transition-colors", $ = (e) => {
|
|
29
|
+
e.preventDefault(), e.stopPropagation();
|
|
30
|
+
}, E = f(
|
|
31
|
+
({
|
|
32
|
+
onClick: e,
|
|
33
|
+
"aria-label": a = "Close",
|
|
34
|
+
"aria-labelledby": r,
|
|
35
|
+
disabled: o = !1,
|
|
36
|
+
loading: t = !1,
|
|
37
|
+
size: i = "small",
|
|
38
|
+
surface: n = "default",
|
|
39
|
+
className: u = "",
|
|
40
|
+
...m
|
|
41
|
+
}, g) => {
|
|
42
|
+
const { target: d, glyph: s } = x[i], c = o && "disabled" || t && "loading" || "enabled", b = c !== "enabled";
|
|
43
|
+
return /* @__PURE__ */ l(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
...m,
|
|
47
|
+
ref: g,
|
|
48
|
+
type: "button",
|
|
49
|
+
"aria-label": a,
|
|
50
|
+
"aria-labelledby": r,
|
|
51
|
+
"aria-disabled": b ? !0 : void 0,
|
|
52
|
+
"aria-busy": t ? !0 : void 0,
|
|
53
|
+
onClick: b ? $ : e,
|
|
54
|
+
className: `${C} ${S(n, c)} ${u}`,
|
|
55
|
+
style: { width: d, height: d },
|
|
56
|
+
children: t ? (
|
|
57
|
+
// Not `Spinner`: its smallest step is 14px inside a min-height wrapper.
|
|
58
|
+
/* @__PURE__ */ l(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
"aria-hidden": !0,
|
|
62
|
+
className: "box-border shrink-0 animate-spin rounded-full border-2 border-solid border-current border-t-transparent motion-reduce:animate-none motion-reduce:border-t-current",
|
|
63
|
+
style: { width: s, height: s }
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
) : /* @__PURE__ */ l(h, { size: s })
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
E.displayName = "CloseButton";
|
|
72
|
+
export {
|
|
73
|
+
E as CloseButton,
|
|
74
|
+
E as default
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=CloseButton.js.map
|