@12-apps/ui 6.24.6 → 6.24.8

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 (131) hide show
  1. package/components-catalog.md +1 -1
  2. package/dist/_internal/components/data-display/CardKit/CardKebab.js +4 -4
  3. package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +4 -4
  4. package/dist/_internal/components/data-display/CardKit/use-row-confirm.js +4 -4
  5. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +19 -19
  6. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +25 -25
  7. package/dist/_internal/components/data-display/DataViews/DeleteViewDialog.js +4 -4
  8. package/dist/_internal/components/data-display/DataViews/ManageViewsDialog.js +5 -5
  9. package/dist/_internal/components/data-display/DataViews/SaveViewModal.js +4 -4
  10. package/dist/_internal/components/data-display/DataViews/data-views-columns.js +5 -5
  11. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +5 -5
  12. package/dist/_internal/components/data-display/DataViews/data-views-empty.js +4 -4
  13. package/dist/_internal/components/data-display/DataViews/data-views-export.js +4 -4
  14. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +6 -6
  15. package/dist/_internal/components/data-display/DataViews/data-views-grid-helpers.js +4 -4
  16. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +16 -16
  17. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
  18. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +4 -4
  19. package/dist/_internal/components/data-display/DataViews/data-views-search.js +1 -1
  20. package/dist/_internal/components/data-display/DataViews/data-views-select-all-strip.js +4 -4
  21. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +9 -9
  22. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +7 -7
  23. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +8 -8
  24. package/dist/_internal/components/data-display/DataViews/data-views-view-nav.js +4 -4
  25. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +6 -6
  26. package/dist/_internal/components/feedback/ConfirmAction/ConfirmButton.js +4 -4
  27. package/dist/_internal/components/feedback/index.js +4 -4
  28. package/dist/_internal/components/form/Button/Button.js +3 -3
  29. package/dist/_internal/components/form/Button/Button.metrics.js +3 -1
  30. package/dist/_internal/components/form/Button/Button.styles.js +2 -2
  31. package/dist/_internal/components/form/index.js +3 -3
  32. package/dist/_internal/components/form/total-form/SubmitButton.js +4 -4
  33. package/dist/_internal/components/index.js +5 -5
  34. package/dist/_internal/components/navigation/AppHeader/AppHeader.details.js +4 -4
  35. package/dist/_internal/components/navigation/index.js +4 -4
  36. package/dist/_internal/social-login-button/social-login-button.js +4 -4
  37. package/dist/_internal/user-avatar/user-avatar.js +4 -4
  38. package/dist/{chunk-6JGPPTLX.js → chunk-2GI7ANEL.js} +3 -3
  39. package/dist/{chunk-U2ZDN2AL.js → chunk-2PBGVMM2.js} +2 -2
  40. package/dist/{chunk-ITGG5OKP.js → chunk-5LQ7PKMW.js} +3 -1
  41. package/dist/chunk-5LQ7PKMW.js.map +1 -0
  42. package/dist/{chunk-4T7TXRGM.js → chunk-6ZY5XU4J.js} +3 -3
  43. package/dist/{chunk-CHPZTF34.js → chunk-7KWBGMQC.js} +2 -2
  44. package/dist/{chunk-JT6TERQL.js → chunk-BB2OUERT.js} +2 -2
  45. package/dist/{chunk-XUJG2HFW.js → chunk-BH5BXLGI.js} +2 -2
  46. package/dist/{chunk-NWNSUHEB.js → chunk-CT5K725B.js} +2 -2
  47. package/dist/{chunk-53PDVLRK.js → chunk-E3PUH5IF.js} +3 -3
  48. package/dist/{chunk-LWRMKF7D.js → chunk-FBI6QB3O.js} +6 -6
  49. package/dist/{chunk-ZRPGVHTR.js → chunk-GGI7GM5O.js} +2 -2
  50. package/dist/{chunk-TX5V42AY.js → chunk-HEBQS7QP.js} +3 -3
  51. package/dist/{chunk-A7Q5AQVD.js → chunk-HZVFAGLZ.js} +29 -14
  52. package/dist/chunk-HZVFAGLZ.js.map +1 -0
  53. package/dist/{chunk-BUQ2JUND.js → chunk-JO6BBSGY.js} +2 -2
  54. package/dist/{chunk-4ZV7N5CU.js → chunk-JXPHU3K5.js} +4 -4
  55. package/dist/{chunk-ZXOT3U6U.js → chunk-KFWIOFMS.js} +4 -4
  56. package/dist/{chunk-TW3BXVEU.js → chunk-KMXAPYU2.js} +2 -2
  57. package/dist/{chunk-CJBBGMIA.js → chunk-MNXVXW4T.js} +2 -2
  58. package/dist/{chunk-I3E2QHTH.js → chunk-NLJV4CPF.js} +3 -3
  59. package/dist/{chunk-BPLFM2GL.js → chunk-NYEA7JAI.js} +3 -3
  60. package/dist/{chunk-O6AZDQJJ.js → chunk-OE2ZVL67.js} +2 -2
  61. package/dist/{chunk-4523RYE7.js → chunk-OG2FP6SE.js} +2 -2
  62. package/dist/{chunk-XKV5ZIBG.js → chunk-OGGPQVIG.js} +2 -2
  63. package/dist/{chunk-SQNW22ZZ.js → chunk-OV4WREMW.js} +3 -3
  64. package/dist/{chunk-6LGWJFQK.js → chunk-PFDM2OIP.js} +2 -2
  65. package/dist/{chunk-YTOYMJPM.js → chunk-SLVQ33AN.js} +15 -5
  66. package/dist/chunk-SLVQ33AN.js.map +1 -0
  67. package/dist/{chunk-5OIH36KX.js → chunk-V4FKHLEB.js} +2 -2
  68. package/dist/{chunk-EMMPNB6D.js → chunk-X4Z62ZQY.js} +2 -2
  69. package/dist/{chunk-OP65D4UR.js → chunk-XR6WBW23.js} +2 -2
  70. package/dist/{chunk-4X4KM4ZB.js → chunk-ZZMZF7PZ.js} +2 -2
  71. package/dist/data-display/CardKit.js +5 -5
  72. package/dist/data-display/DataViews.js +25 -25
  73. package/dist/feedback/ConfirmAction.js +4 -4
  74. package/dist/form/Button.js +3 -3
  75. package/dist/form/total-form.js +4 -4
  76. package/dist/native/chunks/{chunk-ZAQ3AZJM.js → chunk-2R2R35C5.js} +2 -2
  77. package/dist/native/chunks/{chunk-ATS7STZJ.js → chunk-WL5LXVCK.js} +1 -1
  78. package/dist/native/chunks/{chunk-ATS7STZJ.js.map → chunk-WL5LXVCK.js.map} +1 -1
  79. package/dist/native/data-display/EmptyState.js +2 -2
  80. package/dist/native/data-display/ErrorState.js +1 -1
  81. package/dist/native/data-display/FleetMap.js +2 -2
  82. package/dist/native/form/Button.js +1 -1
  83. package/dist/navigation/AppHeader.js +4 -4
  84. package/dist/social-login-button.js +4 -4
  85. package/dist/types/components/data-display/DataViews/data-views-search.d.ts.map +1 -1
  86. package/dist/types/components/form/Button/Button.d.ts +7 -3
  87. package/dist/types/components/form/Button/Button.d.ts.map +1 -1
  88. package/dist/types/components/form/Button/Button.metrics.d.ts +25 -0
  89. package/dist/types/components/form/Button/Button.metrics.d.ts.map +1 -1
  90. package/dist/types/components/form/Button/Button.types.d.ts +37 -4
  91. package/dist/types/components/form/Button/Button.types.d.ts.map +1 -1
  92. package/dist/types-native/components/form/Button/Button.metrics.d.ts +25 -0
  93. package/dist/types-native/components/form/Button/Button.metrics.d.ts.map +1 -1
  94. package/dist/user-avatar.js +4 -4
  95. package/package.json +1 -1
  96. package/src/components/data-display/DataViews/data-views-search.tsx +62 -14
  97. package/src/components/form/Button/Button.md +39 -0
  98. package/src/components/form/Button/Button.metrics.ts +26 -0
  99. package/src/components/form/Button/Button.tsx +77 -4
  100. package/src/components/form/Button/Button.types.ts +47 -5
  101. package/dist/chunk-A7Q5AQVD.js.map +0 -1
  102. package/dist/chunk-ITGG5OKP.js.map +0 -1
  103. package/dist/chunk-YTOYMJPM.js.map +0 -1
  104. /package/dist/{chunk-6JGPPTLX.js.map → chunk-2GI7ANEL.js.map} +0 -0
  105. /package/dist/{chunk-U2ZDN2AL.js.map → chunk-2PBGVMM2.js.map} +0 -0
  106. /package/dist/{chunk-4T7TXRGM.js.map → chunk-6ZY5XU4J.js.map} +0 -0
  107. /package/dist/{chunk-CHPZTF34.js.map → chunk-7KWBGMQC.js.map} +0 -0
  108. /package/dist/{chunk-JT6TERQL.js.map → chunk-BB2OUERT.js.map} +0 -0
  109. /package/dist/{chunk-XUJG2HFW.js.map → chunk-BH5BXLGI.js.map} +0 -0
  110. /package/dist/{chunk-NWNSUHEB.js.map → chunk-CT5K725B.js.map} +0 -0
  111. /package/dist/{chunk-53PDVLRK.js.map → chunk-E3PUH5IF.js.map} +0 -0
  112. /package/dist/{chunk-LWRMKF7D.js.map → chunk-FBI6QB3O.js.map} +0 -0
  113. /package/dist/{chunk-ZRPGVHTR.js.map → chunk-GGI7GM5O.js.map} +0 -0
  114. /package/dist/{chunk-TX5V42AY.js.map → chunk-HEBQS7QP.js.map} +0 -0
  115. /package/dist/{chunk-BUQ2JUND.js.map → chunk-JO6BBSGY.js.map} +0 -0
  116. /package/dist/{chunk-4ZV7N5CU.js.map → chunk-JXPHU3K5.js.map} +0 -0
  117. /package/dist/{chunk-ZXOT3U6U.js.map → chunk-KFWIOFMS.js.map} +0 -0
  118. /package/dist/{chunk-TW3BXVEU.js.map → chunk-KMXAPYU2.js.map} +0 -0
  119. /package/dist/{chunk-CJBBGMIA.js.map → chunk-MNXVXW4T.js.map} +0 -0
  120. /package/dist/{chunk-I3E2QHTH.js.map → chunk-NLJV4CPF.js.map} +0 -0
  121. /package/dist/{chunk-BPLFM2GL.js.map → chunk-NYEA7JAI.js.map} +0 -0
  122. /package/dist/{chunk-O6AZDQJJ.js.map → chunk-OE2ZVL67.js.map} +0 -0
  123. /package/dist/{chunk-4523RYE7.js.map → chunk-OG2FP6SE.js.map} +0 -0
  124. /package/dist/{chunk-XKV5ZIBG.js.map → chunk-OGGPQVIG.js.map} +0 -0
  125. /package/dist/{chunk-SQNW22ZZ.js.map → chunk-OV4WREMW.js.map} +0 -0
  126. /package/dist/{chunk-6LGWJFQK.js.map → chunk-PFDM2OIP.js.map} +0 -0
  127. /package/dist/{chunk-5OIH36KX.js.map → chunk-V4FKHLEB.js.map} +0 -0
  128. /package/dist/{chunk-EMMPNB6D.js.map → chunk-X4Z62ZQY.js.map} +0 -0
  129. /package/dist/{chunk-OP65D4UR.js.map → chunk-XR6WBW23.js.map} +0 -0
  130. /package/dist/{chunk-4X4KM4ZB.js.map → chunk-ZZMZF7PZ.js.map} +0 -0
  131. /package/dist/native/chunks/{chunk-ZAQ3AZJM.js.map → chunk-2R2R35C5.js.map} +0 -0
@@ -10,7 +10,7 @@ import SearchIcon from "@mui/icons-material/Search";
10
10
  import IconButton from "@mui/material/IconButton/index.js";
11
11
  import InputAdornment from "@mui/material/InputAdornment/index.js";
12
12
  import TextField from "@mui/material/TextField/index.js";
13
- import { useEffect, useRef, useState } from "react";
13
+ import { useCallback, useEffect, useRef, useState } from "react";
14
14
 
15
15
  import { Box } from "../../../mui/Box";
16
16
  import { useDataViewsCopy } from "./data-views-copy-context";
@@ -33,6 +33,64 @@ function boxWidth(fill: boolean): { flex: number; minWidth: number; maxWidth: nu
33
33
  return { flex: 1, minWidth: fill ? 0 : 200, maxWidth: fill ? "none" : 384 };
34
34
  }
35
35
 
36
+ /**
37
+ * The box's own draft, and the two ways it reaches `onChange`.
38
+ *
39
+ * Extracted from {@link InlineKeyword} so the commit policy reads in one place
40
+ * rather than split across a render body — and because it is where both halves
41
+ * of a defect that lost searches outright had to be fixed.
42
+ *
43
+ * ## The debounce must not be keyed on the handler's identity
44
+ *
45
+ * Every caller builds `onChange` inline — the grid's own bar passes
46
+ * `(value) => c.patch({ search: value })`, rebuilt on each render. Keying the
47
+ * effect on it re-armed the timer on every render, and a busy admin page
48
+ * re-renders more often than every 350ms as its queries land, so the timer
49
+ * never survived to fire and the term was NEVER committed: measured in one
50
+ * adopter as a dead `?q=` on a server-mode grid, where a 15-second loop pressed
51
+ * Enter over and over and the URL never moved. So the handler is read through a
52
+ * ref and the effect depends on `[draft, value]` alone.
53
+ *
54
+ * ## An immediate commit must read the BOX, not the draft
55
+ *
56
+ * A keystroke can land before React has processed the edit before it — a fast
57
+ * typist, and every Playwright `fill()` followed by `press('Enter')`. `draft` is
58
+ * then still the value from before the edit, and comparing THAT to `value` said
59
+ * "nothing changed" and dropped the keystroke, leaving the term on screen with
60
+ * an unfiltered list behind it. `commitNow` takes what the caller read off the
61
+ * element and syncs the draft to it, which also keeps the box from being reset
62
+ * to a stale draft by the next render when a host ignores the commit.
63
+ */
64
+ function useKeywordDraft(
65
+ value: string,
66
+ onChange: (next: string) => void,
67
+ ): { draft: string; setDraft: (next: string) => void; commitNow: (typed: string) => void } {
68
+ const [draft, setDraft] = useState(value);
69
+ const prev = useRef(value);
70
+ // An external change (a saved view, the URL, "Limpar") wins over the draft.
71
+ if (prev.current !== value) {
72
+ prev.current = value;
73
+ if (draft !== value) setDraft(value);
74
+ }
75
+ const commit = useRef(onChange);
76
+ useEffect(() => {
77
+ commit.current = onChange;
78
+ });
79
+ useEffect(() => {
80
+ if (draft === value) return undefined;
81
+ const timer = setTimeout(() => commit.current(draft), SEARCH_DEBOUNCE_MS);
82
+ return () => clearTimeout(timer);
83
+ }, [draft, value]);
84
+ const commitNow = useCallback(
85
+ (typed: string) => {
86
+ setDraft(typed);
87
+ if (typed !== value) commit.current(typed);
88
+ },
89
+ [value],
90
+ );
91
+ return { draft, setDraft, commitNow };
92
+ }
93
+
36
94
  /**
37
95
  * The bar's keyword box.
38
96
  *
@@ -64,18 +122,7 @@ export function InlineKeyword({
64
122
  fill?: boolean;
65
123
  }): React.JSX.Element {
66
124
  const copy = useDataViewsCopy();
67
- const [draft, setDraft] = useState(value);
68
- const prev = useRef(value);
69
- // An external change (a saved view, the URL, "Limpar") wins over the draft.
70
- if (prev.current !== value) {
71
- prev.current = value;
72
- if (draft !== value) setDraft(value);
73
- }
74
- useEffect(() => {
75
- if (draft === value) return undefined;
76
- const timer = setTimeout(() => onChange(draft), SEARCH_DEBOUNCE_MS);
77
- return () => clearTimeout(timer);
78
- }, [draft, value, onChange]);
125
+ const { draft, setDraft, commitNow } = useKeywordDraft(value, onChange);
79
126
 
80
127
  return (
81
128
  <TextField
@@ -94,7 +141,8 @@ export function InlineKeyword({
94
141
  // presses it has decided, and waiting out the timer reads as a stall.
95
142
  if (event.key !== "Enter") return;
96
143
  event.preventDefault();
97
- if (draft !== value) onChange(draft);
144
+ // The BOX, not `draft` — see {@link useKeywordDraft}.
145
+ commitNow(event.target instanceof HTMLInputElement ? event.target.value : draft);
98
146
  }}
99
147
  placeholder={copy.search.placeholder}
100
148
  inputProps={{ "aria-label": copy.search.allColumnsLabel, "data-testid": testId }}
@@ -12,6 +12,8 @@ The Button component is a versatile and highly customizable interactive element
12
12
  - **Special Effects**: glow, pulse animations
13
13
  - **Loading States**: Built-in loading spinner
14
14
  - **Icon Support**: Leading icons with automatic loading state handling
15
+ - **Renders As Anything**: `component` swaps the element, carrying that
16
+ component's own props through the types (a router link, an `<a>`, a label)
15
17
  - **Accessibility**: Full ARIA support, keyboard navigation
16
18
  - **Responsive Design**: Adapts to different screen sizes
17
19
 
@@ -37,6 +39,14 @@ import { Button } from '@procurement/ui';
37
39
  <Button variant="gradient" glow pulse>
38
40
  Special Button
39
41
  </Button>
42
+
43
+ // As a navigation, painted as a button
44
+ <Button component={Link} to="/orders/42">
45
+ Track my order
46
+ </Button>
47
+
48
+ // Or a plain anchor, when there is no router in the tree
49
+ <Button href="/orders/42">Track my order</Button>
40
50
  ```
41
51
 
42
52
  ## Props
@@ -60,6 +70,35 @@ import { Button } from '@procurement/ui';
60
70
  | `onBlur` | `(event: FocusEvent) => void` | - | Blur event handler |
61
71
  | `children` | `ReactNode` | - | Button content |
62
72
  | `className` | `string` | - | Additional CSS classes |
73
+ | `component` | `ElementType` | `'button'` | Render as this element/component instead |
74
+ | `href` | `string` | - | Renders an `<a>` (MUI's own behaviour) |
75
+
76
+ ## Rendering as something else
77
+
78
+ `component` swaps the element the button renders as, and the button's props are
79
+ typed against it — so the target's own props are checked at the call site rather
80
+ than silently dropped:
81
+
82
+ ```tsx
83
+ import { Link } from 'react-router-dom';
84
+
85
+ <Button component={Link} to="/orders/42" variant="solid" size="lg">
86
+ Track my order
87
+ </Button>;
88
+ ```
89
+
90
+ Three things are worth knowing:
91
+
92
+ - **`to` is typed.** `Button` is an `OverridableComponent`, so the generic
93
+ carries the target's props through. Misspell `to` and it fails to compile.
94
+ - **The paint does not change.** Measured against the same button rendered as a
95
+ `<button>`: identical box, background, padding, radius and type; hover still
96
+ darkens; the class still names the size.
97
+ - **`href` alone is enough** when there is no router involved — MUI renders an
98
+ `<a>` for it without a `component`.
99
+
100
+ The event handlers are typed on `HTMLElement` rather than `HTMLButtonElement`
101
+ for this reason: the element under the pointer is not always a button.
63
102
 
64
103
  ## Variants
65
104
 
@@ -34,6 +34,32 @@ export const ICON_ONLY_PADDING: Record<SizeValue, number> = {
34
34
  xl: 11,
35
35
  };
36
36
 
37
+ /**
38
+ * The start/end icon slot's font-size, in px — MUI's `medium` value, pinned
39
+ * (12-74).
40
+ *
41
+ * MUI sizes that slot off its OWN `size` prop, writing 18/20/22 onto the first
42
+ * child of `.MuiButton-startIcon`. We never passed a `size`, so every button in
43
+ * every size has always drawn medium's 20px; now that `size` IS forwarded — so
44
+ * the rendered class finally names the size the button actually is — the number
45
+ * would start moving with it.
46
+ *
47
+ * WHAT THAT ACTUALLY REACHES, measured in a browser rather than assumed: this
48
+ * Button renders its own wrapper span around the icon (it carries the child
49
+ * test id), so MUI's rule lands on the WRAPPER and reaches the glyph only when
50
+ * the glyph takes its size by inheritance — `fontSize="inherit"`, an em-sized
51
+ * SVG, a text or emoji glyph. With the pin removed, such a glyph measured 18px
52
+ * at `xs`/`sm` and 22px at `lg`/`xl` against the 20px it has always been. An
53
+ * icon carrying an absolute size of its own does not move at all: a default
54
+ * `SvgIcon` stayed 24×24 across all five.
55
+ *
56
+ * A silent resize of the inherited case is still a design decision arriving as
57
+ * a side effect of a class-name fix, so it is pinned to what shipped before —
58
+ * `Button.tsx` puts it on that same wrapper. Scaling the glyph with the button
59
+ * is a ticket of its own.
60
+ */
61
+ export const BUTTON_ICON_GLYPH_SIZE = 20;
62
+
37
63
  export const BUTTON_FONT_WEIGHT = 500;
38
64
  /** `theme.spacing(1)`. */
39
65
  export const BUTTON_RADIUS_UNITS = 1;
@@ -4,6 +4,7 @@ import { styled } from '@mui/material/styles/index.js';
4
4
  import * as React from 'react';
5
5
 
6
6
  import { resolveButtonProps } from './Button.helpers';
7
+ import { BUTTON_ICON_GLYPH_SIZE } from './Button.metrics';
7
8
  import { childTestId, resolveTestId, withoutTestIdProps } from '../../../platform/test-id';
8
9
  import {
9
10
  buttonEmphasisStyles,
@@ -12,7 +13,7 @@ import {
12
13
  iconAlignmentStyles,
13
14
  buttonSize,
14
15
  } from './Button.styles';
15
- import type { ButtonProps } from './Button.types';
16
+ import type { ButtonComponent, ButtonProps } from './Button.types';
16
17
 
17
18
  const StyledButton = styled(MuiButton, {
18
19
  shouldForwardProp: (prop) =>
@@ -49,12 +50,53 @@ const muiVariantFor = (variant?: string): 'outlined' | 'text' | 'contained' => {
49
50
 
50
51
  type MuiButtonColor = 'inherit' | 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'error';
51
52
 
53
+ /**
54
+ * Our five sizes collapse onto MUI's three, so the rendered class NAMES the
55
+ * size the button actually is (12-74).
56
+ *
57
+ * It was never forwarded, so every button — `xs` through `xl` — carried
58
+ * `MuiButton-sizeMedium`. The pixels were right (they come from `buttonSize`'s
59
+ * `sx`, which still decides them) but the DOM said "medium" about a large
60
+ * button: a test could never assert the size, and devtools lied to whoever
61
+ * opened them.
62
+ *
63
+ * MUI's own size styles are padding, font-size and the icon glyph. The first
64
+ * two are overridden by that same `sx`; the third is pinned by
65
+ * `BUTTON_ICON_GLYPH_SIZE`, so forwarding this moves nothing on screen.
66
+ */
67
+ const muiSizeFor = (size: NonNullable<ButtonProps['size']>): 'small' | 'medium' | 'large' => {
68
+ if (size === 'xs' || size === 'sm') return 'small';
69
+ return size === 'md' ? 'medium' : 'large';
70
+ };
71
+
52
72
  /** `danger` and `neutral` are ours; the rest are MUI's own names already. */
53
73
  const muiColorFor = (color: NonNullable<ButtonProps['color']>): MuiButtonColor => {
54
74
  if (color === 'danger') return 'error';
55
75
  return color === 'neutral' ? 'inherit' : color;
56
76
  };
57
77
 
78
+ /**
79
+ * The icon slot's font-size, pinned on the wrapper this component already owns
80
+ * (12-74).
81
+ *
82
+ * MUI writes 18/20/22 there off its own `size`, and that prop is now forwarded,
83
+ * so a glyph that takes its size by INHERITANCE would start moving with the
84
+ * button — measured at 18px on `xs` and 22px on `lg` against the 20px every
85
+ * size has always drawn. See {@link BUTTON_ICON_GLYPH_SIZE} for what that does
86
+ * and does not reach.
87
+ *
88
+ * Written as an inline style rather than a selector in `sx` because MUI's own
89
+ * rule sits on the start/end-icon span itself. Beating it from the root means
90
+ * winning a specificity contest at a distance — true in a browser, but not
91
+ * something a test can hold, since jsdom resolves `getComputedStyle` by
92
+ * document order and never by specificity. Inline wins both, and it sits on the
93
+ * one element we already render for this glyph.
94
+ *
95
+ * A number, not `px()`: React writes `font-size: 20px`, which is exactly the
96
+ * declaration MUI was emitting before.
97
+ */
98
+ const ICON_GLYPH_STYLE = { fontSize: BUTTON_ICON_GLYPH_SIZE } as const;
99
+
58
100
  /**
59
101
  * An icon and nothing else — the shape that should render square.
60
102
  *
@@ -71,7 +113,26 @@ function isIconOnly({
71
113
  return !loading && icon != null && children == null;
72
114
  }
73
115
 
74
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
116
+ /**
117
+ * The design system's button.
118
+ *
119
+ * Typed as an `OverridableComponent` (12-70) rather than a plain `forwardRef`,
120
+ * which is how MUI models a component that can render as something else. The
121
+ * practical difference is one line at a call site:
122
+ *
123
+ * ```tsx
124
+ * <Button component={Link} to="/account/compras/42">Acompanhar pedido</Button>
125
+ * ```
126
+ *
127
+ * `to` is typed here because the generic carries the target component's own
128
+ * props through. A plain `component?: React.ElementType` — what this was —
129
+ * accepts the component and throws its props away, so `to` typechecked
130
+ * nowhere even though it arrived intact at runtime. A host wanting a router
131
+ * navigation painted as a button had to choose between a type assertion and
132
+ * losing the `href`; both were shipped, and both are the reason this ticket
133
+ * exists.
134
+ */
135
+ const ButtonRoot = React.forwardRef<HTMLButtonElement, ButtonProps>(
75
136
  (rawProps, ref) => {
76
137
  const {
77
138
  variant,
@@ -99,7 +160,11 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
99
160
 
100
161
  // Wrap icon with testId if provided
101
162
  const iconWithTestId =
102
- !loading && icon ? <span data-testid={testId('icon')}>{icon}</span> : undefined;
163
+ !loading && icon ? (
164
+ <span data-testid={testId('icon')} style={ICON_GLYPH_STYLE}>
165
+ {icon}
166
+ </span>
167
+ ) : undefined;
103
168
 
104
169
  // Only an icon, so the button is a square rather than a 64px slab.
105
170
  const iconOnly = isIconOnly({ loading, icon, children });
@@ -112,6 +177,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
112
177
  ref={ref}
113
178
  variant={muiVariantFor(variant)}
114
179
  color={muiColorFor(color)}
180
+ size={muiSizeFor(size)}
115
181
  customVariant={variant}
116
182
  customColor={color}
117
183
  glow={glow}
@@ -139,4 +205,11 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
139
205
  },
140
206
  );
141
207
 
142
- Button.displayName = 'Button';
208
+ ButtonRoot.displayName = 'Button';
209
+
210
+ /**
211
+ * The cast is what publishes the generic — `forwardRef` cannot express
212
+ * "renders as C, and takes C's props" on its own, so MUI's own components do
213
+ * exactly this. `ButtonRoot` above is the implementation and is not exported.
214
+ */
215
+ export const Button = ButtonRoot as ButtonComponent;
@@ -1,25 +1,67 @@
1
1
  import type { ButtonProps as MuiButtonProps } from '@mui/material/Button/index.js';
2
+ import type { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent/index.js';
2
3
  import type * as React from 'react';
3
4
 
4
5
  import type { ButtonBaseProps } from './Button.base';
5
6
 
6
7
  export type { ButtonBaseProps, ButtonVariant } from './Button.base';
7
8
 
8
- export interface ButtonProps
9
+ /**
10
+ * Everything this Button adds, WITHOUT the element it renders as.
11
+ *
12
+ * `component` is deliberately omitted here and reintroduced by the type map
13
+ * below: MUI models "render as something else" with a generic, and taking
14
+ * `component?: React.ElementType` as a plain prop is exactly what threw the
15
+ * target's own props away (12-70).
16
+ */
17
+ export interface ButtonOwnProps
9
18
  extends ButtonBaseProps,
10
- Omit<MuiButtonProps, 'variant' | 'color' | 'size' | keyof ButtonBaseProps> {
19
+ Omit<MuiButtonProps, 'variant' | 'color' | 'size' | 'component' | keyof ButtonBaseProps> {
11
20
  /**
12
21
  * Click handler
13
22
  */
14
- onClick?: React.MouseEventHandler<HTMLButtonElement>;
23
+ onClick?: React.MouseEventHandler<HTMLElement>;
15
24
 
16
25
  /**
17
26
  * Focus handler
18
27
  */
19
- onFocus?: React.FocusEventHandler<HTMLButtonElement>;
28
+ onFocus?: React.FocusEventHandler<HTMLElement>;
20
29
 
21
30
  /**
22
31
  * Blur handler
23
32
  */
24
- onBlur?: React.FocusEventHandler<HTMLButtonElement>;
33
+ onBlur?: React.FocusEventHandler<HTMLElement>;
25
34
  }
35
+
36
+ /**
37
+ * What `Button` renders as, and what that brings with it.
38
+ *
39
+ * This is the shape MUI's `OverridableComponent` reads. Handing `component` a
40
+ * router `Link` now also hands the call site that Link's OWN props — `to`,
41
+ * `replace`, `state` — typed, which is the whole point of 12-70. Before this,
42
+ * `component` was `React.ElementType` and `to` typechecked nowhere, so a host
43
+ * that wanted a navigation painted as a button had to reach for a type
44
+ * assertion or throw the `href` away.
45
+ */
46
+ export interface ButtonTypeMap<
47
+ AdditionalProps = object,
48
+ RootComponent extends React.ElementType = 'button',
49
+ > {
50
+ props: AdditionalProps & ButtonOwnProps;
51
+ defaultComponent: RootComponent;
52
+ }
53
+
54
+ /**
55
+ * The props of a Button rendered as `RootComponent` — `button` by default.
56
+ *
57
+ * Generic WITH defaults, so every existing `ButtonProps` (no arguments) keeps
58
+ * meaning exactly what it meant before this change: the button-rendered case.
59
+ */
60
+ export type ButtonProps<
61
+ RootComponent extends React.ElementType = 'button',
62
+ AdditionalProps = object,
63
+ > = OverrideProps<ButtonTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
64
+ component?: React.ElementType;
65
+ };
66
+
67
+ export type ButtonComponent = OverridableComponent<ButtonTypeMap>;
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/DataViews/data-views-search.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * THE INLINE BAR'S SEARCH — the one control on the toolbar that cannot be\n * guessed from an icon's position, which is why it is the LAST thing the\n * degradation ladder takes away. See `data-views-overflow.ts`.\n */\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport SearchIcon from \"@mui/icons-material/Search\";\nimport IconButton from \"@mui/material/IconButton/index.js\";\nimport InputAdornment from \"@mui/material/InputAdornment/index.js\";\nimport TextField from \"@mui/material/TextField/index.js\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { Box } from \"../../../mui/Box\";\nimport { useDataViewsCopy } from \"./data-views-copy-context\";\n\n/** How long the box waits after the last keystroke before it queries. */\nconst SEARCH_DEBOUNCE_MS = 350;\n\n/**\n * How wide the keyword box is allowed to be.\n *\n * The 200px floor is right while the box shares the row: a search you cannot\n * read six characters in is not a search. It is WRONG once the box has been\n * expanded out of its collapsed state on a narrow screen, where the cluster was\n * sized for a magnifier — the floor cannot be met there, so the row scrolled\n * sideways instead (154px of overhang at 320px). In that mode the box shrinks,\n * and if what is left would still be unreadable the bar hands it the whole\n * cluster instead (`searchTakeover`).\n */\nfunction boxWidth(fill: boolean): { flex: number; minWidth: number; maxWidth: number | \"none\" } {\n return { flex: 1, minWidth: fill ? 0 : 200, maxWidth: fill ? \"none\" : 384 };\n}\n\n/**\n * The bar's keyword box.\n *\n * DEBOUNCED rather than committed on Enter/blur. In server mode every commit is\n * a round trip, and a box that only fires on blur makes the operator press a\n * key they were not told about to see any result at all — they type, nothing\n * happens, and the list looks broken. 350ms is long enough that a word costs\n * one query, short enough to feel like live search.\n *\n * `Escape` clears; the X clears without leaving the box. Both exist because\n * clearing by selecting-and-deleting is three gestures for the most common\n * thing anyone does to a search box.\n */\nexport function InlineKeyword({\n value,\n onChange,\n testId,\n autoFocus,\n onEscape,\n fill = false,\n}: {\n value: string;\n onChange: (value: string) => void;\n testId?: string;\n autoFocus?: boolean;\n /** Called on Escape with the box already empty — the collapsed bar closes it. */\n onEscape?: () => void;\n /** Take whatever width there is, with no minimum — see {@link boxWidth}. */\n fill?: boolean;\n}): React.JSX.Element {\n const copy = useDataViewsCopy();\n const [draft, setDraft] = useState(value);\n const prev = useRef(value);\n // An external change (a saved view, the URL, \"Limpar\") wins over the draft.\n if (prev.current !== value) {\n prev.current = value;\n if (draft !== value) setDraft(value);\n }\n useEffect(() => {\n if (draft === value) return undefined;\n const timer = setTimeout(() => onChange(draft), SEARCH_DEBOUNCE_MS);\n return () => clearTimeout(timer);\n }, [draft, value, onChange]);\n\n return (\n <TextField\n size=\"small\"\n value={draft}\n autoFocus={autoFocus}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n if (draft === \"\") onEscape?.();\n setDraft(\"\");\n return;\n }\n // Enter still commits IMMEDIATELY, ahead of the debounce — someone who\n // presses it has decided, and waiting out the timer reads as a stall.\n if (event.key !== \"Enter\") return;\n event.preventDefault();\n if (draft !== value) onChange(draft);\n }}\n placeholder={copy.search.placeholder}\n inputProps={{ \"aria-label\": copy.search.allColumnsLabel, \"data-testid\": testId }}\n InputProps={{\n startAdornment: (\n <InputAdornment position=\"start\">\n <SearchIcon sx={{ fontSize: 16, color: \"text.secondary\" }} />\n </InputAdornment>\n ),\n endAdornment: draft ? (\n <InputAdornment position=\"end\">\n <IconButton\n size=\"small\"\n aria-label={copy.search.clear}\n data-testid={testId ? `${testId}-clear` : undefined}\n onClick={() => setDraft(\"\")}\n sx={{ p: 0.25 }}\n >\n <CloseIcon sx={{ fontSize: 14 }} />\n </IconButton>\n </InputAdornment>\n ) : undefined,\n }}\n // Takes the free space and gives it up first — step 3 of the ladder,\n // which needs no flag because flex does it (see `computeSplit`).\n sx={boxWidth(fill)}\n />\n );\n}\n\n/**\n * Step 4 of the ladder: the search as a magnifier.\n *\n * Carries a dot when a search is APPLIED. Collapsing a control that is\n * currently filtering the list, with nothing saying so, is how an operator ends\n * up staring at 3 rows of 214 and reading it as missing data.\n */\nexport function CollapsedSearch({\n active,\n onOpen,\n testId,\n}: {\n active: boolean;\n onOpen: () => void;\n testId?: string;\n}): React.JSX.Element {\n const copy = useDataViewsCopy();\n return (\n <IconButton\n size=\"small\"\n onClick={onOpen}\n aria-label={copy.search.open}\n aria-expanded={false}\n data-testid={testId ? `${testId}-collapsed` : undefined}\n sx={{\n position: \"relative\",\n border: 1,\n borderRadius: 1,\n borderColor: active ? \"primary.main\" : \"divider\",\n color: active ? \"primary.main\" : \"text.secondary\",\n bgcolor: active ? \"action.selected\" : \"background.paper\",\n }}\n >\n <SearchIcon sx={{ fontSize: 18 }} />\n {active && (\n <Box\n component=\"span\"\n sx={{\n position: \"absolute\",\n top: 2,\n right: 2,\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n bgcolor: \"primary.main\",\n }}\n />\n )}\n </IconButton>\n );\n}\n\n"],"mappings":"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","names":["default"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/Button/Button.metrics.ts"],"sourcesContent":["import { darken, lighten } from '../../../tokens/color';\nimport type { ColorValue, SizeValue } from '../../../tokens/vocabulary';\nimport type { UiPaletteColor, UiTheme } from '../../../tokens/theme';\n\n/**\n * THE NUMBERS BOTH `Button` RENDERERS DRAW WITH.\n *\n * `Button.styles.ts` (web, emotion) and `Button.native.tsx` (React Native)\n * read this one table. The web turns the px into rem and the padding into a\n * shorthand string; native uses the numbers as they are. Neither restates a\n * value, which is what keeps a native `md` button 16px tall of padding and\n * 16px of type exactly like the web one.\n */\nexport interface ButtonSizeMetrics {\n paddingVertical: number;\n paddingHorizontal: number;\n fontSize: number;\n}\n\nexport const BUTTON_SIZES: Record<SizeValue, ButtonSizeMetrics> = {\n xs: { paddingVertical: 2, paddingHorizontal: 8, fontSize: 12 },\n sm: { paddingVertical: 6, paddingHorizontal: 12, fontSize: 14 },\n md: { paddingVertical: 8, paddingHorizontal: 16, fontSize: 16 },\n lg: { paddingVertical: 10, paddingHorizontal: 20, fontSize: 18 },\n xl: { paddingVertical: 12, paddingHorizontal: 24, fontSize: 20 },\n};\n\n/** A button that is only an icon is square: uniform padding, no min-width. */\nexport const ICON_ONLY_PADDING: Record<SizeValue, number> = {\n xs: 2,\n sm: 5,\n md: 7,\n lg: 9,\n xl: 11,\n};\n\nexport const BUTTON_FONT_WEIGHT = 500;\n/** `theme.spacing(1)`. */\nexport const BUTTON_RADIUS_UNITS = 1;\n/** Between the icon and the label: `theme.spacing(0.5)`. */\nexport const BUTTON_ICON_GAP_UNITS = 0.5;\nexport const BUTTON_SPINNER_SIZE = 16;\n/** MUI's `line-height` for button text. */\nexport const BUTTON_LINE_HEIGHT = 1.75;\n\n/** The wash behind an outline, ghost or text button while it is hovered or pressed. */\nexport const BUTTON_WASH_ALPHA = 0.1;\nexport const GLASS_BACKGROUND_ALPHA = 0.1;\nexport const GLASS_BACKGROUND_ALPHA_PRESSED = 0.2;\nexport const GLASS_BORDER_ALPHA = 0.2;\nexport const GLOW_INNER_ALPHA = 0.6;\nexport const GLOW_OUTER_ALPHA = 0.3;\nexport const GLOW_RADIUS = 20;\nexport const PULSE_ALPHA = 0.3;\nexport const PULSE_DURATION_MS = 2000;\nexport const PULSE_SPREAD = 15;\n\n/**\n * `neutral` has no palette slot of its own in MUI, so `Button.styles.ts` has\n * always drawn it from three greys. Same three greys here.\n */\nexport function buttonPalette(theme: UiTheme, color: ColorValue): UiPaletteColor {\n if (color === 'neutral') {\n return {\n main: theme.palette.grey[700],\n dark: theme.palette.grey[800],\n light: theme.palette.grey[500],\n contrastText: '#fff',\n };\n }\n return theme.palette[color];\n}\n\n/**\n * The label colour for `ghost` and `text`: one step darker than `main` in\n * light mode (lighter in dark), because `main` is chosen to sit UNDER white and\n * is not legible AS text on a tinted surface. See the web `quietInk`.\n */\nexport const quietInk = (theme: UiTheme, palette: UiPaletteColor): string =>\n theme.mode === 'dark' ? lighten(palette.main, 0.35) : darken(palette.main, 0.3);\n\n/** Gradients run between two related hues; the named colours pick a specific pair. */\nexport function gradientStops(\n theme: UiTheme,\n color: ColorValue,\n palette: UiPaletteColor,\n): [string, string] {\n switch (color) {\n case 'primary':\n return [theme.palette.primary.main, theme.palette.secondary.main];\n case 'secondary':\n return [theme.palette.secondary.main, theme.palette.primary.main];\n case 'success':\n case 'warning':\n return [theme.palette[color].light, theme.palette[color].dark];\n case 'danger':\n return [theme.palette.danger.light, theme.palette.danger.dark];\n default:\n return [palette.main, palette.dark];\n }\n}\n"],"mappings":";;;;;;AAmBO,IAAM,eAAqD;AAAA,EAChE,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,UAAU,GAAG;AAAA,EAC7D,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,IAAI,UAAU,GAAG;AAAA,EAC9D,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,IAAI,UAAU,GAAG;AAAA,EAC9D,IAAI,EAAE,iBAAiB,IAAI,mBAAmB,IAAI,UAAU,GAAG;AAAA,EAC/D,IAAI,EAAE,iBAAiB,IAAI,mBAAmB,IAAI,UAAU,GAAG;AACjE;AAGO,IAAM,oBAA+C;AAAA,EAC1D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEO,IAAM,qBAAqB;AAE3B,IAAM,sBAAsB;AAE5B,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB;AAG3B,IAAM,oBAAoB;AAC1B,IAAM,yBAAyB;AAC/B,IAAM,iCAAiC;AACvC,IAAM,qBAAqB;AAC3B,IAAM,mBAAmB;AACzB,IAAM,mBAAmB;AACzB,IAAM,cAAc;AACpB,IAAM,cAAc;AACpB,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAMrB,SAAS,cAAc,OAAgB,OAAmC;AAC/E,MAAI,UAAU,WAAW;AACvB,WAAO;AAAA,MACL,MAAM,MAAM,QAAQ,KAAK,GAAG;AAAA,MAC5B,MAAM,MAAM,QAAQ,KAAK,GAAG;AAAA,MAC5B,OAAO,MAAM,QAAQ,KAAK,GAAG;AAAA,MAC7B,cAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO,MAAM,QAAQ,KAAK;AAC5B;AAOO,IAAM,WAAW,CAAC,OAAgB,YACvC,MAAM,SAAS,SAAS,QAAQ,QAAQ,MAAM,IAAI,IAAI,OAAO,QAAQ,MAAM,GAAG;AAGzE,SAAS,cACd,OACA,OACA,SACkB;AAClB,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,CAAC,MAAM,QAAQ,QAAQ,MAAM,MAAM,QAAQ,UAAU,IAAI;AAAA,IAClE,KAAK;AACH,aAAO,CAAC,MAAM,QAAQ,UAAU,MAAM,MAAM,QAAQ,QAAQ,IAAI;AAAA,IAClE,KAAK;AAAA,IACL,KAAK;AACH,aAAO,CAAC,MAAM,QAAQ,KAAK,EAAE,OAAO,MAAM,QAAQ,KAAK,EAAE,IAAI;AAAA,IAC/D,KAAK;AACH,aAAO,CAAC,MAAM,QAAQ,OAAO,OAAO,MAAM,QAAQ,OAAO,IAAI;AAAA,IAC/D;AACE,aAAO,CAAC,QAAQ,MAAM,QAAQ,IAAI;AAAA,EACtC;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/Button/Button.tsx"],"sourcesContent":["import MuiButton from '@mui/material/Button/index.js';\nimport CircularProgress from '@mui/material/CircularProgress/index.js';\nimport { styled } from '@mui/material/styles/index.js';\nimport * as React from 'react';\n\nimport { resolveButtonProps } from './Button.helpers';\nimport { childTestId, resolveTestId, withoutTestIdProps } from '../../../platform/test-id';\nimport {\n buttonEmphasisStyles,\n buttonVariantStyles,\n getColorFromTheme,\n iconAlignmentStyles,\n buttonSize,\n} from './Button.styles';\nimport type { ButtonProps } from './Button.types';\n\nconst StyledButton = styled(MuiButton, {\n shouldForwardProp: (prop) =>\n ['glow', 'pulse', 'loading', 'customVariant', 'customColor', 'ripple'].indexOf(\n prop as string,\n ) === -1,\n})<{\n customVariant?: string;\n customColor?: string;\n glow?: boolean;\n pulse?: boolean;\n ripple?: boolean;\n}>(({ theme, customVariant, customColor = 'primary', glow, pulse }) => {\n const colorPalette = getColorFromTheme(theme, customColor);\n\n return {\n textTransform: 'none',\n fontWeight: 500,\n borderRadius: theme.spacing(1),\n transition: 'all 0.3s ease',\n position: 'relative',\n overflow: 'hidden',\n ...iconAlignmentStyles(theme),\n ...buttonVariantStyles(theme, customVariant, colorPalette, customColor),\n ...buttonEmphasisStyles(colorPalette, glow, pulse),\n };\n});\n\n/** Our six variants collapse onto MUI's three; only outline and text differ. */\nconst muiVariantFor = (variant?: string): 'outlined' | 'text' | 'contained' => {\n if (variant === 'outline') return 'outlined';\n return variant === 'text' ? 'text' : 'contained';\n};\n\ntype MuiButtonColor = 'inherit' | 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'error';\n\n/** `danger` and `neutral` are ours; the rest are MUI's own names already. */\nconst muiColorFor = (color: NonNullable<ButtonProps['color']>): MuiButtonColor => {\n if (color === 'danger') return 'error';\n return color === 'neutral' ? 'inherit' : color;\n};\n\n/**\n * An icon and nothing else — the shape that should render square.\n *\n * A separate predicate rather than three clauses inline: the component function\n * is already at the complexity ceiling, and \"what counts as an icon button\" is\n * a rule worth naming anyway. `loading` disqualifies, because a spinner\n * replaces the children and would otherwise make every loading button square.\n */\nfunction isIconOnly({\n loading,\n icon,\n children,\n}: Pick<ButtonProps, 'loading' | 'icon' | 'children'>): boolean {\n return !loading && icon != null && children == null;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (rawProps, ref) => {\n const {\n variant,\n color,\n size,\n loading,\n icon,\n iconPosition,\n glow,\n pulse,\n ripple,\n active,\n children,\n disabled,\n onClick,\n onFocus,\n onBlur,\n ...others\n } = resolveButtonProps(rawProps);\n // Every spelling of the test id the shared contract allows, mapped to the\n // one the DOM reads; the native Button does the same in reverse.\n const ownTestId = resolveTestId(others, 'button');\n const testId = (suffix: string) => childTestId(others, suffix, 'button');\n const props = withoutTestIdProps(others);\n\n // Wrap icon with testId if provided\n const iconWithTestId =\n !loading && icon ? <span data-testid={testId('icon')}>{icon}</span> : undefined;\n\n // Only an icon, so the button is a square rather than a 64px slab.\n const iconOnly = isIconOnly({ loading, icon, children });\n\n const mergedClassName =\n [active ? 'active' : '', props.className].filter(Boolean).join(' ') || undefined;\n\n return (\n <StyledButton\n ref={ref}\n variant={muiVariantFor(variant)}\n color={muiColorFor(color)}\n customVariant={variant}\n customColor={color}\n glow={glow}\n pulse={pulse}\n ripple={ripple}\n disabled={disabled || loading}\n disableRipple={!ripple}\n startIcon={iconPosition === 'left' ? iconWithTestId : undefined}\n endIcon={iconPosition === 'right' ? iconWithTestId : undefined}\n onClick={onClick}\n onFocus={onFocus}\n onBlur={onBlur}\n sx={buttonSize(size, iconOnly)}\n data-testid={ownTestId}\n {...props}\n className={mergedClassName}\n >\n {loading ? (\n <CircularProgress size={16} color=\"inherit\" data-testid={testId('loading')} />\n ) : (\n children\n )}\n </StyledButton>\n );\n },\n);\n\nButton.displayName = 'Button';\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,OAAO,eAAe;AACtB,OAAO,sBAAsB;AAC7B,SAAS,cAAc;AACvB,YAAY,WAAW;AAkGE;AArFzB,IAAM,eAAe,OAAO,WAAW;AAAA,EACrC,mBAAmB,CAAC,SAClB,CAAC,QAAQ,SAAS,WAAW,iBAAiB,eAAe,QAAQ,EAAE;AAAA,IACrE;AAAA,EACF,MAAM;AACV,CAAC,EAME,CAAC,EAAE,OAAO,eAAe,cAAc,WAAW,MAAM,MAAM,MAAM;AACrE,QAAM,eAAe,kBAAkB,OAAO,WAAW;AAEzD,SAAO;AAAA,IACL,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,cAAc,MAAM,QAAQ,CAAC;AAAA,IAC7B,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,UAAU;AAAA,IACV,GAAG,oBAAoB,KAAK;AAAA,IAC5B,GAAG,oBAAoB,OAAO,eAAe,cAAc,WAAW;AAAA,IACtE,GAAG,qBAAqB,cAAc,MAAM,KAAK;AAAA,EACnD;AACF,CAAC;AAGD,IAAM,gBAAgB,CAAC,YAAwD;AAC7E,MAAI,YAAY,UAAW,QAAO;AAClC,SAAO,YAAY,SAAS,SAAS;AACvC;AAKA,IAAM,cAAc,CAAC,UAA6D;AAChF,MAAI,UAAU,SAAU,QAAO;AAC/B,SAAO,UAAU,YAAY,YAAY;AAC3C;AAUA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AACF,GAAgE;AAC9D,SAAO,CAAC,WAAW,QAAQ,QAAQ,YAAY;AACjD;AAEO,IAAM,SAAe;AAAA,EAC1B,CAAC,UAAU,QAAQ;AACjB,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL,IAAI,mBAAmB,QAAQ;AAG/B,UAAM,YAAY,cAAc,QAAQ,QAAQ;AAChD,UAAM,SAAS,CAAC,WAAmB,YAAY,QAAQ,QAAQ,QAAQ;AACvE,UAAM,QAAQ,mBAAmB,MAAM;AAGvC,UAAM,iBACJ,CAAC,WAAW,OAAO,oBAAC,UAAK,eAAa,OAAO,MAAM,GAAI,gBAAK,IAAU;AAGxE,UAAM,WAAW,WAAW,EAAE,SAAS,MAAM,SAAS,CAAC;AAEvD,UAAM,kBACJ,CAAC,SAAS,WAAW,IAAI,MAAM,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAEzE,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,SAAS,cAAc,OAAO;AAAA,QAC9B,OAAO,YAAY,KAAK;AAAA,QACxB,eAAe;AAAA,QACf,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,YAAY;AAAA,QACtB,eAAe,CAAC;AAAA,QAChB,WAAW,iBAAiB,SAAS,iBAAiB;AAAA,QACtD,SAAS,iBAAiB,UAAU,iBAAiB;AAAA,QACrD;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI,WAAW,MAAM,QAAQ;AAAA,QAC7B,eAAa;AAAA,QACZ,GAAG;AAAA,QACJ,WAAW;AAAA,QAEV,oBACC,oBAAC,oBAAiB,MAAM,IAAI,OAAM,WAAU,eAAa,OAAO,SAAS,GAAG,IAE5E;AAAA;AAAA,IAEJ;AAAA,EAEJ;AACF;AAEA,OAAO,cAAc;","names":[]}