@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.
- package/components-catalog.md +1 -1
- package/dist/_internal/components/data-display/CardKit/CardKebab.js +4 -4
- package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +4 -4
- package/dist/_internal/components/data-display/CardKit/use-row-confirm.js +4 -4
- package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +19 -19
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +25 -25
- package/dist/_internal/components/data-display/DataViews/DeleteViewDialog.js +4 -4
- package/dist/_internal/components/data-display/DataViews/ManageViewsDialog.js +5 -5
- package/dist/_internal/components/data-display/DataViews/SaveViewModal.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-columns.js +5 -5
- package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +5 -5
- package/dist/_internal/components/data-display/DataViews/data-views-empty.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-export.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-grid-helpers.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +16 -16
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-search.js +1 -1
- package/dist/_internal/components/data-display/DataViews/data-views-select-all-strip.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +9 -9
- package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +7 -7
- package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +8 -8
- package/dist/_internal/components/data-display/DataViews/data-views-view-nav.js +4 -4
- package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +6 -6
- package/dist/_internal/components/feedback/ConfirmAction/ConfirmButton.js +4 -4
- package/dist/_internal/components/feedback/index.js +4 -4
- package/dist/_internal/components/form/Button/Button.js +3 -3
- package/dist/_internal/components/form/Button/Button.metrics.js +3 -1
- package/dist/_internal/components/form/Button/Button.styles.js +2 -2
- package/dist/_internal/components/form/index.js +3 -3
- package/dist/_internal/components/form/total-form/SubmitButton.js +4 -4
- package/dist/_internal/components/index.js +5 -5
- package/dist/_internal/components/navigation/AppHeader/AppHeader.details.js +4 -4
- package/dist/_internal/components/navigation/index.js +4 -4
- package/dist/_internal/social-login-button/social-login-button.js +4 -4
- package/dist/_internal/user-avatar/user-avatar.js +4 -4
- package/dist/{chunk-6JGPPTLX.js → chunk-2GI7ANEL.js} +3 -3
- package/dist/{chunk-U2ZDN2AL.js → chunk-2PBGVMM2.js} +2 -2
- package/dist/{chunk-ITGG5OKP.js → chunk-5LQ7PKMW.js} +3 -1
- package/dist/chunk-5LQ7PKMW.js.map +1 -0
- package/dist/{chunk-4T7TXRGM.js → chunk-6ZY5XU4J.js} +3 -3
- package/dist/{chunk-CHPZTF34.js → chunk-7KWBGMQC.js} +2 -2
- package/dist/{chunk-JT6TERQL.js → chunk-BB2OUERT.js} +2 -2
- package/dist/{chunk-XUJG2HFW.js → chunk-BH5BXLGI.js} +2 -2
- package/dist/{chunk-NWNSUHEB.js → chunk-CT5K725B.js} +2 -2
- package/dist/{chunk-53PDVLRK.js → chunk-E3PUH5IF.js} +3 -3
- package/dist/{chunk-LWRMKF7D.js → chunk-FBI6QB3O.js} +6 -6
- package/dist/{chunk-ZRPGVHTR.js → chunk-GGI7GM5O.js} +2 -2
- package/dist/{chunk-TX5V42AY.js → chunk-HEBQS7QP.js} +3 -3
- package/dist/{chunk-A7Q5AQVD.js → chunk-HZVFAGLZ.js} +29 -14
- package/dist/chunk-HZVFAGLZ.js.map +1 -0
- package/dist/{chunk-BUQ2JUND.js → chunk-JO6BBSGY.js} +2 -2
- package/dist/{chunk-4ZV7N5CU.js → chunk-JXPHU3K5.js} +4 -4
- package/dist/{chunk-ZXOT3U6U.js → chunk-KFWIOFMS.js} +4 -4
- package/dist/{chunk-TW3BXVEU.js → chunk-KMXAPYU2.js} +2 -2
- package/dist/{chunk-CJBBGMIA.js → chunk-MNXVXW4T.js} +2 -2
- package/dist/{chunk-I3E2QHTH.js → chunk-NLJV4CPF.js} +3 -3
- package/dist/{chunk-BPLFM2GL.js → chunk-NYEA7JAI.js} +3 -3
- package/dist/{chunk-O6AZDQJJ.js → chunk-OE2ZVL67.js} +2 -2
- package/dist/{chunk-4523RYE7.js → chunk-OG2FP6SE.js} +2 -2
- package/dist/{chunk-XKV5ZIBG.js → chunk-OGGPQVIG.js} +2 -2
- package/dist/{chunk-SQNW22ZZ.js → chunk-OV4WREMW.js} +3 -3
- package/dist/{chunk-6LGWJFQK.js → chunk-PFDM2OIP.js} +2 -2
- package/dist/{chunk-YTOYMJPM.js → chunk-SLVQ33AN.js} +15 -5
- package/dist/chunk-SLVQ33AN.js.map +1 -0
- package/dist/{chunk-5OIH36KX.js → chunk-V4FKHLEB.js} +2 -2
- package/dist/{chunk-EMMPNB6D.js → chunk-X4Z62ZQY.js} +2 -2
- package/dist/{chunk-OP65D4UR.js → chunk-XR6WBW23.js} +2 -2
- package/dist/{chunk-4X4KM4ZB.js → chunk-ZZMZF7PZ.js} +2 -2
- package/dist/data-display/CardKit.js +5 -5
- package/dist/data-display/DataViews.js +25 -25
- package/dist/feedback/ConfirmAction.js +4 -4
- package/dist/form/Button.js +3 -3
- package/dist/form/total-form.js +4 -4
- package/dist/native/chunks/{chunk-ZAQ3AZJM.js → chunk-2R2R35C5.js} +2 -2
- package/dist/native/chunks/{chunk-ATS7STZJ.js → chunk-WL5LXVCK.js} +1 -1
- package/dist/native/chunks/{chunk-ATS7STZJ.js.map → chunk-WL5LXVCK.js.map} +1 -1
- package/dist/native/data-display/EmptyState.js +2 -2
- package/dist/native/data-display/ErrorState.js +1 -1
- package/dist/native/data-display/FleetMap.js +2 -2
- package/dist/native/form/Button.js +1 -1
- package/dist/navigation/AppHeader.js +4 -4
- package/dist/social-login-button.js +4 -4
- package/dist/types/components/data-display/DataViews/data-views-search.d.ts.map +1 -1
- package/dist/types/components/form/Button/Button.d.ts +7 -3
- package/dist/types/components/form/Button/Button.d.ts.map +1 -1
- package/dist/types/components/form/Button/Button.metrics.d.ts +25 -0
- package/dist/types/components/form/Button/Button.metrics.d.ts.map +1 -1
- package/dist/types/components/form/Button/Button.types.d.ts +37 -4
- package/dist/types/components/form/Button/Button.types.d.ts.map +1 -1
- package/dist/types-native/components/form/Button/Button.metrics.d.ts +25 -0
- package/dist/types-native/components/form/Button/Button.metrics.d.ts.map +1 -1
- package/dist/user-avatar.js +4 -4
- package/package.json +1 -1
- package/src/components/data-display/DataViews/data-views-search.tsx +62 -14
- package/src/components/form/Button/Button.md +39 -0
- package/src/components/form/Button/Button.metrics.ts +26 -0
- package/src/components/form/Button/Button.tsx +77 -4
- package/src/components/form/Button/Button.types.ts +47 -5
- package/dist/chunk-A7Q5AQVD.js.map +0 -1
- package/dist/chunk-ITGG5OKP.js.map +0 -1
- package/dist/chunk-YTOYMJPM.js.map +0 -1
- /package/dist/{chunk-6JGPPTLX.js.map → chunk-2GI7ANEL.js.map} +0 -0
- /package/dist/{chunk-U2ZDN2AL.js.map → chunk-2PBGVMM2.js.map} +0 -0
- /package/dist/{chunk-4T7TXRGM.js.map → chunk-6ZY5XU4J.js.map} +0 -0
- /package/dist/{chunk-CHPZTF34.js.map → chunk-7KWBGMQC.js.map} +0 -0
- /package/dist/{chunk-JT6TERQL.js.map → chunk-BB2OUERT.js.map} +0 -0
- /package/dist/{chunk-XUJG2HFW.js.map → chunk-BH5BXLGI.js.map} +0 -0
- /package/dist/{chunk-NWNSUHEB.js.map → chunk-CT5K725B.js.map} +0 -0
- /package/dist/{chunk-53PDVLRK.js.map → chunk-E3PUH5IF.js.map} +0 -0
- /package/dist/{chunk-LWRMKF7D.js.map → chunk-FBI6QB3O.js.map} +0 -0
- /package/dist/{chunk-ZRPGVHTR.js.map → chunk-GGI7GM5O.js.map} +0 -0
- /package/dist/{chunk-TX5V42AY.js.map → chunk-HEBQS7QP.js.map} +0 -0
- /package/dist/{chunk-BUQ2JUND.js.map → chunk-JO6BBSGY.js.map} +0 -0
- /package/dist/{chunk-4ZV7N5CU.js.map → chunk-JXPHU3K5.js.map} +0 -0
- /package/dist/{chunk-ZXOT3U6U.js.map → chunk-KFWIOFMS.js.map} +0 -0
- /package/dist/{chunk-TW3BXVEU.js.map → chunk-KMXAPYU2.js.map} +0 -0
- /package/dist/{chunk-CJBBGMIA.js.map → chunk-MNXVXW4T.js.map} +0 -0
- /package/dist/{chunk-I3E2QHTH.js.map → chunk-NLJV4CPF.js.map} +0 -0
- /package/dist/{chunk-BPLFM2GL.js.map → chunk-NYEA7JAI.js.map} +0 -0
- /package/dist/{chunk-O6AZDQJJ.js.map → chunk-OE2ZVL67.js.map} +0 -0
- /package/dist/{chunk-4523RYE7.js.map → chunk-OG2FP6SE.js.map} +0 -0
- /package/dist/{chunk-XKV5ZIBG.js.map → chunk-OGGPQVIG.js.map} +0 -0
- /package/dist/{chunk-SQNW22ZZ.js.map → chunk-OV4WREMW.js.map} +0 -0
- /package/dist/{chunk-6LGWJFQK.js.map → chunk-PFDM2OIP.js.map} +0 -0
- /package/dist/{chunk-5OIH36KX.js.map → chunk-V4FKHLEB.js.map} +0 -0
- /package/dist/{chunk-EMMPNB6D.js.map → chunk-X4Z62ZQY.js.map} +0 -0
- /package/dist/{chunk-OP65D4UR.js.map → chunk-XR6WBW23.js.map} +0 -0
- /package/dist/{chunk-4X4KM4ZB.js.map → chunk-ZZMZF7PZ.js.map} +0 -0
- /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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
23
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Focus handler
|
|
18
27
|
*/
|
|
19
|
-
onFocus?: React.FocusEventHandler<
|
|
28
|
+
onFocus?: React.FocusEventHandler<HTMLElement>;
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Blur handler
|
|
23
32
|
*/
|
|
24
|
-
onBlur?: React.FocusEventHandler<
|
|
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":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|