@12-apps/ui 4.0.0 → 4.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "type": "module",
@@ -11,7 +11,6 @@
11
11
  "yarn": "please-use-pnpm"
12
12
  },
13
13
  "main": "./dist/index.js",
14
- "module": "./dist/index.mjs",
15
14
  "types": "./dist/index.d.ts",
16
15
  "exports": {
17
16
  ".": "./src/index.ts",
@@ -2,7 +2,7 @@ import Error from '@mui/icons-material/Error';
2
2
  import Info from '@mui/icons-material/Info';
3
3
  import CheckCircle from '@mui/icons-material/CheckCircle';
4
4
  import Warning from '@mui/icons-material/Warning';
5
- import { alpha, keyframes } from '@mui/material';
5
+ import { alpha, darken, keyframes, lighten } from '@mui/material';
6
6
  import type { CSSObject, Theme } from '@mui/material/styles';
7
7
  import React from 'react';
8
8
 
@@ -117,58 +117,165 @@ export const getVariantIcon = (variant: string) => {
117
117
  // which is narrower than MUI's PaletteColor.
118
118
  export type AlertPalette = { main: string; light?: string; dark?: string };
119
119
 
120
- // The six visual variants. Each block is spread in only when it matches, which
121
- // is how the original inline version read; keeping that shape here means the
122
- // styled() callback carries one branch instead of six.
123
- export const alertVariantStyles = (
120
+ /** The four semantic variants, which differ only in which palette they read. */
121
+ const SEMANTIC_VARIANTS = new Set(['info', 'success', 'warning', 'danger']);
122
+
123
+ /**
124
+ * The room the message is given — padding, the stack's rhythm, and where the
125
+ * icon and the close button sit relative to it.
126
+ *
127
+ * Extracted for the same reason {@link alertVariantStyles} and
128
+ * {@link alertEmphasisStyles} were: `styled()`'s callback is one function, and
129
+ * every block written inline is counted against its length by the complexity
130
+ * gate. Keeping the layout here means the callback assembles four named pieces
131
+ * instead of carrying all of them.
132
+ */
133
+ export const alertLayoutStyles = (
124
134
  theme: Theme,
125
- customVariant: string | undefined,
126
135
  colorPalette: AlertPalette,
136
+ animate: boolean | undefined,
127
137
  ): CSSObject => ({
128
- ...(customVariant === 'info' && {
129
- backgroundColor: alpha(colorPalette.main, 0.1),
130
- color: colorPalette.main,
131
- border: `1px solid ${alpha(colorPalette.main, 0.2)}`,
132
- '.MuiAlert-icon': {
133
- color: colorPalette.main,
134
- },
135
- }),
138
+ // ROOM TO READ.
139
+ //
140
+ // MUI's `6px 16px` is sized for one line of text and nothing else. This Alert
141
+ // routinely carries three things stacked — a sentence, a smaller explanation,
142
+ // then a control — and at 6px they touched top and bottom and ran together
143
+ // vertically, so the whole card read as one dense block instead of as a
144
+ // message with parts. The generosity is what makes the structure legible; it
145
+ // is not decoration.
146
+ //
147
+ // Padding stays overridable by an `sx` (a full-bleed strip legitimately wants
148
+ // it tight), because this is a default rather than a rule.
149
+ padding: theme.spacing(1.75, 2),
136
150
 
137
- ...(customVariant === 'success' && {
138
- backgroundColor: alpha(colorPalette.main, 0.1),
139
- color: colorPalette.main,
140
- border: `1px solid ${alpha(colorPalette.main, 0.2)}`,
141
- '.MuiAlert-icon': {
142
- color: colorPalette.main,
151
+ '.MuiAlert-message': {
152
+ display: 'flex',
153
+ flexDirection: 'column',
154
+ // 0.5 put a title, its explanation and a button 4px apart, which reads as a
155
+ // spacing bug rather than as a group. 1 separates the lines; the extra
156
+ // margin below goes to whatever CONTROL sits at the end, because the gap
157
+ // between prose and a thing you press has to be bigger than the gap between
158
+ // two lines of prose or the button looks like part of the text.
159
+ gap: theme.spacing(1),
160
+ fontSize: '0.95rem',
161
+ lineHeight: 1.5,
162
+ // No padding of its own — the root's is now doing that job, and MUI's
163
+ // default `8px 0` on top of it would double the vertical space.
164
+ padding: 0,
165
+ // A CONTROL ON A TINTED PANEL NEEDS AN EDGE.
166
+ //
167
+ // `ghost` and `text` paint no background and no border — bare labels, which
168
+ // works on white where the surrounding page is obviously not clickable.
169
+ // Inside a coloured Alert it stops working: the label is one more coloured
170
+ // phrase among several, and a "Cancelar" beside two lines of prose reads as
171
+ // part of the prose.
172
+ //
173
+ // Only the BORDER is set, never the background or the colour, and that is
174
+ // what makes it safe to apply to every button rather than just the bare
175
+ // ones: a `solid` keeps its fill and its light label and gains a hairline
176
+ // in the same hue, while a `ghost` gains the whole affordance.
177
+ '.MuiButton-root': {
178
+ marginTop: theme.spacing(0.5),
179
+ border: `1px solid ${alpha(colorPalette.main, 0.45)}` },
143
180
  },
144
- }),
145
181
 
146
- ...(customVariant === 'warning' && {
147
- backgroundColor: alpha(colorPalette.main, 0.1),
148
- color: colorPalette.main,
149
- border: `1px solid ${alpha(colorPalette.main, 0.2)}`,
150
182
  '.MuiAlert-icon': {
151
- color: colorPalette.main,
152
- },
153
- }),
183
+ transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
184
+ alignItems: 'center',
185
+ // Clear of the words rather than nearly touching them, and aligned to the
186
+ // FIRST line instead of centred on the whole block — an icon floating
187
+ // halfway down a three-line message points at nothing.
188
+ alignSelf: 'flex-start',
189
+ marginRight: theme.spacing(1.75),
190
+ paddingTop: theme.spacing(0.25),
191
+ animation: animate ? `${iconRotate} 0.6s ease-out` : 'none' },
154
192
 
155
- ...(customVariant === 'danger' && {
156
- backgroundColor: alpha(colorPalette.main, 0.1),
157
- color: colorPalette.main,
158
- border: `1px solid ${alpha(colorPalette.main, 0.2)}`,
159
- '.MuiAlert-icon': {
160
- color: colorPalette.main,
161
- },
162
- }),
193
+ // The close button, kept off the text it sits beside.
194
+ '.MuiAlert-action': {
195
+ alignItems: 'flex-start',
196
+ paddingLeft: theme.spacing(2) },
197
+ });
198
+
199
+ /**
200
+ * A semantic Alert's surface and ink.
201
+ *
202
+ * ## Why this is not `alpha(main, 0.1)`
203
+ *
204
+ * It used to be, four times over, with `color: main` for the text. Both halves
205
+ * were wrong, and independently so.
206
+ *
207
+ * The fill was TRANSLUCENT. A 10% wash only reads as a banner when something
208
+ * opaque is behind it, so the component silently depended on where a consumer
209
+ * put it. Placed over content — a floating notice, an invite anchored above a
210
+ * catalogue — the page came straight through and the sentence interleaved with
211
+ * whatever it was over. That is not a faint banner; it is an unreadable one,
212
+ * and nothing in the component said so.
213
+ *
214
+ * The ink was the SEVERITY HUE at body size. `#0288d1`, this library's info
215
+ * blue, is 3.03:1 on white — under the 4.5:1 WCAG AA floor before any wash is
216
+ * involved. So the text failed on a plain white page too, where the fill was
217
+ * behaving exactly as intended.
218
+ *
219
+ * ## Why `lighten`/`darken` rather than a hand-picked pair
220
+ *
221
+ * These are the same transforms MUI's own `standard` Alert uses, at the same
222
+ * ratios — the recipe this component overrode. Reusing it means an OPAQUE tint
223
+ * with a dark ink of the same hue, contrast that holds on any surface because
224
+ * it no longer depends on one, and a pairing already proven across MUI's
225
+ * palette rather than eyeballed here. Both modes are handled: on a dark theme
226
+ * the tint darkens and the ink lightens, which an alpha wash could never do
227
+ * because it carries the seed's own lightness through unchanged.
228
+ *
229
+ * The border keeps an alpha — it is a hairline over the tint it belongs to, so
230
+ * translucency there costs nothing and lets the edge follow the fill.
231
+ */
232
+ const semanticSurface = (theme: Theme, colorPalette: AlertPalette): CSSObject => {
233
+ const dark = theme.palette.mode === 'dark';
234
+ return {
235
+ backgroundColor: dark ? darken(colorPalette.main, 0.8) : lighten(colorPalette.main, 0.9),
236
+ color: dark ? lighten(colorPalette.main, 0.6) : darken(colorPalette.main, 0.6),
237
+ border: `1px solid ${alpha(colorPalette.main, 0.35)}`,
238
+ '.MuiAlert-icon': {
239
+ color: colorPalette.main,
240
+ },
241
+ };
242
+ };
243
+
244
+ // The six visual variants. Each block is spread in only when it matches, which
245
+ // is how the original inline version read; keeping that shape here means the
246
+ // styled() callback carries one branch instead of six. The four semantics were
247
+ // four IDENTICAL copies of the same block — they now share one, so the next
248
+ // change to a banner's surface cannot land on three of them.
249
+ export const alertVariantStyles = (
250
+ theme: Theme,
251
+ customVariant: string | undefined,
252
+ colorPalette: AlertPalette,
253
+ ): CSSObject => ({
254
+ ...(customVariant !== undefined &&
255
+ SEMANTIC_VARIANTS.has(customVariant) &&
256
+ semanticSurface(theme, colorPalette)),
163
257
 
164
258
  ...(customVariant === 'glass' && {
165
- backgroundColor: alpha(theme.palette.background.paper, 0.1),
166
- backdropFilter: 'blur(20px)',
167
- border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
259
+ // 85%, not 10%. This is the variant that is MEANT to be see-through, and it
260
+ // was the same mistake in a costume: at 0.1 the blur had almost nothing to
261
+ // sit on, so `glass` was not frosted, it was a window. A frosted pane has to
262
+ // be mostly pane — the backdrop should be legible AS texture behind the
263
+ // text, never as competition with it.
264
+ backgroundColor: alpha(theme.palette.background.paper, 0.85),
265
+ backdropFilter: 'blur(20px) saturate(180%)',
266
+ WebkitBackdropFilter: 'blur(20px) saturate(180%)',
267
+ border: `1px solid ${alpha(theme.palette.divider, 0.4)}`,
168
268
  color: theme.palette.text.primary,
169
269
  '.MuiAlert-icon': {
170
270
  color: theme.palette.primary.main,
171
271
  },
272
+ // Safari before 18 and Firefox with the filter disabled paint the declared
273
+ // alpha with nothing blurred behind it — the see-through banner this is
274
+ // fixing. There is no degraded frost to fall back to, so it falls back to an
275
+ // opaque pane: less pretty, still readable.
276
+ '@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)))': {
277
+ backgroundColor: theme.palette.background.paper,
278
+ },
172
279
  }),
173
280
 
174
281
  ...(customVariant === 'gradient' && {
@@ -11,11 +11,11 @@ import React from 'react';
11
11
 
12
12
  import {
13
13
  alertEmphasisStyles,
14
+ alertLayoutStyles,
14
15
  alertVariantStyles,
15
16
  fadeInScale,
16
17
  getColorFromTheme,
17
- getVariantIcon,
18
- iconRotate } from './Alert.styles';
18
+ getVariantIcon } from './Alert.styles';
19
19
  import type { AlertColor } from '@mui/material';
20
20
  import type { AlertProps } from './Alert.types';
21
21
 
@@ -39,18 +39,7 @@ const StyledAlert = styled(MuiAlert, {
39
39
  animation: animate ? `${fadeInScale} 0.3s ease-out` : 'none',
40
40
  willChange: 'transform, opacity',
41
41
 
42
- // Enhanced base styles
43
- '.MuiAlert-message': {
44
- display: 'flex',
45
- flexDirection: 'column',
46
- gap: theme.spacing(0.5),
47
- fontSize: '0.95rem',
48
- lineHeight: 1.5 },
49
-
50
- '.MuiAlert-icon': {
51
- transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
52
- alignItems: 'center',
53
- animation: animate ? `${iconRotate} 0.6s ease-out` : 'none' },
42
+ ...alertLayoutStyles(theme, colorPalette, animate),
54
43
 
55
44
  // Hover effects
56
45
  '&:hover': {
@@ -1,4 +1,4 @@
1
- import { alpha, keyframes } from '@mui/material';
1
+ import { alpha, darken, keyframes, lighten } from '@mui/material';
2
2
  import type { CSSObject, Theme } from '@mui/material';
3
3
 
4
4
  // Define pulse animation globally
@@ -146,6 +146,25 @@ const gradientFor = (theme: Theme, color: string, palette: ColorPalette): string
146
146
  return `linear-gradient(135deg, ${from} 0%, ${to} 100%)`;
147
147
  };
148
148
 
149
+ /**
150
+ * The label colour for the two variants that paint no background of their own.
151
+ *
152
+ * `ghost` and `text` used `palette.main` directly, which is the same mistake
153
+ * the Alert made with its body text: a palette's `main` is chosen to sit UNDER
154
+ * white — it is the fill of a solid button — and it is not chosen to be read AS
155
+ * text. `#6366F1`, the default primary, is 3.6:1 on white and worse on any
156
+ * tinted surface, so a "Cancelar" or an "Agora não" placed on a coloured panel
157
+ * turned into a smudge. That is precisely where these two variants belong: they
158
+ * exist to be the quiet option NEXT to a solid one, so they are the buttons
159
+ * most likely to be sitting on something other than plain paper.
160
+ *
161
+ * One step darker (lighter, on a dark theme) keeps the hue the caller asked for
162
+ * and buys the contrast back. `main` still drives the hover wash and every
163
+ * bordered variant, so nothing that was already legible moves.
164
+ */
165
+ const quietInk = (theme: Theme, palette: ColorPalette): string =>
166
+ theme.palette.mode === 'dark' ? lighten(palette.main, 0.35) : darken(palette.main, 0.3);
167
+
149
168
  const VARIANT_STYLES: Record<
150
169
  string,
151
170
  (theme: Theme, palette: ColorPalette, color: string) => CSSObject
@@ -168,23 +187,23 @@ const VARIANT_STYLES: Record<
168
187
  borderColor: palette.dark,
169
188
  },
170
189
  }),
171
- ghost: (_theme, palette) => ({
190
+ ghost: (theme, palette) => ({
172
191
  backgroundColor: 'transparent',
173
- color: palette.main,
192
+ color: quietInk(theme, palette),
174
193
  '&:hover': {
175
194
  backgroundColor: alpha(palette.main, 0.1),
176
195
  },
177
196
  }),
178
- text: (_theme, palette) => ({
197
+ text: (theme, palette) => ({
179
198
  backgroundColor: 'transparent',
180
- color: palette.main,
199
+ color: quietInk(theme, palette),
181
200
  border: `none`,
182
201
  '&:hover': {
183
202
  backgroundColor: alpha(palette.main, 0.1),
184
203
  },
185
204
  '&.active': {
186
205
  backgroundColor: alpha(palette.main, 0.1),
187
- color: palette.main,
206
+ color: quietInk(theme, palette),
188
207
  },
189
208
  }),
190
209
  glass: (theme, palette) => ({