Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions docs/guidelines/architecture.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/guidelines/component-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ app/theme/
├── augmentations.d.ts # module augmentation de MUI (variantes, keys de paleta)
├── utils.ts # helpers (rem)
└── components/
├── alert.ts # muiAlert(): el aviso del sistema (Alert.dc.html)
├── button.ts # muiButton(mode)
├── card.ts # muiCard(mode)
├── input.ts # muiOutlinedInput() / muiTextField()
Expand Down
46 changes: 46 additions & 0 deletions src/app/theme/components/alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Components, Theme } from '@mui/material/styles'

import { radius } from '../tokens'

// El aviso del sistema del diseño (Alert.dc.html): fondo tonal de la intención,
// borde de su color base, ícono y texto en el tono `onContainer`, radio 8 y
// 12 × 16 de relleno.
//
// Aplica a `standard` y `outlined`, que en la app se usaban indistintamente
// para lo mismo: el diseño tiene un solo aviso, así que las dos variantes pintan
// igual. `filled` queda como la trae MUI; nada la usa.
export function muiAlert(): Components<Theme>['MuiAlert'] {
return {
// MUI rotula la X en inglés ("Close") y la app no carga su locale: sin esto,
// el lector de pantalla anuncia en inglés el cierre de cada toast.
defaultProps: { closeText: 'Cerrar' },
styleOverrides: {
root: ({ theme, ownerState }) => {
if (ownerState.variant === 'filled') return {}

// Mismo orden que MUI: `color` pisa a `severity`, y sin ninguno es `success`.
const color = theme.vars.palette[ownerState.color ?? ownerState.severity ?? 'success']

return {
...theme.typography.bodyMd,
// 4 + los 8 que MUI ya le pone al mensaje = los 12 del diseño.
padding: theme.spacing(0.5, 2),
borderRadius: radius.base,
border: `1px solid ${color.main}`,
backgroundColor: color.container,
color: color.onContainer,
'& .MuiAlert-icon': { color: color.onContainer, opacity: 1 },
// Sólo la X va en gris, como en el diseño. La zona de acción también
// aloja botones como "Reintentar" (`color="inherit"`), y esos tienen
// que conservar el tono de la intención: son la salida del aviso.
'& .MuiAlert-action > .MuiIconButton-root': { color: theme.vars.palette.text.secondary },
// Con título, el título lleva el tono y el cuerpo baja a secundario.
'& .MuiAlertTitle-root': { fontWeight: 600, color: color.onContainer },
'&:has(.MuiAlertTitle-root) .MuiAlert-message': {
color: theme.vars.palette.text.secondary,
},
}
},
},
}
}
2 changes: 2 additions & 0 deletions src/app/theme/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ThemeOptions } from '@mui/material/styles'

import { muiAlert } from './alert'
import { muiAppBar } from './appBar'
import { muiButton } from './button'
import { muiCard } from './card'
Expand All @@ -25,6 +26,7 @@ export function buildComponents(): ThemeOptions['components'] {
MuiOutlinedInput: muiOutlinedInput(),
MuiTextField: muiTextField(),
MuiCard: muiCard(),
MuiAlert: muiAlert(),
MuiDialog: muiDialog(),
MuiBackdrop: muiBackdrop(),
MuiTypography: muiTypography(),
Expand Down
60 changes: 60 additions & 0 deletions src/features/design-system/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ export const dsCopy = {
title: 'Tabla de datos estándar',
subtitle: 'Pestañas, selección, badges de estado, acciones por fila y paginación',
},
alerts: {
title: 'Avisos del sistema y toasts',
subtitle:
'Alert — en línea dentro de la pantalla o flotando como toast, en sus cuatro intenciones',
},
confirm: {
title: 'Modal de confirmación',
subtitle: 'ModalFrame — estándar, destructiva y rechazada; tamaños sm · md · lg',
},
},
progressGroups: {
load: 'Columna Load — canal neutro',
Expand Down Expand Up @@ -421,3 +430,54 @@ export const dataTableCopy = {
page: (page: number) => `Ir a la página ${page}`,
selectedCount: (count: number) => `${count} seleccionadas`,
} as const

// Las tres variantes del modal de confirmación. La "rechazada" es la destructiva
// después de que el backend dijo que no: queda el motivo y sólo la salida.
export const confirmDialogSamples = {
default: {
trigger: 'Estándar',
title: '¿Confirmar el despacho?',
description: 'Se va a emitir la etiqueta de la orden #ORD-7721-X con Andreani.',
confirm: 'Confirmar despacho',
},
destructive: {
trigger: 'Destructiva',
title: '¿Eliminar la orden?',
description: 'Se va a eliminar la orden #ORD-7721-X. Esta acción no se puede deshacer.',
confirm: 'Eliminar orden',
},
rejected: {
trigger: 'Rechazada',
title: '¿Eliminar la orden?',
description: 'Se va a eliminar la orden #ORD-7721-X. Esta acción no se puede deshacer.',
confirm: 'Eliminar orden',
error: 'No se puede eliminar: la orden ya fue despachada.',
},
} as const

export type ConfirmSampleKey = keyof typeof confirmDialogSamples

export const confirmDialogCopy = {
cancel: 'Cancelar',
close: 'Cerrar',
sizeLabel: 'Tamaño del diálogo',
} as const

// Avisos del sistema (Alert.dc.html). Los mismos cuatro textos sirven en línea y
// como toast: el componente es uno solo, lo que cambia es dónde se monta.
export const alertSamples = [
{ severity: 'success', label: 'Éxito', message: 'Orden #ORD-7721-X procesada.' },
{ severity: 'info', label: 'Información', message: 'Envío pendiente de despacho en CD Ezeiza.' },
{ severity: 'warning', label: 'Advertencia', message: 'Stock bajo en 3 productos del catálogo.' },
{ severity: 'error', label: 'Error', message: 'Falló la sincronización con Tiendanube.' },
] as const

export const alertCopy = {
inline: 'En línea',
toast: 'Toast — se dispara el real, por notify()',
withTitle: {
title: 'La orden cambió mientras la editabas',
body: 'Revisá los cambios antes de guardar: guardar ahora pisa lo que cambió la otra persona.',
},
withAction: { message: 'No pudimos cargar el panel.', action: 'Reintentar' },
} as const
141 changes: 140 additions & 1 deletion src/features/design-system/pages/DesignSystemPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ import ViewColumnOutlinedIcon from '@mui/icons-material/ViewColumnOutlined'
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'
import {
Alert,
AlertTitle,
Box,
Button,
Card,
Divider,
IconButton,
Stack,
TextField,
ToggleButton,
ToggleButtonGroup,
Typography,
useTheme,
} from '@mui/material'
import { useState } from 'react'
import type { ReactElement, ReactNode } from 'react'
import {
CompactStatCard,
ConfirmDialog,
DataTable,
Logo,
PageWrapper,
Expand All @@ -44,20 +49,26 @@ import {
StepsProgress,
TopNavBar,
type DataTableColumn,
type ModalSize,
type StatusVariant,
type TopNavThemeMode,
} from 'shared/components'
import { notify } from 'shared/store'
import { useThemeMode } from 'shared/store/uiStore'

import { FulfillmentPanel } from '../components/FulfillmentPanel'
import { OperationalStatusCard } from '../components/OperationalStatusCard'
import {
alertCopy,
alertSamples,
badgeSamples,
badgeSizes,
buttonHierarchies,
buttonIntents,
compactGroups,
compactStatSamples,
confirmDialogCopy,
confirmDialogSamples,
dataTableCopy,
dataTableOrders,
dataTableTabs,
Expand All @@ -78,7 +89,7 @@ import {
topNavDemoUser,
typeSpecs,
} from '../content'
import type { CompactSampleKey, DemoOrder, StatSampleKey } from '../content'
import type { CompactSampleKey, ConfirmSampleKey, DemoOrder, StatSampleKey } from '../content'

// Grillas del catálogo: colapsan a una columna en mobile para que las tarjetas
// no se compriman.
Expand Down Expand Up @@ -279,6 +290,122 @@ function DataTableDemo() {
)
}

// Los toasts de la vitrina son los reales: `notify()` escribe en el store y los
// dibuja el `NotificationHost` de los providers, como en cualquier pantalla.
function AlertsDemo() {
return (
<Stack spacing={3}>
<Stack spacing={1.5}>
<Typography variant="labelCaps" color="text.secondary">
{alertCopy.inline}
</Typography>
{alertSamples.map((sample) => (
<Alert key={sample.severity} severity={sample.severity} onClose={() => {}}>
{sample.message}
</Alert>
))}
<Alert severity="warning">
<AlertTitle>{alertCopy.withTitle.title}</AlertTitle>
{alertCopy.withTitle.body}
</Alert>
<Alert
severity="error"
action={
<Button color="inherit" size="small">
{alertCopy.withAction.action}
</Button>
}
>
{alertCopy.withAction.message}
</Alert>
</Stack>
<Stack spacing={1.5}>
<Typography variant="labelCaps" color="text.secondary">
{alertCopy.toast}
</Typography>
<Stack direction="row" spacing={1.5} useFlexGap sx={{ flexWrap: 'wrap' }}>
{alertSamples.map((sample) => (
<Button
key={sample.severity}
variant="glass"
color={sample.severity}
onClick={() => notify(sample.message, sample.severity)}
>
{sample.label}
</Button>
))}
</Stack>
</Stack>
</Stack>
)
}

const CONFIRM_SAMPLE_KEYS = Object.keys(confirmDialogSamples) as ConfirmSampleKey[]
const CONFIRM_SIZES: ModalSize[] = ['sm', 'md', 'lg']

// Inerte como el resto del catálogo: confirmar sólo cierra el diálogo.
function ConfirmDialogDemo() {
// La muestra y el estado abierto van separados: al cerrar, el diálogo se
// desvanece con la muestra que tenía, en vez de saltar a otra a mitad del
// fundido.
const [sampleKey, setSampleKey] = useState<ConfirmSampleKey>('default')
const [open, setOpen] = useState(false)
const [size, setSize] = useState<ModalSize>('sm')
const sample = confirmDialogSamples[sampleKey]
const close = () => setOpen(false)

return (
<>
<Stack direction="row" spacing={1.5} useFlexGap sx={{ flexWrap: 'wrap' }}>
<ToggleButtonGroup
exclusive
value={size}
onChange={(_, next: ModalSize | null) => {
// Un toggle exclusivo devuelve `null` al reclickear el activo: se
// ignora para que siempre haya un tamaño elegido.
if (next !== null) setSize(next)
}}
aria-label={confirmDialogCopy.sizeLabel}
>
{CONFIRM_SIZES.map((option) => (
<ToggleButton key={option} value={option}>
{option}
</ToggleButton>
))}
</ToggleButtonGroup>
{CONFIRM_SAMPLE_KEYS.map((key) => (
<Button
key={key}
variant="outlined"
color={key === 'default' ? 'primary' : 'error'}
onClick={() => {
setSampleKey(key)
setOpen(true)
}}
>
{confirmDialogSamples[key].trigger}
</Button>
))}
</Stack>
<ConfirmDialog
open={open}
tone={sampleKey === 'default' ? 'default' : 'destructive'}
size={size}
title={sample.title}
description={sample.description}
confirmLabel={sample.confirm}
cancelLabel={confirmDialogCopy.cancel}
closeLabel={confirmDialogCopy.close}
canConfirm={!('error' in sample)}
onConfirm={close}
onClose={close}
>
{'error' in sample ? <Alert severity="error">{sample.error}</Alert> : null}
</ConfirmDialog>
</>
)
}

export function DesignSystemPage() {
// Demo autocontenido: estado local, no toca el uiStore real. Un catálogo
// debe ser inerte — antes clickear la hamburguesa del ejemplo colapsaba el
Expand Down Expand Up @@ -814,6 +941,18 @@ export function DesignSystemPage() {
>
<DataTableDemo />
</Section>

<Divider />

<Section title={dsCopy.sections.alerts.title} subtitle={dsCopy.sections.alerts.subtitle}>
<AlertsDemo />
</Section>

<Divider />

<Section title={dsCopy.sections.confirm.title} subtitle={dsCopy.sections.confirm.subtitle}>
<ConfirmDialogDemo />
</Section>
</Stack>
</PageWrapper>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Box, Button } from '@mui/material'
import { styled } from '@mui/material/styles'

// El shell (root, header, form, body, footer, campos) viene de
// `ProductModalShell`. Acá quedan las piezas propias del alta.
// El marco (cabecera, form, cuerpo y pie) es el `ModalFrame` de
// `shared/components`. Acá quedan las piezas propias del alta.
//
// El encabezado de sección de este frame es distinto al de la edición: ícono en
// un recuadro tintado en vez de la barra de acento. Es la versión v2 del
Expand Down
Loading
Loading