asma-ui-core 3.68.1 → 3.68.3
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/Readme.md
CHANGED
|
@@ -1,76 +1,517 @@
|
|
|
1
|
-
#
|
|
1
|
+
# asma-ui-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The ASMA design system — the shared React component library that ASMA frontends build their UI from.
|
|
4
|
+
Form controls, selects and comboboxes, dialogs, menus, tooltips, tables, date/time pickers, ~180
|
|
5
|
+
icons, and the colour and typography tokens behind them.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- MUI often has documentation on how to create composed components.
|
|
7
|
+
**This library has no MUI and no Base UI.** Every component is a native implementation on plain HTML,
|
|
8
|
+
ARIA and Tailwind. That rewrite cut the dependency footprint from ~324 KB to ~57 KB gzipped — see
|
|
9
|
+
[Bundle size](#bundle-size-and-the-mui-removal).
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
- **New here?** Start with [Quick start](#quick-start), then browse [What's in the box](#whats-in-the-box).
|
|
12
|
+
- **Upgrading an app from an older version?** Read [Migrating from the MUI-era version](#migrating-from-the-mui-era-version) first — some things break silently.
|
|
13
|
+
- **Something looks wrong?** [Troubleshooting](#troubleshooting) covers the usual suspects.
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
---
|
|
13
16
|
|
|
14
|
-
##
|
|
17
|
+
## Contents
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
- [Requirements](#requirements)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [What's in the box](#whats-in-the-box)
|
|
22
|
+
- [Figma ↔ React matching](#figma--react-matching)
|
|
23
|
+
- [Styling API](#styling-api)
|
|
24
|
+
- [Theming](#theming)
|
|
25
|
+
- [Bundle size and the MUI removal](#bundle-size-and-the-mui-removal)
|
|
26
|
+
- [Migrating from the MUI-era version](#migrating-from-the-mui-era-version)
|
|
27
|
+
- [Troubleshooting](#troubleshooting)
|
|
28
|
+
- [Contributing to the library](#contributing-to-the-library)
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
---
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
## Requirements
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
| | |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| React | 18 or 19 (peer dependency) |
|
|
37
|
+
| Tailwind CSS | v3 or v4 — both are in use across ASMA apps; wiring differs slightly, see below |
|
|
38
|
+
| Bundler | Anything ESM-native (Vite is what every ASMA app uses) |
|
|
39
|
+
| Package | Public on npm as `asma-ui-core` — no registry setup needed |
|
|
23
40
|
|
|
24
|
-
|
|
41
|
+
The library ships prebuilt ESM in `dist/` with TypeScript declarations. Its own runtime dependencies
|
|
42
|
+
(`@floating-ui/react`, `@tanstack/react-table`, `@dnd-kit/*`, `date-fns`, `notistack`,
|
|
43
|
+
`react-day-picker`, `clsx`) install transitively — you do not declare them yourself.
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
- ctix is used for automatically create index files for exporting componens. Run "npm run create-index"
|
|
28
|
-
- 'src/styles/index.ccss' is imported to 'src/theme/customMuiColors.ts' so exporting works with ctix.
|
|
45
|
+
---
|
|
29
46
|
|
|
30
|
-
##
|
|
47
|
+
## Quick start
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
### 1. Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm add asma-ui-core
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Import the stylesheet
|
|
56
|
+
|
|
57
|
+
`dist/style.css` carries the design tokens, the component styles and a required reset for native
|
|
58
|
+
popover elements. **Nothing renders correctly without it.** Import it *before* Tailwind so your own
|
|
59
|
+
utilities can win.
|
|
60
|
+
|
|
61
|
+
**Tailwind v4:**
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* src/styles/index.css */
|
|
65
|
+
@config '../../tailwind.config.ts';
|
|
66
|
+
|
|
67
|
+
@import 'asma-ui-core/dist/style.css';
|
|
68
|
+
@import 'tailwindcss';
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Tailwind v3:**
|
|
72
|
+
|
|
73
|
+
```scss
|
|
74
|
+
// src/styles/index.scss
|
|
75
|
+
@use 'asma-ui-core/dist/style.css' as core;
|
|
76
|
+
@use 'tailwindcss/base';
|
|
77
|
+
@use 'tailwindcss/components';
|
|
78
|
+
@use 'tailwindcss/utilities';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Extend your Tailwind config with the shared tokens
|
|
82
|
+
|
|
83
|
+
The design tokens live in `tw-configs/twConfigs.json` — colours, shadows, animations, keyframes and
|
|
84
|
+
the font stack. Spread them into your own config rather than redefining them; a token change belongs
|
|
85
|
+
in this library, not in your app.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// tailwind.config.ts
|
|
89
|
+
import twConfigs from 'asma-ui-core/tw-configs/twConfigs.json'
|
|
90
|
+
import type { Config } from 'tailwindcss'
|
|
91
|
+
|
|
92
|
+
const { boxShadow, animation, keyframes, colors, fontFamily } = twConfigs
|
|
93
|
+
|
|
94
|
+
export default {
|
|
95
|
+
content: ['index.html', 'src/**/*.{tsx,ts,jsx,js}'],
|
|
96
|
+
darkMode: 'media',
|
|
97
|
+
important: true,
|
|
98
|
+
plugins: [],
|
|
99
|
+
theme: {
|
|
100
|
+
extend: { animation, boxShadow, colors, keyframes },
|
|
101
|
+
fontFamily,
|
|
52
102
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
} satisfies Config
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Two notes on this config:
|
|
107
|
+
|
|
108
|
+
- **`important: true` is recommended, not decorative.** This library's own compiled CSS is built with
|
|
109
|
+
it, so its utilities emit as `!important`. If your app's utilities are *not*, you will lose class
|
|
110
|
+
conflicts against the library in confusing ways. Match it.
|
|
111
|
+
- ASMA apps additionally import `twScreens` from `asma-types` and pass it as `theme.extend.screens`
|
|
112
|
+
to share breakpoints. That package is internal — outside ASMA, just use your own breakpoints.
|
|
113
|
+
|
|
114
|
+
### 4. Render something
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { StyledButton, StyledInputField } from 'asma-ui-core'
|
|
118
|
+
|
|
119
|
+
export const Example = () => (
|
|
120
|
+
<form className='flex flex-col gap-4'>
|
|
121
|
+
<StyledInputField label='Full name' fullWidth />
|
|
122
|
+
<StyledButton variant='contained' type='submit'>
|
|
123
|
+
Save
|
|
124
|
+
</StyledButton>
|
|
125
|
+
</form>
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> **Pass `fullWidth` when you want a field to fill its container.** Without it, `StyledInputField`
|
|
130
|
+
> is pinned to 235px by design. This is the single most common surprise for new users.
|
|
131
|
+
|
|
132
|
+
### 5. Add the snackbar provider (optional)
|
|
133
|
+
|
|
134
|
+
There is no theme provider — that went away with emotion. The one provider most apps mount is the
|
|
135
|
+
snackbar host, needed only if you use toasts:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { SnackbarProvider, enqueueSnackbar } from 'asma-ui-core'
|
|
139
|
+
|
|
140
|
+
<SnackbarProvider>
|
|
141
|
+
<App />
|
|
142
|
+
</SnackbarProvider>
|
|
143
|
+
|
|
144
|
+
// anywhere below it
|
|
145
|
+
enqueueSnackbar('Saved', { variant: 'alert', severity: 'success' })
|
|
62
146
|
```
|
|
63
147
|
|
|
64
|
-
|
|
148
|
+
It wraps [notistack](https://notistack.com) with the ASMA variants (`alert`, `info`, `default`) and
|
|
149
|
+
re-parents the toast stack into the topmost open modal `<dialog>`, so toasts stay visible and
|
|
150
|
+
clickable above a `StyledDialog`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## What's in the box
|
|
155
|
+
|
|
156
|
+
Every component is exported from the package root. Browse them interactively with Storybook (see
|
|
157
|
+
[below](#browsing-the-components)); this table is for discovery.
|
|
158
|
+
|
|
159
|
+
| Area | Components |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| **Buttons & actions** | `StyledButton`, `StyledLink`, `CopyButton`, `CopyWrapper` |
|
|
162
|
+
| **Text inputs** | `StyledInputField`, `StyledTextarea`, `StyledSearchField`, `StyledLabel` |
|
|
163
|
+
| **Selects** | `StyledSelect` + `StyledSelectItem` (single), `StyledSelectAutocomplete` (combobox), `StyledDynamicSelect` (chip multi-select), `Listbox` (headless) |
|
|
164
|
+
| **Toggles** | `StyledCheckbox`, `StyledRadio` / `StyledRadioGroup`, `StyledSwitch`, `StyledSlider` |
|
|
165
|
+
| **Form scaffolding** | `StyledFormControl`, `StyledFormControlLabel`, `StyledFormGroup`, `StyledFormLabel`, `StyledFormHelperText`, `StyledInputLabel` |
|
|
166
|
+
| **Date & time** | `StyledDatePicker`, `StyledTimePicker`, plus `setMidnightTime` / `setZeroTime` helpers |
|
|
167
|
+
| **Overlays** | `StyledDialog` (+ `Title` / `Content` / `Actions`), `MinimizableDialog`, `StyledDrawer`, `StyledPopover`, `StyledTooltip`, `createDialogStack` |
|
|
168
|
+
| **Menus** | `StyledMenu`, `StyledMenuList`, `StyledMenuItem`, `StyledFilterMenu`, `StyledFilterButton` |
|
|
169
|
+
| **Navigation** | `StyledTabs`, `StyledTab`, `StyledAccordion` (+ `Summary` / `Details`) |
|
|
170
|
+
| **Data display** | `StyledTable`, `VirtualList`, `StyledTypography`, `StyledBadge`, `StyledChip`, `StyledInteractiveChip`, `StyledAIDisclosure` |
|
|
171
|
+
| **Feedback** | `StyledAlert`, `StyledSnackbar`, `SnackbarProvider`, `StyledLoading`, `StyledEmptyPage`, `StyledFilteredEmptyState` |
|
|
172
|
+
| **Layout / widgets** | `StyledWidget`, `StyledWidgetHeader`, `StyledWidgetTitle`, `StyledModuleTitle`, `DynamicToolbar` |
|
|
173
|
+
| **Primitives** | `Paper`, `Stack`, `Container`, `Avatar`, `Skeleton`, `Fade`, `Popper`, `ClickAwayListener` |
|
|
174
|
+
| **Hooks & utils** | `usePopupState` (+ `bindTrigger` / `bindPopover` / `bindPopper`), `useSnackbar`, `cn`, `omit`, `prepareForSlot` |
|
|
175
|
+
|
|
176
|
+
### Icons
|
|
177
|
+
|
|
178
|
+
Around 180 icons ship on a dedicated subpath so you only pay for the ones you use — each is its own
|
|
179
|
+
module of roughly 0.5 KB:
|
|
65
180
|
|
|
66
181
|
```tsx
|
|
67
|
-
import {
|
|
182
|
+
import { CloseIcon } from 'asma-ui-core/icons'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Conventions worth knowing before you start
|
|
186
|
+
|
|
187
|
+
- **Body text is 16px (`text-base`)** on fields, selects, dropdown options and table bodies. The
|
|
188
|
+
`size` prop changes height and padding, not font size. This matches the Figma spec — resist
|
|
189
|
+
forcing `text-sm`. The Figma source for each component is linked from JSDoc (`@figmaNode` /
|
|
190
|
+
`@figmaProp`); see [Figma ↔ React matching](#figma--react-matching).
|
|
191
|
+
- **Text colour depends on the surface.** Dropdown option rows are `text-delta-700`; the selected
|
|
192
|
+
value in a closed field is `text-delta-800`; placeholders and floating labels are `text-delta-500`.
|
|
193
|
+
If you pass custom `renderOption` / `renderLabel` / `renderValue` content, match the surface it
|
|
194
|
+
renders into.
|
|
195
|
+
- **`dataTest` is required** on interactive form controls (`StyledCheckbox`, `StyledRadio`,
|
|
196
|
+
`StyledSwitch`, …) — it drives end-to-end test selectors. Their `onChange` signature is
|
|
197
|
+
`(event, checked)`.
|
|
198
|
+
- **Reach for `StyledDynamicSelect`** before hand-rolling a chip multi-select. It already handles tag
|
|
199
|
+
chips with delete, `+N` overflow, checkbox option rows, a growing field and the trigger icon. It
|
|
200
|
+
renders a chip group for ≤5 options and a searchable autocomplete for 6+.
|
|
201
|
+
- **Accessibility is part of the component contract** — roles, keyboard interaction and focus
|
|
202
|
+
management ship with each component rather than being left to the call site.
|
|
203
|
+
|
|
204
|
+
### Browsing the components
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
git clone https://github.com/Carasent-ASMA/asma-ui-core.git
|
|
208
|
+
cd asma-ui-core && pnpm install && pnpm storybook
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Storybook runs on `:6006` with docs, controls and an accessibility addon for every component.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Figma ↔ React matching
|
|
216
|
+
|
|
217
|
+
The React components were matched to the ASMA Design System in Figma using **Figma node IDs** plus
|
|
218
|
+
**JSDoc comments** on the component and its props. That is the in-source contract for "this React
|
|
219
|
+
component is this Figma component, and this prop is this Figma property" — not tribal knowledge, and
|
|
220
|
+
not a separate mapping spreadsheet.
|
|
68
221
|
|
|
69
|
-
|
|
222
|
+
The Design System file is [`wXrXt5uKNNzV2DnQCgyYZH`](https://www.figma.com/design/wXrXt5uKNNzV2DnQCgyYZH).
|
|
70
223
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
224
|
+
### How to read the mapping
|
|
225
|
+
|
|
226
|
+
Open any component source. Two JSDoc tags do the work:
|
|
227
|
+
|
|
228
|
+
| Tag | Where | Meaning |
|
|
229
|
+
| --- | --- | --- |
|
|
230
|
+
| `@figmaNode <fileKey>#<node-id>` | Once, on the component or its props type | The Figma component this React type maps to |
|
|
231
|
+
| `@figmaProp <FigmaProperty> = <value mapping>` | On each prop that has a Figma counterpart | How React values map onto Figma variant values |
|
|
232
|
+
| `@figmaProp none — <reason>` | On a prop with no Figma counterpart | Unmapped on purpose (`behavioral`, `a11y`, `ref`, `test hook`) — not forgotten |
|
|
233
|
+
|
|
234
|
+
Turn a node id into a Figma URL by replacing `-` with `:` in the node id:
|
|
235
|
+
|
|
236
|
+
`wXrXt5uKNNzV2DnQCgyYZH#13431-18852` →
|
|
237
|
+
https://www.figma.com/design/wXrXt5uKNNzV2DnQCgyYZH?node-id=13431-18852
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
/**
|
|
241
|
+
* @figmaNode wXrXt5uKNNzV2DnQCgyYZH#13431-18852
|
|
242
|
+
*/
|
|
243
|
+
export type StyledButtonProps = …
|
|
244
|
+
interface commonProps {
|
|
245
|
+
/** @figmaProp Size = medium→"Medium" (h40, text16/24) | small→"Small" (h32, text14/20) */
|
|
246
|
+
size?: 'large' | 'small' | 'medium'
|
|
247
|
+
/** @figmaProp none — test hook */
|
|
248
|
+
dataTest: string
|
|
249
|
+
}
|
|
250
|
+
interface buttonStandartVariantsProps {
|
|
251
|
+
/** @figmaProp Type = contained→"Primary (Contained)" | outlined→"Secondary (Outlined)" | text→"Tertiary" */
|
|
252
|
+
variant?: 'contained' | 'outlined' | 'text' | …
|
|
253
|
+
/** @figmaProp Danger = true→"on" | false→"off" */
|
|
254
|
+
error?: boolean
|
|
255
|
+
}
|
|
76
256
|
```
|
|
257
|
+
|
|
258
|
+
Figma **State** (Enabled / Hovered / Focused / Pressed / Disabled) is usually **not** a React prop —
|
|
259
|
+
it is derived at runtime from native pseudo-classes and attributes. The JSDoc says so when that is
|
|
260
|
+
the case.
|
|
261
|
+
|
|
262
|
+
A component with no Design System counterpart is marked `@figmaNode none` with a reason, so the
|
|
263
|
+
absence is explicit.
|
|
264
|
+
|
|
265
|
+
### Why this exists
|
|
266
|
+
|
|
267
|
+
The tags were how the Figma-to-React parity pass was done: pull the node (size, radius, padding,
|
|
268
|
+
tokens, every variant × state), annotate every prop, then align the implementation until it matches.
|
|
269
|
+
The same tags are what you use when something "looks off" — open the node, compare the `@figmaProp`
|
|
270
|
+
mapping, and you know whether the call site is using the wrong prop or the component is drifting
|
|
271
|
+
from design.
|
|
272
|
+
|
|
273
|
+
The per-component ledger (node id, spec, states covered, remaining deltas) lives in
|
|
274
|
+
[`visual-tests/FIGMA-PARITY.md`](visual-tests/FIGMA-PARITY.md).
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Styling API
|
|
279
|
+
|
|
280
|
+
### `className` is the primary escape hatch
|
|
281
|
+
|
|
282
|
+
Pass Tailwind utilities directly. The whole library is built around this.
|
|
283
|
+
|
|
284
|
+
### `sx` works, but **flat keys only**
|
|
285
|
+
|
|
286
|
+
`sx` survives as a compatibility shim for the MUI-era API. [`resolveSx`](src/helpers/sx.ts) flattens
|
|
287
|
+
MUI's spacing shorthand and colour aliases into plain `CSSProperties`, so flat keys behave as you'd
|
|
288
|
+
expect: `mt`, `px`, `bgcolor`, `color`, `width`, `flex`, `minWidth`. Numeric spacing still uses MUI's
|
|
289
|
+
8px unit, so `mt: 2` is `16px`.
|
|
290
|
+
|
|
291
|
+
Anything **nested is dropped at runtime** with a `console.warn` and has no effect:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
// ❌ silently does nothing
|
|
295
|
+
// logs: resolveSx: unsupported nested/responsive sx key "…" dropped
|
|
296
|
+
sx={{ '& .MuiInputBase-root': { minHeight: 80 }, '&:hover': { color: 'red' } }}
|
|
297
|
+
|
|
298
|
+
// ✅
|
|
299
|
+
className='min-h-20 hover:text-error-500'
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
That covers nested selectors, pseudo-classes, responsive arrays/objects and theme-callback functions.
|
|
303
|
+
`slotProps.input.sx` is likewise never read — the input slot takes `style` / `className` / `ref` only.
|
|
304
|
+
|
|
305
|
+
**Prefer `className` for new code.** `sx` exists to keep migrated call sites compiling.
|
|
306
|
+
|
|
307
|
+
### Two ordering footguns
|
|
308
|
+
|
|
309
|
+
These explain most "my override doesn't work" reports, so they are worth reading once up front.
|
|
310
|
+
|
|
311
|
+
**`cn` is plain clsx — it does not dedupe conflicting utilities.** tailwind-merge was deliberately
|
|
312
|
+
removed. If a component hardcodes `px-0` and you pass `p-4`, *both* land in the class list, and
|
|
313
|
+
Tailwind's own output order (axis utilities after shorthands) means the component's `px-0` wins. Your
|
|
314
|
+
override silently does nothing.
|
|
315
|
+
|
|
316
|
+
Where a default is meant to be overridable, the component gates it with
|
|
317
|
+
[`consumerOverrides`](src/helpers/classOverride.ts) — already applied to `StyledMenuList` padding,
|
|
318
|
+
the `StyledTabs` container inset, and `StyledChip` width. If you hit a default that is not yet gated,
|
|
319
|
+
that is a library fix worth raising, not something to hack around at the call site.
|
|
320
|
+
|
|
321
|
+
**The library's compiled CSS is `!important`.** Its utilities emit as `min-width: 0 !important` and
|
|
322
|
+
similar, which beat your *inline* `style` / `sx` / `slotProps`. Override with a class, not an inline
|
|
323
|
+
style. (If you are authoring components here: a layout default on an element consumers are expected
|
|
324
|
+
to size inline must be an inline-style default set before `resolveSx(...)`, never a utility class.)
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Theming
|
|
329
|
+
|
|
330
|
+
Themes are CSS custom properties switched by a `data-theme` attribute on a wrapping element, usually
|
|
331
|
+
`<body>`. There is no JS theme object and no provider:
|
|
332
|
+
|
|
333
|
+
```html
|
|
334
|
+
<body data-theme="fretex">
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
| `data-theme` | Theme |
|
|
338
|
+
| --- | --- |
|
|
339
|
+
| absent, or `default` | ASMA default (blue) |
|
|
340
|
+
| `fretex` | Fretex |
|
|
341
|
+
| `greenish` | Jade / green |
|
|
342
|
+
|
|
343
|
+
Each theme maps the semantic token scale — `alpha`, `beta`, `gama`, `delta`, `error` — onto concrete
|
|
344
|
+
palette values, and the Tailwind colour names reference those tokens. **Always use the semantic
|
|
345
|
+
names** (`text-delta-700`, `bg-alpha-500`); a raw palette colour like `text-blue-500` will not follow
|
|
346
|
+
the active theme.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Bundle size and the MUI removal
|
|
351
|
+
|
|
352
|
+
MUI is an all-or-nothing dependency. Its runtime engine (`@mui/system`, `@mui/styled-engine`,
|
|
353
|
+
`@mui/utils`, `@emotion/*`, `stylis`) is shared by every MUI component, so keeping even one — the
|
|
354
|
+
Autocomplete, say — ships the entire ~143 KB gzipped infrastructure. There is no partial removal. The
|
|
355
|
+
same held for `@base-ui/react`, which was carrying ~143 KB gzipped to serve four simple components.
|
|
356
|
+
|
|
357
|
+
So both went, along with four smaller dependencies that were cheaper to inline than to keep:
|
|
358
|
+
|
|
359
|
+
| Removed | Was (gzipped) | Replaced by |
|
|
360
|
+
| --- | --- | --- |
|
|
361
|
+
| `@mui/material` | ~143 KB | Native HTML + ARIA components styled with Tailwind |
|
|
362
|
+
| `@base-ui/react` | ~143 KB | Native checkbox, switch, radio, accordion, dialog, tabs, slider, chip |
|
|
363
|
+
| `@emotion/react` + `@emotion/styled` | ~16 KB | Tailwind and CSS modules; flat `sx` via `resolveSx` |
|
|
364
|
+
| `lodash-es` | ~15 KB | Small native helpers |
|
|
365
|
+
| `tailwind-merge` | ~3 KB | `clsx`-only `cn()` plus `consumerOverrides` |
|
|
366
|
+
| `@react-input/mask` | ~3 KB | Dependency-free `useInputMask` |
|
|
367
|
+
| `material-ui-popup-state` | ~1 KB | In-repo `usePopupState` hook |
|
|
368
|
+
|
|
369
|
+
**~324 KB gzipped of dependencies became ~57 KB.** The only meaningful addition was
|
|
370
|
+
`@floating-ui/react` (~3 KB gzipped), which now powers tooltips, popovers, menus, drawers, selects
|
|
371
|
+
and the combobox.
|
|
372
|
+
|
|
373
|
+
### Measured output
|
|
374
|
+
|
|
375
|
+
From a clean `pnpm build` (600 preserved ESM modules):
|
|
376
|
+
|
|
377
|
+
| Artefact | Raw | Gzipped |
|
|
378
|
+
| --- | --- | --- |
|
|
379
|
+
| Library JS, whole package | 512 KB | **129 KB** |
|
|
380
|
+
| Library CSS | — | 22.5 KB |
|
|
381
|
+
| `index.js` barrel entry alone | 32 KB | 6.6 KB |
|
|
382
|
+
|
|
383
|
+
Down from roughly 1.2 MB before the rewrite.
|
|
384
|
+
|
|
385
|
+
**Your app downloads far less than 129 KB.** The build preserves ESM modules instead of emitting one
|
|
386
|
+
blob, so that figure is the whole-library footprint, not a per-consumer one. The barrel is pure
|
|
387
|
+
re-exports and module bodies load on demand. Icons are the clearest case: 41.8 KB gzipped in total,
|
|
388
|
+
but emitted as one module per icon, so importing one costs ~0.5 KB rather than the whole block.
|
|
389
|
+
Remaining subtrees, gzipped: non-icon components 45.1 KB, table 19.2 KB, date/time 11.9 KB.
|
|
390
|
+
|
|
391
|
+
Third-party runtime dependencies are deliberately **not** bundled into `dist` — they stay as bare
|
|
392
|
+
imports resolved by your bundler (or, inside ASMA, by a shared import map so several micro-frontends
|
|
393
|
+
load one copy). The numbers above are this library's own code only.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Migrating from the MUI-era version
|
|
398
|
+
|
|
399
|
+
*Skip this section if you are adopting the library fresh.*
|
|
400
|
+
|
|
401
|
+
Published versions `^3.43.x` and earlier wrapped MUI. The rewrite deliberately preserved the public
|
|
402
|
+
prop surface, so **your code still compiles — it just renders wrong in specific places.** The
|
|
403
|
+
compiler catches almost none of it. Audit every input, select, chip and table call site.
|
|
404
|
+
|
|
405
|
+
1. **Dead `sx` selectors.** Every `'& .Mui*'`, nested, pseudo or responsive `sx` key stops applying.
|
|
406
|
+
Watch the console for `resolveSx: unsupported nested/responsive sx key`.
|
|
407
|
+
2. **Chip `startAdornment` must be a real array.** A single wrapped element (`<div>{chips}</div>`)
|
|
408
|
+
fails the internal `Array.isArray` check, so the field stays a fixed 40px row and chips overlap.
|
|
409
|
+
Pass `[...chips]` and drop `multiline`.
|
|
410
|
+
3. **Fields pin to 235px without `fullWidth`**, and `StyledFormControl` defaults to `inline-flex`, so
|
|
411
|
+
a form that used to fill its container now collapses. Pass `fullWidth` on **both**.
|
|
412
|
+
4. **Text moved to 16px** on fields, selects and table bodies. Intended — re-baseline your visual
|
|
413
|
+
snapshots rather than forcing `text-sm`.
|
|
414
|
+
5. **Direct `@mui/material` imports must be swapped** for the `Styled*` equivalents. `Checkbox` →
|
|
415
|
+
`StyledCheckbox`, `FormControlLabel` → `StyledFormControlLabel`, `Radio` / `Switch` / `Tooltip`
|
|
416
|
+
likewise. Note the `(event, checked)` handler shape and required `dataTest`.
|
|
417
|
+
6. **Some `slotProps` shapes narrowed** — these *do* fail `tsc`. `StyledMenu`'s `slotProps.root` is
|
|
418
|
+
gone; only `paper` remains.
|
|
419
|
+
|
|
420
|
+
> **Breaking change:** `@mui/material` is no longer a dependency or peer dependency. An app that
|
|
421
|
+
> imports MUI directly must now declare it itself.
|
|
422
|
+
|
|
423
|
+
**Verifying a bump:** `tsc --noEmit` is necessary but far from sufficient. Also confirm the browser
|
|
424
|
+
console is free of `resolveSx` warnings, and visually check every touched field in its empty,
|
|
425
|
+
one-chip, many-chip, disabled, error and read-only states.
|
|
426
|
+
|
|
427
|
+
ASMA engineers: the full audit procedure, detection greps and per-recipe worked fixes live in the
|
|
428
|
+
`ui-core-mui-free-migration` skill in the `asma-modules` monorepo
|
|
429
|
+
(`.github/skills/ui-core-mui-free-migration/SKILL.md`).
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Troubleshooting
|
|
434
|
+
|
|
435
|
+
| Symptom | Cause and fix |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| Console: `resolveSx: unsupported nested/responsive sx key … dropped` | Nested `sx` is not supported. Move it to `className`. |
|
|
438
|
+
| My `className` override does nothing | `cn` is plain clsx with no dedupe, so the component's hardcoded utility wins on source order. The default needs a `consumerOverrides` gate — raise it. |
|
|
439
|
+
| My inline `style` / `sx` is ignored | The library's CSS is `!important`. Override with a class instead. |
|
|
440
|
+
| Every field is ~235px and the form collapsed | Pass `fullWidth` on both `StyledInputField` **and** the wrapping `StyledFormControl`. |
|
|
441
|
+
| Chips overlap the input, field won't grow | `startAdornment` must be an **array**, not a single wrapped element. Remove `multiline`. |
|
|
442
|
+
| Popover or calendar shows a black UA border | `dist/style.css` isn't imported. It carries the required `[popover]` reset. |
|
|
443
|
+
| Nothing is styled at all | Same — the stylesheet import is missing, or lands after Tailwind. |
|
|
444
|
+
| Text is 16px where it used to be 14px | Intended Figma parity. Don't force `text-sm`. |
|
|
445
|
+
| Select/menu inside a `StyledDialog` can't be clicked | Fixed in later versions — modal `<dialog>` marks outside content inert, so the popover must portal into the dialog. Upgrade. |
|
|
446
|
+
| Stray dark tooltip bubble on hover | `title={cond && 'text'}` yielded `false`. Fixed in later versions; upgrade. |
|
|
447
|
+
| A library fix isn't showing after rebuilding | Apps consume the prebuilt `dist`, not `src`. See the note below. |
|
|
448
|
+
|
|
449
|
+
**The `dist` trap (monorepo work only).** If you are developing this library alongside a consuming
|
|
450
|
+
app, your change is invisible until you rebuild (`pnpm --filter asma-ui-core build`) and restart the
|
|
451
|
+
app's dev server with `vite --force`. Worse, with peer dependencies pnpm injects a *snapshot copy*
|
|
452
|
+
into its virtual store rather than symlinking, so rebuilding `dist` never reaches the app and
|
|
453
|
+
`pnpm install` reports "Already up to date". Re-sync explicitly:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
RESOLVED=$(readlink -f node_modules/asma-ui-core)
|
|
457
|
+
rsync -a --delete shared/asma-ui-core/dist/ "$RESOLVED/dist/"
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Check the `dist` mtime before debugging anything else.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Contributing to the library
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
pnpm install
|
|
468
|
+
pnpm storybook # component workbench on :6006
|
|
469
|
+
pnpm build # ts:check + vite build into dist/
|
|
470
|
+
pnpm ts:check # types only
|
|
471
|
+
pnpm lint # eslint (lint:fix to autofix)
|
|
472
|
+
pnpm test-storybook # vitest against the stories
|
|
473
|
+
pnpm vrt # visual regression vs committed baselines (needs Docker)
|
|
474
|
+
pnpm vrt:accept # re-baseline after an intended visual change
|
|
475
|
+
pnpm vrt:report # open the last Playwright report
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Node 24 is required (see `engines`).
|
|
479
|
+
|
|
480
|
+
### Component conventions
|
|
481
|
+
|
|
482
|
+
- Compose from existing styled components; check whether one already exists before styling from
|
|
483
|
+
scratch.
|
|
484
|
+
- Question whether a new "composed component" is needed at all — often documenting how to combine
|
|
485
|
+
the existing parts is enough.
|
|
486
|
+
- Every component is documented and tested in Storybook **before** the PR opens.
|
|
487
|
+
- New or restyled components get the `@figmaNode` / `@figmaProp` JSDoc contract described in
|
|
488
|
+
[Figma ↔ React matching](#figma--react-matching) — including `@figmaProp none` when a prop has no
|
|
489
|
+
Design System counterpart.
|
|
490
|
+
- Accessibility ships with the component: role, keyboard behaviour and focus management, not a
|
|
491
|
+
follow-up ticket.
|
|
492
|
+
|
|
493
|
+
### The export surface is frozen
|
|
494
|
+
|
|
495
|
+
`src/api-surface.test.ts` snapshots every runtime export of the barrel. If that snapshot changes you
|
|
496
|
+
have made a breaking API change, and it must be a deliberate reviewed decision rather than an
|
|
497
|
+
incidental side effect of a refactor. `src/index.ts` is maintained by hand — add new exports there
|
|
498
|
+
explicitly.
|
|
499
|
+
|
|
500
|
+
### Visual regression
|
|
501
|
+
|
|
502
|
+
Storybook stories are captured and compared inside a pinned Playwright Docker image, so local and CI
|
|
503
|
+
runs share one code path. A diff fails the build until a human confirms it is an intended design
|
|
504
|
+
change, at which point `pnpm vrt:accept` rewrites the baselines. Per-component Figma parity decisions
|
|
505
|
+
are tracked in [`visual-tests/FIGMA-PARITY.md`](visual-tests/FIGMA-PARITY.md).
|
|
506
|
+
|
|
507
|
+
### Changelog and releases
|
|
508
|
+
|
|
509
|
+
The project uses [changesets](https://github.com/changesets/changesets). For each feature or fix:
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
npx changeset
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Choose `patch` for a fix, `minor` for a feature, `major` for a breaking change, and commit the
|
|
516
|
+
generated markdown file with your PR. A PR may carry several changesets. On merge to master the
|
|
517
|
+
pipeline bumps the version, writes the changelog and publishes to npm.
|