@stackone/malachite 2.23.3 → 2.24.1

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.
Files changed (33) hide show
  1. package/COMPONENTS.md +7 -7
  2. package/dist/components/ActionMenu.js +103 -87
  3. package/dist/components/ActionMenu.js.map +1 -1
  4. package/dist/components/AppLogo.js.map +1 -1
  5. package/dist/components/CodeBlockHighlighter.js +4 -3
  6. package/dist/components/CodeBlockHighlighter.js.map +1 -1
  7. package/dist/components/Dropdown.js +374 -360
  8. package/dist/components/Dropdown.js.map +1 -1
  9. package/dist/components/MultiSelect.js +295 -284
  10. package/dist/components/MultiSelect.js.map +1 -1
  11. package/dist/components/Search.js +17 -14
  12. package/dist/components/Search.js.map +1 -1
  13. package/dist/components/Tabs.js +52 -31
  14. package/dist/components/Tabs.js.map +1 -1
  15. package/dist/components/Toolbar.js +47 -14
  16. package/dist/components/Toolbar.js.map +1 -1
  17. package/dist/components/Tooltip.js +55 -55
  18. package/dist/components/Tooltip.js.map +1 -1
  19. package/dist/components/Typography/EllipsisText.js +32 -17
  20. package/dist/components/Typography/EllipsisText.js.map +1 -1
  21. package/dist/components/controlWidth.js +5 -0
  22. package/dist/components/controlWidth.js.map +1 -0
  23. package/dist/components/floatingMenuGeometry.js +71 -0
  24. package/dist/components/floatingMenuGeometry.js.map +1 -0
  25. package/dist/components/portalOverlay.js +25 -23
  26. package/dist/components/portalOverlay.js.map +1 -1
  27. package/dist/components/queryInputGeometry.js +6 -6
  28. package/dist/components/queryInputGeometry.js.map +1 -1
  29. package/dist/index.d.ts +52 -3
  30. package/dist/index.js +360 -356
  31. package/package.json +1 -1
  32. package/dist/components/dropdownPanelPosition.js +0 -38
  33. package/dist/components/dropdownPanelPosition.js.map +0 -1
package/COMPONENTS.md CHANGED
@@ -16,7 +16,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
16
16
  - **`PageLayout`** — Standard page chrome: a bordered card holding a breadcrumb bar, an optional alert strip, and a header block (subtitle, title + suffix, description) above the page body. Used as `PageLayout.Regular`; `PageLayout.Empty` is the chrome-only variant for pre-auth and error screens, with `headerActions` and `background` slots. Actions are declared as data (`buttons`) rather than children, and any that do not fit collapse into an overflow `ActionMenu` via `OverflowRow`. A client-side-routed app must pass `renderLink` so crumbs and the back button go through its router instead of reloading the document. Both layouts render a `main` landmark, demoted via `role` when the app owns its own. Deliberately not a `Surface` — the page card is 16px and carries no shadow. _Key props:_ `PageLayout.Regular`: `breadcrumbLinks`, `breadcrumbActions`, `renderLink`, `title`, `titleSuffix`, `subtitle`, `description`, `buttons`, `loading`, `centered`, `noPadding`, `contentGap` (`small` | `medium` | `large` | `xlarge`, default `small`), `alert`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`. `PageLayout.Empty`: `title`, `centered`, `uniformPadding`, `headerActions`, `background`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`.
17
17
  - **`WizardLayout`** — Full-screen wizard scaffold: top bar (back button, right-aligned stepper, trailing slot), pinned `header` slot, centered scrolling content column, and a pinned footer (above sticky body content). `footerGlass` gives the footer a frosted translucent + backdrop-blur treatment. _Key props:_ `showBack`, `onBack`, `stepper`, `trailing`, `header`, `footer`, `footerGlass`, `center`, `contentWidth`, `minWidth`.
18
18
  - **`PageActions`** — A page's action buttons, collapsing into an overflow menu as the row narrows. `PageLayout.Regular` renders these from its `buttons` prop; import it directly when a page builds its own header block (e.g. one that scrolls the title away) so the collapse, the dividers and the href handling come with it rather than being hand-rolled. _Key props:_ `buttons` (`PageAction[]`).
19
- - **`Toolbar`** — Flat, opaque horizontal bar for a sub-header / filter row (e.g. a Search). Owns its chrome — opaque `card-background` (so it can pin over scrolling content) + optional bottom `divider` — so consumers don’t hand-roll background/border tokens. Positioning (incl. sticky) is left to the consumer, as is the gutter when `padding="none"` — use that when the bar must sit flush with the content it pins over. _Key props:_ `divider`, `padding` (`none` | `compact` | `default`), `className`, `style`.
19
+ - **`Toolbar`** — Flat, opaque horizontal bar for a sub-header / filter row (e.g. a Search). Owns its chrome — opaque `card-background` (so it can pin over scrolling content) + optional bottom `divider` — so consumers don’t hand-roll background/border tokens. Its `TOOLBAR_FILTER_WIDTH_PX` / `TOOLBAR_FILTER_MIN_WIDTH_PX` / `TOOLBAR_FILL_BASIS_PX` / `TOOLBAR_FILL_MIN_WIDTH_PX` are exported so a control outside the bar can line up with one inside it without re-typing the number. Positioning (incl. sticky) is left to the consumer, as is the gutter when `padding="none"` — use that when the bar must sit flush with the content it pins over. `layout="filters"` makes it own the layout too, which consumers were each re-deriving and getting wrong the same way: a `flex: 1` search with no floor collapses to nothing on a narrow viewport, and a row that cannot wrap runs off the page. Compose it as `Toolbar.Fill` for the search and `Toolbar.Item` per filter, as flat siblings; `grow={false}` holds a slot at its basis for a row that already spaces its children. The row packs greedily from its basis widths and each line's spare space is shared out by grow factor, which is deliberately lopsided: the search outweighs the filters a hundred to one, so a shared line leaves them at their 200px while a filter left alone on a wrapped line grows into the whole width instead of stranding beside a gap. A filter drops to the next line once its full 200px basis will not fit, and the filters sharing a line then compress toward a 160px floor. A search and a filter are laid out differently rather than uniformly, because a search field's useful width is "as much as you can spare" where a filter's is the width of a value. _Key props:_ `divider`, `padding` (`none` | `compact` | `default`), `layout` (`plain` | `filters`), `className`, `style`. `Fill`: `minWidth`. `Item`: `width`, `minWidth`, `grow`.
20
20
  - **`Flex`** — Flexbox layout helper with semantic direction/align/justify/gap props. _Key props:_ `FlexDirection`, `FlexAlign`, `FlexJustify`, `FlexGapSize`.
21
21
  - **`Padded`** — Padding wrapper applying consistent design-system spacing.
22
22
  - **`Spacer`** — Empty element that adds fixed spacing between siblings.
@@ -52,9 +52,9 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
52
52
  - **`DefinitionRow`** — Row showing an icon, label, optional tag and description — for definition/detail lists. _Key props:_ `icon`, `label`, `tag`, `description`.
53
53
  - **`PreviewTree`** — Nested, indented tree for previewing hierarchical structure.
54
54
  - **`PreviewTreeRow`** — A single labelled row within a PreviewTree. _Key props:_ `label`.
55
- - **`Tabs`** — Tabbed navigation switching between content panels. _Key props:_ `TabItem[]`.
55
+ - **`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`.
56
56
  - **`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`.
57
- - **`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. _Key props:_ `ActionMenuItem[]`, `size`, `label`, `triggerIcon`, `minWidth`, `showLabel`, `variant`.
57
+ - **`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, pinned to the trigger's right edge, 14px below it, rounding tighter (10px) and padding wider (8px) than the menu a `Dropdown` opens. 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`.
58
58
  - **`FooterNav`** — Footer navigation bar of links. _Key props:_ `FooterNavLink[]`.
59
59
  - **`FooterLinks`** — Inline list of footer links (e.g. terms, privacy, support).
60
60
  - **`StepTabs`** — Header for a short, ordered flow — a two-step drawer or wizard panel. Each step is a button carrying `aria-current="step"`, not a tab: the body beneath is the step’s form, so it keeps the normal tab order instead of adopting a `tablist`’s arrow-key contract. States are current / complete (a check, still reachable so the user can go back) / upcoming. For a read-only progress readout use `Stepper`. _Key props:_ `items` (`key`, `label`, `hint`, `disabled`), `activeKey`, `completedKeys`, `onChange`, `aria-label`, `aria-labelledby`, `className`.
@@ -71,13 +71,13 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
71
71
  - **`FieldBlock`** — Something a form has to show but does not ask for — a computed list, a generated value — wearing a field’s anatomy: label, optional `badge` beside it and `action` at the end of the row, content, then helper text, all on a field’s own metrics and inset. A real control brings its anatomy with it; this is for everything that has no control to bring one. _Key props:_ `label`, `badge`, `action`, `helperText`, `children`, `className`, `style`.
72
72
  - **`Input`** — Single-line text input. Passing `formControl` is enough to show validation: the resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired and the in-field error icon standing down once the message carries the meaning, and the control reports its edits back so a rejection clears as the customer fixes the field rather than standing until the next submit. Given a `formControl` the form owns the value: the field paints an explicit `value` first, then the value the form holds, then `defaultValue`, then empty — the ladder react-hook-form's own `useController` computes — so `reset()` and `setValue()` repaint the field and the text on screen cannot drift from the value being validated, which had put a rejection under a field the customer could see was filled. Passing `defaultValue` to a bound field warns in development, naming the component and the field: where the form has an entry for it, it is ignored, and where it has none it paints text the form will never submit. Set it through `useForm({ defaultValues })` instead. A consumer's own `helperText` keeps its line above it and stays dimmed, since it explains the field rather than reporting the fault — replacing one with the other left a field ringed in red with nothing saying what was wrong. An explicit `error` still wins, for a rejection the resolver knows nothing about, and `error={false}` suppresses the resolver outright, message included. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. _Key props:_ `label`, `type`, `InputSize`, `InputVariant` (`default` | `ghost`), `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `value`, `defaultValue`, `showCopyButton`, `showPasswordToggle`, `aria-label`, `aria-labelledby`, `id`.
73
73
  - **`TextArea`** — Multi-line text input. Shows resolver validation from `formControl` on exactly the same terms as `Input`: the message gets its own line beneath any `helperText`, which keeps its own, and the value climbs the same ladder — an explicit `value`, then the value the form holds, then `defaultValue`, then empty — a disabled field included, which used to show `defaultValue` whatever the form or the consumer had said. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. _Key props:_ `label`, `rows`, `maxLength`, `resize`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `value`, `defaultValue`, `aria-label`, `aria-labelledby`, `id`.
74
- - **`Search`** — Search input with a leading icon; `filled` and `muted` variants. _Key props:_ `value`, `onChange`, `placeholder`, `variant`, `size`, `disabled`.
74
+ - **`Search`** — Search input with a leading icon; `filled` and `muted` variants. `width` fixes the field's width, the same prop `Dropdown` and `MultiSelect` take, so a toolbar can put its controls on one width rather than letting each size itself; without it the field fills its container, and a container narrower than the value still wins. _Key props:_ `value`, `onChange`, `placeholder`, `variant`, `size`, `width`, `disabled`.
75
75
  - **`Checkbox`** — Checkbox supporting checked/indeterminate/disabled. Pass `presentational` to render a visual-only, non-focusable indicator for use inside a clickable row/card. _Key props:_ `checked`, `indeterminate`, `disabled`, `onChange`, `presentational`.
76
76
  - **`Radio`** — Single radio control with hover state; parent manages the group. Pass `presentational` to render a visual-only, non-focusable indicator for use inside a clickable row/card. _Key props:_ `checked`, `onChange`, `disabled`, `size`, `presentational`.
77
77
  - **`SegmentedControl`** — A rounded track of mutually-exclusive options — the segmented “one of these” control. Owns the track background, the selected-thumb chrome and the `radiogroup` semantics, so consumers never hand-roll a track around `PillButton` (whose `default` variant tokens are tuned for standalone filter pills, not a sunken track). Single tab stop with arrow-key move-and-select; selection is conveyed via `aria-checked` and a foreground change, not background colour alone. Restyle via the `segmentedControl` theme tokens. _Key props:_ `options` (`value`, `label`, `icon`, `disabled`), `value`, `onChange`, `SegmentedControlSize` (`small` | `medium` | `large`), `fullWidth`, `disabled`, `aria-label`, `aria-labelledby`, `className`.
78
78
  - **`Toggle`** — On/off switch. Add `label` for a visible caption, and `variant` to give it standalone toolbar-filter chrome — pick the family it sits beside: `pill` matches the filter-bar inputs (`Search`, `MultiSelect`), `button` renders the whole control as an outline `Button` with the switch as its trailing icon. `bordered` is shorthand for `variant="pill"`. `tone="pending"` paints the checked track amber, for a switch whose change has not been written yet — pair it with a `SelectionBar` reporting the same diff. The unchecked track never changes: nothing is pending about a switch that is off. Forwards a ref to its root element, so it can anchor an `InlineConfirm`. _Key props:_ `checked`, `onChange`, `disabled`, `ToggleSize` (`small` | `medium`), `ToggleTone` (`default` | `pending`), `label`, `ToggleVariant` (`pill` | `button`), `bordered`, `aria-label`, `aria-labelledby`, `aria-describedby`, `id`.
79
- - **`Dropdown`** — Single-select dropdown. `DropdownSize` is `small` (28px) | `medium` (32px) | `large` (42px), matching Button; `fullWidth` fills the container instead of sizing to content and matches the panel to the trigger, which is what a table cell or a toolbar slot needs — without it the field column sizes to its content, so a consumer has to compute a width; `small` is the chip that sits beside a page title and rounds tighter (10px) than the taller sizes. These names each moved up a tier in 0.71 — the old `xsmall`/`small`/`medium` are now `small`/`medium`/`large`. Pass `value` for a controlled selection whose source of truth lives elsewhere (URL or app state): the trigger follows it without the consumer remounting via `key`, which used to discard focus mid-interaction. `defaultValue` keeps the uncontrolled behaviour. `clearable` adds a clear control to the trigger once something is selected and reports it as `onItemSelected(undefined)` — without it a selection cannot be undone, so filter bars had to invent a sentinel "any" item to carry that meaning. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. `showSubtitleInTrigger` promotes the selected item's `subtitle` into the closed trigger, on one line under the value, so the consequence of a choice stays on screen once the menu shuts — the trigger trades its fixed height for a floor and grows by that line, and either line too long for it ellipsises, with a single tooltip carrying whichever lines are cut off, value first. It does not combine with `search`, whose trigger is a single-line text input with nowhere to put a second line, so the type rejects the pair rather than letting a consumer ask for a subtitle and get none. `subtitleLayout` decides how a subtitle sits in the open menu: the default `inline` keeps it on the label's row after a divider, which suits a short qualifier, while `stacked` gives it its own line beneath the label, clamped to two — inline, a sentence-length subtitle is ellipsised away to a few words. Passing `formControl` is enough to show validation on the same terms as `Input`, though only validation: the trigger still paints `value` or `defaultValue`, never the value the form holds. The resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired, while an explicit `helperText` keeps its own line above it and stays dimmed, and `error={false}` suppresses the resolver outright. _Key props:_ `DropdownItem[]`, `DropdownSize`, `fullWidth`, `value`, `defaultValue`, `label`, `search`, `showSubtitleInTrigger`, `subtitleLayout`, `clearable`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `aria-label`, `aria-labelledby`, `id`.
80
- - **`MultiSelect`** — Multi-select dropdown. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
79
+ - **`Dropdown`** — Single-select dropdown. `DropdownSize` is `small` (28px) | `medium` (32px) | `large` (42px), matching Button; `fullWidth` fills the container instead of sizing to content, which is what a table cell or a toolbar slot needs — without it the field column sizes to its content, so a consumer has to compute a width; `width` fixes that width outright, the way `MultiSelect` does, for a toolbar lining the two up rather than letting each hug its own label — a narrower container still wins, since the width clamps to it; Until this release a `search` trigger could not go below the intrinsic width of its bare `input`, so a `width` under roughly 200px was ignored; it now shrinks with the rest. The open menu fills the trigger and left-aligns under it, as Figma draws every tier of `Dropdown Component - Standard`, so `width` sizes both — the menu is the width of the control that opened it and does not widen for a long option, which ellipsises instead, so a row of controls and their open menus stay on one set of edges; below 160px the menu keeps that floor rather than shrinking to a chip nobody can read, and `minWidth` raises that floor without widening the trigger — for a picker a column pins narrow whose options carry a subtitle the trigger never shows; near a viewport edge it anchors to the trigger's other side before falling back to the viewport, so the two share an edge wherever one fits; `small` is the chip that sits beside a page title and rounds tighter (10px) than the taller sizes. These names each moved up a tier in 0.71 — the old `xsmall`/`small`/`medium` are now `small`/`medium`/`large`. Pass `value` for a controlled selection whose source of truth lives elsewhere (URL or app state): the trigger follows it without the consumer remounting via `key`, which used to discard focus mid-interaction. `defaultValue` keeps the uncontrolled behaviour. `clearable` adds a clear control to the trigger once something is selected and reports it as `onItemSelected(undefined)` — without it a selection cannot be undone, so filter bars had to invent a sentinel "any" item to carry that meaning. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. `showSubtitleInTrigger` promotes the selected item's `subtitle` into the closed trigger, on one line under the value, so the consequence of a choice stays on screen once the menu shuts — the trigger trades its fixed height for a floor and grows by that line, and either line too long for it ellipsises, with a single tooltip carrying whichever lines are cut off, value first. It does not combine with `search`, whose trigger is a single-line text input with nowhere to put a second line, so the type rejects the pair rather than letting a consumer ask for a subtitle and get none. `subtitleLayout` decides how a subtitle sits in the open menu: the default `inline` keeps it on the label's row after a divider, which suits a short qualifier, while `stacked` gives it its own line beneath the label, clamped to two — inline, a sentence-length subtitle is ellipsised away to a few words. Passing `formControl` is enough to show validation on the same terms as `Input`, though only validation: the trigger still paints `value` or `defaultValue`, never the value the form holds. The resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired, while an explicit `helperText` keeps its own line above it and stays dimmed, and `error={false}` suppresses the resolver outright. _Key props:_ `DropdownItem[]`, `DropdownSize`, `fullWidth`, `width`, `value`, `defaultValue`, `label`, `search`, `showSubtitleInTrigger`, `subtitleLayout`, `clearable`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `aria-label`, `aria-labelledby`, `id`.
80
+ - **`MultiSelect`** — Multi-select dropdown. `width` fixes the trigger, as `Dropdown` and `Search` take it, so a toolbar can put its controls on one width — and, as with those two, a container narrower than the value still wins; the menu fills the trigger and left-aligns under it, so that one prop sizes both; `minWidth` raises the 160px floor a narrow trigger falls back to. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `width`, `minWidth`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
81
81
  - **`QueryInput`** — A textbox with inline syntax colouring and a suggestion menu anchored to the caret. It carries no grammar of its own: the consumer parses the value, hands back `tokens` to colour and `suggestions` to offer, so the vocabulary lives in the consuming app rather than here. _Key props:_ `value`, `onChange`, `tokens`, `suggestions`, `activeRange`, `onAccept`, `onQueryContextChange`, `onSubmit`, `placeholder`, `size`, `disabled`, `loading`, `emptyMessage`, `className`, `style`, `aria-label`, `aria-labelledby`.
82
82
  - **`SettingRow`** — One setting: a labelled row with its control on the trailing edge, and the sentence that explains it underneath. `control` may be a render prop receiving the label’s id, so a control that no `<label htmlFor>` can reach — `Toggle` renders a `<button role="switch">` — still names itself via `aria-labelledby`. _Key props:_ `label`, `badge`, `description`, `control`, `className`.
83
83
 
@@ -92,7 +92,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
92
92
  ## Typography
93
93
 
94
94
  - **`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.
95
- - **`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"`; `Typography.CodeText` takes `variant="inline"` for a chip mid-sentence; `Typography.MarkdownBlock` takes `variant="flow"` for use inside `ClampText` and `normalizeSource` for machine-generated descriptions that omit blank lines between blocks.
95
+ - **`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"`; `Typography.CodeText` takes `variant="inline"` for a chip mid-sentence; `Typography.MarkdownBlock` takes `variant="flow"` for use inside `ClampText` 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.
96
96
 
97
97
  ## Data display
98
98
 
@@ -1,20 +1,25 @@
1
- import { jsxs as m, Fragment as O, jsx as s } from "react/jsx-runtime";
2
- import { useState as E, useRef as S, useCallback as _, useEffect as H, useLayoutEffect as j, useInsertionEffect as D } from "react";
3
- import { createPortal as N } from "react-dom";
4
- import { Button as A } from "./Button.js";
5
- import G from "./CustomIcons/MoreHorizontalIcon.js";
6
- import { pushEscapeLayer as W } from "./escapeLayers.js";
7
- import { ensureOverlayStyles as Y, overlayIdentity as $ } from "./portalOverlay.js";
8
- import { useTopLayer as F } from "./topLayer.js";
9
- const Z = {
1
+ import { jsxs as p, Fragment as I, jsx as i } from "react/jsx-runtime";
2
+ import { useState as S, useRef as g, useCallback as T, useEffect as _, useLayoutEffect as B, useInsertionEffect as O } from "react";
3
+ import { createPortal as W } from "react-dom";
4
+ import { Button as H } from "./Button.js";
5
+ import N from "./CustomIcons/MoreHorizontalIcon.js";
6
+ import { pushEscapeLayer as Y } from "./escapeLayers.js";
7
+ import { measureContentWidth as j, measureMenuGeometry as A, MENU_SPECS as l } from "./floatingMenuGeometry.js";
8
+ import { ensureOverlayStyles as D, overlayIdentity as G } from "./portalOverlay.js";
9
+ import { useTopLayer as $ } from "./topLayer.js";
10
+ const F = {
11
+ minWidth: 0,
12
+ overflow: "hidden",
13
+ textOverflow: "ellipsis"
14
+ }, U = {
10
15
  padding: "6px 10px",
11
16
  fontSize: "var(--malachite-font-size-small, 13px)",
12
17
  color: "var(--malachite-text-dimmed, rgb(161, 161, 161))",
13
18
  userSelect: "none"
14
- }, q = {
19
+ }, Z = {
15
20
  small: { icon: 16, iconBox: "text-[16px] [&_svg]:size-[16px]!" },
16
21
  xsmall: { icon: 11, iconBox: "text-[11px] [&_svg]:size-[11px]!" }
17
- }, L = (n) => n.type === "divider" ? {
22
+ }, k = (o) => o.type === "divider" ? {
18
23
  height: 1,
19
24
  background: "var(--malachite-card-border, rgb(232, 232, 232))",
20
25
  margin: "4px 0"
@@ -26,109 +31,120 @@ const Z = {
26
31
  borderRadius: 8,
27
32
  fontSize: "var(--malachite-font-size-small, 13px)",
28
33
  fontWeight: 400,
29
- color: n.danger ? "var(--malachite-text-destructive, rgb(237, 53, 54))" : "var(--malachite-text-primary, rgb(10, 10, 10))",
30
- cursor: n.disabled ? "not-allowed" : "pointer",
31
- opacity: n.disabled ? 0.5 : 1,
34
+ color: o.danger ? "var(--malachite-text-destructive, rgb(237, 53, 54))" : "var(--malachite-text-primary, rgb(10, 10, 10))",
35
+ cursor: o.disabled ? "not-allowed" : "pointer",
36
+ opacity: o.disabled ? 0.5 : 1,
32
37
  userSelect: "none",
38
+ whiteSpace: "nowrap",
39
+ overflow: "hidden",
33
40
  background: "transparent",
34
41
  border: "none",
35
42
  textAlign: "left",
36
43
  width: "100%"
37
- }, oe = ({
38
- items: n,
39
- label: b = "More actions",
40
- triggerIcon: M,
41
- minWidth: v = 180,
42
- size: h = "small",
44
+ }, re = ({
45
+ items: o,
46
+ label: h = "More actions",
47
+ triggerIcon: L,
48
+ minWidth: m,
49
+ size: v = "small",
43
50
  showLabel: d = !1,
44
- variant: z = "outline"
51
+ variant: M = "outline"
45
52
  }) => {
46
- const y = q[h], [o, p] = E(!1), [r, P] = E(null), x = S(null), a = S(null);
47
- F(a, o);
48
- const l = _(() => {
49
- var w, k;
50
- const e = x.current;
51
- if (!e)
53
+ const y = Z[v], [r, c] = S(!1), [t, x] = S(null), f = g(null), a = g(null), u = g(void 0);
54
+ $(a, r);
55
+ const s = T(() => {
56
+ const e = f.current;
57
+ e && (u.current = j(a.current) ?? u.current, x(
58
+ A(a.current, {
59
+ triggerRect: e.getBoundingClientRect(),
60
+ preset: "options",
61
+ minWidth: m,
62
+ contentWidth: u.current,
63
+ viewportWidth: window.innerWidth,
64
+ viewportHeight: window.innerHeight
65
+ })
66
+ ));
67
+ }, [m]);
68
+ _(() => {
69
+ if (!r) {
70
+ x(null), u.current = void 0;
52
71
  return;
53
- const t = e.getBoundingClientRect(), u = ((w = a.current) == null ? void 0 : w.offsetWidth) || v, c = ((k = a.current) == null ? void 0 : k.offsetHeight) || 0, i = 8, f = t.right - u, g = window.innerWidth - u - i, I = Math.max(i, Math.min(f, g)), T = window.innerHeight - t.bottom, B = c > 0 && T < c + i ? t.top - c - 4 : t.bottom + 4;
54
- P({ top: B, left: I });
55
- }, [v]);
56
- H(() => {
57
- if (!o)
58
- return;
59
- l();
60
- const e = () => l(), t = (c) => {
61
- var f, g;
62
- const i = c.target;
63
- !((f = x.current) != null && f.contains(i)) && !((g = a.current) != null && g.contains(i)) && p(!1);
64
- }, u = W(() => p(!1));
65
- return window.addEventListener("scroll", e, !0), window.addEventListener("resize", e), document.addEventListener("mousedown", t, !0), () => {
66
- window.removeEventListener("scroll", e, !0), window.removeEventListener("resize", e), document.removeEventListener("mousedown", t, !0), u();
72
+ }
73
+ s();
74
+ const e = () => s(), n = (P) => {
75
+ var w, E;
76
+ const b = P.target;
77
+ !((w = f.current) != null && w.contains(b)) && !((E = a.current) != null && E.contains(b)) && c(!1);
78
+ }, C = Y(() => c(!1));
79
+ return window.addEventListener("scroll", e, !0), window.addEventListener("resize", e), document.addEventListener("mousedown", n, !0), () => {
80
+ window.removeEventListener("scroll", e, !0), window.removeEventListener("resize", e), document.removeEventListener("mousedown", n, !0), C();
67
81
  };
68
- }, [o, l]), j(() => {
69
- o && a.current && l();
70
- }, [o, l]);
82
+ }, [r, s]), B(() => {
83
+ r && a.current && s();
84
+ }, [r, s]);
71
85
  const R = (e) => {
72
- var t;
73
- e.type === "divider" || e.disabled || ((t = e.onClick) == null || t.call(e), p(!1));
86
+ var n;
87
+ e.type === "divider" || e.disabled || ((n = e.onClick) == null || n.call(e), c(!1));
74
88
  };
75
- D(() => {
76
- o && Y();
77
- }, [o]);
78
- const C = {
89
+ O(() => {
90
+ r && D();
91
+ }, [r]);
92
+ const z = {
79
93
  position: "fixed",
80
- top: (r == null ? void 0 : r.top) ?? 0,
81
- left: (r == null ? void 0 : r.left) ?? 0,
82
- minWidth: v,
83
- background: "var(--malachite-card-background, rgb(255, 255, 255))",
84
- border: "1px solid var(--malachite-card-border, rgb(232, 232, 232))",
85
- borderRadius: 12,
86
- boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.08)",
87
- padding: 4,
94
+ top: (t == null ? void 0 : t.top) ?? 0,
95
+ left: (t == null ? void 0 : t.left) ?? 0,
96
+ width: t == null ? void 0 : t.width,
97
+ maxHeight: t == null ? void 0 : t.maxHeight,
98
+ overflowY: "auto",
99
+ background: l.options.background,
100
+ border: l.options.border,
101
+ borderRadius: l.options.radius,
102
+ boxShadow: l.options.shadow,
103
+ padding: l.options.padding,
88
104
  display: "flex",
89
105
  flexDirection: "column",
90
106
  gap: 2,
91
- visibility: r ? "visible" : "hidden",
92
- opacity: r ? 1 : 0
107
+ visibility: t ? "visible" : "hidden",
108
+ opacity: t ? 1 : 0
93
109
  };
94
- return /* @__PURE__ */ m(O, { children: [
95
- /* @__PURE__ */ m(
96
- A,
110
+ return /* @__PURE__ */ p(I, { children: [
111
+ /* @__PURE__ */ p(
112
+ H,
97
113
  {
98
- ref: x,
114
+ ref: f,
99
115
  type: "button",
100
- variant: z,
101
- size: h,
116
+ variant: M,
117
+ size: v,
102
118
  iconOnly: !d,
103
- "aria-label": d ? void 0 : b,
119
+ "aria-label": d ? void 0 : h,
104
120
  "aria-haspopup": "menu",
105
- "aria-expanded": o,
121
+ "aria-expanded": r,
106
122
  className: `shrink-0${d ? " gap-2" : ""}`,
107
123
  onClick: (e) => {
108
- e.stopPropagation(), p((t) => !t);
124
+ e.stopPropagation(), c((n) => !n);
109
125
  },
110
126
  onMouseDown: (e) => e.stopPropagation(),
111
127
  children: [
112
- /* @__PURE__ */ s("span", { className: `inline-flex items-center justify-center ${y.iconBox}`, children: M ?? /* @__PURE__ */ s(G, { size: y.icon }) }),
113
- d && /* @__PURE__ */ s("span", { children: b })
128
+ /* @__PURE__ */ i("span", { className: `inline-flex items-center justify-center ${y.iconBox}`, children: L ?? /* @__PURE__ */ i(N, { size: y.icon }) }),
129
+ d && /* @__PURE__ */ i("span", { children: h })
114
130
  ]
115
131
  }
116
132
  ),
117
- o && N(
118
- /* @__PURE__ */ m(
133
+ r && W(
134
+ /* @__PURE__ */ p(
119
135
  "div",
120
136
  {
121
137
  ref: a,
122
138
  role: "menu",
123
- ...$("actionmenu"),
124
- style: C,
139
+ ...G("actionmenu"),
140
+ style: z,
125
141
  onMouseDown: (e) => e.stopPropagation(),
126
142
  onClick: (e) => e.stopPropagation(),
127
143
  children: [
128
- n.length === 0 && // role="menu" takes menuitem children only, so the notice is one.
129
- /* @__PURE__ */ s("div", { role: "menuitem", "aria-disabled": "true", style: Z, children: "No actions available" }),
130
- n.map(
131
- (e) => e.type === "divider" ? /* @__PURE__ */ s("div", { style: L(e) }, e.key) : /* @__PURE__ */ m(
144
+ o.length === 0 && // role="menu" takes menuitem children only, so the notice is one.
145
+ /* @__PURE__ */ i("div", { role: "menuitem", "aria-disabled": "true", style: U, children: "No actions available" }),
146
+ o.map(
147
+ (e) => e.type === "divider" ? /* @__PURE__ */ i("div", { style: k(e) }, e.key) : /* @__PURE__ */ p(
132
148
  "button",
133
149
  {
134
150
  type: "button",
@@ -136,15 +152,15 @@ const Z = {
136
152
  disabled: e.disabled,
137
153
  title: e.tooltip,
138
154
  onClick: () => R(e),
139
- onMouseEnter: (t) => {
140
- e.disabled || (t.currentTarget.style.background = "rgba(0, 0, 0, 0.04)");
155
+ onMouseEnter: (n) => {
156
+ e.disabled || (n.currentTarget.style.background = "rgba(0, 0, 0, 0.04)");
141
157
  },
142
- onMouseLeave: (t) => {
143
- t.currentTarget.style.background = "transparent";
158
+ onMouseLeave: (n) => {
159
+ n.currentTarget.style.background = "transparent";
144
160
  },
145
- style: L(e),
161
+ style: k(e),
146
162
  children: [
147
- e.icon && /* @__PURE__ */ s(
163
+ e.icon && /* @__PURE__ */ i(
148
164
  "span",
149
165
  {
150
166
  style: {
@@ -155,7 +171,7 @@ const Z = {
155
171
  children: e.icon
156
172
  }
157
173
  ),
158
- e.label
174
+ /* @__PURE__ */ i("span", { style: F, children: e.label })
159
175
  ]
160
176
  },
161
177
  e.key
@@ -169,6 +185,6 @@ const Z = {
169
185
  ] });
170
186
  };
171
187
  export {
172
- oe as default
188
+ re as default
173
189
  };
174
190
  //# sourceMappingURL=ActionMenu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, {\n useState,\n useRef,\n useEffect,\n useInsertionEffect,\n useLayoutEffect,\n useCallback,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport { pushEscapeLayer } from './escapeLayers';\nimport { ensureOverlayStyles, overlayIdentity } from './portalOverlay';\nimport { useTopLayer } from './topLayer';\n\nconst EMPTY_ITEM_STYLE: React.CSSProperties = {\n padding: '6px 10px',\n fontSize: 'var(--malachite-font-size-small, 13px)',\n color: 'var(--malachite-text-dimmed, rgb(161, 161, 161))',\n userSelect: 'none',\n};\n\nexport type ActionMenuItem =\n | {\n type?: 'item';\n key: string;\n label: React.ReactNode;\n onClick?: () => void;\n disabled?: boolean;\n danger?: boolean;\n tooltip?: string;\n icon?: React.ReactNode;\n }\n | {\n type: 'divider';\n key: string;\n };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n}\n\n// No box here: pinning `--malachite-control-height-small` beat Button's own `size-[28px]`, so a\n// theme that moved that token moved the kebab and left the Button behind. Only the glyph is\n// left to size, Button sizing one only for its `icon` prop.\nconst TRIGGER_SIZES: Record<'small' | 'xsmall', { icon: number; iconBox: string }> = {\n small: { icon: 16, iconBox: 'text-[16px] [&_svg]:size-[16px]!' },\n xsmall: { icon: 11, iconBox: 'text-[11px] [&_svg]:size-[11px]!' },\n};\n\nconst itemStyle = (item: ActionMenuItem): React.CSSProperties => {\n if (item.type === 'divider') {\n return {\n height: 1,\n background: 'var(--malachite-card-border, rgb(232, 232, 232))',\n margin: '4px 0',\n };\n }\n return {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n padding: '6px 10px',\n borderRadius: 8,\n fontSize: 'var(--malachite-font-size-small, 13px)',\n fontWeight: 400,\n color: item.danger\n ? 'var(--malachite-text-destructive, rgb(237, 53, 54))'\n : 'var(--malachite-text-primary, rgb(10, 10, 10))',\n cursor: item.disabled ? 'not-allowed' : 'pointer',\n opacity: item.disabled ? 0.5 : 1,\n userSelect: 'none',\n background: 'transparent',\n border: 'none',\n textAlign: 'left',\n width: '100%',\n };\n};\n\n/**\n * ActionMenu is a kebab-style overflow action menu. The trigger is a square\n * icon-only outline Button (three-dot icon by default); on click, a popover\n * appears with the given items. The popover is rendered to document.body via\n * createPortal so it escapes any Shadow DOM or overflow:hidden ancestors —\n * which is also why the popover is styled inline rather than with Tailwind:\n * the scoped stylesheet does not reach document.body.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth = 180,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n}) => {\n const trigger = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<{ top: number; left: number } | null>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n\n useTopLayer(popoverRef, open);\n\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) {\n return;\n }\n const rect = trigger.getBoundingClientRect();\n const popoverWidth = popoverRef.current?.offsetWidth || minWidth;\n const popoverHeight = popoverRef.current?.offsetHeight || 0;\n const viewportPadding = 8;\n\n const desiredLeft = rect.right - popoverWidth;\n const maxLeft = window.innerWidth - popoverWidth - viewportPadding;\n const left = Math.max(viewportPadding, Math.min(desiredLeft, maxLeft));\n\n const spaceBelow = window.innerHeight - rect.bottom;\n const top =\n popoverHeight > 0 && spaceBelow < popoverHeight + viewportPadding\n ? rect.top - popoverHeight - 4\n : rect.bottom + 4;\n\n setPosition({ top, left });\n }, [minWidth]);\n\n useEffect(() => {\n if (!open) {\n return;\n }\n updatePosition();\n const handleScrollOrResize = () => updatePosition();\n const handleClickOutside = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!triggerRef.current?.contains(target) && !popoverRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n // ESC via the shared layer stack: closes only the topmost overlay.\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n document.addEventListener('mousedown', handleClickOutside, true);\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n document.removeEventListener('mousedown', handleClickOutside, true);\n popEscapeLayer();\n };\n }, [open, updatePosition]);\n\n // Re-measure once the popover is in the DOM so the right-edge alignment\n // and viewport clamping use the real width — not the minWidth fallback.\n useLayoutEffect(() => {\n if (open && popoverRef.current) {\n updatePosition();\n }\n }, [open, updatePosition]);\n\n const handleItemClick = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n useInsertionEffect(() => {\n if (open) {\n ensureOverlayStyles();\n }\n }, [open]);\n\n const popoverStyle: React.CSSProperties = {\n position: 'fixed',\n top: position?.top ?? 0,\n left: position?.left ?? 0,\n minWidth,\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n borderRadius: 12,\n boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.08)',\n padding: 4,\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n visibility: position ? 'visible' : 'hidden',\n opacity: position ? 1 : 0,\n };\n\n return (\n <>\n <Button\n ref={triggerRef}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n // With the label drawn, it is already the accessible name; repeating it here\n // would have a screen reader announce it twice.\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n // The engaged border while open is each variant's own, keyed off aria-expanded\n // in the stylesheet — pinning the outline token here stripped it from every\n // other variant.\n //\n // Button only spaces the icon it owns via its `icon` prop; this one is a child,\n // so the gap is set here rather than left to collapse against the label. `gap-2`\n // because the stylesheet is precompiled: a utility no source file already uses\n // is never emitted, and the class then does nothing with nothing to show for it.\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={(e) => {\n e.stopPropagation();\n setOpen((v) => !v);\n }}\n onMouseDown={(e) => e.stopPropagation()}\n >\n {/* `!important` because every CustomIcon writes its size inline; `text-[…]` for a\n glyph that is not an svg at all, which the svg rule never matches. */}\n <span className={`inline-flex items-center justify-center ${trigger.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={trigger.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n {open &&\n createPortal(\n <div\n ref={popoverRef}\n role=\"menu\"\n {...overlayIdentity('actionmenu')}\n style={popoverStyle}\n onMouseDown={(e) => e.stopPropagation()}\n onClick={(e) => e.stopPropagation()}\n >\n {items.length === 0 && (\n // role=\"menu\" takes menuitem children only, so the notice is one.\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) =>\n item.type === 'divider' ? (\n <div key={item.key} style={itemStyle(item)} />\n ) : (\n <button\n key={item.key}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n title={item.tooltip}\n onClick={() => handleItemClick(item)}\n onMouseEnter={(e) => {\n if (!item.disabled) {\n (\n e.currentTarget as HTMLButtonElement\n ).style.background = 'rgba(0, 0, 0, 0.04)';\n }\n }}\n onMouseLeave={(e) => {\n (e.currentTarget as HTMLButtonElement).style.background =\n 'transparent';\n }}\n style={itemStyle(item)}\n >\n {/* Leading: every caller's icon names the action itself —\n copy, open, delete — and an icon that is part of an\n item's identity belongs where the eye starts, not\n right-aligned where it reads as a status column. */}\n {item.icon && (\n <span\n style={{\n display: 'inline-flex',\n flexShrink: 0,\n color: 'var(--malachite-icon-secondary, rgb(148, 148, 148))',\n }}\n >\n {item.icon}\n </span>\n )}\n {item.label}\n </button>\n ),\n )}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["EMPTY_ITEM_STYLE","TRIGGER_SIZES","itemStyle","item","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","trigger","open","setOpen","useState","position","setPosition","triggerRef","useRef","popoverRef","useTopLayer","updatePosition","useCallback","rect","popoverWidth","_a","popoverHeight","_b","viewportPadding","desiredLeft","maxLeft","left","spaceBelow","top","useEffect","handleScrollOrResize","handleClickOutside","e","target","popEscapeLayer","pushEscapeLayer","useLayoutEffect","handleItemClick","useInsertionEffect","ensureOverlayStyles","popoverStyle","jsxs","Fragment","Button","v","jsx","MoreHorizontalIcon","createPortal","overlayIdentity"],"mappings":"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"}
1
+ {"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, {\n useState,\n useRef,\n useEffect,\n useInsertionEffect,\n useLayoutEffect,\n useCallback,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport { pushEscapeLayer } from './escapeLayers';\nimport {\n MENU_SPECS,\n type MenuGeometry,\n measureContentWidth,\n measureMenuGeometry,\n} from './floatingMenuGeometry';\nimport { ensureOverlayStyles, overlayIdentity } from './portalOverlay';\nimport { useTopLayer } from './topLayer';\n\nconst LABEL_STYLE: React.CSSProperties = {\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n};\n\nconst EMPTY_ITEM_STYLE: React.CSSProperties = {\n padding: '6px 10px',\n fontSize: 'var(--malachite-font-size-small, 13px)',\n color: 'var(--malachite-text-dimmed, rgb(161, 161, 161))',\n userSelect: 'none',\n};\n\nexport type ActionMenuItem =\n | {\n type?: 'item';\n key: string;\n label: React.ReactNode;\n onClick?: () => void;\n disabled?: boolean;\n danger?: boolean;\n tooltip?: string;\n icon?: React.ReactNode;\n }\n | {\n type: 'divider';\n key: string;\n };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n}\n\n// No box here: pinning `--malachite-control-height-small` beat Button's own `size-[28px]`, so a\n// theme that moved that token moved the kebab and left the Button behind. Only the glyph is\n// left to size, Button sizing one only for its `icon` prop.\nconst TRIGGER_SIZES: Record<'small' | 'xsmall', { icon: number; iconBox: string }> = {\n small: { icon: 16, iconBox: 'text-[16px] [&_svg]:size-[16px]!' },\n xsmall: { icon: 11, iconBox: 'text-[11px] [&_svg]:size-[11px]!' },\n};\n\nconst itemStyle = (item: ActionMenuItem): React.CSSProperties => {\n if (item.type === 'divider') {\n return {\n height: 1,\n background: 'var(--malachite-card-border, rgb(232, 232, 232))',\n margin: '4px 0',\n };\n }\n return {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n padding: '6px 10px',\n borderRadius: 8,\n fontSize: 'var(--malachite-font-size-small, 13px)',\n fontWeight: 400,\n color: item.danger\n ? 'var(--malachite-text-destructive, rgb(237, 53, 54))'\n : 'var(--malachite-text-primary, rgb(10, 10, 10))',\n cursor: item.disabled ? 'not-allowed' : 'pointer',\n opacity: item.disabled ? 0.5 : 1,\n userSelect: 'none',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n background: 'transparent',\n border: 'none',\n textAlign: 'left',\n width: '100%',\n };\n};\n\n/**\n * ActionMenu is a kebab-style overflow action menu. The trigger is a square\n * icon-only outline Button (three-dot icon by default); on click, a popover\n * appears with the given items. The popover is rendered to document.body via\n * createPortal so it escapes any Shadow DOM or overflow:hidden ancestors —\n * which is also why the popover is styled inline rather than with Tailwind:\n * the scoped stylesheet does not reach document.body.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n}) => {\n const trigger = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const [position, setPosition] = useState<MenuGeometry | null>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const contentWidthRef = useRef<number | undefined>(undefined);\n\n useTopLayer(popoverRef, open);\n\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) {\n return;\n }\n contentWidthRef.current =\n measureContentWidth(popoverRef.current) ?? contentWidthRef.current;\n\n setPosition(\n measureMenuGeometry(popoverRef.current, {\n triggerRect: trigger.getBoundingClientRect(),\n preset: 'options',\n minWidth,\n contentWidth: contentWidthRef.current,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n }),\n );\n }, [minWidth]);\n\n useEffect(() => {\n if (!open) {\n setPosition(null);\n contentWidthRef.current = undefined;\n return;\n }\n updatePosition();\n const handleScrollOrResize = () => updatePosition();\n const handleClickOutside = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!triggerRef.current?.contains(target) && !popoverRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n // ESC via the shared layer stack: closes only the topmost overlay.\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n window.addEventListener('scroll', handleScrollOrResize, true);\n window.addEventListener('resize', handleScrollOrResize);\n document.addEventListener('mousedown', handleClickOutside, true);\n return () => {\n window.removeEventListener('scroll', handleScrollOrResize, true);\n window.removeEventListener('resize', handleScrollOrResize);\n document.removeEventListener('mousedown', handleClickOutside, true);\n popEscapeLayer();\n };\n }, [open, updatePosition]);\n\n useLayoutEffect(() => {\n if (open && popoverRef.current) {\n updatePosition();\n }\n }, [open, updatePosition]);\n\n const handleItemClick = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n useInsertionEffect(() => {\n if (open) {\n ensureOverlayStyles();\n }\n }, [open]);\n\n const popoverStyle: React.CSSProperties = {\n position: 'fixed',\n top: position?.top ?? 0,\n left: position?.left ?? 0,\n width: position?.width,\n maxHeight: position?.maxHeight,\n overflowY: 'auto',\n background: MENU_SPECS.options.background,\n border: MENU_SPECS.options.border,\n borderRadius: MENU_SPECS.options.radius,\n boxShadow: MENU_SPECS.options.shadow,\n padding: MENU_SPECS.options.padding,\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n visibility: position ? 'visible' : 'hidden',\n opacity: position ? 1 : 0,\n };\n\n return (\n <>\n <Button\n ref={triggerRef}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n // With the label drawn, it is already the accessible name; repeating it here\n // would have a screen reader announce it twice.\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n // The engaged border while open is each variant's own, keyed off aria-expanded\n // in the stylesheet — pinning the outline token here stripped it from every\n // other variant.\n //\n // Button only spaces the icon it owns via its `icon` prop; this one is a child,\n // so the gap is set here rather than left to collapse against the label. `gap-2`\n // because the stylesheet is precompiled: a utility no source file already uses\n // is never emitted, and the class then does nothing with nothing to show for it.\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={(e) => {\n e.stopPropagation();\n setOpen((v) => !v);\n }}\n onMouseDown={(e) => e.stopPropagation()}\n >\n {/* `!important` because every CustomIcon writes its size inline; `text-[…]` for a\n glyph that is not an svg at all, which the svg rule never matches. */}\n <span className={`inline-flex items-center justify-center ${trigger.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={trigger.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n {open &&\n createPortal(\n <div\n ref={popoverRef}\n role=\"menu\"\n {...overlayIdentity('actionmenu')}\n style={popoverStyle}\n onMouseDown={(e) => e.stopPropagation()}\n onClick={(e) => e.stopPropagation()}\n >\n {items.length === 0 && (\n // role=\"menu\" takes menuitem children only, so the notice is one.\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) =>\n item.type === 'divider' ? (\n <div key={item.key} style={itemStyle(item)} />\n ) : (\n <button\n key={item.key}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n title={item.tooltip}\n onClick={() => handleItemClick(item)}\n onMouseEnter={(e) => {\n if (!item.disabled) {\n (\n e.currentTarget as HTMLButtonElement\n ).style.background = 'rgba(0, 0, 0, 0.04)';\n }\n }}\n onMouseLeave={(e) => {\n (e.currentTarget as HTMLButtonElement).style.background =\n 'transparent';\n }}\n style={itemStyle(item)}\n >\n {/* Leading: every caller's icon names the action itself —\n copy, open, delete — and an icon that is part of an\n item's identity belongs where the eye starts, not\n right-aligned where it reads as a status column. */}\n {item.icon && (\n <span\n style={{\n display: 'inline-flex',\n flexShrink: 0,\n color: 'var(--malachite-icon-secondary, rgb(148, 148, 148))',\n }}\n >\n {item.icon}\n </span>\n )}\n <span style={LABEL_STYLE}>{item.label}</span>\n </button>\n ),\n )}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["LABEL_STYLE","EMPTY_ITEM_STYLE","TRIGGER_SIZES","itemStyle","item","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","trigger","open","setOpen","useState","position","setPosition","triggerRef","useRef","popoverRef","contentWidthRef","useTopLayer","updatePosition","useCallback","measureContentWidth","measureMenuGeometry","useEffect","handleScrollOrResize","handleClickOutside","e","target","_a","_b","popEscapeLayer","pushEscapeLayer","useLayoutEffect","handleItemClick","useInsertionEffect","ensureOverlayStyles","popoverStyle","MENU_SPECS","jsxs","Fragment","Button","v","jsx","MoreHorizontalIcon","createPortal","overlayIdentity"],"mappings":";;;;;;;;;AAsBA,MAAMA,IAAmC;AAAA,EACrC,UAAU;AAAA,EACV,UAAU;AAAA,EACV,cAAc;AAClB,GAEMC,IAAwC;AAAA,EAC1C,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA,EACP,YAAY;AAChB,GAqCMC,IAA+E;AAAA,EACjF,OAAO,EAAE,MAAM,IAAI,SAAS,mCAAA;AAAA,EAC5B,QAAQ,EAAE,MAAM,IAAI,SAAS,mCAAA;AACjC,GAEMC,IAAY,CAACC,MACXA,EAAK,SAAS,YACP;AAAA,EACH,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,QAAQ;AAAA,IAGT;AAAA,EACH,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,KAAK;AAAA,EACL,SAAS;AAAA,EACT,cAAc;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,OAAOA,EAAK,SACN,wDACA;AAAA,EACN,QAAQA,EAAK,WAAW,gBAAgB;AAAA,EACxC,SAASA,EAAK,WAAW,MAAM;AAAA,EAC/B,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,OAAO;AAAA,GAYTC,KAAwC,CAAC;AAAA,EAC3C,OAAAC;AAAA,EACA,OAAAC,IAAQ;AAAA,EACR,aAAAC;AAAA,EACA,UAAAC;AAAA,EACA,MAAAC,IAAO;AAAA,EACP,WAAAC,IAAY;AAAA,EACZ,SAAAC,IAAU;AACd,MAAM;AACF,QAAMC,IAAUX,EAAcQ,CAAI,GAC5B,CAACI,GAAMC,CAAO,IAAIC,EAAS,EAAK,GAChC,CAACC,GAAUC,CAAW,IAAIF,EAA8B,IAAI,GAC5DG,IAAaC,EAA0B,IAAI,GAC3CC,IAAaD,EAAuB,IAAI,GACxCE,IAAkBF,EAA2B,MAAS;AAE5D,EAAAG,EAAYF,GAAYP,CAAI;AAE5B,QAAMU,IAAiBC,EAAY,MAAM;AACrC,UAAMZ,IAAUM,EAAW;AAC3B,IAAKN,MAGLS,EAAgB,UACZI,EAAoBL,EAAW,OAAO,KAAKC,EAAgB,SAE/DJ;AAAA,MACIS,EAAoBN,EAAW,SAAS;AAAA,QACpC,aAAaR,EAAQ,sBAAA;AAAA,QACrB,QAAQ;AAAA,QACR,UAAAJ;AAAA,QACA,cAAca,EAAgB;AAAA,QAC9B,eAAe,OAAO;AAAA,QACtB,gBAAgB,OAAO;AAAA,MAAA,CAC1B;AAAA,IAAA;AAAA,EAET,GAAG,CAACb,CAAQ,CAAC;AAEb,EAAAmB,EAAU,MAAM;AACZ,QAAI,CAACd,GAAM;AACP,MAAAI,EAAY,IAAI,GAChBI,EAAgB,UAAU;AAC1B;AAAA,IACJ;AACA,IAAAE,EAAA;AACA,UAAMK,IAAuB,MAAML,EAAA,GAC7BM,IAAqB,CAACC,MAAkB;;AAC1C,YAAMC,IAASD,EAAE;AACjB,MAAI,GAACE,IAAAd,EAAW,YAAX,QAAAc,EAAoB,SAASD,OAAW,GAACE,IAAAb,EAAW,YAAX,QAAAa,EAAoB,SAASF,OACvEjB,EAAQ,EAAK;AAAA,IAErB,GAEMoB,IAAiBC,EAAgB,MAAMrB,EAAQ,EAAK,CAAC;AAC3D,kBAAO,iBAAiB,UAAUc,GAAsB,EAAI,GAC5D,OAAO,iBAAiB,UAAUA,CAAoB,GACtD,SAAS,iBAAiB,aAAaC,GAAoB,EAAI,GACxD,MAAM;AACT,aAAO,oBAAoB,UAAUD,GAAsB,EAAI,GAC/D,OAAO,oBAAoB,UAAUA,CAAoB,GACzD,SAAS,oBAAoB,aAAaC,GAAoB,EAAI,GAClEK,EAAA;AAAA,IACJ;AAAA,EACJ,GAAG,CAACrB,GAAMU,CAAc,CAAC,GAEzBa,EAAgB,MAAM;AAClB,IAAIvB,KAAQO,EAAW,WACnBG,EAAA;AAAA,EAER,GAAG,CAACV,GAAMU,CAAc,CAAC;AAEzB,QAAMc,IAAkB,CAAClC,MAAyB;;AAC9C,IAAIA,EAAK,SAAS,aAAaA,EAAK,cAGpC6B,IAAA7B,EAAK,YAAL,QAAA6B,EAAA,KAAA7B,IACAW,EAAQ,EAAK;AAAA,EACjB;AAEA,EAAAwB,EAAmB,MAAM;AACrB,IAAIzB,KACA0B,EAAA;AAAA,EAER,GAAG,CAAC1B,CAAI,CAAC;AAET,QAAM2B,IAAoC;AAAA,IACtC,UAAU;AAAA,IACV,MAAKxB,KAAA,gBAAAA,EAAU,QAAO;AAAA,IACtB,OAAMA,KAAA,gBAAAA,EAAU,SAAQ;AAAA,IACxB,OAAOA,KAAA,gBAAAA,EAAU;AAAA,IACjB,WAAWA,KAAA,gBAAAA,EAAU;AAAA,IACrB,WAAW;AAAA,IACX,YAAYyB,EAAW,QAAQ;AAAA,IAC/B,QAAQA,EAAW,QAAQ;AAAA,IAC3B,cAAcA,EAAW,QAAQ;AAAA,IACjC,WAAWA,EAAW,QAAQ;AAAA,IAC9B,SAASA,EAAW,QAAQ;AAAA,IAC5B,SAAS;AAAA,IACT,eAAe;AAAA,IACf,KAAK;AAAA,IACL,YAAYzB,IAAW,YAAY;AAAA,IACnC,SAASA,IAAW,IAAI;AAAA,EAAA;AAG5B,SACI,gBAAA0B,EAAAC,GAAA,EACI,UAAA;AAAA,IAAA,gBAAAD;AAAA,MAACE;AAAA,MAAA;AAAA,QACG,KAAK1B;AAAA,QACL,MAAK;AAAA,QACL,SAAAP;AAAA,QACA,MAAAF;AAAA,QACA,UAAU,CAACC;AAAA,QAGX,cAAYA,IAAY,SAAYJ;AAAA,QACpC,iBAAc;AAAA,QACd,iBAAeO;AAAA,QASf,WAAW,WAAWH,IAAY,WAAW,EAAE;AAAA,QAC/C,SAAS,CAAC,MAAM;AACZ,YAAE,gBAAA,GACFI,EAAQ,CAAC+B,MAAM,CAACA,CAAC;AAAA,QACrB;AAAA,QACA,aAAa,CAAC,MAAM,EAAE,gBAAA;AAAA,QAItB,UAAA;AAAA,UAAA,gBAAAC,EAAC,QAAA,EAAK,WAAW,2CAA2ClC,EAAQ,OAAO,IACtE,UAAAL,KAAe,gBAAAuC,EAACC,GAAA,EAAmB,MAAMnC,EAAQ,KAAA,CAAM,GAC5D;AAAA,UACCF,KAAa,gBAAAoC,EAAC,QAAA,EAAM,UAAAxC,EAAA,CAAM;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAE9BO,KACGmC;AAAA,MACI,gBAAAN;AAAA,QAAC;AAAA,QAAA;AAAA,UACG,KAAKtB;AAAA,UACL,MAAK;AAAA,UACJ,GAAG6B,EAAgB,YAAY;AAAA,UAChC,OAAOT;AAAA,UACP,aAAa,CAAC,MAAM,EAAE,gBAAA;AAAA,UACtB,SAAS,CAAC,MAAM,EAAE,gBAAA;AAAA,UAEjB,UAAA;AAAA,YAAAnC,EAAM,WAAW;AAAA,YAEd,gBAAAyC,EAAC,SAAI,MAAK,YAAW,iBAAc,QAAO,OAAO9C,GAAkB,UAAA,uBAAA,CAEnE;AAAA,YAEHK,EAAM;AAAA,cAAI,CAACF,MACRA,EAAK,SAAS,YACV,gBAAA2C,EAAC,OAAA,EAAmB,OAAO5C,EAAUC,CAAI,EAAA,GAA/BA,EAAK,GAA6B,IAE5C,gBAAAuC;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBAEG,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,UAAUvC,EAAK;AAAA,kBACf,OAAOA,EAAK;AAAA,kBACZ,SAAS,MAAMkC,EAAgBlC,CAAI;AAAA,kBACnC,cAAc,CAAC2B,MAAM;AACjB,oBAAK3B,EAAK,aAEF2B,EAAE,cACJ,MAAM,aAAa;AAAA,kBAE7B;AAAA,kBACA,cAAc,CAACA,MAAM;AAChB,oBAAAA,EAAE,cAAoC,MAAM,aACzC;AAAA,kBACR;AAAA,kBACA,OAAO5B,EAAUC,CAAI;AAAA,kBAMpB,UAAA;AAAA,oBAAAA,EAAK,QACF,gBAAA2C;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACG,OAAO;AAAA,0BACH,SAAS;AAAA,0BACT,YAAY;AAAA,0BACZ,OAAO;AAAA,wBAAA;AAAA,wBAGV,UAAA3C,EAAK;AAAA,sBAAA;AAAA,oBAAA;AAAA,oBAGd,gBAAA2C,EAAC,QAAA,EAAK,OAAO/C,GAAc,YAAK,MAAA,CAAM;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAlCjCI,EAAK;AAAA,cAAA;AAAA,YAmCd;AAAA,UAER;AAAA,QAAA;AAAA,MAAA;AAAA,MAEJ,SAAS;AAAA,IAAA;AAAA,EACb,GACR;AAER;"}
@@ -1 +1 @@
1
- {"version":3,"file":"AppLogo.js","sources":["../../src/components/AppLogo.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from './Avatar';\nimport Tag from './Tag';\nimport Typography from './Typography';\nimport { getFontSize, getFontWeight } from './Typography/textStyles';\nimport type { FontSize, FontWeight } from './Typography/Text';\n\nexport interface AppLogoProps {\n /** Connector logo URL. Falls back to a coloured initial tile when missing or broken. */\n src?: string;\n /** Connector name — the accessible label, the fallback initial, and the visible text. */\n name: string;\n /** Renders the name beside the mark. Omit for a bare logo (e.g. inside a dense row). */\n showName?: boolean;\n /** Pixel size of the square mark. */\n size?: number;\n nameSize?: FontSize;\n /** Defaults to `normal`. Raise it when the logo doubles as a page title. */\n nameWeight?: FontWeight;\n /** Rendered as a tag after the name, e.g. `v1.2.0` or `Legacy`. */\n version?: string;\n /** Truncates a long name with an ellipsis and reveals the full value on hover. */\n truncate?: boolean;\n className?: string;\n}\n\nconst AppLogo: React.FC<AppLogoProps> = ({\n src,\n name,\n showName = true,\n size = 24,\n nameSize = 'medium',\n nameWeight = 'normal',\n version,\n truncate = false,\n className = '',\n}) => {\n const gap = size <= 18 ? 'gap-2' : 'gap-3';\n\n if (!showName) {\n return <Avatar src={src} name={name} size={size} shape=\"square\" className={className} />;\n }\n\n return (\n // `max-w-full` is what makes `truncate` work — without it the box is floored at the name's min-content width.\n <span className={`inline-flex items-center ${gap} max-w-full ${className}`.trim()}>\n <Avatar src={src} name={name} size={size} shape=\"square\" className=\"shrink-0\" />\n <span className=\"inline-flex items-center gap-2 min-w-0\">\n {truncate ? (\n <Typography.EllipsisText\n maxWidth=\"100%\"\n tooltip={name}\n style={{\n fontSize: getFontSize(nameSize),\n fontWeight: getFontWeight(nameWeight),\n }}\n >\n {name}\n </Typography.EllipsisText>\n ) : (\n <Typography.Text size={nameSize} fontWeight={nameWeight}>\n {name}\n </Typography.Text>\n )}\n {version ? (\n <span className=\"shrink-0\">\n <Tag value={version} textTransform=\"none\" paddingSize=\"compact\" />\n </span>\n ) : null}\n </span>\n </span>\n );\n};\n\nexport default AppLogo;\n"],"names":["AppLogo","src","name","showName","size","nameSize","nameWeight","version","truncate","className","gap","jsxs","jsx","Avatar","Typography","getFontSize","getFontWeight","Tag"],"mappings":";;;;;AA0BA,MAAMA,IAAkC,CAAC;AAAA,EACrC,KAAAC;AAAA,EACA,MAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,MAAAC,IAAO;AAAA,EACP,UAAAC,IAAW;AAAA,EACX,YAAAC,IAAa;AAAA,EACb,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AACF,QAAMC,IAAMN,KAAQ,KAAK,UAAU;AAEnC,SAAKD;AAAA;AAAA,IAMD,gBAAAQ,EAAC,UAAK,WAAW,4BAA4BD,CAAG,eAAeD,CAAS,GAAG,KAAA,GACvE,UAAA;AAAA,MAAA,gBAAAG,EAACC,KAAO,KAAAZ,GAAU,MAAAC,GAAY,MAAAE,GAAY,OAAM,UAAS,WAAU,YAAW;AAAA,MAC9E,gBAAAO,EAAC,QAAA,EAAK,WAAU,0CACX,UAAA;AAAA,QAAAH,IACG,gBAAAI;AAAA,UAACE,EAAW;AAAA,UAAX;AAAA,YACG,UAAS;AAAA,YACT,SAASZ;AAAA,YACT,OAAO;AAAA,cACH,UAAUa,EAAYV,CAAQ;AAAA,cAC9B,YAAYW,EAAcV,CAAU;AAAA,YAAA;AAAA,YAGvC,UAAAJ;AAAA,UAAA;AAAA,QAAA,sBAGJY,EAAW,MAAX,EAAgB,MAAMT,GAAU,YAAYC,GACxC,UAAAJ,GACL;AAAA,QAEHK,IACG,gBAAAK,EAAC,QAAA,EAAK,WAAU,YACZ,UAAA,gBAAAA,EAACK,GAAA,EAAI,OAAOV,GAAS,eAAc,QAAO,aAAY,UAAA,CAAU,GACpE,IACA;AAAA,MAAA,EAAA,CACR;AAAA,IAAA,EAAA,CACJ;AAAA,wBA9BQM,GAAA,EAAO,KAAAZ,GAAU,MAAAC,GAAY,MAAAE,GAAY,OAAM,UAAS,WAAAK,GAAsB;AAgC9F;"}
1
+ {"version":3,"file":"AppLogo.js","sources":["../../src/components/AppLogo.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from './Avatar';\nimport Tag from './Tag';\nimport Typography from './Typography';\nimport type { FontSize, FontWeight } from './Typography/Text';\nimport { getFontSize, getFontWeight } from './Typography/textStyles';\n\nexport interface AppLogoProps {\n /** Connector logo URL. Falls back to a coloured initial tile when missing or broken. */\n src?: string;\n /** Connector name — the accessible label, the fallback initial, and the visible text. */\n name: string;\n /** Renders the name beside the mark. Omit for a bare logo (e.g. inside a dense row). */\n showName?: boolean;\n /** Pixel size of the square mark. */\n size?: number;\n nameSize?: FontSize;\n /** Defaults to `normal`. Raise it when the logo doubles as a page title. */\n nameWeight?: FontWeight;\n /** Rendered as a tag after the name, e.g. `v1.2.0` or `Legacy`. */\n version?: string;\n /** Truncates a long name with an ellipsis and reveals the full value on hover. */\n truncate?: boolean;\n className?: string;\n}\n\nconst AppLogo: React.FC<AppLogoProps> = ({\n src,\n name,\n showName = true,\n size = 24,\n nameSize = 'medium',\n nameWeight = 'normal',\n version,\n truncate = false,\n className = '',\n}) => {\n const gap = size <= 18 ? 'gap-2' : 'gap-3';\n\n if (!showName) {\n return <Avatar src={src} name={name} size={size} shape=\"square\" className={className} />;\n }\n\n return (\n // `max-w-full` is what makes `truncate` work — without it the box is floored at the name's min-content width.\n <span className={`inline-flex items-center ${gap} max-w-full ${className}`.trim()}>\n <Avatar src={src} name={name} size={size} shape=\"square\" className=\"shrink-0\" />\n <span className=\"inline-flex items-center gap-2 min-w-0\">\n {truncate ? (\n <Typography.EllipsisText\n maxWidth=\"100%\"\n tooltip={name}\n style={{\n fontSize: getFontSize(nameSize),\n fontWeight: getFontWeight(nameWeight),\n }}\n >\n {name}\n </Typography.EllipsisText>\n ) : (\n <Typography.Text size={nameSize} fontWeight={nameWeight}>\n {name}\n </Typography.Text>\n )}\n {version ? (\n <span className=\"shrink-0\">\n <Tag value={version} textTransform=\"none\" paddingSize=\"compact\" />\n </span>\n ) : null}\n </span>\n </span>\n );\n};\n\nexport default AppLogo;\n"],"names":["AppLogo","src","name","showName","size","nameSize","nameWeight","version","truncate","className","gap","jsxs","jsx","Avatar","Typography","getFontSize","getFontWeight","Tag"],"mappings":";;;;;AA0BA,MAAMA,IAAkC,CAAC;AAAA,EACrC,KAAAC;AAAA,EACA,MAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,MAAAC,IAAO;AAAA,EACP,UAAAC,IAAW;AAAA,EACX,YAAAC,IAAa;AAAA,EACb,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AACF,QAAMC,IAAMN,KAAQ,KAAK,UAAU;AAEnC,SAAKD;AAAA;AAAA,IAMD,gBAAAQ,EAAC,UAAK,WAAW,4BAA4BD,CAAG,eAAeD,CAAS,GAAG,KAAA,GACvE,UAAA;AAAA,MAAA,gBAAAG,EAACC,KAAO,KAAAZ,GAAU,MAAAC,GAAY,MAAAE,GAAY,OAAM,UAAS,WAAU,YAAW;AAAA,MAC9E,gBAAAO,EAAC,QAAA,EAAK,WAAU,0CACX,UAAA;AAAA,QAAAH,IACG,gBAAAI;AAAA,UAACE,EAAW;AAAA,UAAX;AAAA,YACG,UAAS;AAAA,YACT,SAASZ;AAAA,YACT,OAAO;AAAA,cACH,UAAUa,EAAYV,CAAQ;AAAA,cAC9B,YAAYW,EAAcV,CAAU;AAAA,YAAA;AAAA,YAGvC,UAAAJ;AAAA,UAAA;AAAA,QAAA,sBAGJY,EAAW,MAAX,EAAgB,MAAMT,GAAU,YAAYC,GACxC,UAAAJ,GACL;AAAA,QAEHK,IACG,gBAAAK,EAAC,QAAA,EAAK,WAAU,YACZ,UAAA,gBAAAA,EAACK,GAAA,EAAI,OAAOV,GAAS,eAAc,QAAO,aAAY,UAAA,CAAU,GACpE,IACA;AAAA,MAAA,EAAA,CACR;AAAA,IAAA,EAAA,CACJ;AAAA,wBA9BQM,GAAA,EAAO,KAAAZ,GAAU,MAAAC,GAAY,MAAAE,GAAY,OAAM,UAAS,WAAAK,GAAsB;AAgC9F;"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as n } from "react/jsx-runtime";
2
- import { PrismLight as m, createElement as y } from "react-syntax-highlighter";
2
+ import y from "react-syntax-highlighter/dist/esm/create-element";
3
+ import m from "react-syntax-highlighter/dist/esm/prism-light";
3
4
  import w from "refractor/yaml";
4
5
  import f from "./prismCedar.js";
5
6
  import { CodeLine as L } from "./codeBlockLine.js";
@@ -47,7 +48,7 @@ const R = () => {
47
48
  return o;
48
49
  const c = e[e.length - 1], a = d(c);
49
50
  return a === c ? o : { ...o, children: [...e.slice(0, -1), a] };
50
- }, _ = ({
51
+ }, A = ({
51
52
  code: o,
52
53
  language: e,
53
54
  showLineNumbers: c = !0,
@@ -98,6 +99,6 @@ const R = () => {
98
99
  );
99
100
  };
100
101
  export {
101
- _ as default
102
+ A as default
102
103
  };
103
104
  //# sourceMappingURL=CodeBlockHighlighter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CodeBlockHighlighter.js","sources":["../../src/components/CodeBlockHighlighter.tsx"],"sourcesContent":["import type React from 'react';\nimport { PrismLight, createElement } from 'react-syntax-highlighter';\nimport yaml from 'refractor/yaml';\nimport cedar from './prismCedar';\nimport { CodeLine, type CodeLineDecoration } from './codeBlockLine';\nimport { CODE_FONT } from './codeBlockStyles';\n\n// Registered on first render rather than at import. A bare top-level call is an import-time\n// side effect, which `sideEffects: false` in package.json tells a bundler it may drop; the\n// same reason the font faces go in through ensureInterFontFace and not a top-level call.\nlet languagesRegistered = false;\n\nconst ensureLanguagesRegistered = (): void => {\n if (languagesRegistered) {\n return;\n }\n languagesRegistered = true;\n PrismLight.registerLanguage('yaml', yaml);\n PrismLight.registerLanguage('cedar', cedar);\n};\n\n// Prism token colors bound to the codeblock theme tokens so highlighting follows the theme.\nconst PRISM_MALACHITE_THEME: { [key: string]: React.CSSProperties } = {\n 'code[class*=\"language-\"]': CODE_FONT,\n 'pre[class*=\"language-\"]': CODE_FONT,\n comment: { color: 'var(--malachite-codeblock-comment)' },\n prolog: { color: 'var(--malachite-codeblock-comment)' },\n cdata: { color: 'var(--malachite-codeblock-comment)' },\n punctuation: { color: 'var(--malachite-codeblock-foreground)' },\n key: { color: 'var(--malachite-codeblock-keyword)' },\n atrule: { color: 'var(--malachite-codeblock-keyword)' },\n 'attr-name': { color: 'var(--malachite-codeblock-keyword)' },\n directive: { color: 'var(--malachite-codeblock-keyword)' },\n keyword: { color: 'var(--malachite-codeblock-keyword)' },\n property: { color: 'var(--malachite-codeblock-keyword)' },\n boolean: { color: 'var(--malachite-codeblock-keyword)' },\n important: { color: 'var(--malachite-codeblock-keyword)' },\n null: { color: 'var(--malachite-codeblock-keyword)' },\n tag: { color: 'var(--malachite-codeblock-keyword)' },\n anchor: { color: 'var(--malachite-codeblock-keyword)' },\n alias: { color: 'var(--malachite-codeblock-keyword)' },\n // Cedar's entity paths (`StackOne::Action`) are types, which is what this token is for.\n 'class-name': { color: 'var(--malachite-codeblock-type)' },\n // The scope variables. Given the one remaining distinct hue so they read as language\n // rather than as another blue keyword.\n builtin: { color: 'var(--malachite-codeblock-number)' },\n operator: { color: 'var(--malachite-codeblock-line-number)' },\n string: { color: 'var(--malachite-codeblock-string)' },\n scalar: { color: 'var(--malachite-codeblock-string)' },\n char: { color: 'var(--malachite-codeblock-string)' },\n url: { color: 'var(--malachite-codeblock-string)' },\n number: { color: 'var(--malachite-codeblock-number)' },\n datetime: { color: 'var(--malachite-codeblock-number)' },\n};\n\ntype SyntaxNode = {\n type: 'text' | 'element';\n value?: string | number;\n children?: SyntaxNode[];\n};\n\n/** Local, not the ambient `rendererProps`: that global leaks out of DefinitelyTyped by accident. */\ntype RowRendererProps = {\n rows: SyntaxNode[];\n stylesheet: { [key: string]: React.CSSProperties };\n useInlineStyles: boolean;\n};\n\n/** Prism keeps the source newline on each row's last token; `CodeLine` supplies the break now. */\nconst withoutTrailingNewline = <T extends SyntaxNode>(node: T): T => {\n if (node.type === 'text') {\n return typeof node.value === 'string' && node.value.endsWith('\\n')\n ? { ...node, value: node.value.slice(0, -1) }\n : node;\n }\n const children = node.children;\n if (!children?.length) {\n return node;\n }\n const last = children[children.length - 1];\n const trimmed = withoutTrailingNewline(last);\n return trimmed === last ? node : { ...node, children: [...children.slice(0, -1), trimmed] };\n};\n\nexport interface CodeBlockHighlighterProps {\n code: string;\n language: 'yaml' | 'cedar';\n showLineNumbers?: boolean;\n wrapLines?: boolean;\n /** Right padding reserving space for the floating copy button. */\n copyClearance?: boolean;\n /**\n * Per-line markup, by index into `code`. How a diff tints its rows and numbers them from\n * the text each line actually came from.\n */\n decorate?: (index: number) => CodeLineDecoration | undefined;\n}\n\nconst CodeBlockHighlighter: React.FC<CodeBlockHighlighterProps> = ({\n code,\n language,\n showLineNumbers = true,\n wrapLines = false,\n copyClearance = false,\n decorate,\n}) => {\n ensureLanguagesRegistered();\n\n const lines = code.split('\\n');\n\n // We lay rows out ourselves rather than passing `showLineNumbers`/`wrapLongLines` down:\n // that mode makes every line a flex container, so the Prism token spans become flex items\n // and the line shreds into out-of-order fragments.\n const renderRows = ({ rows, stylesheet, useInlineStyles }: RowRendererProps) =>\n rows.map((row, index) => {\n const decoration = decorate?.(index);\n // A decoration's own number wins: in a diff the row's position in the merged\n // text is not its line number in either of the texts it came from.\n const lineNumber = decoration?.lineNumber ?? (showLineNumbers ? index + 1 : undefined);\n\n return (\n <CodeLine\n key={index}\n line={lines[index] ?? ''}\n lineNumber={lineNumber}\n wrap={wrapLines}\n decoration={decoration}\n >\n {createElement({\n node: withoutTrailingNewline(row),\n stylesheet,\n useInlineStyles,\n key: `code-line-${index}`,\n })}\n </CodeLine>\n );\n });\n\n return (\n <PrismLight\n language={language}\n style={PRISM_MALACHITE_THEME}\n wrapLines\n renderer={renderRows}\n customStyle={{\n ...CODE_FONT,\n margin: 0,\n padding: '16px',\n paddingRight: copyClearance ? '44px' : '16px',\n overflow: 'auto',\n }}\n // The `code` element is the rows' wrapper here, so it takes the sizing the plain\n // path puts on a div of its own.\n codeTagProps={{\n style: { ...CODE_FONT, display: 'block', width: 'max-content', minWidth: '100%' },\n }}\n >\n {code}\n </PrismLight>\n );\n};\n\nexport default CodeBlockHighlighter;\n"],"names":["languagesRegistered","ensureLanguagesRegistered","PrismLight","yaml","cedar","PRISM_MALACHITE_THEME","CODE_FONT","withoutTrailingNewline","node","children","last","trimmed","CodeBlockHighlighter","code","language","showLineNumbers","wrapLines","copyClearance","decorate","lines","jsx","rows","stylesheet","useInlineStyles","row","index","decoration","lineNumber","CodeLine","createElement"],"mappings":"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"}
1
+ {"version":3,"file":"CodeBlockHighlighter.js","sources":["../../src/components/CodeBlockHighlighter.tsx"],"sourcesContent":["import type React from 'react';\nimport createElement from 'react-syntax-highlighter/dist/esm/create-element';\nimport PrismLight from 'react-syntax-highlighter/dist/esm/prism-light';\nimport yaml from 'refractor/yaml';\nimport cedar from './prismCedar';\nimport { CodeLine, type CodeLineDecoration } from './codeBlockLine';\nimport { CODE_FONT } from './codeBlockStyles';\n\n// Registered on first render rather than at import. A bare top-level call is an import-time\n// side effect, which `sideEffects: false` in package.json tells a bundler it may drop; the\n// same reason the font faces go in through ensureInterFontFace and not a top-level call.\nlet languagesRegistered = false;\n\nconst ensureLanguagesRegistered = (): void => {\n if (languagesRegistered) {\n return;\n }\n languagesRegistered = true;\n PrismLight.registerLanguage('yaml', yaml);\n PrismLight.registerLanguage('cedar', cedar);\n};\n\n// Prism token colors bound to the codeblock theme tokens so highlighting follows the theme.\nconst PRISM_MALACHITE_THEME: { [key: string]: React.CSSProperties } = {\n 'code[class*=\"language-\"]': CODE_FONT,\n 'pre[class*=\"language-\"]': CODE_FONT,\n comment: { color: 'var(--malachite-codeblock-comment)' },\n prolog: { color: 'var(--malachite-codeblock-comment)' },\n cdata: { color: 'var(--malachite-codeblock-comment)' },\n punctuation: { color: 'var(--malachite-codeblock-foreground)' },\n key: { color: 'var(--malachite-codeblock-keyword)' },\n atrule: { color: 'var(--malachite-codeblock-keyword)' },\n 'attr-name': { color: 'var(--malachite-codeblock-keyword)' },\n directive: { color: 'var(--malachite-codeblock-keyword)' },\n keyword: { color: 'var(--malachite-codeblock-keyword)' },\n property: { color: 'var(--malachite-codeblock-keyword)' },\n boolean: { color: 'var(--malachite-codeblock-keyword)' },\n important: { color: 'var(--malachite-codeblock-keyword)' },\n null: { color: 'var(--malachite-codeblock-keyword)' },\n tag: { color: 'var(--malachite-codeblock-keyword)' },\n anchor: { color: 'var(--malachite-codeblock-keyword)' },\n alias: { color: 'var(--malachite-codeblock-keyword)' },\n // Cedar's entity paths (`StackOne::Action`) are types, which is what this token is for.\n 'class-name': { color: 'var(--malachite-codeblock-type)' },\n // The scope variables. Given the one remaining distinct hue so they read as language\n // rather than as another blue keyword.\n builtin: { color: 'var(--malachite-codeblock-number)' },\n operator: { color: 'var(--malachite-codeblock-line-number)' },\n string: { color: 'var(--malachite-codeblock-string)' },\n scalar: { color: 'var(--malachite-codeblock-string)' },\n char: { color: 'var(--malachite-codeblock-string)' },\n url: { color: 'var(--malachite-codeblock-string)' },\n number: { color: 'var(--malachite-codeblock-number)' },\n datetime: { color: 'var(--malachite-codeblock-number)' },\n};\n\ntype SyntaxNode = {\n type: 'text' | 'element';\n value?: string | number;\n children?: SyntaxNode[];\n};\n\n/** Local, not the ambient `rendererProps`: that global leaks out of DefinitelyTyped by accident. */\ntype RowRendererProps = {\n rows: SyntaxNode[];\n stylesheet: { [key: string]: React.CSSProperties };\n useInlineStyles: boolean;\n};\n\n/** Prism keeps the source newline on each row's last token; `CodeLine` supplies the break now. */\nconst withoutTrailingNewline = <T extends SyntaxNode>(node: T): T => {\n if (node.type === 'text') {\n return typeof node.value === 'string' && node.value.endsWith('\\n')\n ? { ...node, value: node.value.slice(0, -1) }\n : node;\n }\n const children = node.children;\n if (!children?.length) {\n return node;\n }\n const last = children[children.length - 1];\n const trimmed = withoutTrailingNewline(last);\n return trimmed === last ? node : { ...node, children: [...children.slice(0, -1), trimmed] };\n};\n\nexport interface CodeBlockHighlighterProps {\n code: string;\n language: 'yaml' | 'cedar';\n showLineNumbers?: boolean;\n wrapLines?: boolean;\n /** Right padding reserving space for the floating copy button. */\n copyClearance?: boolean;\n /**\n * Per-line markup, by index into `code`. How a diff tints its rows and numbers them from\n * the text each line actually came from.\n */\n decorate?: (index: number) => CodeLineDecoration | undefined;\n}\n\nconst CodeBlockHighlighter: React.FC<CodeBlockHighlighterProps> = ({\n code,\n language,\n showLineNumbers = true,\n wrapLines = false,\n copyClearance = false,\n decorate,\n}) => {\n ensureLanguagesRegistered();\n\n const lines = code.split('\\n');\n\n // We lay rows out ourselves rather than passing `showLineNumbers`/`wrapLongLines` down:\n // that mode makes every line a flex container, so the Prism token spans become flex items\n // and the line shreds into out-of-order fragments.\n const renderRows = ({ rows, stylesheet, useInlineStyles }: RowRendererProps) =>\n rows.map((row, index) => {\n const decoration = decorate?.(index);\n // A decoration's own number wins: in a diff the row's position in the merged\n // text is not its line number in either of the texts it came from.\n const lineNumber = decoration?.lineNumber ?? (showLineNumbers ? index + 1 : undefined);\n\n return (\n <CodeLine\n key={index}\n line={lines[index] ?? ''}\n lineNumber={lineNumber}\n wrap={wrapLines}\n decoration={decoration}\n >\n {createElement({\n node: withoutTrailingNewline(row),\n stylesheet,\n useInlineStyles,\n key: `code-line-${index}`,\n })}\n </CodeLine>\n );\n });\n\n return (\n <PrismLight\n language={language}\n style={PRISM_MALACHITE_THEME}\n wrapLines\n renderer={renderRows}\n customStyle={{\n ...CODE_FONT,\n margin: 0,\n padding: '16px',\n paddingRight: copyClearance ? '44px' : '16px',\n overflow: 'auto',\n }}\n // The `code` element is the rows' wrapper here, so it takes the sizing the plain\n // path puts on a div of its own.\n codeTagProps={{\n style: { ...CODE_FONT, display: 'block', width: 'max-content', minWidth: '100%' },\n }}\n >\n {code}\n </PrismLight>\n );\n};\n\nexport default CodeBlockHighlighter;\n"],"names":["languagesRegistered","ensureLanguagesRegistered","PrismLight","yaml","cedar","PRISM_MALACHITE_THEME","CODE_FONT","withoutTrailingNewline","node","children","last","trimmed","CodeBlockHighlighter","code","language","showLineNumbers","wrapLines","copyClearance","decorate","lines","jsx","rows","stylesheet","useInlineStyles","row","index","decoration","lineNumber","CodeLine","createElement"],"mappings":"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"}