@neo-design/mui 1.0.0-rc.6 → 1.0.0-rc.7

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": "@neo-design/mui",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "Neo — the Material UI theme and the Neo* React components, on top of @neo-design/foundations.",
5
5
  "homepage": "https://github.com/jzamorano-ui/neo-ds/blob/main/docs/dev/INTEGRATION.md",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "theme"
30
30
  ],
31
31
  "dependencies": {
32
- "@neo-design/foundations": "1.0.0-rc.6"
32
+ "@neo-design/foundations": "1.0.0-rc.7"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@emotion/react": "^11",
@@ -398,6 +398,73 @@ export const components = {
398
398
  },
399
399
  },
400
400
 
401
+ MuiDrawer: {
402
+ styleOverrides: {
403
+ paper: ({ theme }) => ({
404
+ '&.MuiDrawer-paperAnchorBottom': {
405
+ backgroundColor: 'var(--surface--base--default)',
406
+ borderStartStartRadius: 'var(--neo-radius-xl)', borderStartEndRadius: 'var(--neo-radius-xl)',
407
+ borderEndStartRadius: 0, borderEndEndRadius: 0,
408
+ maxHeight: '90dvh',
409
+ display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
410
+ '& > *': { alignSelf: 'stretch' },
411
+ '&:focus': { outline: 0 },
412
+ },
413
+ '& .NeoBottomSheet-handle': {
414
+ inlineSize: '40px', blockSize: '4px', borderRadius: 'var(--neo-radius-pill)',
415
+ backgroundColor: 'var(--fill--base--medium)',
416
+ marginInline: 'auto', marginBlock: 'var(--neo-space-sm-rem)', flex: '0 0 auto',
417
+ },
418
+ '&[data-image="true"] .NeoBottomSheet-handle': { backgroundColor: 'var(--fill--base--default)' },
419
+
420
+ '& .NeoBottomSheet-header': {
421
+ display: 'flex', flexDirection: 'column', flex: '0 0 auto',
422
+ justifyContent: 'center', alignItems: 'flex-end',
423
+ '& > *': { alignSelf: 'stretch' },
424
+ paddingInline: 'var(--neo-space-lg-rem)', paddingBlock: `0 var(--neo-space-xs-rem)`,
425
+ },
426
+ '&[data-scroll="top"] .NeoBottomSheet-header, &[data-scroll="mid"] .NeoBottomSheet-header, &[data-scroll="bottom"] .NeoBottomSheet-header': {
427
+ borderBlockEnd: '1px solid var(--border--base--secondary)',
428
+ },
429
+ '& .NeoBottomSheet-headerRow': {
430
+ display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
431
+ blockSize: '40px', gap: 'var(--neo-space-none-rem)',
432
+ },
433
+ '& .NeoBottomSheet-title': {
434
+ ...theme.typography.bodyLgBold, color: 'var(--text--base--default)',
435
+ flex: '1 1 auto', minInlineSize: 0, margin: 0,
436
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
437
+ },
438
+
439
+ '& .NeoBottomSheet-headerImage': {
440
+ position: 'relative', blockSize: '160px', flex: '0 0 auto', overflow: 'hidden',
441
+ '& > img, & > picture, & > video': { inlineSize: '100%', blockSize: '100%', objectFit: 'cover', display: 'block' },
442
+ '& .NeoBottomSheet-handle': { position: 'absolute', insetInline: 0, insetBlockStart: 0 },
443
+ '& .NeoBottomSheet-close': {
444
+ position: 'absolute', insetBlockStart: 'var(--neo-space-lg-rem)', insetInlineEnd: 'var(--neo-space-lg-rem)',
445
+ },
446
+ },
447
+
448
+ '& .NeoBottomSheet-content': {
449
+ ...theme.typography.bodyLgRegular, color: 'var(--text--base--default)',
450
+ flex: '1 1 auto', overflowY: 'auto', minBlockSize: '200px',
451
+ paddingInline: 'var(--neo-space-lg-rem)', paddingBlock: `0 var(--neo-space-lg-rem)`,
452
+ '&:focus-visible': { outline: 'none', boxShadow: focusRingInset.boxShadow },
453
+ },
454
+
455
+ '& .NeoBottomSheet-actions': {
456
+ display: 'flex', flexDirection: 'column', gap: 'var(--neo-space-sm-rem)', flex: '0 0 auto',
457
+ paddingInline: 'var(--neo-space-lg-rem)',
458
+ paddingBlock: `var(--neo-space-lg-rem) var(--neo-space-2xl-rem)`,
459
+ backgroundColor: 'var(--surface--base--default)',
460
+ '& > div': { display: 'contents' },
461
+ '& .MuiButton-root': { inlineSize: '100%' },
462
+ '&[data-scroll="top"], &[data-scroll="mid"]': { borderBlockStart: '1px solid var(--border--base--secondary)' },
463
+ },
464
+ }),
465
+ },
466
+ },
467
+
401
468
  MuiCheckbox: {
402
469
  defaultProps: { disableRipple: true, checkedIcon: _h(CheckboxCheckedIcon), indeterminateIcon: _h(CheckboxIndeterminateIcon) },
403
470
  styleOverrides: {
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ // packages/mui/ui/BottomSheet.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
3
+ import React from 'react';
4
+ import { withAliases } from './_alias-props.mjs';
5
+ import { BottomSheetBase } from './_sheet-base.mjs';
6
+ const h = React.createElement;
7
+
8
+ export const NeoBottomSheet = React.forwardRef(function NeoBottomSheet(rawProps, ref) {
9
+ const { open, onClose, title, image, actions, children, ...rest } = withAliases('NeoBottomSheet', rawProps);
10
+ return h(BottomSheetBase, {
11
+ open, onClose, title, image, actions, closable: true,
12
+ ...rest, ref,
13
+ }, children);
14
+ });
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, useMediaQuery } from '@mui/material';
5
5
  import { NeoButtonIcon } from './ButtonIcon.mjs';
6
6
  import { renderIcon } from './_icon.mjs';
7
+ import { useScrollState } from './_scroll-state.mjs';
7
8
  const h = React.createElement;
8
9
 
9
10
  const ICON_BY_TYPE = {
@@ -12,9 +13,12 @@ const ICON_BY_TYPE = {
12
13
  };
13
14
 
14
15
 
15
- const unaPorUna = (acciones) => {
16
+ export const unaPorUna = (acciones) => {
16
17
  const lista = React.Children.toArray(acciones);
17
- return lista.length === 1 && lista[0].type === React.Fragment ? React.Children.toArray(lista[0].props.children) : lista;
18
+ if (lista.length !== 1 || !React.isValidElement(lista[0])) return lista;
19
+ const [unico] = lista;
20
+ const pelado = unico.type === 'div' && !unico.props.className && !unico.props.style;
21
+ return unico.type === React.Fragment || pelado ? React.Children.toArray(unico.props.children) : lista;
18
22
  };
19
23
 
20
24
  export const ModalDialogBase = React.forwardRef(function ModalDialogBase({
@@ -27,31 +31,7 @@ export const ModalDialogBase = React.forwardRef(function ModalDialogBase({
27
31
  const apiladas = fullScreen ? angosto : size === 'xs';
28
32
  const accionesEnOrden = apiladas && actions ? unaPorUna(actions).reverse() : actions;
29
33
 
30
- const contentRef = React.useRef(null);
31
- const [contentNode, setContentNode] = React.useState(null);
32
- const setContent = React.useCallback((node) => { contentRef.current = node; setContentNode(node); }, []);
33
- const [scroll, setScroll] = React.useState('none');
34
- const [overflows, setOverflows] = React.useState(false);
35
- const watchScroll = React.useCallback(() => {
36
- const el = contentRef.current;
37
- if (!el) return;
38
- const extra = el.scrollHeight - el.clientHeight;
39
- setOverflows(extra > 1);
40
- if (extra <= 1) return setScroll('none');
41
- if (el.scrollTop <= 1) return setScroll('top');
42
- if (el.scrollTop >= extra - 1) return setScroll('bottom');
43
- setScroll('mid');
44
- }, []);
45
- React.useEffect(() => {
46
- const el = contentNode;
47
- if (!el) return undefined;
48
- watchScroll();
49
- if (typeof ResizeObserver === 'undefined') return undefined;
50
- const ro = new ResizeObserver(watchScroll);
51
- ro.observe(el);
52
- for (const h of el.children) ro.observe(h);
53
- return () => ro.disconnect();
54
- }, [watchScroll, open, children, contentNode]);
34
+ const { setRegion: setContent, watchScroll, scroll, overflows } = useScrollState([open, children]);
55
35
  const titleId = `neo-modal-${auto}-title`;
56
36
  const descriptionId = description ? `neo-modal-${auto}-description` : undefined;
57
37
  const rowId = `neo-modal-${auto}-row`;
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+ // packages/mui/ui/_scroll-state.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
3
+ import React from 'react';
4
+
5
+ export function useScrollState(deps = []) {
6
+ const ref = React.useRef(null);
7
+ const [node, setNode] = React.useState(null);
8
+ const setRegion = React.useCallback((n) => { ref.current = n; setNode(n); }, []);
9
+ const [scroll, setScroll] = React.useState('none');
10
+ const [overflows, setOverflows] = React.useState(false);
11
+
12
+ const watchScroll = React.useCallback(() => {
13
+ const el = ref.current;
14
+ if (!el) return;
15
+ const extra = el.scrollHeight - el.clientHeight;
16
+ setOverflows(extra > 1);
17
+ if (extra <= 1) return setScroll('none');
18
+ if (el.scrollTop <= 1) return setScroll('top');
19
+ if (el.scrollTop >= extra - 1) return setScroll('bottom');
20
+ setScroll('mid');
21
+ }, []);
22
+
23
+ React.useEffect(() => {
24
+ const el = node;
25
+ if (!el) return undefined;
26
+ watchScroll();
27
+ if (typeof ResizeObserver === 'undefined') return undefined;
28
+ const ro = new ResizeObserver(watchScroll);
29
+ ro.observe(el);
30
+ for (const hijo of el.children) ro.observe(hijo);
31
+ return () => ro.disconnect();
32
+ }, [watchScroll, node, ...deps]);
33
+
34
+ return { setRegion, watchScroll, scroll, overflows };
35
+ }
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+ // packages/mui/ui/_sheet-base.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
3
+ import React from 'react';
4
+ import { SwipeableDrawer } from '@mui/material';
5
+ import { NeoButtonIcon } from './ButtonIcon.mjs';
6
+ import { renderIcon } from './_icon.mjs';
7
+ import { useScrollState } from './_scroll-state.mjs';
8
+ import { unaPorUna } from './_modal-base.mjs';
9
+ const h = React.createElement;
10
+
11
+
12
+ export const BottomSheetBase = React.forwardRef(function BottomSheetBase({
13
+ open, onClose, onOpen, title, image, actions, children, closable = true,
14
+ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, ...rest
15
+ }, ref) {
16
+ const auto = React.useId();
17
+ const titleId = title && !image ? `neo-sheet-${auto}-title` : undefined;
18
+
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ if (!title && !ariaLabel && !ariaLabelledBy) {
21
+ console.warn('[Neo] BottomSheet: NeoBottomSheet needs a name: pass `title`, or —when you pass `image`— give the content a heading and point `aria-labelledby` at it.');
22
+ }
23
+ if (!onClose) {
24
+ console.warn('[Neo] BottomSheet: NeoBottomSheet needs `onClose`: without it the sheet has no ✕, no swipe and no way out.');
25
+ }
26
+ if (title && image) {
27
+ console.warn('[Neo] BottomSheet: with `image` the header has no title slot — the master draws one or the other. The `title` will not be shown.');
28
+ }
29
+ }
30
+
31
+ const { setRegion, watchScroll, scroll, overflows } = useScrollState([open, children]);
32
+
33
+ const accionesEnOrden = actions ? unaPorUna(actions).reverse() : actions;
34
+
35
+ const close = closable && onClose
36
+ ? h(NeoButtonIcon, { key: 'x', onClick: onClose, 'aria-label': 'Cerrar',
37
+ variant: image ? 'primary' : 'tertiary', surface: image ? 'inverse' : 'default',
38
+ size: 'medium', className: 'NeoBottomSheet-close' },
39
+ renderIcon('system-close'))
40
+ : null;
41
+
42
+ const handle = h('div', { key: 'h', className: 'NeoBottomSheet-handle', 'aria-hidden': 'true' });
43
+
44
+ const header = image
45
+ ? h('div', { key: 'hdr', className: 'NeoBottomSheet-headerImage' }, image, handle, close)
46
+ : h('header', { key: 'hdr', className: 'NeoBottomSheet-header' },
47
+ handle,
48
+ h('div', { className: 'NeoBottomSheet-headerRow' },
49
+ title ? h('h2', { id: titleId, className: 'NeoBottomSheet-title' }, title) : null,
50
+ close));
51
+
52
+ return h(SwipeableDrawer, {
53
+ ref,
54
+ anchor: 'bottom',
55
+ open: !!open,
56
+ onClose,
57
+ onOpen: onOpen || (() => {}),
58
+ disableSwipeToOpen: true,
59
+ slotProps: { backdrop: { 'aria-hidden': 'true' } },
60
+ className,
61
+ ...rest,
62
+ PaperProps: {
63
+ ...rest.PaperProps,
64
+ className: ['NeoBottomSheet', rest.PaperProps?.className].filter(Boolean).join(' '),
65
+ 'data-image': image ? 'true' : 'false',
66
+ 'data-scroll': scroll,
67
+ role: 'dialog',
68
+ 'aria-modal': 'true',
69
+ 'aria-labelledby': ariaLabelledBy || titleId,
70
+ 'aria-label': (ariaLabelledBy || titleId) ? undefined : ariaLabel,
71
+ tabIndex: -1,
72
+ },
73
+ ...(rest.slotProps ? { slotProps: { backdrop: { 'aria-hidden': 'true' }, ...rest.slotProps } } : {}),
74
+ },
75
+ header,
76
+ h('div', { key: 'c', className: 'NeoBottomSheet-content', ref: setRegion, onScroll: watchScroll,
77
+ 'data-scroll': scroll,
78
+ ...(overflows ? { tabIndex: 0, role: 'region', 'aria-labelledby': ariaLabelledBy || titleId } : null) },
79
+ children),
80
+ actions ? h('footer', { key: 'a', className: 'NeoBottomSheet-actions', 'data-scroll': scroll }, accionesEnOrden) : null,
81
+ );
82
+ });
package/ui/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  /// <reference path="../theme/neo.d.ts" />
10
10
  import type { ReactNode, ReactElement, ForwardRefExoticComponent, RefAttributes, Ref, ChangeEvent, SyntheticEvent, MouseEvent, HTMLAttributes } from 'react';
11
11
  import type { SxProps, Theme, ThemeOptions } from '@mui/material/styles';
12
- import type { AlertProps, AutocompleteProps, BadgeProps, ButtonProps, CheckboxProps, ChipProps, CircularProgressProps, DialogProps, IconButtonProps, LinkProps, MenuProps, OutlinedInputProps, RadioProps, RadioGroupProps, SelectProps, SnackbarProps, SwitchProps, TabsProps, TooltipProps } from '@mui/material';
12
+ import type { AlertProps, AutocompleteProps, BadgeProps, ButtonProps, CheckboxProps, ChipProps, CircularProgressProps, DialogProps, IconButtonProps, LinkProps, MenuProps, OutlinedInputProps, RadioProps, RadioGroupProps, SelectProps, SnackbarProps, SwipeableDrawerProps, SwitchProps, TabsProps, TooltipProps } from '@mui/material';
13
13
 
14
14
  /** A handler checked bivariantly, the way React types its own event handlers: one typed with MUI's type still fits. */
15
15
  type NeoHandler<A extends unknown[]> = { bivarianceHack(...args: A): void }['bivarianceHack'];
@@ -103,6 +103,27 @@ export interface NeoBannerProps extends Partial<Omit<HTMLAttributes<HTMLElement>
103
103
  }
104
104
  export declare const NeoBanner: ForwardRefExoticComponent<NeoBannerProps & ({ title: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>>;
105
105
 
106
+ /** Rises from the bottom edge on mobile with complementary content that can wait, or a short task, while the person can still see and return to the page underneath. NO: If the content demands a decision, or reports an error the person has to resolve, it is `modal-dialog`. If it is a long task or dense information, it is `modal-fullscreen`. On desktop the same content moves to a lateral drawer: the sheet is mobile-only. */
107
+ export interface NeoBottomSheetProps extends Partial<Omit<SwipeableDrawerProps, 'open' | 'onClose' | 'title' | 'image' | 'actions' | 'children' | 'className' | 'sx' | 'ref'>> {
108
+ /** The panel or the bubble, open (controlled). */
109
+ open?: boolean;
110
+ /** The close handler. Its presence draws the ✕. */
111
+ onClose?: (...args: unknown[]) => void;
112
+ /** The title. It is a prop and not content because `aria-labelledby` anchors to it. */
113
+ title?: ReactNode;
114
+ /** */
115
+ image?: ReactNode;
116
+ /** The call to action. One at most (C1). */
117
+ actions?: ReactNode;
118
+ /** The content. Whoever uses the component provides it; in the stories it comes from the minimal example. */
119
+ children?: ReactNode;
120
+ /** Classes from the consumer. They merge with the component ones; they do not replace them. */
121
+ className?: string;
122
+ /** The MUI `sx`: per-instance styles, with access to the theme. */
123
+ sx?: SxProps<Theme>;
124
+ }
125
+ export declare const NeoBottomSheet: ForwardRefExoticComponent<NeoBottomSheetProps & ({ title: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>>;
126
+
106
127
  /** Runs actions. NO: To navigate it is `link`; for an on/off option it is `toggle`. */
107
128
  export interface NeoButtonProps extends Partial<Omit<ButtonProps, 'family' | 'variant' | 'surface' | 'size' | 'label' | 'iconLeft' | 'iconRight' | 'loading' | 'disabled' | 'children' | 'className' | 'sx' | 'color' | 'ref'>> {
108
129
  /** semantic family Default: 'system'. */
package/ui/index.mjs CHANGED
@@ -19,6 +19,7 @@ export { NeoEmptyState } from './EmptyState.mjs';
19
19
  export { NeoMenu } from './Menu.mjs';
20
20
  export { NeoModalDialog } from './ModalDialog.mjs';
21
21
  export { NeoModalFullscreen } from './ModalFullscreen.mjs';
22
+ export { NeoBottomSheet } from './BottomSheet.mjs';
22
23
  export { NeoTabs } from './Tabs.mjs';
23
24
  export { NeoBadge } from './Badge.mjs';
24
25
  export { NeoTag } from './Tag.mjs';