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

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
@@ -14,10 +14,10 @@ entry point.
14
14
 
15
15
  ## Install
16
16
 
17
- While the version is a release candidate, install it with the `next` tag, together with the font:
17
+ Install it together with the font:
18
18
 
19
19
  ```sh
20
- npm i @neo-design/foundations@next @neo-design/mui@next @fontsource/noto-sans
20
+ npm i @neo-design/foundations @neo-design/mui @fontsource/noto-sans
21
21
  ```
22
22
 
23
23
  npm brings React, Material UI and Emotion as peers, in the versions this package supports —the `peerDependencies`
@@ -25,7 +25,7 @@ of [`package.json`](package.json)—. **With pnpm**, which does not let your app
25
25
  itself, name them:
26
26
 
27
27
  ```sh
28
- pnpm add @neo-design/foundations@next @neo-design/mui@next @fontsource/noto-sans \
28
+ pnpm add @neo-design/foundations @neo-design/mui @fontsource/noto-sans \
29
29
  @mui/material@6 @emotion/react @emotion/styled react react-dom
30
30
  ```
31
31
 
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",
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"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@emotion/react": "^11",
@@ -398,6 +398,119 @@ 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
+ '&.MuiDrawer-paperAnchorRight': {
466
+ backgroundColor: 'var(--surface--base--default)',
467
+ inlineSize: '540px', maxInlineSize: '100vw', blockSize: '100dvh', borderRadius: 0,
468
+ display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
469
+ '& > *': { alignSelf: 'stretch' },
470
+ '&:focus': { outline: 0 },
471
+ },
472
+ '& .NeoDrawer-header': {
473
+ display: 'flex', flexDirection: 'column', flex: '0 0 auto',
474
+ justifyContent: 'center', alignItems: 'flex-end',
475
+ '& > *': { alignSelf: 'stretch' },
476
+ paddingInline: 'var(--neo-space-xl-rem)', paddingBlock: 'var(--neo-space-lg-rem)', gap: 'var(--neo-space-md-rem)',
477
+ },
478
+ '&[data-scroll="top"] .NeoDrawer-header, &[data-scroll="mid"] .NeoDrawer-header, &[data-scroll="bottom"] .NeoDrawer-header': {
479
+ borderBlockEnd: '1px solid var(--border--base--secondary)',
480
+ },
481
+ '& .NeoDrawer-headerRow': {
482
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
483
+ blockSize: '48px', gap: 'var(--neo-space-none-rem)',
484
+ },
485
+ '& .NeoDrawer-title': {
486
+ ...theme.typography.titleSmBold, color: 'var(--text--base--default)',
487
+ flex: '1 1 auto', minInlineSize: 0, margin: 0,
488
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
489
+ },
490
+ '& .NeoDrawer-content': {
491
+ ...theme.typography.bodyLgRegular, color: 'var(--text--base--default)',
492
+ flex: '1 1 auto', overflowY: 'auto',
493
+ paddingInline: 'var(--neo-space-xl-rem)', paddingBlock: `0 var(--neo-space-lg-rem)`,
494
+ '&:focus-visible': { outline: 'none', boxShadow: focusRingInset.boxShadow },
495
+ },
496
+ '& .NeoDrawer-image': {
497
+ blockSize: '224px', overflow: 'hidden',
498
+ marginInline: 'calc(-1 * var(--neo-space-xl-rem))',
499
+ '& > img, & > picture, & > video': { inlineSize: '100%', blockSize: '100%', objectFit: 'cover', display: 'block' },
500
+ },
501
+ '& .NeoDrawer-actions': {
502
+ display: 'flex', flexDirection: 'column', gap: 'var(--neo-space-md-rem)', flex: '0 0 auto',
503
+ paddingInline: 'var(--neo-space-xl-rem)',
504
+ paddingBlock: `var(--neo-space-lg-rem) var(--neo-space-2xl-rem)`,
505
+ backgroundColor: 'var(--surface--base--default)',
506
+ '& > div': { display: 'contents' },
507
+ '& .MuiButton-root': { inlineSize: '100%' },
508
+ '&[data-scroll="top"], &[data-scroll="mid"]': { borderBlockStart: '1px solid var(--border--base--secondary)' },
509
+ },
510
+ }),
511
+ },
512
+ },
513
+
401
514
  MuiCheckbox: {
402
515
  defaultProps: { disableRipple: true, checkedIcon: _h(CheckboxCheckedIcon), indeterminateIcon: _h(CheckboxIndeterminateIcon) },
403
516
  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
+ });
package/ui/Drawer.mjs ADDED
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ // packages/mui/ui/Drawer.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 { DrawerBase } from './_sheet-base.mjs';
6
+ const h = React.createElement;
7
+
8
+ export const NeoDrawer = React.forwardRef(function NeoDrawer(rawProps, ref) {
9
+ const { open, onClose, title, image, actions, children, ...rest } = withAliases('NeoDrawer', rawProps);
10
+ return h(DrawerBase, {
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,139 @@
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, Drawer } 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
+ });
83
+
84
+
85
+ export const DrawerBase = React.forwardRef(function DrawerBase({
86
+ open, onClose, title, image, actions, children, closable = true,
87
+ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, ...rest
88
+ }, ref) {
89
+ const auto = React.useId();
90
+ const titleId = title ? `neo-drawer-${auto}-title` : undefined;
91
+
92
+ if (process.env.NODE_ENV !== 'production') {
93
+ if (!title) console.warn('[Neo] Drawer: NeoDrawer needs `title`: it is the drawer\'s name for a screen reader, and the header always shows it.');
94
+ if (!onClose) console.warn('[Neo] Drawer: NeoDrawer needs `onClose`: without it the drawer has no ✕ and no way out.');
95
+ }
96
+
97
+ const { setRegion, watchScroll, scroll, overflows } = useScrollState([open, children]);
98
+ const accionesEnOrden = actions ? unaPorUna(actions).reverse() : actions;
99
+
100
+ const close = closable && onClose
101
+ ? h(NeoButtonIcon, { key: 'x', onClick: onClose, 'aria-label': 'Cerrar', variant: 'tertiary', surface: 'default',
102
+ size: 'large', className: 'NeoDrawer-close' },
103
+ renderIcon('system-close'))
104
+ : null;
105
+
106
+ const header = h('header', { key: 'hdr', className: 'NeoDrawer-header' },
107
+ h('div', { className: 'NeoDrawer-headerRow' },
108
+ title ? h('h2', { id: titleId, className: 'NeoDrawer-title' }, title) : null,
109
+ close));
110
+
111
+ return h(Drawer, {
112
+ ref,
113
+ anchor: 'right',
114
+ open: !!open,
115
+ onClose,
116
+ slotProps: { backdrop: { 'aria-hidden': 'true' } },
117
+ className,
118
+ ...rest,
119
+ PaperProps: {
120
+ ...rest.PaperProps,
121
+ className: ['NeoDrawer', rest.PaperProps?.className].filter(Boolean).join(' '),
122
+ 'data-scroll': scroll,
123
+ role: 'dialog',
124
+ 'aria-modal': 'true',
125
+ 'aria-labelledby': ariaLabelledBy || titleId,
126
+ 'aria-label': (ariaLabelledBy || titleId) ? undefined : ariaLabel,
127
+ tabIndex: -1,
128
+ },
129
+ ...(rest.slotProps ? { slotProps: { backdrop: { 'aria-hidden': 'true' }, ...rest.slotProps } } : {}),
130
+ },
131
+ header,
132
+ h('div', { key: 'c', className: 'NeoDrawer-content', ref: setRegion, onScroll: watchScroll,
133
+ 'data-scroll': scroll,
134
+ ...(overflows ? { tabIndex: 0, role: 'region', 'aria-labelledby': ariaLabelledBy || titleId } : null) },
135
+ image ? h('div', { key: 'img', className: 'NeoDrawer-image' }, image) : null,
136
+ children),
137
+ actions ? h('footer', { key: 'a', className: 'NeoDrawer-actions', 'data-scroll': scroll }, accionesEnOrden) : null,
138
+ );
139
+ });
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, DrawerProps, 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'. */
@@ -253,6 +274,27 @@ export interface NeoComboboxProps<T = string, Multiple extends boolean | undefin
253
274
  }
254
275
  export declare const NeoCombobox: <T = string, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false>(props: NeoComboboxProps<T, Multiple, DisableClearable, FreeSolo> & ({ label: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>) => ReactElement | null;
255
276
 
277
+ /** Enters from the right edge on the web with complementary or extensive content that the person opens on demand, while the page underneath stays in view. 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 with steps, it is `modal-fullscreen`. On mobile the same content goes in a `bottom-sheet`: the drawer is web-only. */
278
+ export interface NeoDrawerProps extends Partial<Omit<DrawerProps, 'open' | 'onClose' | 'title' | 'image' | 'actions' | 'children' | 'className' | 'sx' | 'ref'>> {
279
+ /** The panel or the bubble, open (controlled). */
280
+ open?: boolean;
281
+ /** The close handler. Its presence draws the ✕. */
282
+ onClose?: (...args: unknown[]) => void;
283
+ /** The title. It is a prop and not content because `aria-labelledby` anchors to it. */
284
+ title: ReactNode;
285
+ /** */
286
+ image?: ReactNode;
287
+ /** The call to action. One at most (C1). */
288
+ actions?: ReactNode;
289
+ /** The content. Whoever uses the component provides it; in the stories it comes from the minimal example. */
290
+ children?: ReactNode;
291
+ /** Classes from the consumer. They merge with the component ones; they do not replace them. */
292
+ className?: string;
293
+ /** The MUI `sx`: per-instance styles, with access to the theme. */
294
+ sx?: SxProps<Theme>;
295
+ }
296
+ export declare const NeoDrawer: ForwardRefExoticComponent<NeoDrawerProps & RefAttributes<HTMLElement>>;
297
+
256
298
  /** Takes the place of content that does not exist yet and says what can be done about it. NO: To communicate something that just happened, which is `alert`. For the wait while data loads, which is `spinner`: empty and loading are not the same, and mixing them up leaves the person waiting for something that is not coming. */
257
299
  export interface NeoEmptyStateProps extends Partial<Omit<HTMLAttributes<HTMLElement>, 'icon' | 'title' | 'description' | 'actions' | 'headingLevel' | 'className' | 'ref'>> {
258
300
  /** The icon node. Use the system icons (icons/), not loose SVGs. */
package/ui/index.mjs CHANGED
@@ -19,6 +19,8 @@ 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';
23
+ export { NeoDrawer } from './Drawer.mjs';
22
24
  export { NeoTabs } from './Tabs.mjs';
23
25
  export { NeoBadge } from './Badge.mjs';
24
26
  export { NeoTag } from './Tag.mjs';