@stackcraftmx/calcita 0.27.0 → 0.30.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 +149 -0
- package/dist/DataTable/AuditTrailDialog.js +18 -14
- package/dist/DataTable/AuditTrailDialog.js.map +1 -1
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js +12 -6
- package/dist/DataTable/ColumnFilters/HeaderFilterPopover.js.map +1 -1
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js +8 -3
- package/dist/DataTable/ColumnFilters/MultiSelectColumnFilter.js.map +1 -1
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js +26 -18
- package/dist/DataTable/ColumnFilters/SortableFilterHeader.js.map +1 -1
- package/dist/DataTable/EmptyState.js +8 -3
- package/dist/DataTable/EmptyState.js.map +1 -1
- package/dist/DataTable/FilterChips.js +11 -5
- package/dist/DataTable/FilterChips.js.map +1 -1
- package/dist/DataTable/GoToPage.js +9 -4
- package/dist/DataTable/GoToPage.js.map +1 -1
- package/dist/DataTable/ServerTable.js +74 -61
- package/dist/DataTable/ServerTable.js.map +1 -1
- package/dist/DataTable/columns.js +1 -8
- package/dist/DataTable/columns.js.map +1 -1
- package/dist/access/PermissionMatrix.js +18 -9
- package/dist/access/PermissionMatrix.js.map +1 -1
- package/dist/account/AccountSecuritySection.js +8 -3
- package/dist/account/AccountSecuritySection.js.map +1 -1
- package/dist/account/DemoTotpCodeButton.js +8 -3
- package/dist/account/DemoTotpCodeButton.js.map +1 -1
- package/dist/account/ProfileScreen.js +19 -13
- package/dist/account/ProfileScreen.js.map +1 -1
- package/dist/account/SignInScreen.js +6 -2
- package/dist/account/SignInScreen.js.map +1 -1
- package/dist/appShell/AccountMenu.js +13 -5
- package/dist/appShell/AccountMenu.js.map +1 -1
- package/dist/appShell/AppLayout.js +100 -71
- package/dist/appShell/AppLayout.js.map +1 -1
- package/dist/appShell/AppearanceSettings.js +21 -13
- package/dist/appShell/AppearanceSettings.js.map +1 -1
- package/dist/appShell/AuthLayout.js +26 -13
- package/dist/appShell/AuthLayout.js.map +1 -1
- package/dist/appShell/BarActions.js +10 -5
- package/dist/appShell/BarActions.js.map +1 -1
- package/dist/appShell/BarCapsule.js +9 -4
- package/dist/appShell/BarCapsule.js.map +1 -1
- package/dist/appShell/BarSearch.js +7 -3
- package/dist/appShell/BarSearch.js.map +1 -1
- package/dist/appShell/HeaderTools.js +20 -15
- package/dist/appShell/HeaderTools.js.map +1 -1
- package/dist/appShell/MobileTabBar.js +25 -14
- package/dist/appShell/MobileTabBar.js.map +1 -1
- package/dist/appShell/SettingsMenu.js +7 -3
- package/dist/appShell/SettingsMenu.js.map +1 -1
- package/dist/appShell/SideMenu.js +53 -35
- package/dist/appShell/SideMenu.js.map +1 -1
- package/dist/appShell/SideMenuEntry.js +18 -9
- package/dist/appShell/SideMenuEntry.js.map +1 -1
- package/dist/appShell/SubmenuActionsMenu.js +6 -2
- package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
- package/dist/appShell/TabBarEditor.js +16 -7
- package/dist/appShell/TabBarEditor.js.map +1 -1
- package/dist/appShell/TopMenu.js +12 -7
- package/dist/appShell/TopMenu.js.map +1 -1
- package/dist/appShell/Wordmark.js +6 -2
- package/dist/appShell/Wordmark.js.map +1 -1
- package/dist/auth/PasswordChecklist.js +14 -7
- package/dist/auth/PasswordChecklist.js.map +1 -1
- package/dist/calendar/EventCalendar.js +26 -19
- package/dist/calendar/EventCalendar.js.map +1 -1
- package/dist/calendar/EventDetails.js +28 -13
- package/dist/calendar/EventDetails.js.map +1 -1
- package/dist/downloads/DownloadManager.js +34 -18
- package/dist/downloads/DownloadManager.js.map +1 -1
- package/dist/feedback/ConfirmDialog.js +22 -17
- package/dist/feedback/ConfirmDialog.js.map +1 -1
- package/dist/feedback/ModalWaiting.js +12 -5
- package/dist/feedback/ModalWaiting.js.map +1 -1
- package/dist/feedback/NewBuildNotice.js +16 -8
- package/dist/feedback/NewBuildNotice.js.map +1 -1
- package/dist/filepond/FileDropzone.js +39 -31
- package/dist/filepond/FileDropzone.js.map +1 -1
- package/dist/forms/FormProblemAlert.js +4 -1
- package/dist/forms/FormProblemAlert.js.map +1 -1
- package/dist/forms/auth/SignInForm.js +6 -2
- package/dist/forms/auth/SignInForm.js.map +1 -1
- package/dist/forms/security/SecondStepForm.js +6 -2
- package/dist/forms/security/SecondStepForm.js.map +1 -1
- package/dist/forms/security/TwoStepVerificationCard.js +19 -9
- package/dist/forms/security/TwoStepVerificationCard.js.map +1 -1
- package/dist/hookForm/FormProblemAlert.js +4 -1
- package/dist/hookForm/FormProblemAlert.js.map +1 -1
- package/dist/inbox/InboxLayout.js +47 -29
- package/dist/inbox/InboxLayout.js.map +1 -1
- package/dist/inbox/MessageList.js +53 -32
- package/dist/inbox/MessageList.js.map +1 -1
- package/dist/inbox/MessageView.js +31 -15
- package/dist/inbox/MessageView.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/inputs/ColorInput.js +22 -17
- package/dist/inputs/ColorInput.js.map +1 -1
- package/dist/inputs/FormSection.js +66 -63
- package/dist/inputs/FormSection.js.map +1 -1
- package/dist/inputs/SelectInput.js +21 -16
- package/dist/inputs/SelectInput.js.map +1 -1
- package/dist/inputs/TextInput.js +25 -20
- package/dist/inputs/TextInput.js.map +1 -1
- package/dist/inputs/WizardLayout.js +21 -15
- package/dist/inputs/WizardLayout.js.map +1 -1
- package/dist/kanban/KanbanBoard.js +82 -50
- package/dist/kanban/KanbanBoard.js.map +1 -1
- package/dist/maps/MapView.js +19 -14
- package/dist/maps/MapView.js.map +1 -1
- package/dist/pwa/InstallAppSetting.js +4 -1
- package/dist/pwa/InstallAppSetting.js.map +1 -1
- package/dist/pwa/InstallInstructionsDialog.js +6 -2
- package/dist/pwa/InstallInstructionsDialog.js.map +1 -1
- package/dist/pwa/InstallPromptBanner.js +10 -5
- package/dist/pwa/InstallPromptBanner.js.map +1 -1
- package/dist/security/ActivityLog.js +6 -2
- package/dist/security/ActivityLog.js.map +1 -1
- package/dist/security/RecoveryCodesDialog.js +8 -3
- package/dist/security/RecoveryCodesDialog.js.map +1 -1
- package/dist/security/SessionsCard.js +12 -5
- package/dist/security/SessionsCard.js.map +1 -1
- package/dist/theme/components/glassComponents.js +19 -16
- package/dist/theme/components/glassComponents.js.map +1 -1
- package/dist/theme/components/minimalComponents.js +10 -6
- package/dist/theme/components/minimalComponents.js.map +1 -1
- package/dist/theme/createToolkitTheme.js +23 -6
- package/dist/theme/createToolkitTheme.js.map +1 -1
- package/dist/theme/cssTokens.js +23 -0
- package/dist/theme/cssTokens.js.map +1 -0
- package/dist/theme/glassSurface.js +64 -10
- package/dist/theme/glassSurface.js.map +1 -1
- package/dist/theme/glassToasterStyles.js +2 -3
- package/dist/theme/glassToasterStyles.js.map +1 -1
- package/dist/theme/themePieces.js +16 -0
- package/dist/theme/themePieces.js.map +1 -0
- package/dist/types/DataTable/AuditTrailDialog.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/HeaderFilterPopover.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/MultiSelectColumnFilter.d.ts +1 -1
- package/dist/types/DataTable/ColumnFilters/SortableFilterHeader.d.ts +1 -1
- package/dist/types/DataTable/EmptyState.d.ts +1 -1
- package/dist/types/DataTable/FilterChips.d.ts +1 -1
- package/dist/types/DataTable/GoToPage.d.ts +1 -1
- package/dist/types/DataTable/ServerTable.d.ts +1 -1
- package/dist/types/access/PermissionMatrix.d.ts +1 -1
- package/dist/types/account/AccountSecuritySection.d.ts +1 -1
- package/dist/types/account/DemoTotpCodeButton.d.ts +1 -1
- package/dist/types/account/ProfileScreen.d.ts +1 -1
- package/dist/types/account/SignInScreen.d.ts +1 -1
- package/dist/types/appShell/AccountMenu.d.ts +1 -1
- package/dist/types/appShell/AppLayout.d.ts +1 -1
- package/dist/types/appShell/AppearanceSettings.d.ts +1 -1
- package/dist/types/appShell/AuthLayout.d.ts +1 -1
- package/dist/types/appShell/BarActions.d.ts +1 -1
- package/dist/types/appShell/BarCapsule.d.ts +1 -1
- package/dist/types/appShell/BarSearch.d.ts +1 -1
- package/dist/types/appShell/HeaderTools.d.ts +1 -1
- package/dist/types/appShell/MobileTabBar.d.ts +1 -1
- package/dist/types/appShell/SettingsMenu.d.ts +1 -1
- package/dist/types/appShell/SideMenu.d.ts +1 -1
- package/dist/types/appShell/SideMenuEntry.d.ts +1 -1
- package/dist/types/appShell/SubmenuActionsMenu.d.ts +1 -1
- package/dist/types/appShell/TabBarEditor.d.ts +1 -1
- package/dist/types/appShell/TopMenu.d.ts +1 -1
- package/dist/types/appShell/Wordmark.d.ts +1 -1
- package/dist/types/auth/PasswordChecklist.d.ts +1 -1
- package/dist/types/calendar/EventCalendar.d.ts +1 -1
- package/dist/types/calendar/EventDetails.d.ts +1 -1
- package/dist/types/feedback/ConfirmDialog.d.ts +1 -1
- package/dist/types/feedback/ModalWaiting.d.ts +1 -1
- package/dist/types/feedback/NewBuildNotice.d.ts +1 -1
- package/dist/types/filepond/FileDropzone.d.ts +1 -1
- package/dist/types/forms/auth/SignInForm.d.ts +1 -1
- package/dist/types/forms/security/SecondStepForm.d.ts +1 -1
- package/dist/types/forms/security/TwoStepVerificationCard.d.ts +1 -1
- package/dist/types/inbox/InboxLayout.d.ts +1 -1
- package/dist/types/inbox/MessageList.d.ts +1 -1
- package/dist/types/inbox/MessageView.d.ts +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/inputs/ColorInput.d.ts +1 -1
- package/dist/types/inputs/FormSection.d.ts +2 -2
- package/dist/types/inputs/SelectInput.d.ts +1 -1
- package/dist/types/inputs/TextInput.d.ts +1 -1
- package/dist/types/inputs/WizardLayout.d.ts +1 -1
- package/dist/types/kanban/KanbanBoard.d.ts +1 -1
- package/dist/types/maps/MapView.d.ts +1 -1
- package/dist/types/pwa/InstallInstructionsDialog.d.ts +1 -1
- package/dist/types/pwa/InstallPromptBanner.d.ts +1 -1
- package/dist/types/security/ActivityLog.d.ts +1 -1
- package/dist/types/security/RecoveryCodesDialog.d.ts +1 -1
- package/dist/types/security/SessionsCard.d.ts +1 -1
- package/dist/types/theme/createToolkitTheme.d.ts +33 -1
- package/dist/types/theme/cssTokens.d.ts +46 -0
- package/dist/types/theme/glassSurface.d.ts +57 -1
- package/dist/types/theme/themePieces.d.ts +20 -0
- package/dist/types/ui/actions/ActionsMenu.d.ts +1 -1
- package/dist/types/ui/actions/DownloadMenu.d.ts +1 -1
- package/dist/types/ui/actions/SelectionActions.d.ts +1 -1
- package/dist/types/ui/filters/DateRangeFilter.d.ts +1 -1
- package/dist/types/ui/filters/PeriodRangeSelector.d.ts +1 -1
- package/dist/types/ui/filters/QuickFilters.d.ts +1 -1
- package/dist/types/ui/filters/SegmentedControl.d.ts +7 -2
- package/dist/types/ui/labels/StatusChip.d.ts +1 -1
- package/dist/types/ui/labels/Tag.d.ts +1 -1
- package/dist/types/ui/layout/CollapsibleSection.d.ts +1 -1
- package/dist/types/ui/layout/ContentDialog.d.ts +1 -1
- package/dist/types/ui/layout/DefinitionList.d.ts +1 -1
- package/dist/types/ui/layout/DetailHeader.d.ts +1 -1
- package/dist/types/ui/layout/ErrorPage.d.ts +1 -1
- package/dist/types/ui/layout/GlassPanel.d.ts +1 -1
- package/dist/types/ui/layout/PageHeader.d.ts +1 -1
- package/dist/types/ui/layout/PageHeading.d.ts +1 -1
- package/dist/types/ui/layout/ProfileHeader.d.ts +1 -1
- package/dist/types/ui/layout/ScrollableRow.d.ts +1 -1
- package/dist/types/ui/layout/SectionCard.d.ts +1 -1
- package/dist/types/ui/metrics/BarList.d.ts +1 -1
- package/dist/types/ui/metrics/CalendarHeatmap.d.ts +1 -1
- package/dist/types/ui/metrics/ChartTooltip.d.ts +1 -1
- package/dist/types/ui/metrics/ColumnChart.d.ts +1 -1
- package/dist/types/ui/metrics/LineChart.d.ts +1 -1
- package/dist/types/ui/metrics/MetricCard.d.ts +1 -1
- package/dist/types/ui/metrics/TableToggle.d.ts +1 -1
- package/dist/types/ui/navigation/ColorSchemeToggle.d.ts +1 -1
- package/dist/types/ui/navigation/FontSizeToggle.d.ts +1 -1
- package/dist/types/ui/navigation/LanguageToggle.d.ts +1 -1
- package/dist/types/ui/navigation/PageBreadcrumbs.d.ts +1 -1
- package/dist/types/ui/timeline/GroupedTimeline.d.ts +1 -1
- package/dist/types/upload/FileDropzone.d.ts +1 -1
- package/dist/ui/actions/ActionsMenu.js +9 -4
- package/dist/ui/actions/ActionsMenu.js.map +1 -1
- package/dist/ui/actions/DownloadMenu.js +21 -17
- package/dist/ui/actions/DownloadMenu.js.map +1 -1
- package/dist/ui/actions/SelectionActions.js +15 -7
- package/dist/ui/actions/SelectionActions.js.map +1 -1
- package/dist/ui/filters/DateRangeFilter.js +11 -5
- package/dist/ui/filters/DateRangeFilter.js.map +1 -1
- package/dist/ui/filters/PeriodRangeSelector.js +11 -5
- package/dist/ui/filters/PeriodRangeSelector.js.map +1 -1
- package/dist/ui/filters/QuickFilters.js +16 -8
- package/dist/ui/filters/QuickFilters.js.map +1 -1
- package/dist/ui/filters/SegmentedControl.js +12 -5
- package/dist/ui/filters/SegmentedControl.js.map +1 -1
- package/dist/ui/labels/StatusChip.js +35 -25
- package/dist/ui/labels/StatusChip.js.map +1 -1
- package/dist/ui/labels/Tag.js +8 -3
- package/dist/ui/labels/Tag.js.map +1 -1
- package/dist/ui/layout/CollapsibleSection.js +18 -8
- package/dist/ui/layout/CollapsibleSection.js.map +1 -1
- package/dist/ui/layout/ContentDialog.js +13 -6
- package/dist/ui/layout/ContentDialog.js.map +1 -1
- package/dist/ui/layout/DefinitionList.js +13 -6
- package/dist/ui/layout/DefinitionList.js.map +1 -1
- package/dist/ui/layout/DetailHeader.js +14 -6
- package/dist/ui/layout/DetailHeader.js.map +1 -1
- package/dist/ui/layout/ErrorPage.js +20 -15
- package/dist/ui/layout/ErrorPage.js.map +1 -1
- package/dist/ui/layout/FullPageLoader.js +4 -1
- package/dist/ui/layout/FullPageLoader.js.map +1 -1
- package/dist/ui/layout/GlassPanel.js +8 -4
- package/dist/ui/layout/GlassPanel.js.map +1 -1
- package/dist/ui/layout/PageHeader.js +10 -4
- package/dist/ui/layout/PageHeader.js.map +1 -1
- package/dist/ui/layout/PageHeading.js +17 -8
- package/dist/ui/layout/PageHeading.js.map +1 -1
- package/dist/ui/layout/ProfileHeader.js +21 -10
- package/dist/ui/layout/ProfileHeader.js.map +1 -1
- package/dist/ui/layout/ScrollableRow.js +19 -15
- package/dist/ui/layout/ScrollableRow.js.map +1 -1
- package/dist/ui/layout/SectionCard.js +10 -4
- package/dist/ui/layout/SectionCard.js.map +1 -1
- package/dist/ui/metrics/BarList.js +9 -4
- package/dist/ui/metrics/BarList.js.map +1 -1
- package/dist/ui/metrics/CalendarHeatmap.js +40 -21
- package/dist/ui/metrics/CalendarHeatmap.js.map +1 -1
- package/dist/ui/metrics/ChartTooltip.js +6 -2
- package/dist/ui/metrics/ChartTooltip.js.map +1 -1
- package/dist/ui/metrics/ColumnChart.js +22 -18
- package/dist/ui/metrics/ColumnChart.js.map +1 -1
- package/dist/ui/metrics/LineChart.js +20 -16
- package/dist/ui/metrics/LineChart.js.map +1 -1
- package/dist/ui/metrics/MetricCard.js +42 -24
- package/dist/ui/metrics/MetricCard.js.map +1 -1
- package/dist/ui/metrics/TableToggle.js +7 -2
- package/dist/ui/metrics/TableToggle.js.map +1 -1
- package/dist/ui/metrics/chartParts.js +14 -5
- package/dist/ui/metrics/chartParts.js.map +1 -1
- package/dist/ui/navigation/ColorSchemeToggle.js +8 -4
- package/dist/ui/navigation/ColorSchemeToggle.js.map +1 -1
- package/dist/ui/navigation/FontSizeToggle.js +6 -2
- package/dist/ui/navigation/FontSizeToggle.js.map +1 -1
- package/dist/ui/navigation/LanguageToggle.js +8 -4
- package/dist/ui/navigation/LanguageToggle.js.map +1 -1
- package/dist/ui/navigation/PageBreadcrumbs.js +12 -6
- package/dist/ui/navigation/PageBreadcrumbs.js.map +1 -1
- package/dist/ui/timeline/GroupedTimeline.js +26 -13
- package/dist/ui/timeline/GroupedTimeline.js.map +1 -1
- package/dist/upload/FileDropzone.js +35 -25
- package/dist/upload/FileDropzone.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventCalendar.js","sources":["../../src/calendar/EventCalendar.jsx"],"sourcesContent":["// Un calendario de eventos (@stackcraftmx/calcita/calendar): FullCalendar por dentro, con la barra del framework (anterior y\n// siguiente, «Hoy», el título y la vista con el control segmentado) y los colores del tema. En el idioma de\n// ToolkitProvider, la semana empieza en lunes y marca la hora actual. Las citas van en bloques de su color (el color es\n// información: el tipo de cita), no en puntitos.\n//\n// <EventCalendar events={[{id: '1', title: 'Entrega P-2375', start: '2026-10-20T10:00', end: '2026-10-20T11:00', tone: 'info'}]}\n// onEventClick={openDetail} onDateSelect={openNewEvent} onEventChange={(change) => api.patch(...)} />\n//\n// Arrastrar un evento lo cambia de día u hora: onEventChange recibe lo nuevo y, si truena (el servidor dijo que no), el\n// evento regresa a donde estaba. Sin onEventChange no se puede arrastrar; sin onDateSelect no se pueden elegir días.\n//\n// Se extiende sin tocar el framework: onDatesChange avisa el rango que se ve (para que el backend mande solo esas citas),\n// onViewChange la vista, renderEvent pinta cada cita a su manera (recibe la cita tal cual, con sus campos propios),\n// fullCalendarProps pasa lo demás de FullCalendar (horario laboral, días ocultos, horas visibles…) y texts los textos. El\n// detalle de una cita para su ventana: EventDetails.\n//\n// OJO: las horas se pintan en la zona del navegador (FullCalendar pide un complemento aparte para zonas con nombre).\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useRef, useState} from 'react';\nimport FullCalendar from '@fullcalendar/react';\nimport dayGridPlugin from '@fullcalendar/daygrid';\nimport interactionPlugin from '@fullcalendar/interaction';\nimport listPlugin from '@fullcalendar/list';\nimport timeGridPlugin from '@fullcalendar/timegrid';\nimport spanishLocale from '@fullcalendar/core/locales/es';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';\nimport ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';\n\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {useToolkitSettings} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport SegmentedControl from '@/ui/filters/SegmentedControl.jsx';\n\n/** Las vistas del calendario (los nombres de FullCalendar). */\nexport const CALENDAR_VIEW = Object.freeze({month: 'dayGridMonth', week: 'timeGridWeek', day: 'timeGridDay', list: 'listWeek'});\n\n// el texto de cada vista en los textos del paquete (calendar.views.*)\nconst VIEW_TEXT_KEY = Object.freeze({\n [CALENDAR_VIEW.month]: 'month', [CALENDAR_VIEW.week]: 'week', [CALENDAR_VIEW.day]: 'day', [CALENDAR_VIEW.list]: 'list',\n});\n// cuántos eventos caben en un día del mes antes del «+2 más»\nconst DAY_MAX_EVENTS = 3;\nconst MONDAY = 1;\n\n/**\n * La primera letra en mayúscula y lo demás como viene: «octubre de 2026» → «Octubre de 2026». OJO: con CSS\n * (text-transform: capitalize) salía «Octubre De 2026»: en español solo va la primera.\n *\n * @param {string} text\n * @returns {string}\n */\nfunction capitalizeFirst(text) {\n return text ? text.charAt(0).toLocaleUpperCase() + text.slice(1) : text;\n}\n\n/**\n * Los colores del calendario sobre el tema (las variables CSS de FullCalendar), en claro y en oscuro.\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction calendarStylesOf(theme) {\n const palette = paletteOf(theme);\n return {\n '--fc-border-color': palette.divider,\n '--fc-page-bg-color': 'transparent',\n '--fc-neutral-bg-color': palette.action.hover,\n '--fc-today-bg-color': `color-mix(in srgb, ${palette.primary.main} 8%, transparent)`,\n '--fc-list-event-hover-bg-color': palette.action.hover,\n '--fc-now-indicator-color': palette.error.main,\n '--fc-highlight-color': `color-mix(in srgb, ${palette.primary.main} 14%, transparent)`,\n '& .fc': {fontFamily: 'inherit', fontSize: theme.typography.body2.fontSize},\n // los números de los días y los encabezados son ligas en FullCalendar: sin subrayado y del color del texto\n '& .fc a': {color: 'inherit', textDecoration: 'none'},\n '& .fc-col-header-cell-cushion': {fontWeight: 600, py: 1},\n '& .fc-event': {borderRadius: '6px', px: 0.5},\n // la hora no se encoge: en un día angosto se recortaba («1C» en lugar de «10»); lo que se corta es el nombre\n '& .fc-event-time': {flexShrink: 0, overflow: 'visible'},\n '& .fc-scrollgrid': {borderRadius: '12px', overflow: 'hidden'},\n '& .fc-list': {borderRadius: '12px'},\n };\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, title: string, start: string, end?: string, allDay?: boolean, tone?: string}>} props.events\n * start y end como texto ISO; tone: un color del tema (por omisión primary)\n * @param {string} [props.initialView] una de CALENDAR_VIEW\n * @param {string[]} [props.views] las que se ofrecen en el control segmentado\n * @param {(eventId: string) => void} [props.onEventClick]\n * @param {(range: {start: string, end: string, allDay: boolean}) => void} [props.onDateSelect] al elegir días u horas\n * @param {(change: {id: string, start: string, end: string|null, allDay: boolean}) => Promise<unknown>|void} [props.onEventChange]\n * al arrastrar un evento; si truena, el evento regresa\n * @param {number|'auto'} [props.height]\n * @param {string} props.ariaLabel ya traducido\n * @param {(range: {start: string, end: string, view: string}) => void} [props.onDatesChange] cada que cambia lo que se ve (al\n * abrir, al ir al anterior o siguiente, al cambiar de vista): para pedir al backend solo las citas de ese rango\n * @param {(view: string) => void} [props.onViewChange] al cambiar de vista (CALENDAR_VIEW)\n * @param {(event: any) => import('react').ReactNode} [props.renderEvent] lo de adentro de cada cita (recibe la cita de events\n * tal cual, con sus campos propios)\n * @param {object} [props.fullCalendarProps] lo demás de FullCalendar (businessHours, hiddenDays, slotMinTime, locale…); lo\n * que el calendario controla (las citas, la barra, los eventos de arriba) no se reemplaza desde aquí\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventCalendar({\n events, initialView = CALENDAR_VIEW.month, views = Object.values(CALENDAR_VIEW), onEventClick, onDateSelect, onEventChange, height = 'auto', ariaLabel,\n onDatesChange, onViewChange, renderEvent, fullCalendarProps, texts,\n}) {\n const theme = useTheme();\n const palette = paletteOf(theme);\n const {t} = useToolkitTranslation('calendar', texts);\n const {language} = useToolkitSettings();\n const calendarRef = useRef(null);\n const [title, setTitle] = useState('');\n const [view, setView] = useState(initialView);\n const calendarApi = () => calendarRef.current?.getApi();\n\n const coloredEvents = events.map((event) => {\n const color = palette[event.tone] ?? palette.primary;\n return {...event, backgroundColor: color.main, borderColor: color.main, textColor: color.contrastText};\n });\n\n const changeEvent = async ({event, revert}) => {\n try {\n await onEventChange({id: event.id, start: event.startStr, end: event.endStr || null, allDay: event.allDay});\n } catch {\n // el servidor dijo que no: el evento regresa a donde estaba (quien llama ya avisó el error)\n revert();\n }\n };\n\n return (\n <Stack spacing={2} role=\"region\" aria-label={ariaLabel}>\n <Stack direction={{xs: 'column', md: 'row'}} spacing={1.5} sx={{alignItems: {md: 'center'}, justifyContent: 'space-between'}}>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Tooltip title={t('previous')}>\n <IconButton aria-label={t('previous')} onClick={() => calendarApi()?.prev()}><ChevronLeftRoundedIcon /></IconButton>\n </Tooltip>\n <Tooltip title={t('next')}>\n <IconButton aria-label={t('next')} onClick={() => calendarApi()?.next()}><ChevronRightRoundedIcon /></IconButton>\n </Tooltip>\n <Button variant=\"outlined\" size=\"small\" onClick={() => calendarApi()?.today()}>{t('today')}</Button>\n <Typography variant=\"h6\" component=\"h2\" sx={{pl: 1}} aria-live=\"polite\">{capitalizeFirst(title)}</Typography>\n </Stack>\n {views.length > 1 && (\n <SegmentedControl ariaLabel={t('view')} value={view} onChange={(nextView) => calendarApi()?.changeView(nextView)}\n options={views.map((value) => ({value, label: t(`views.${VIEW_TEXT_KEY[value]}`)}))} />\n )}\n </Stack>\n <Box sx={calendarStylesOf}>\n {/* primero lo del proyecto (fullCalendarProps); lo de abajo es lo que el calendario controla y le gana */}\n <FullCalendar locale={language === 'es' ? spanishLocale : 'en'} firstDay={MONDAY} {...fullCalendarProps}\n ref={calendarRef} plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin, ...(fullCalendarProps?.plugins ?? [])]}\n initialView={initialView} headerToolbar={false} height={height}\n nowIndicator dayMaxEvents={DAY_MAX_EVENTS} events={coloredEvents} eventDisplay=\"block\"\n eventContent={renderEvent ? (contentInfo) => renderEvent(events.find((event) => event.id === contentInfo.event.id) ?? contentInfo.event) : undefined}\n datesSet={(dateInfo) => {\n setTitle(dateInfo.view.title);\n if (dateInfo.view.type !== view) {\n onViewChange?.(dateInfo.view.type);\n }\n setView(dateInfo.view.type);\n onDatesChange?.({start: dateInfo.startStr, end: dateInfo.endStr, view: dateInfo.view.type});\n }}\n eventClick={onEventClick ? (clickInfo) => onEventClick(clickInfo.event.id) : undefined}\n selectable={Boolean(onDateSelect)} selectMirror\n select={onDateSelect ? (selectInfo) => {\n onDateSelect({start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay});\n calendarApi()?.unselect();\n } : undefined}\n editable={Boolean(onEventChange)} eventChange={onEventChange ? changeEvent : undefined} />\n </Box>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EventCalendar.js","sources":["../../src/calendar/EventCalendar.jsx"],"sourcesContent":["// Un calendario de eventos (@stackcraftmx/calcita/calendar): FullCalendar por dentro, con la barra del framework (anterior y\n// siguiente, «Hoy», el título y la vista con el control segmentado) y los colores del tema. En el idioma de\n// ToolkitProvider, la semana empieza en lunes y marca la hora actual. Las citas van en bloques de su color (el color es\n// información: el tipo de cita), no en puntitos.\n//\n// <EventCalendar events={[{id: '1', title: 'Entrega P-2375', start: '2026-10-20T10:00', end: '2026-10-20T11:00', tone: 'info'}]}\n// onEventClick={openDetail} onDateSelect={openNewEvent} onEventChange={(change) => api.patch(...)} />\n//\n// Arrastrar un evento lo cambia de día u hora: onEventChange recibe lo nuevo y, si truena (el servidor dijo que no), el\n// evento regresa a donde estaba. Sin onEventChange no se puede arrastrar; sin onDateSelect no se pueden elegir días.\n//\n// Se extiende sin tocar el framework: onDatesChange avisa el rango que se ve (para que el backend mande solo esas citas),\n// onViewChange la vista, renderEvent pinta cada cita a su manera (recibe la cita tal cual, con sus campos propios),\n// fullCalendarProps pasa lo demás de FullCalendar (horario laboral, días ocultos, horas visibles…) y texts los textos. El\n// detalle de una cita para su ventana: EventDetails.\n//\n// OJO: las horas se pintan en la zona del navegador (FullCalendar pide un complemento aparte para zonas con nombre).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaEventCalendar con defaultProps y\n// styleOverrides de sus partes: toolbar, navigation, title, calendar.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useRef, useState} from 'react';\nimport FullCalendar from '@fullcalendar/react';\nimport dayGridPlugin from '@fullcalendar/daygrid';\nimport interactionPlugin from '@fullcalendar/interaction';\nimport listPlugin from '@fullcalendar/list';\nimport timeGridPlugin from '@fullcalendar/timegrid';\nimport spanishLocale from '@fullcalendar/core/locales/es';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport IconButton from '@mui/material/IconButton';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';\nimport ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';\n\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {useToolkitSettings} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport SegmentedControl from '@/ui/filters/SegmentedControl.jsx';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaEventCalendar)\nconst THEME_NAME = 'CalcitaEventCalendar';\nconst CalendarToolbar = piecePart(THEME_NAME, 'Toolbar', Stack);\nconst CalendarNavigation = piecePart(THEME_NAME, 'Navigation', Stack);\nconst CalendarTitle = piecePart(THEME_NAME, 'Title', Typography);\nconst CalendarCalendar = piecePart(THEME_NAME, 'Calendar', Box);\n\n/** Las vistas del calendario (los nombres de FullCalendar). */\nexport const CALENDAR_VIEW = Object.freeze({month: 'dayGridMonth', week: 'timeGridWeek', day: 'timeGridDay', list: 'listWeek'});\n\n// el texto de cada vista en los textos del paquete (calendar.views.*)\nconst VIEW_TEXT_KEY = Object.freeze({\n [CALENDAR_VIEW.month]: 'month', [CALENDAR_VIEW.week]: 'week', [CALENDAR_VIEW.day]: 'day', [CALENDAR_VIEW.list]: 'list',\n});\n// cuántos eventos caben en un día del mes antes del «+2 más»\nconst DAY_MAX_EVENTS = 3;\nconst MONDAY = 1;\n\n/**\n * La primera letra en mayúscula y lo demás como viene: «octubre de 2026» → «Octubre de 2026». OJO: con CSS\n * (text-transform: capitalize) salía «Octubre De 2026»: en español solo va la primera.\n *\n * @param {string} text\n * @returns {string}\n */\nfunction capitalizeFirst(text) {\n return text ? text.charAt(0).toLocaleUpperCase() + text.slice(1) : text;\n}\n\n/**\n * Los colores del calendario sobre el tema (las variables CSS de FullCalendar), en claro y en oscuro.\n *\n * @param {object} theme\n * @returns {object}\n */\nfunction calendarStylesOf(theme) {\n const palette = paletteOf(theme);\n return {\n '--fc-border-color': palette.divider,\n '--fc-page-bg-color': 'transparent',\n '--fc-neutral-bg-color': palette.action.hover,\n '--fc-today-bg-color': `color-mix(in srgb, ${palette.primary.main} 8%, transparent)`,\n '--fc-list-event-hover-bg-color': palette.action.hover,\n '--fc-now-indicator-color': palette.error.main,\n '--fc-highlight-color': `color-mix(in srgb, ${palette.primary.main} 14%, transparent)`,\n '& .fc': {fontFamily: 'inherit', fontSize: theme.typography.body2.fontSize},\n // los números de los días y los encabezados son ligas en FullCalendar: sin subrayado y del color del texto\n '& .fc a': {color: 'inherit', textDecoration: 'none'},\n '& .fc-col-header-cell-cushion': {fontWeight: 600, py: 1},\n '& .fc-event': {borderRadius: '6px', px: 0.5},\n // la hora no se encoge: en un día angosto se recortaba («1C» en lugar de «10»); lo que se corta es el nombre\n '& .fc-event-time': {flexShrink: 0, overflow: 'visible'},\n '& .fc-scrollgrid': {borderRadius: '12px', overflow: 'hidden'},\n '& .fc-list': {borderRadius: '12px'},\n };\n}\n\n/**\n * @param {object} props\n * @param {Array<{id: string, title: string, start: string, end?: string, allDay?: boolean, tone?: string}>} props.events\n * start y end como texto ISO; tone: un color del tema (por omisión primary)\n * @param {string} [props.initialView] una de CALENDAR_VIEW\n * @param {string[]} [props.views] las que se ofrecen en el control segmentado\n * @param {(eventId: string) => void} [props.onEventClick]\n * @param {(range: {start: string, end: string, allDay: boolean}) => void} [props.onDateSelect] al elegir días u horas\n * @param {(change: {id: string, start: string, end: string|null, allDay: boolean}) => Promise<unknown>|void} [props.onEventChange]\n * al arrastrar un evento; si truena, el evento regresa\n * @param {number|'auto'} [props.height]\n * @param {string} props.ariaLabel ya traducido\n * @param {(range: {start: string, end: string, view: string}) => void} [props.onDatesChange] cada que cambia lo que se ve (al\n * abrir, al ir al anterior o siguiente, al cambiar de vista): para pedir al backend solo las citas de ese rango\n * @param {(view: string) => void} [props.onViewChange] al cambiar de vista (CALENDAR_VIEW)\n * @param {(event: any) => import('react').ReactNode} [props.renderEvent] lo de adentro de cada cita (recibe la cita de events\n * tal cual, con sus campos propios)\n * @param {object} [props.fullCalendarProps] lo demás de FullCalendar (businessHours, hiddenDays, slotMinTime, locale…); lo\n * que el calendario controla (las citas, la barra, los eventos de arriba) no se reemplaza desde aquí\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventCalendar(props) {\n // lo que el proyecto puso en su tema (components.CalcitaEventCalendar.defaultProps) y encima lo que llega aquí\n const {\n events, initialView = CALENDAR_VIEW.month, views = Object.values(CALENDAR_VIEW), onEventClick, onDateSelect, onEventChange, height = 'auto', ariaLabel,\n onDatesChange, onViewChange, renderEvent, fullCalendarProps, texts,\n} = usePieceProps(props, THEME_NAME);\n const theme = useTheme();\n const palette = paletteOf(theme);\n const {t} = useToolkitTranslation('calendar', texts);\n const {language} = useToolkitSettings();\n const calendarRef = useRef(null);\n const [title, setTitle] = useState('');\n const [view, setView] = useState(initialView);\n const calendarApi = () => calendarRef.current?.getApi();\n\n const coloredEvents = events.map((event) => {\n const color = palette[event.tone] ?? palette.primary;\n return {...event, backgroundColor: color.main, borderColor: color.main, textColor: color.contrastText};\n });\n\n const changeEvent = async ({event, revert}) => {\n try {\n await onEventChange({id: event.id, start: event.startStr, end: event.endStr || null, allDay: event.allDay});\n } catch {\n // el servidor dijo que no: el evento regresa a donde estaba (quien llama ya avisó el error)\n revert();\n }\n };\n\n return (\n <Stack spacing={2} role=\"region\" aria-label={ariaLabel}>\n <CalendarToolbar direction={{xs: 'column', md: 'row'}} spacing={1.5} ownSx={{alignItems: {md: 'center'}, justifyContent: 'space-between'}}>\n <CalendarNavigation direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n <Tooltip title={t('previous')}>\n <IconButton aria-label={t('previous')} onClick={() => calendarApi()?.prev()}><ChevronLeftRoundedIcon /></IconButton>\n </Tooltip>\n <Tooltip title={t('next')}>\n <IconButton aria-label={t('next')} onClick={() => calendarApi()?.next()}><ChevronRightRoundedIcon /></IconButton>\n </Tooltip>\n <Button variant=\"outlined\" size=\"small\" onClick={() => calendarApi()?.today()}>{t('today')}</Button>\n <CalendarTitle variant=\"h6\" component=\"h2\" ownSx={{pl: 1}} aria-live=\"polite\">{capitalizeFirst(title)}</CalendarTitle>\n </CalendarNavigation>\n {views.length > 1 && (\n <SegmentedControl ariaLabel={t('view')} value={view} onChange={(nextView) => calendarApi()?.changeView(nextView)}\n options={views.map((value) => ({value, label: t(`views.${VIEW_TEXT_KEY[value]}`)}))} />\n )}\n </CalendarToolbar>\n <CalendarCalendar ownSx={calendarStylesOf}>\n {/* primero lo del proyecto (fullCalendarProps); lo de abajo es lo que el calendario controla y le gana */}\n <FullCalendar locale={language === 'es' ? spanishLocale : 'en'} firstDay={MONDAY} {...fullCalendarProps}\n ref={calendarRef} plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin, ...(fullCalendarProps?.plugins ?? [])]}\n initialView={initialView} headerToolbar={false} height={height}\n nowIndicator dayMaxEvents={DAY_MAX_EVENTS} events={coloredEvents} eventDisplay=\"block\"\n eventContent={renderEvent ? (contentInfo) => renderEvent(events.find((event) => event.id === contentInfo.event.id) ?? contentInfo.event) : undefined}\n datesSet={(dateInfo) => {\n setTitle(dateInfo.view.title);\n if (dateInfo.view.type !== view) {\n onViewChange?.(dateInfo.view.type);\n }\n setView(dateInfo.view.type);\n onDatesChange?.({start: dateInfo.startStr, end: dateInfo.endStr, view: dateInfo.view.type});\n }}\n eventClick={onEventClick ? (clickInfo) => onEventClick(clickInfo.event.id) : undefined}\n selectable={Boolean(onDateSelect)} selectMirror\n select={onDateSelect ? (selectInfo) => {\n onDateSelect({start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay});\n calendarApi()?.unselect();\n } : undefined}\n editable={Boolean(onEventChange)} eventChange={onEventChange ? changeEvent : undefined} />\n </CalendarCalendar>\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,20 @@ import { initialsOf } from "../appShell/menuItemText.js";
|
|
|
16
16
|
import { reminderOf, isOpenableMeetingUrl, REMINDER_UNIT } from "./calendarEvents.js";
|
|
17
17
|
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
18
18
|
import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
|
|
19
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
20
|
+
const THEME_NAME = "CalcitaEventDetails";
|
|
21
|
+
const EventRow = piecePart(THEME_NAME, "Row", Stack);
|
|
22
|
+
const EventRowContent = piecePart(THEME_NAME, "RowContent", Box);
|
|
23
|
+
const EventTitleRow = piecePart(THEME_NAME, "TitleRow", Stack);
|
|
24
|
+
const EventToneDot = piecePart(THEME_NAME, "ToneDot", Box);
|
|
25
|
+
const EventLinkRow = piecePart(THEME_NAME, "LinkRow", Stack);
|
|
26
|
+
const EventLink = piecePart(THEME_NAME, "Link", Typography);
|
|
27
|
+
const EventGuestsTitle = piecePart(THEME_NAME, "GuestsTitle", Typography);
|
|
28
|
+
const EventGuest = piecePart(THEME_NAME, "Guest", Stack);
|
|
29
|
+
const EventGuestAvatar = piecePart(THEME_NAME, "GuestAvatar", Avatar);
|
|
30
|
+
const EventGuestName = piecePart(THEME_NAME, "GuestName", Typography);
|
|
31
|
+
const EventNotes = piecePart(THEME_NAME, "Notes", Typography);
|
|
32
|
+
const EventActions = piecePart(THEME_NAME, "Actions", Stack);
|
|
19
33
|
const ATTENDEE_RESPONSE = Object.freeze({ accepted: "accepted", declined: "declined", pending: "pending" });
|
|
20
34
|
const RESPONSE_COLOR = Object.freeze({
|
|
21
35
|
[ATTENDEE_RESPONSE.accepted]: "success",
|
|
@@ -25,12 +39,13 @@ const RESPONSE_COLOR = Object.freeze({
|
|
|
25
39
|
const WHOLE_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
40
|
const ICON_SIZE = 20;
|
|
27
41
|
function DetailRow({ Icon, label, children }) {
|
|
28
|
-
return /* @__PURE__ */ jsxs(
|
|
42
|
+
return /* @__PURE__ */ jsxs(EventRow, { direction: "row", spacing: 1.5, ownSx: { alignItems: "flex-start" }, "aria-label": label, role: "group", children: [
|
|
29
43
|
/* @__PURE__ */ jsx(Icon, { sx: { fontSize: ICON_SIZE, color: "text.secondary", mt: 0.25 }, "aria-hidden": "true" }),
|
|
30
|
-
/* @__PURE__ */ jsx(
|
|
44
|
+
/* @__PURE__ */ jsx(EventRowContent, { ownSx: { minWidth: 0, flexGrow: 1 }, children })
|
|
31
45
|
] });
|
|
32
46
|
}
|
|
33
|
-
function EventDetails(
|
|
47
|
+
function EventDetails(props) {
|
|
48
|
+
const { event, actions, titleId, texts } = usePieceProps(props, THEME_NAME);
|
|
34
49
|
const { t } = useToolkitTranslation("calendar", texts);
|
|
35
50
|
const { formatDate, formatTime } = useFormatters();
|
|
36
51
|
const isWholeDay = event.allDay || WHOLE_DAY_PATTERN.test(event.start);
|
|
@@ -41,30 +56,30 @@ function EventDetails({ event, actions, titleId, texts }) {
|
|
|
41
56
|
const reminder = reminderOf(event.reminderMinutes);
|
|
42
57
|
const canJoin = isOpenableMeetingUrl(event.meetingUrl);
|
|
43
58
|
return /* @__PURE__ */ jsxs(Stack, { spacing: 2, children: [
|
|
44
|
-
/* @__PURE__ */ jsxs(
|
|
45
|
-
/* @__PURE__ */ jsx(
|
|
59
|
+
/* @__PURE__ */ jsxs(EventTitleRow, { direction: "row", spacing: 1.5, ownSx: { alignItems: "center" }, children: [
|
|
60
|
+
/* @__PURE__ */ jsx(EventToneDot, { "aria-hidden": "true", ownSx: { width: 12, height: 12, borderRadius: "4px", flexShrink: 0, bgcolor: `${event.tone ?? "primary"}.main` } }),
|
|
46
61
|
/* @__PURE__ */ jsx(Typography, { id: titleId, variant: "h6", component: "h2", children: event.title })
|
|
47
62
|
] }),
|
|
48
63
|
/* @__PURE__ */ jsx(DetailRow, { Icon: AccessTimeRoundedIcon, label: t("details.when"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: isWholeDay ? t("details.allDay", { date: when }) : when }) }),
|
|
49
64
|
event.location && /* @__PURE__ */ jsx(DetailRow, { Icon: PlaceOutlinedIcon, label: t("details.location"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: event.location }) }),
|
|
50
|
-
event.meetingUrl && /* @__PURE__ */ jsx(DetailRow, { Icon: VideocamOutlinedIcon, label: t("details.meeting"), children: /* @__PURE__ */ jsxs(
|
|
65
|
+
event.meetingUrl && /* @__PURE__ */ jsx(DetailRow, { Icon: VideocamOutlinedIcon, label: t("details.meeting"), children: /* @__PURE__ */ jsxs(EventLinkRow, { direction: "row", spacing: 1, ownSx: { alignItems: "center", flexWrap: "wrap" }, useFlexGap: true, children: [
|
|
51
66
|
canJoin && /* @__PURE__ */ jsx(Button, { variant: "contained", size: "small", href: event.meetingUrl, target: "_blank", rel: "noreferrer", children: t("details.join") }),
|
|
52
|
-
/* @__PURE__ */ jsx(
|
|
67
|
+
/* @__PURE__ */ jsx(EventLink, { variant: "caption", color: "text.secondary", ownSx: { wordBreak: "break-all" }, children: event.meetingUrl })
|
|
53
68
|
] }) }),
|
|
54
69
|
event.attendees?.length > 0 && /* @__PURE__ */ jsxs(DetailRow, { Icon: PeopleOutlineRoundedIcon, label: t("details.attendees"), children: [
|
|
55
|
-
/* @__PURE__ */ jsx(
|
|
70
|
+
/* @__PURE__ */ jsx(EventGuestsTitle, { variant: "body2", ownSx: { mb: 0.75 }, children: t("details.attendeesCount", { count: event.attendees.length }) }),
|
|
56
71
|
/* @__PURE__ */ jsx(Stack, { spacing: 0.75, children: event.attendees.map((attendee) => {
|
|
57
72
|
const response = attendee.response ?? ATTENDEE_RESPONSE.pending;
|
|
58
|
-
return /* @__PURE__ */ jsxs(
|
|
59
|
-
/* @__PURE__ */ jsx(
|
|
60
|
-
/* @__PURE__ */ jsx(
|
|
73
|
+
return /* @__PURE__ */ jsxs(EventGuest, { direction: "row", spacing: 1, ownSx: { alignItems: "center" }, children: [
|
|
74
|
+
/* @__PURE__ */ jsx(EventGuestAvatar, { ownSx: { width: 24, height: 24, fontSize: 11 }, children: initialsOf(attendee.name) }),
|
|
75
|
+
/* @__PURE__ */ jsx(EventGuestName, { variant: "body2", ownSx: { flexGrow: 1, minWidth: 0 }, noWrap: true, children: attendee.name }),
|
|
61
76
|
/* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", color: RESPONSE_COLOR[response] ?? "default", label: t(`details.response.${response}`) })
|
|
62
77
|
] }, attendee.id);
|
|
63
78
|
}) })
|
|
64
79
|
] }),
|
|
65
80
|
reminder.unit !== REMINDER_UNIT.none && /* @__PURE__ */ jsx(DetailRow, { Icon: NotificationsNoneRoundedIcon, label: t("details.reminder"), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", children: t(`details.reminderUnit.${reminder.unit}`, { count: reminder.count }) }) }),
|
|
66
|
-
event.description && /* @__PURE__ */ jsx(DetailRow, { Icon: NotesRoundedIcon, label: t("details.notes"), children: /* @__PURE__ */ jsx(
|
|
67
|
-
actions && /* @__PURE__ */ jsx(
|
|
81
|
+
event.description && /* @__PURE__ */ jsx(DetailRow, { Icon: NotesRoundedIcon, label: t("details.notes"), children: /* @__PURE__ */ jsx(EventNotes, { variant: "body2", ownSx: { whiteSpace: "pre-wrap" }, children: event.description }) }),
|
|
82
|
+
actions && /* @__PURE__ */ jsx(EventActions, { direction: "row", spacing: 1, useFlexGap: true, ownSx: { justifyContent: "flex-end", flexWrap: "wrap" }, children: actions })
|
|
68
83
|
] });
|
|
69
84
|
}
|
|
70
85
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventDetails.js","sources":["../../src/calendar/EventDetails.jsx"],"sourcesContent":["// El detalle de una cita, para la ventana que se abre al picarla en el calendario: su título con el color de su tipo,\n// cuándo (el día y las horas), dónde, la liga de la reunión con «Unirse», los invitados con su respuesta, el recordatorio y\n// las notas. Pinta lo que la cita traiga; lo que no trae no ocupa lugar.\n//\n// <EventDetails event={{title: 'Junta de cierre', start: '2026-10-20T10:00', end: '2026-10-20T11:00', location: 'Sala 2',\n// meetingUrl: 'https://meet.example.com/abc', attendees: [{id: 'ana', name: 'Ana', response: 'accepted'}],\n// reminderMinutes: 15}}\n// actions={<Button onClick={edit}>Editar</Button>} />\n//\n// No llama a ningún servidor ni abre la reunión por su cuenta: «Unirse» es una liga (solo http o https) y lo demás lo pone la\n// pantalla en actions. Sus textos se cambian con texts (la forma de `calendar` en los textos del paquete).\n//\n// Genérico del paquete: function (regla de la casa).\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AccessTimeRoundedIcon from '@mui/icons-material/AccessTimeRounded';\nimport NotesRoundedIcon from '@mui/icons-material/NotesRounded';\nimport NotificationsNoneRoundedIcon from '@mui/icons-material/NotificationsNoneRounded';\nimport PeopleOutlineRoundedIcon from '@mui/icons-material/PeopleOutlineRounded';\nimport PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';\nimport VideocamOutlinedIcon from '@mui/icons-material/VideocamOutlined';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {REMINDER_UNIT, isOpenableMeetingUrl, reminderOf} from '@/calendar/calendarEvents.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Las respuestas de un invitado. */\nexport const ATTENDEE_RESPONSE = Object.freeze({accepted: 'accepted', declined: 'declined', pending: 'pending'});\n// el color de cada respuesta (el color es información: verde va, rojo no va, gris no ha contestado)\nconst RESPONSE_COLOR = Object.freeze({\n [ATTENDEE_RESPONSE.accepted]: 'success', [ATTENDEE_RESPONSE.declined]: 'error', [ATTENDEE_RESPONSE.pending]: 'default',\n});\n// un día completo viene sin hora («2026-10-20»)\nconst WHOLE_DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst ICON_SIZE = 20;\n\n/** Un renglón del detalle: su ícono a la izquierda y lo de adentro. */\nfunction DetailRow({Icon, label, children}) {\n return (\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'flex-start'}} aria-label={label} role=\"group\">\n <Icon sx={{fontSize: ICON_SIZE, color: 'text.secondary', mt: 0.25}} aria-hidden=\"true\" />\n <Box sx={{minWidth: 0, flexGrow: 1}}>{children}</Box>\n </Stack>\n );\n}\n\n/**\n * @param {object} props\n * @param {{title: string, start: string, end?: string|null, allDay?: boolean, tone?: string, location?: string, meetingUrl?: string, attendees?: Array<{id: string, name: string, email?: string, response?: 'accepted'|'declined'|'pending'}>, reminderMinutes?: number|null, description?: string}} props.event\n * start y end como texto ISO (o AAAA-MM-DD un día completo); tone: el color de su tipo\n * @param {import('react').ReactNode} [props.actions] botones de la pantalla (editar, borrar, responder…)\n * @param {string} [props.titleId] el id del título: la ventana que lo contiene se nombra con él (aria-labelledby)\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventDetails({event, actions, titleId, texts}) {\n const {t} = useToolkitTranslation('calendar', texts);\n const {formatDate, formatTime} = useFormatters();\n const isWholeDay = event.allDay || WHOLE_DAY_PATTERN.test(event.start);\n let when = formatDate(event.start);\n if (!isWholeDay) {\n when = t('details.timeRange', {date: formatDate(event.start), start: formatTime(event.start), end: event.end ? formatTime(event.end) : ''});\n }\n const reminder = reminderOf(event.reminderMinutes);\n const canJoin = isOpenableMeetingUrl(event.meetingUrl);\n\n return (\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1.5} sx={{alignItems: 'center'}}>\n <Box aria-hidden=\"true\" sx={{width: 12, height: 12, borderRadius: '4px', flexShrink: 0, bgcolor: `${event.tone ?? 'primary'}.main`}} />\n <Typography id={titleId} variant=\"h6\" component=\"h2\">{event.title}</Typography>\n </Stack>\n <DetailRow Icon={AccessTimeRoundedIcon} label={t('details.when')}>\n <Typography variant=\"body2\">{isWholeDay ? t('details.allDay', {date: when}) : when}</Typography>\n </DetailRow>\n {event.location && (\n <DetailRow Icon={PlaceOutlinedIcon} label={t('details.location')}>\n <Typography variant=\"body2\">{event.location}</Typography>\n </DetailRow>\n )}\n {event.meetingUrl && (\n <DetailRow Icon={VideocamOutlinedIcon} label={t('details.meeting')}>\n <Stack direction=\"row\" spacing={1} sx={{alignItems: 'center', flexWrap: 'wrap'}} useFlexGap>\n {canJoin && (\n <Button variant=\"contained\" size=\"small\" href={event.meetingUrl} target=\"_blank\" rel=\"noreferrer\">{t('details.join')}</Button>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{wordBreak: 'break-all'}}>{event.meetingUrl}</Typography>\n </Stack>\n </DetailRow>\n )}\n {event.attendees?.length > 0 && (\n <DetailRow Icon={PeopleOutlineRoundedIcon} label={t('details.attendees')}>\n <Typography variant=\"body2\" sx={{mb: 0.75}}>{t('details.attendeesCount', {count: event.attendees.length})}</Typography>\n <Stack spacing={0.75}>\n {event.attendees.map((attendee) => {\n const response = attendee.response ?? ATTENDEE_RESPONSE.pending;\n return (\n <Stack key={attendee.id} direction=\"row\" spacing={1} sx={{alignItems: 'center'}}>\n <Avatar sx={{width: 24, height: 24, fontSize: 11}}>{initialsOf(attendee.name)}</Avatar>\n <Typography variant=\"body2\" sx={{flexGrow: 1, minWidth: 0}} noWrap>{attendee.name}</Typography>\n <Chip size=\"small\" variant=\"outlined\" color={RESPONSE_COLOR[response] ?? 'default'} label={t(`details.response.${response}`)} />\n </Stack>\n );\n })}\n </Stack>\n </DetailRow>\n )}\n {reminder.unit !== REMINDER_UNIT.none && (\n <DetailRow Icon={NotificationsNoneRoundedIcon} label={t('details.reminder')}>\n <Typography variant=\"body2\">{t(`details.reminderUnit.${reminder.unit}`, {count: reminder.count})}</Typography>\n </DetailRow>\n )}\n {event.description && (\n <DetailRow Icon={NotesRoundedIcon} label={t('details.notes')}>\n <Typography variant=\"body2\" sx={{whiteSpace: 'pre-wrap'}}>{event.description}</Typography>\n </DetailRow>\n )}\n {actions && <Stack direction=\"row\" spacing={1} useFlexGap sx={{justifyContent: 'flex-end', flexWrap: 'wrap'}}>{actions}</Stack>}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EventDetails.js","sources":["../../src/calendar/EventDetails.jsx"],"sourcesContent":["// El detalle de una cita, para la ventana que se abre al picarla en el calendario: su título con el color de su tipo,\n// cuándo (el día y las horas), dónde, la liga de la reunión con «Unirse», los invitados con su respuesta, el recordatorio y\n// las notas. Pinta lo que la cita traiga; lo que no trae no ocupa lugar.\n//\n// <EventDetails event={{title: 'Junta de cierre', start: '2026-10-20T10:00', end: '2026-10-20T11:00', location: 'Sala 2',\n// meetingUrl: 'https://meet.example.com/abc', attendees: [{id: 'ana', name: 'Ana', response: 'accepted'}],\n// reminderMinutes: 15}}\n// actions={<Button onClick={edit}>Editar</Button>} />\n//\n// No llama a ningún servidor ni abre la reunión por su cuenta: «Unirse» es una liga (solo http o https) y lo demás lo pone la\n// pantalla en actions. Sus textos se cambian con texts (la forma de `calendar` en los textos del paquete).\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaEventDetails con defaultProps y\n// styleOverrides de sus partes: row, rowContent, titleRow, toneDot, linkRow, link, guestsTitle, guest, guestAvatar,\n// guestName, notes, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Chip from '@mui/material/Chip';\nimport Stack from '@mui/material/Stack';\nimport Typography from '@mui/material/Typography';\nimport AccessTimeRoundedIcon from '@mui/icons-material/AccessTimeRounded';\nimport NotesRoundedIcon from '@mui/icons-material/NotesRounded';\nimport NotificationsNoneRoundedIcon from '@mui/icons-material/NotificationsNoneRounded';\nimport PeopleOutlineRoundedIcon from '@mui/icons-material/PeopleOutlineRounded';\nimport PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';\nimport VideocamOutlinedIcon from '@mui/icons-material/VideocamOutlined';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {REMINDER_UNIT, isOpenableMeetingUrl, reminderOf} from '@/calendar/calendarEvents.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaEventDetails)\nconst THEME_NAME = 'CalcitaEventDetails';\nconst EventRow = piecePart(THEME_NAME, 'Row', Stack);\nconst EventRowContent = piecePart(THEME_NAME, 'RowContent', Box);\nconst EventTitleRow = piecePart(THEME_NAME, 'TitleRow', Stack);\nconst EventToneDot = piecePart(THEME_NAME, 'ToneDot', Box);\nconst EventLinkRow = piecePart(THEME_NAME, 'LinkRow', Stack);\nconst EventLink = piecePart(THEME_NAME, 'Link', Typography);\nconst EventGuestsTitle = piecePart(THEME_NAME, 'GuestsTitle', Typography);\nconst EventGuest = piecePart(THEME_NAME, 'Guest', Stack);\nconst EventGuestAvatar = piecePart(THEME_NAME, 'GuestAvatar', Avatar);\nconst EventGuestName = piecePart(THEME_NAME, 'GuestName', Typography);\nconst EventNotes = piecePart(THEME_NAME, 'Notes', Typography);\nconst EventActions = piecePart(THEME_NAME, 'Actions', Stack);\n\n/** Las respuestas de un invitado. */\nexport const ATTENDEE_RESPONSE = Object.freeze({accepted: 'accepted', declined: 'declined', pending: 'pending'});\n// el color de cada respuesta (el color es información: verde va, rojo no va, gris no ha contestado)\nconst RESPONSE_COLOR = Object.freeze({\n [ATTENDEE_RESPONSE.accepted]: 'success', [ATTENDEE_RESPONSE.declined]: 'error', [ATTENDEE_RESPONSE.pending]: 'default',\n});\n// un día completo viene sin hora («2026-10-20»)\nconst WHOLE_DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst ICON_SIZE = 20;\n\n/** Un renglón del detalle: su ícono a la izquierda y lo de adentro. */\nfunction DetailRow({Icon, label, children}) {\n return (\n <EventRow direction=\"row\" spacing={1.5} ownSx={{alignItems: 'flex-start'}} aria-label={label} role=\"group\">\n <Icon sx={{fontSize: ICON_SIZE, color: 'text.secondary', mt: 0.25}} aria-hidden=\"true\" />\n <EventRowContent ownSx={{minWidth: 0, flexGrow: 1}}>{children}</EventRowContent>\n </EventRow>\n );\n}\n\n/**\n * @param {object} props\n * @param {{title: string, start: string, end?: string|null, allDay?: boolean, tone?: string, location?: string, meetingUrl?: string, attendees?: Array<{id: string, name: string, email?: string, response?: 'accepted'|'declined'|'pending'}>, reminderMinutes?: number|null, description?: string}} props.event\n * start y end como texto ISO (o AAAA-MM-DD un día completo); tone: el color de su tipo\n * @param {import('react').ReactNode} [props.actions] botones de la pantalla (editar, borrar, responder…)\n * @param {string} [props.titleId] el id del título: la ventana que lo contiene se nombra con él (aria-labelledby)\n * @param {object} [props.texts] los textos que cambian, con la forma de `calendar` en los textos del paquete\n */\nexport default function EventDetails(props) {\n // lo que el proyecto puso en su tema (components.CalcitaEventDetails.defaultProps) y encima lo que llega aquí\n const {event, actions, titleId, texts} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('calendar', texts);\n const {formatDate, formatTime} = useFormatters();\n const isWholeDay = event.allDay || WHOLE_DAY_PATTERN.test(event.start);\n let when = formatDate(event.start);\n if (!isWholeDay) {\n when = t('details.timeRange', {date: formatDate(event.start), start: formatTime(event.start), end: event.end ? formatTime(event.end) : ''});\n }\n const reminder = reminderOf(event.reminderMinutes);\n const canJoin = isOpenableMeetingUrl(event.meetingUrl);\n\n return (\n <Stack spacing={2}>\n <EventTitleRow direction=\"row\" spacing={1.5} ownSx={{alignItems: 'center'}}>\n <EventToneDot aria-hidden=\"true\" ownSx={{width: 12, height: 12, borderRadius: '4px', flexShrink: 0, bgcolor: `${event.tone ?? 'primary'}.main`}} />\n <Typography id={titleId} variant=\"h6\" component=\"h2\">{event.title}</Typography>\n </EventTitleRow>\n <DetailRow Icon={AccessTimeRoundedIcon} label={t('details.when')}>\n <Typography variant=\"body2\">{isWholeDay ? t('details.allDay', {date: when}) : when}</Typography>\n </DetailRow>\n {event.location && (\n <DetailRow Icon={PlaceOutlinedIcon} label={t('details.location')}>\n <Typography variant=\"body2\">{event.location}</Typography>\n </DetailRow>\n )}\n {event.meetingUrl && (\n <DetailRow Icon={VideocamOutlinedIcon} label={t('details.meeting')}>\n <EventLinkRow direction=\"row\" spacing={1} ownSx={{alignItems: 'center', flexWrap: 'wrap'}} useFlexGap>\n {canJoin && (\n <Button variant=\"contained\" size=\"small\" href={event.meetingUrl} target=\"_blank\" rel=\"noreferrer\">{t('details.join')}</Button>\n )}\n <EventLink variant=\"caption\" color=\"text.secondary\" ownSx={{wordBreak: 'break-all'}}>{event.meetingUrl}</EventLink>\n </EventLinkRow>\n </DetailRow>\n )}\n {event.attendees?.length > 0 && (\n <DetailRow Icon={PeopleOutlineRoundedIcon} label={t('details.attendees')}>\n <EventGuestsTitle variant=\"body2\" ownSx={{mb: 0.75}}>{t('details.attendeesCount', {count: event.attendees.length})}</EventGuestsTitle>\n <Stack spacing={0.75}>\n {event.attendees.map((attendee) => {\n const response = attendee.response ?? ATTENDEE_RESPONSE.pending;\n return (\n <EventGuest key={attendee.id} direction=\"row\" spacing={1} ownSx={{alignItems: 'center'}}>\n <EventGuestAvatar ownSx={{width: 24, height: 24, fontSize: 11}}>{initialsOf(attendee.name)}</EventGuestAvatar>\n <EventGuestName variant=\"body2\" ownSx={{flexGrow: 1, minWidth: 0}} noWrap>{attendee.name}</EventGuestName>\n <Chip size=\"small\" variant=\"outlined\" color={RESPONSE_COLOR[response] ?? 'default'} label={t(`details.response.${response}`)} />\n </EventGuest>\n );\n })}\n </Stack>\n </DetailRow>\n )}\n {reminder.unit !== REMINDER_UNIT.none && (\n <DetailRow Icon={NotificationsNoneRoundedIcon} label={t('details.reminder')}>\n <Typography variant=\"body2\">{t(`details.reminderUnit.${reminder.unit}`, {count: reminder.count})}</Typography>\n </DetailRow>\n )}\n {event.description && (\n <DetailRow Icon={NotesRoundedIcon} label={t('details.notes')}>\n <EventNotes variant=\"body2\" ownSx={{whiteSpace: 'pre-wrap'}}>{event.description}</EventNotes>\n </DetailRow>\n )}\n {actions && <EventActions direction=\"row\" spacing={1} useFlexGap ownSx={{justifyContent: 'flex-end', flexWrap: 'wrap'}}>{actions}</EventActions>}\n </Stack>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,22 @@ import { DOWNLOAD_STATUS } from "./downloadsMachine.js";
|
|
|
27
27
|
import { useFormatters } from "../toolkit/useFormatters.js";
|
|
28
28
|
import { isGlassTheme, glassStylesOf } from "../theme/glassSurface.js";
|
|
29
29
|
import ComponentTexts from "../toolkit/ComponentTexts.js";
|
|
30
|
+
import { piecePart } from "../theme/themePieces.js";
|
|
31
|
+
const THEME_NAME = "CalcitaDownloadManager";
|
|
32
|
+
const DownloadsDoneIcon = piecePart(THEME_NAME, "DoneIcon", CheckCircleIcon);
|
|
33
|
+
const DownloadsFailedIcon = piecePart(THEME_NAME, "FailedIcon", ErrorOutlineIcon);
|
|
34
|
+
const DownloadsCanceledIcon = piecePart(THEME_NAME, "CanceledIcon", BlockOutlinedIcon);
|
|
35
|
+
const DownloadsCancelButton = piecePart(THEME_NAME, "CancelButton", IconButton);
|
|
36
|
+
const DownloadsProgress = piecePart(THEME_NAME, "Progress", CircularProgress);
|
|
37
|
+
const DownloadsCancelIcon = piecePart(THEME_NAME, "CancelIcon", CloseIcon);
|
|
38
|
+
const DownloadsItem = piecePart(THEME_NAME, "Item", ListItem);
|
|
39
|
+
const DownloadsItemText = piecePart(THEME_NAME, "ItemText", Box);
|
|
40
|
+
const DownloadsItemStatus = piecePart(THEME_NAME, "ItemStatus", Typography);
|
|
41
|
+
const DownloadsItemActions = piecePart(THEME_NAME, "ItemActions", Box);
|
|
42
|
+
const DownloadsPanel = piecePart(THEME_NAME, "Panel", Paper);
|
|
43
|
+
const DownloadsHeader = piecePart(THEME_NAME, "Header", Stack);
|
|
44
|
+
const DownloadsTitle = piecePart(THEME_NAME, "Title", Typography);
|
|
45
|
+
const DownloadsList = piecePart(THEME_NAME, "List", List);
|
|
30
46
|
const AUTO_DISMISS_MILLISECONDS = 5e3;
|
|
31
47
|
const SCREEN_EDGE_SPACING = 3;
|
|
32
48
|
const PANEL_WIDTH_PIXELS = 360;
|
|
@@ -67,34 +83,34 @@ function summaryTextOf(downloads, t) {
|
|
|
67
83
|
function DownloadStatusIndicator({ download, displayName }) {
|
|
68
84
|
const { t } = useToolkitTranslation("common");
|
|
69
85
|
if (download.status === DOWNLOAD_STATUS.completed) {
|
|
70
|
-
return /* @__PURE__ */ jsx(
|
|
86
|
+
return /* @__PURE__ */ jsx(DownloadsDoneIcon, { ownSx: { color: "success.main" }, "aria-label": t("downloads.completedOf", { name: displayName }) });
|
|
71
87
|
}
|
|
72
88
|
if (download.status === DOWNLOAD_STATUS.failed) {
|
|
73
|
-
return /* @__PURE__ */ jsx(
|
|
89
|
+
return /* @__PURE__ */ jsx(DownloadsFailedIcon, { ownSx: { color: "error.main" }, "aria-label": t("downloads.failedOf", { name: displayName }) });
|
|
74
90
|
}
|
|
75
91
|
if (download.status === DOWNLOAD_STATUS.canceled) {
|
|
76
|
-
return /* @__PURE__ */ jsx(
|
|
92
|
+
return /* @__PURE__ */ jsx(DownloadsCanceledIcon, { ownSx: { color: "text.disabled" }, "aria-label": t("downloads.canceledOf", { name: displayName }) });
|
|
77
93
|
}
|
|
78
94
|
const hasKnownSize = Boolean(download.totalBytes);
|
|
79
95
|
return /* @__PURE__ */ jsx(Tooltip, { title: t("downloads.cancel"), children: /* @__PURE__ */ jsxs(
|
|
80
|
-
|
|
96
|
+
DownloadsCancelButton,
|
|
81
97
|
{
|
|
82
98
|
size: "small",
|
|
83
99
|
"aria-label": t("downloads.cancelOf", { name: displayName }),
|
|
84
100
|
onClick: () => cancelDownload(download.id),
|
|
85
|
-
|
|
101
|
+
ownSx: { position: "relative", width: PROGRESS_RING_PIXELS + 10, height: PROGRESS_RING_PIXELS + 10 },
|
|
86
102
|
children: [
|
|
87
103
|
/* @__PURE__ */ jsx(
|
|
88
|
-
|
|
104
|
+
DownloadsProgress,
|
|
89
105
|
{
|
|
90
106
|
size: PROGRESS_RING_PIXELS,
|
|
91
107
|
thickness: 4,
|
|
92
108
|
variant: hasKnownSize ? "determinate" : "indeterminate",
|
|
93
109
|
value: hasKnownSize ? Math.min(100, download.loadedBytes * 100 / download.totalBytes) : void 0,
|
|
94
|
-
|
|
110
|
+
ownSx: { position: "absolute" }
|
|
95
111
|
}
|
|
96
112
|
),
|
|
97
|
-
/* @__PURE__ */ jsx(
|
|
113
|
+
/* @__PURE__ */ jsx(DownloadsCancelIcon, { ownSx: { fontSize: 12 } })
|
|
98
114
|
]
|
|
99
115
|
}
|
|
100
116
|
) });
|
|
@@ -107,22 +123,22 @@ function DownloadRow({ download }) {
|
|
|
107
123
|
const isFailed = download.status === DOWNLOAD_STATUS.failed;
|
|
108
124
|
return (
|
|
109
125
|
// minWidth 0: sin él un nombre largo empuja el renglón y la lista enseña una barra horizontal
|
|
110
|
-
/* @__PURE__ */ jsxs(
|
|
126
|
+
/* @__PURE__ */ jsxs(DownloadsItem, { disableGutters: true, ownSx: { gap: 1.5, px: 2, py: 1, minWidth: 0 }, children: [
|
|
111
127
|
/* @__PURE__ */ jsx(Icon, { fontSize: "small", sx: { color, flexShrink: 0 } }),
|
|
112
|
-
/* @__PURE__ */ jsxs(
|
|
128
|
+
/* @__PURE__ */ jsxs(DownloadsItemText, { ownSx: { minWidth: 0, flex: 1 }, children: [
|
|
113
129
|
/* @__PURE__ */ jsx(Tooltip, { title: displayName, children: /* @__PURE__ */ jsx(Typography, { variant: "body2", noWrap: true, children: displayName }) }),
|
|
114
130
|
/* @__PURE__ */ jsx(
|
|
115
|
-
|
|
131
|
+
DownloadsItemStatus,
|
|
116
132
|
{
|
|
117
133
|
variant: "caption",
|
|
118
134
|
component: "p",
|
|
119
135
|
noWrap: !isFailed,
|
|
120
|
-
|
|
136
|
+
ownSx: { color: isFailed ? "error.main" : "text.secondary" },
|
|
121
137
|
children: detailTextOf(download, t, formatFileSize)
|
|
122
138
|
}
|
|
123
139
|
)
|
|
124
140
|
] }),
|
|
125
|
-
/* @__PURE__ */ jsx(
|
|
141
|
+
/* @__PURE__ */ jsx(DownloadsItemActions, { ownSx: { flexShrink: 0, display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(DownloadStatusIndicator, { download, displayName }) })
|
|
126
142
|
] })
|
|
127
143
|
);
|
|
128
144
|
}
|
|
@@ -147,12 +163,12 @@ function DownloadManager({ texts } = {}) {
|
|
|
147
163
|
return () => clearTimeout(dismissTimer);
|
|
148
164
|
}, [allSucceeded, downloads.length]);
|
|
149
165
|
return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "common", texts, children: /* @__PURE__ */ jsx(Slide, { direction: "up", in: downloads.length > 0, mountOnEnter: true, unmountOnExit: true, children: /* @__PURE__ */ jsxs(
|
|
150
|
-
|
|
166
|
+
DownloadsPanel,
|
|
151
167
|
{
|
|
152
168
|
elevation: 8,
|
|
153
169
|
role: "region",
|
|
154
170
|
"aria-label": t("downloads.title"),
|
|
155
|
-
|
|
171
|
+
ownSx: [{
|
|
156
172
|
position: "fixed",
|
|
157
173
|
right: (theme) => theme.spacing(SCREEN_EDGE_SPACING),
|
|
158
174
|
bottom: 0,
|
|
@@ -164,8 +180,8 @@ function DownloadManager({ texts } = {}) {
|
|
|
164
180
|
zIndex: (theme) => theme.zIndex.snackbar
|
|
165
181
|
}, (theme) => isGlassTheme(theme) ? glassStylesOf(theme) : {}],
|
|
166
182
|
children: [
|
|
167
|
-
/* @__PURE__ */ jsxs(
|
|
168
|
-
/* @__PURE__ */ jsx(
|
|
183
|
+
/* @__PURE__ */ jsxs(DownloadsHeader, { direction: "row", ownSx: { alignItems: "center", gap: 0.5, pl: 2, pr: 1, py: 1, bgcolor: "action.hover" }, children: [
|
|
184
|
+
/* @__PURE__ */ jsx(DownloadsTitle, { variant: "subtitle2", component: "h2", noWrap: true, ownSx: { flex: 1 }, "aria-live": "polite", children: summaryTextOf(downloads, t) }),
|
|
169
185
|
/* @__PURE__ */ jsx(Tooltip, { title: isExpanded ? t("downloads.collapse") : t("downloads.expand"), children: /* @__PURE__ */ jsx(
|
|
170
186
|
IconButton,
|
|
171
187
|
{
|
|
@@ -180,7 +196,7 @@ function DownloadManager({ texts } = {}) {
|
|
|
180
196
|
] }),
|
|
181
197
|
/* @__PURE__ */ jsxs(Collapse, { in: isExpanded, children: [
|
|
182
198
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
183
|
-
/* @__PURE__ */ jsx(
|
|
199
|
+
/* @__PURE__ */ jsx(DownloadsList, { dense: true, disablePadding: true, ownSx: { maxHeight: LIST_MAX_HEIGHT, overflowY: "auto", overflowX: "hidden" }, children: downloads.map((download, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
184
200
|
index > 0 && /* @__PURE__ */ jsx(Divider, { component: "li" }),
|
|
185
201
|
/* @__PURE__ */ jsx(DownloadRow, { download })
|
|
186
202
|
] }, download.id)) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DownloadManager.js","sources":["../../src/downloads/DownloadManager.jsx"],"sourcesContent":["// Panel de descargas al estilo de Google Drive: se pega abajo a la derecha SOLO mientras hay descargas, con una barra que\n// resume («Descargando 2 archivos…», «3 descargas listas») y la lista con el ícono del tipo de archivo y un anillo de\n// avance que termina en ✓. Con MUI y XState, sin Redux.\n//\n// Qué hace para no hacer ruido:\n// - no hay botón fijo: sin descargas no se pinta nada;\n// - se pliega a la barra con la flecha y se vuelve a abrir cuando entra otra descarga;\n// - cuando todo terminó bien se va solo a los AUTO_DISMISS_MILLISECONDS; si algo falló o se canceló se queda hasta que\n// la persona lo cierra, para que vea el motivo;\n// - la X solo aparece cuando ya nada descarga (cerrar a media descarga dejaría el archivo sin aviso de que terminó).\n//\n// Se monta UNA sola vez en la raíz de la aplicación. Lee la lista de downloadsActor; las descargas las arranca startDownload desde cada\n// pantalla. Los avisos (sonner) salen abajo a la izquierda, así que el panel va a la derecha.\n// Con el vidrio del tema (createToolkitTheme({glass: true})) el panel es de vidrio: flota sobre la pantalla.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {Fragment, useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useSelector} from '@xstate/react';\nimport Box from '@mui/material/Box';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Collapse from '@mui/material/Collapse';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport Paper from '@mui/material/Paper';\nimport Slide from '@mui/material/Slide';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport BlockOutlinedIcon from '@mui/icons-material/BlockOutlined';\nimport CheckCircleIcon from '@mui/icons-material/CheckCircle';\nimport CloseIcon from '@mui/icons-material/Close';\nimport ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';\nimport ExpandLessIcon from '@mui/icons-material/ExpandLess';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\n\nimport {cancelDownload, clearFinishedDownloads, countActiveDownloads, downloadsActor} from '@/downloads/downloadQueue.js';\nimport {UNKNOWN_FILE_TYPE, useFileTypes} from '@/downloads/fileTypes.js';\nimport {DOWNLOAD_STATUS} from '@/downloads/downloadsMachine.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\n\n// cuánto se queda el panel después de que TODAS las descargas terminaron bien\nconst AUTO_DISMISS_MILLISECONDS = 5000;\n\n// separación del borde derecho y ancho del panel (en el teléfono ocupa el ancho menos los márgenes)\nconst SCREEN_EDGE_SPACING = 3;\nconst PANEL_WIDTH_PIXELS = 360;\nconst LIST_MAX_HEIGHT = '40vh';\nconst PROGRESS_RING_PIXELS = 22;\n\n/**\n * Tipo de archivo de una descarga: el que dijo quien la arrancó o, al terminar, la extensión del nombre. Los tipos son los de\n * la casa y los del proyecto (ToolkitProvider fileTypes).\n *\n * @param {object} download renglón de la lista (downloadsMachine)\n * @param {Record<string, {Icon: import('react').ElementType, color?: string}>} fileTypes\n * @returns {{Icon: import('react').ElementType, color: string}}\n */\nfunction fileTypeAppearanceOf(download, fileTypes) {\n const extension = download.fileName?.split('.').pop()?.toLowerCase();\n const fileType = fileTypes[download.format ?? extension];\n return fileType ? {Icon: fileType.Icon ?? UNKNOWN_FILE_TYPE.Icon, color: fileType.color ?? UNKNOWN_FILE_TYPE.color} : UNKNOWN_FILE_TYPE;\n}\n\n/**\n * Segunda línea del renglón: cuánto lleva, cuánto pesó o por qué no se pudo.\n *\n * @param {object} download\n * @param {Function} t traductor de common\n * @param {(bytes: number) => string} formatFileSize el de useFormatters (el idioma del proveedor más cercano)\n * @returns {string}\n */\nfunction detailTextOf(download, t, formatFileSize) {\n if (download.status === DOWNLOAD_STATUS.completed) {\n return formatFileSize(download.loadedBytes);\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return download.errorMessage ?? t('downloads.failed');\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return t('downloads.canceled');\n }\n if (download.loadedBytes === 0) {\n return t('downloads.preparing');\n }\n // un archivo que se arma al vuelo (el log) no dice su tamaño: se enseña lo que ya bajó\n return download.totalBytes\n ? t('downloads.progressOf', {loaded: formatFileSize(download.loadedBytes), total: formatFileSize(download.totalBytes)})\n : t('downloads.progressLoaded', {loaded: formatFileSize(download.loadedBytes)});\n}\n\n/**\n * Resumen de la barra, como en Drive: lo que está pasando ahora o cómo terminó.\n *\n * @param {Array<object>} downloads\n * @param {Function} t traductor de common\n * @returns {string}\n */\nfunction summaryTextOf(downloads, t) {\n const activeCount = countActiveDownloads(downloads);\n if (activeCount > 0) {\n return t('downloads.summaryDownloading', {count: activeCount});\n }\n const failedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.failed).length;\n if (failedCount > 0) {\n return t('downloads.summaryFailed', {count: failedCount});\n }\n const completedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.completed).length;\n return t('downloads.summaryCompleted', {count: completedCount});\n}\n\n/**\n * Lo que va a la derecha del renglón: el anillo de avance con su botón para cancelar, o cómo terminó.\n *\n * @param {{download: object, displayName: string}} props\n */\nfunction DownloadStatusIndicator({download, displayName}) {\n const {t} = useToolkitTranslation('common');\n\n if (download.status === DOWNLOAD_STATUS.completed) {\n return <CheckCircleIcon sx={{color: 'success.main'}} aria-label={t('downloads.completedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return <ErrorOutlineIcon sx={{color: 'error.main'}} aria-label={t('downloads.failedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return <BlockOutlinedIcon sx={{color: 'text.disabled'}} aria-label={t('downloads.canceledOf', {name: displayName})} />;\n }\n\n const hasKnownSize = Boolean(download.totalBytes);\n // el anillo y, encima, la X para cancelar: como en Drive, un solo lugar para ver y detener\n return (\n <Tooltip title={t('downloads.cancel')}>\n <IconButton size=\"small\" aria-label={t('downloads.cancelOf', {name: displayName})} onClick={() => cancelDownload(download.id)}\n sx={{position: 'relative', width: PROGRESS_RING_PIXELS + 10, height: PROGRESS_RING_PIXELS + 10}}>\n <CircularProgress\n size={PROGRESS_RING_PIXELS}\n thickness={4}\n variant={hasKnownSize ? 'determinate' : 'indeterminate'}\n value={hasKnownSize ? Math.min(100, (download.loadedBytes * 100) / download.totalBytes) : undefined}\n sx={{position: 'absolute'}}\n />\n <CloseIcon sx={{fontSize: 12}} />\n </IconButton>\n </Tooltip>\n );\n}\n\n/**\n * Un renglón: ícono del tipo de archivo, nombre, detalle y estatus.\n *\n * @param {{download: object}} props\n */\nfunction DownloadRow({download}) {\n const {t} = useToolkitTranslation('common');\n const {formatFileSize} = useFormatters();\n const {Icon, color} = fileTypeAppearanceOf(download, useFileTypes());\n const displayName = download.fileName ?? download.label;\n const isFailed = download.status === DOWNLOAD_STATUS.failed;\n\n return (\n // minWidth 0: sin él un nombre largo empuja el renglón y la lista enseña una barra horizontal\n <ListItem disableGutters sx={{gap: 1.5, px: 2, py: 1, minWidth: 0}}>\n <Icon fontSize=\"small\" sx={{color, flexShrink: 0}} />\n <Box sx={{minWidth: 0, flex: 1}}>\n <Tooltip title={displayName}>\n <Typography variant=\"body2\" noWrap>{displayName}</Typography>\n </Tooltip>\n <Typography variant=\"caption\" component=\"p\" noWrap={!isFailed}\n sx={{color: isFailed ? 'error.main' : 'text.secondary'}}>\n {detailTextOf(download, t, formatFileSize)}\n </Typography>\n </Box>\n <Box sx={{flexShrink: 0, display: 'flex', alignItems: 'center'}}>\n <DownloadStatusIndicator download={download} displayName={displayName} />\n </Box>\n </ListItem>\n );\n}\n\n/**\n * @param {object} [props]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function DownloadManager({texts} = {}) {\n const {t} = useToolkitTranslation('common', texts);\n const downloads = useSelector(downloadsActor, (snapshot) => snapshot.context.downloads);\n const [isExpanded, setIsExpanded] = useState(true);\n const previousCountRef = useRef(downloads.length);\n\n const activeCount = countActiveDownloads(downloads);\n const allSucceeded = downloads.length > 0 && downloads.every((download) => download.status === DOWNLOAD_STATUS.completed);\n\n // una descarga nueva vuelve a desplegar el panel aunque se haya plegado\n useEffect(() => {\n if (downloads.length > previousCountRef.current) {\n setIsExpanded(true);\n }\n previousCountRef.current = downloads.length;\n }, [downloads.length]);\n\n // todo terminó bien: el panel se va solo (con un error o una cancelada se queda para que se lea)\n useEffect(() => {\n if (!allSucceeded) {\n return undefined;\n }\n const dismissTimer = setTimeout(clearFinishedDownloads, AUTO_DISMISS_MILLISECONDS);\n return () => clearTimeout(dismissTimer);\n }, [allSucceeded, downloads.length]);\n\n return (\n <ComponentTexts namespace=\"common\" texts={texts}>\n <Slide direction=\"up\" in={downloads.length > 0} mountOnEnter unmountOnExit>\n <Paper\n elevation={8}\n role=\"region\"\n aria-label={t('downloads.title')}\n sx={[{\n position: 'fixed',\n right: (theme) => theme.spacing(SCREEN_EDGE_SPACING),\n bottom: 0,\n width: (theme) => `min(${PANEL_WIDTH_PIXELS}px, calc(100vw - ${theme.spacing(SCREEN_EDGE_SPACING * 2)}))`,\n // pegado al borde de abajo como en Drive: solo se redondean las esquinas de arriba\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n overflow: 'hidden',\n zIndex: (theme) => theme.zIndex.snackbar,\n }, (theme) => (isGlassTheme(theme) ? glassStylesOf(theme) : {})]}\n >\n <Stack direction=\"row\" sx={{alignItems: 'center', gap: 0.5, pl: 2, pr: 1, py: 1, bgcolor: 'action.hover'}}>\n <Typography variant=\"subtitle2\" component=\"h2\" noWrap sx={{flex: 1}} aria-live=\"polite\">\n {summaryTextOf(downloads, t)}\n </Typography>\n <Tooltip title={isExpanded ? t('downloads.collapse') : t('downloads.expand')}>\n <IconButton size=\"small\" aria-expanded={isExpanded}\n aria-label={isExpanded ? t('downloads.collapse') : t('downloads.expand')}\n onClick={() => setIsExpanded((wasExpanded) => !wasExpanded)}>\n {isExpanded ? <ExpandMoreIcon fontSize=\"small\" /> : <ExpandLessIcon fontSize=\"small\" />}\n </IconButton>\n </Tooltip>\n {/* cerrar a media descarga dejaría el archivo sin aviso de que terminó: la X sale cuando ya nada baja */}\n {activeCount === 0 && (\n <Tooltip title={t('downloads.close')}>\n <IconButton size=\"small\" aria-label={t('downloads.close')} onClick={clearFinishedDownloads}>\n <CloseIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </Stack>\n <Collapse in={isExpanded}>\n <Divider />\n {/* OJO: overflowY auto también prende el scroll horizontal ante 1 px de más; se apaga explícito */}\n <List dense disablePadding sx={{maxHeight: LIST_MAX_HEIGHT, overflowY: 'auto', overflowX: 'hidden'}}>\n {downloads.map((download, index) => (\n <Fragment key={download.id}>\n {index > 0 && <Divider component=\"li\" />}\n <DownloadRow download={download} />\n </Fragment>\n ))}\n </List>\n </Collapse>\n </Paper>\n </Slide>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DownloadManager.js","sources":["../../src/downloads/DownloadManager.jsx"],"sourcesContent":["// Panel de descargas al estilo de Google Drive: se pega abajo a la derecha SOLO mientras hay descargas, con una barra que\n// resume («Descargando 2 archivos…», «3 descargas listas») y la lista con el ícono del tipo de archivo y un anillo de\n// avance que termina en ✓. Con MUI y XState, sin Redux.\n//\n// Qué hace para no hacer ruido:\n// - no hay botón fijo: sin descargas no se pinta nada;\n// - se pliega a la barra con la flecha y se vuelve a abrir cuando entra otra descarga;\n// - cuando todo terminó bien se va solo a los AUTO_DISMISS_MILLISECONDS; si algo falló o se canceló se queda hasta que\n// la persona lo cierra, para que vea el motivo;\n// - la X solo aparece cuando ya nada descarga (cerrar a media descarga dejaría el archivo sin aviso de que terminó).\n//\n// Se monta UNA sola vez en la raíz de la aplicación. Lee la lista de downloadsActor; las descargas las arranca startDownload desde cada\n// pantalla. Los avisos (sonner) salen abajo a la izquierda, así que el panel va a la derecha.\n// Con el vidrio del tema (createToolkitTheme({glass: true})) el panel es de vidrio: flota sobre la pantalla.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaDownloadManager con defaultProps y\n// styleOverrides de sus partes: doneIcon, failedIcon, canceledIcon, cancelButton, progress, cancelIcon, item, itemText,\n// itemStatus, itemActions, panel, header, title, list.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {Fragment, useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useSelector} from '@xstate/react';\nimport Box from '@mui/material/Box';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Collapse from '@mui/material/Collapse';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport Paper from '@mui/material/Paper';\nimport Slide from '@mui/material/Slide';\nimport Stack from '@mui/material/Stack';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport BlockOutlinedIcon from '@mui/icons-material/BlockOutlined';\nimport CheckCircleIcon from '@mui/icons-material/CheckCircle';\nimport CloseIcon from '@mui/icons-material/Close';\nimport ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';\nimport ExpandLessIcon from '@mui/icons-material/ExpandLess';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\n\nimport {cancelDownload, clearFinishedDownloads, countActiveDownloads, downloadsActor} from '@/downloads/downloadQueue.js';\nimport {UNKNOWN_FILE_TYPE, useFileTypes} from '@/downloads/fileTypes.js';\nimport {DOWNLOAD_STATUS} from '@/downloads/downloadsMachine.js';\nimport {useFormatters} from '@/toolkit/useFormatters.js';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport ComponentTexts from '@/toolkit/ComponentTexts.jsx';\nimport {piecePart} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaDownloadManager)\nconst THEME_NAME = 'CalcitaDownloadManager';\nconst DownloadsDoneIcon = piecePart(THEME_NAME, 'DoneIcon', CheckCircleIcon);\nconst DownloadsFailedIcon = piecePart(THEME_NAME, 'FailedIcon', ErrorOutlineIcon);\nconst DownloadsCanceledIcon = piecePart(THEME_NAME, 'CanceledIcon', BlockOutlinedIcon);\nconst DownloadsCancelButton = piecePart(THEME_NAME, 'CancelButton', IconButton);\nconst DownloadsProgress = piecePart(THEME_NAME, 'Progress', CircularProgress);\nconst DownloadsCancelIcon = piecePart(THEME_NAME, 'CancelIcon', CloseIcon);\nconst DownloadsItem = piecePart(THEME_NAME, 'Item', ListItem);\nconst DownloadsItemText = piecePart(THEME_NAME, 'ItemText', Box);\nconst DownloadsItemStatus = piecePart(THEME_NAME, 'ItemStatus', Typography);\nconst DownloadsItemActions = piecePart(THEME_NAME, 'ItemActions', Box);\nconst DownloadsPanel = piecePart(THEME_NAME, 'Panel', Paper);\nconst DownloadsHeader = piecePart(THEME_NAME, 'Header', Stack);\nconst DownloadsTitle = piecePart(THEME_NAME, 'Title', Typography);\nconst DownloadsList = piecePart(THEME_NAME, 'List', List);\n\n// cuánto se queda el panel después de que TODAS las descargas terminaron bien\nconst AUTO_DISMISS_MILLISECONDS = 5000;\n\n// separación del borde derecho y ancho del panel (en el teléfono ocupa el ancho menos los márgenes)\nconst SCREEN_EDGE_SPACING = 3;\nconst PANEL_WIDTH_PIXELS = 360;\nconst LIST_MAX_HEIGHT = '40vh';\nconst PROGRESS_RING_PIXELS = 22;\n\n/**\n * Tipo de archivo de una descarga: el que dijo quien la arrancó o, al terminar, la extensión del nombre. Los tipos son los de\n * la casa y los del proyecto (ToolkitProvider fileTypes).\n *\n * @param {object} download renglón de la lista (downloadsMachine)\n * @param {Record<string, {Icon: import('react').ElementType, color?: string}>} fileTypes\n * @returns {{Icon: import('react').ElementType, color: string}}\n */\nfunction fileTypeAppearanceOf(download, fileTypes) {\n const extension = download.fileName?.split('.').pop()?.toLowerCase();\n const fileType = fileTypes[download.format ?? extension];\n return fileType ? {Icon: fileType.Icon ?? UNKNOWN_FILE_TYPE.Icon, color: fileType.color ?? UNKNOWN_FILE_TYPE.color} : UNKNOWN_FILE_TYPE;\n}\n\n/**\n * Segunda línea del renglón: cuánto lleva, cuánto pesó o por qué no se pudo.\n *\n * @param {object} download\n * @param {Function} t traductor de common\n * @param {(bytes: number) => string} formatFileSize el de useFormatters (el idioma del proveedor más cercano)\n * @returns {string}\n */\nfunction detailTextOf(download, t, formatFileSize) {\n if (download.status === DOWNLOAD_STATUS.completed) {\n return formatFileSize(download.loadedBytes);\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return download.errorMessage ?? t('downloads.failed');\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return t('downloads.canceled');\n }\n if (download.loadedBytes === 0) {\n return t('downloads.preparing');\n }\n // un archivo que se arma al vuelo (el log) no dice su tamaño: se enseña lo que ya bajó\n return download.totalBytes\n ? t('downloads.progressOf', {loaded: formatFileSize(download.loadedBytes), total: formatFileSize(download.totalBytes)})\n : t('downloads.progressLoaded', {loaded: formatFileSize(download.loadedBytes)});\n}\n\n/**\n * Resumen de la barra, como en Drive: lo que está pasando ahora o cómo terminó.\n *\n * @param {Array<object>} downloads\n * @param {Function} t traductor de common\n * @returns {string}\n */\nfunction summaryTextOf(downloads, t) {\n const activeCount = countActiveDownloads(downloads);\n if (activeCount > 0) {\n return t('downloads.summaryDownloading', {count: activeCount});\n }\n const failedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.failed).length;\n if (failedCount > 0) {\n return t('downloads.summaryFailed', {count: failedCount});\n }\n const completedCount = downloads.filter((download) => download.status === DOWNLOAD_STATUS.completed).length;\n return t('downloads.summaryCompleted', {count: completedCount});\n}\n\n/**\n * Lo que va a la derecha del renglón: el anillo de avance con su botón para cancelar, o cómo terminó.\n *\n * @param {{download: object, displayName: string}} props\n */\nfunction DownloadStatusIndicator({download, displayName}) {\n const {t} = useToolkitTranslation('common');\n\n if (download.status === DOWNLOAD_STATUS.completed) {\n return <DownloadsDoneIcon ownSx={{color: 'success.main'}} aria-label={t('downloads.completedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.failed) {\n return <DownloadsFailedIcon ownSx={{color: 'error.main'}} aria-label={t('downloads.failedOf', {name: displayName})} />;\n }\n if (download.status === DOWNLOAD_STATUS.canceled) {\n return <DownloadsCanceledIcon ownSx={{color: 'text.disabled'}} aria-label={t('downloads.canceledOf', {name: displayName})} />;\n }\n\n const hasKnownSize = Boolean(download.totalBytes);\n // el anillo y, encima, la X para cancelar: como en Drive, un solo lugar para ver y detener\n return (\n <Tooltip title={t('downloads.cancel')}>\n <DownloadsCancelButton size=\"small\" aria-label={t('downloads.cancelOf', {name: displayName})} onClick={() => cancelDownload(download.id)}\n ownSx={{position: 'relative', width: PROGRESS_RING_PIXELS + 10, height: PROGRESS_RING_PIXELS + 10}}>\n <DownloadsProgress\n size={PROGRESS_RING_PIXELS}\n thickness={4}\n variant={hasKnownSize ? 'determinate' : 'indeterminate'}\n value={hasKnownSize ? Math.min(100, (download.loadedBytes * 100) / download.totalBytes) : undefined}\n ownSx={{position: 'absolute'}}\n />\n <DownloadsCancelIcon ownSx={{fontSize: 12}} />\n </DownloadsCancelButton>\n </Tooltip>\n );\n}\n\n/**\n * Un renglón: ícono del tipo de archivo, nombre, detalle y estatus.\n *\n * @param {{download: object}} props\n */\nfunction DownloadRow({download}) {\n const {t} = useToolkitTranslation('common');\n const {formatFileSize} = useFormatters();\n const {Icon, color} = fileTypeAppearanceOf(download, useFileTypes());\n const displayName = download.fileName ?? download.label;\n const isFailed = download.status === DOWNLOAD_STATUS.failed;\n\n return (\n // minWidth 0: sin él un nombre largo empuja el renglón y la lista enseña una barra horizontal\n <DownloadsItem disableGutters ownSx={{gap: 1.5, px: 2, py: 1, minWidth: 0}}>\n <Icon fontSize=\"small\" sx={{color, flexShrink: 0}} />\n <DownloadsItemText ownSx={{minWidth: 0, flex: 1}}>\n <Tooltip title={displayName}>\n <Typography variant=\"body2\" noWrap>{displayName}</Typography>\n </Tooltip>\n <DownloadsItemStatus variant=\"caption\" component=\"p\" noWrap={!isFailed}\n ownSx={{color: isFailed ? 'error.main' : 'text.secondary'}}>\n {detailTextOf(download, t, formatFileSize)}\n </DownloadsItemStatus>\n </DownloadsItemText>\n <DownloadsItemActions ownSx={{flexShrink: 0, display: 'flex', alignItems: 'center'}}>\n <DownloadStatusIndicator download={download} displayName={displayName} />\n </DownloadsItemActions>\n </DownloadsItem>\n );\n}\n\n/**\n * @param {object} [props]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function DownloadManager({texts} = {}) {\n const {t} = useToolkitTranslation('common', texts);\n const downloads = useSelector(downloadsActor, (snapshot) => snapshot.context.downloads);\n const [isExpanded, setIsExpanded] = useState(true);\n const previousCountRef = useRef(downloads.length);\n\n const activeCount = countActiveDownloads(downloads);\n const allSucceeded = downloads.length > 0 && downloads.every((download) => download.status === DOWNLOAD_STATUS.completed);\n\n // una descarga nueva vuelve a desplegar el panel aunque se haya plegado\n useEffect(() => {\n if (downloads.length > previousCountRef.current) {\n setIsExpanded(true);\n }\n previousCountRef.current = downloads.length;\n }, [downloads.length]);\n\n // todo terminó bien: el panel se va solo (con un error o una cancelada se queda para que se lea)\n useEffect(() => {\n if (!allSucceeded) {\n return undefined;\n }\n const dismissTimer = setTimeout(clearFinishedDownloads, AUTO_DISMISS_MILLISECONDS);\n return () => clearTimeout(dismissTimer);\n }, [allSucceeded, downloads.length]);\n\n return (\n <ComponentTexts namespace=\"common\" texts={texts}>\n <Slide direction=\"up\" in={downloads.length > 0} mountOnEnter unmountOnExit>\n <DownloadsPanel\n elevation={8}\n role=\"region\"\n aria-label={t('downloads.title')}\n ownSx={[{\n position: 'fixed',\n right: (theme) => theme.spacing(SCREEN_EDGE_SPACING),\n bottom: 0,\n width: (theme) => `min(${PANEL_WIDTH_PIXELS}px, calc(100vw - ${theme.spacing(SCREEN_EDGE_SPACING * 2)}))`,\n // pegado al borde de abajo como en Drive: solo se redondean las esquinas de arriba\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n overflow: 'hidden',\n zIndex: (theme) => theme.zIndex.snackbar,\n }, (theme) => (isGlassTheme(theme) ? glassStylesOf(theme) : {})]}\n >\n <DownloadsHeader direction=\"row\" ownSx={{alignItems: 'center', gap: 0.5, pl: 2, pr: 1, py: 1, bgcolor: 'action.hover'}}>\n <DownloadsTitle variant=\"subtitle2\" component=\"h2\" noWrap ownSx={{flex: 1}} aria-live=\"polite\">\n {summaryTextOf(downloads, t)}\n </DownloadsTitle>\n <Tooltip title={isExpanded ? t('downloads.collapse') : t('downloads.expand')}>\n <IconButton size=\"small\" aria-expanded={isExpanded}\n aria-label={isExpanded ? t('downloads.collapse') : t('downloads.expand')}\n onClick={() => setIsExpanded((wasExpanded) => !wasExpanded)}>\n {isExpanded ? <ExpandMoreIcon fontSize=\"small\" /> : <ExpandLessIcon fontSize=\"small\" />}\n </IconButton>\n </Tooltip>\n {/* cerrar a media descarga dejaría el archivo sin aviso de que terminó: la X sale cuando ya nada baja */}\n {activeCount === 0 && (\n <Tooltip title={t('downloads.close')}>\n <IconButton size=\"small\" aria-label={t('downloads.close')} onClick={clearFinishedDownloads}>\n <CloseIcon fontSize=\"small\" />\n </IconButton>\n </Tooltip>\n )}\n </DownloadsHeader>\n <Collapse in={isExpanded}>\n <Divider />\n {/* OJO: overflowY auto también prende el scroll horizontal ante 1 px de más; se apaga explícito */}\n <DownloadsList dense disablePadding ownSx={{maxHeight: LIST_MAX_HEIGHT, overflowY: 'auto', overflowX: 'hidden'}}>\n {downloads.map((download, index) => (\n <Fragment key={download.id}>\n {index > 0 && <Divider component=\"li\" />}\n <DownloadRow download={download} />\n </Fragment>\n ))}\n </DownloadsList>\n </Collapse>\n </DownloadsPanel>\n </Slide>\n </ComponentTexts>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAM,aAAa;AACnB,MAAM,oBAAoB,UAAU,YAAY,YAAY,eAAe;AAC3E,MAAM,sBAAsB,UAAU,YAAY,cAAc,gBAAgB;AAChF,MAAM,wBAAwB,UAAU,YAAY,gBAAgB,iBAAiB;AACrF,MAAM,wBAAwB,UAAU,YAAY,gBAAgB,UAAU;AAC9E,MAAM,oBAAoB,UAAU,YAAY,YAAY,gBAAgB;AAC5E,MAAM,sBAAsB,UAAU,YAAY,cAAc,SAAS;AACzE,MAAM,gBAAgB,UAAU,YAAY,QAAQ,QAAQ;AAC5D,MAAM,oBAAoB,UAAU,YAAY,YAAY,GAAG;AAC/D,MAAM,sBAAsB,UAAU,YAAY,cAAc,UAAU;AAC1E,MAAM,uBAAuB,UAAU,YAAY,eAAe,GAAG;AACrE,MAAM,iBAAiB,UAAU,YAAY,SAAS,KAAK;AAC3D,MAAM,kBAAkB,UAAU,YAAY,UAAU,KAAK;AAC7D,MAAM,iBAAiB,UAAU,YAAY,SAAS,UAAU;AAChE,MAAM,gBAAgB,UAAU,YAAY,QAAQ,IAAI;AAGxD,MAAM,4BAA4B;AAGlC,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;AAU7B,SAAS,qBAAqB,UAAU,WAAW;AAC/C,QAAM,YAAY,SAAS,UAAU,MAAM,GAAG,EAAE,IAAA,GAAO,YAAA;AACvD,QAAM,WAAW,UAAU,SAAS,UAAU,SAAS;AACvD,SAAO,WAAW,EAAC,MAAM,SAAS,QAAQ,kBAAkB,MAAM,OAAO,SAAS,SAAS,kBAAkB,MAAA,IAAS;AAC1H;AAUA,SAAS,aAAa,UAAU,GAAG,gBAAgB;AAC/C,MAAI,SAAS,WAAW,gBAAgB,WAAW;AAC/C,WAAO,eAAe,SAAS,WAAW;AAAA,EAC9C;AACA,MAAI,SAAS,WAAW,gBAAgB,QAAQ;AAC5C,WAAO,SAAS,gBAAgB,EAAE,kBAAkB;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,gBAAgB,UAAU;AAC9C,WAAO,EAAE,oBAAoB;AAAA,EACjC;AACA,MAAI,SAAS,gBAAgB,GAAG;AAC5B,WAAO,EAAE,qBAAqB;AAAA,EAClC;AAEA,SAAO,SAAS,aACV,EAAE,wBAAwB,EAAC,QAAQ,eAAe,SAAS,WAAW,GAAG,OAAO,eAAe,SAAS,UAAU,EAAA,CAAE,IACpH,EAAE,4BAA4B,EAAC,QAAQ,eAAe,SAAS,WAAW,GAAE;AACtF;AASA,SAAS,cAAc,WAAW,GAAG;AACjC,QAAM,cAAc,qBAAqB,SAAS;AAClD,MAAI,cAAc,GAAG;AACjB,WAAO,EAAE,gCAAgC,EAAC,OAAO,aAAY;AAAA,EACjE;AACA,QAAM,cAAc,UAAU,OAAO,CAAC,aAAa,SAAS,WAAW,gBAAgB,MAAM,EAAE;AAC/F,MAAI,cAAc,GAAG;AACjB,WAAO,EAAE,2BAA2B,EAAC,OAAO,aAAY;AAAA,EAC5D;AACA,QAAM,iBAAiB,UAAU,OAAO,CAAC,aAAa,SAAS,WAAW,gBAAgB,SAAS,EAAE;AACrG,SAAO,EAAE,8BAA8B,EAAC,OAAO,gBAAe;AAClE;AAOA,SAAS,wBAAwB,EAAC,UAAU,eAAc;AACtD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAE1C,MAAI,SAAS,WAAW,gBAAgB,WAAW;AAC/C,WAAO,oBAAC,mBAAA,EAAkB,OAAO,EAAC,OAAO,eAAA,GAAiB,cAAY,EAAE,yBAAyB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EAC3H;AACA,MAAI,SAAS,WAAW,gBAAgB,QAAQ;AAC5C,WAAO,oBAAC,qBAAA,EAAoB,OAAO,EAAC,OAAO,aAAA,GAAe,cAAY,EAAE,sBAAsB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EACxH;AACA,MAAI,SAAS,WAAW,gBAAgB,UAAU;AAC9C,WAAO,oBAAC,uBAAA,EAAsB,OAAO,EAAC,OAAO,gBAAA,GAAkB,cAAY,EAAE,wBAAwB,EAAC,MAAM,YAAA,CAAY,EAAA,CAAG;AAAA,EAC/H;AAEA,QAAM,eAAe,QAAQ,SAAS,UAAU;AAEhD,SACI,oBAAC,SAAA,EAAQ,OAAO,EAAE,kBAAkB,GAChC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MAAsB,MAAK;AAAA,MAAQ,cAAY,EAAE,sBAAsB,EAAC,MAAM,aAAY;AAAA,MAAG,SAAS,MAAM,eAAe,SAAS,EAAE;AAAA,MAC3H,OAAO,EAAC,UAAU,YAAY,OAAO,uBAAuB,IAAI,QAAQ,uBAAuB,GAAA;AAAA,MACvG,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACG,MAAM;AAAA,YACN,WAAW;AAAA,YACX,SAAS,eAAe,gBAAgB;AAAA,YACxC,OAAO,eAAe,KAAK,IAAI,KAAM,SAAS,cAAc,MAAO,SAAS,UAAU,IAAI;AAAA,YAC1F,OAAO,EAAC,UAAU,WAAA;AAAA,UAAU;AAAA,QAAA;AAAA,4BAE/B,qBAAA,EAAoB,OAAO,EAAC,UAAU,KAAE,CAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAEpD;AAER;AAOA,SAAS,YAAY,EAAC,YAAW;AAC7B,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,eAAA,IAAkB,cAAA;AACzB,QAAM,EAAC,MAAM,MAAA,IAAS,qBAAqB,UAAU,cAAc;AACnE,QAAM,cAAc,SAAS,YAAY,SAAS;AAClD,QAAM,WAAW,SAAS,WAAW,gBAAgB;AAErD;AAAA;AAAA,IAEI,qBAAC,eAAA,EAAc,gBAAc,MAAC,OAAO,EAAC,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,UAAU,KACpE,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAI,EAAC,OAAO,YAAY,KAAI;AAAA,MACnD,qBAAC,qBAAkB,OAAO,EAAC,UAAU,GAAG,MAAM,KAC1C,UAAA;AAAA,QAAA,oBAAC,SAAA,EAAQ,OAAO,aACZ,UAAA,oBAAC,YAAA,EAAW,SAAQ,SAAQ,QAAM,MAAE,UAAA,YAAA,CAAY,GACpD;AAAA,QACA;AAAA,UAAC;AAAA,UAAA;AAAA,YAAoB,SAAQ;AAAA,YAAU,WAAU;AAAA,YAAI,QAAQ,CAAC;AAAA,YAClD,OAAO,EAAC,OAAO,WAAW,eAAe,iBAAA;AAAA,YAChD,UAAA,aAAa,UAAU,GAAG,cAAc;AAAA,UAAA;AAAA,QAAA;AAAA,MAC7C,GACJ;AAAA,MACA,oBAAC,sBAAA,EAAqB,OAAO,EAAC,YAAY,GAAG,SAAS,QAAQ,YAAY,YACtE,UAAA,oBAAC,yBAAA,EAAwB,UAAoB,aAA0B,EAAA,CAC3E;AAAA,IAAA,EAAA,CACJ;AAAA;AAER;AAMA,SAAwB,gBAAgB,EAAC,MAAA,IAAS,IAAI;AAClD,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AACjD,QAAM,YAAY,YAAY,gBAAgB,CAAC,aAAa,SAAS,QAAQ,SAAS;AACtF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,IAAI;AACjD,QAAM,mBAAmB,OAAO,UAAU,MAAM;AAEhD,QAAM,cAAc,qBAAqB,SAAS;AAClD,QAAM,eAAe,UAAU,SAAS,KAAK,UAAU,MAAM,CAAC,aAAa,SAAS,WAAW,gBAAgB,SAAS;AAGxH,YAAU,MAAM;AACZ,QAAI,UAAU,SAAS,iBAAiB,SAAS;AAC7C,oBAAc,IAAI;AAAA,IACtB;AACA,qBAAiB,UAAU,UAAU;AAAA,EACzC,GAAG,CAAC,UAAU,MAAM,CAAC;AAGrB,YAAU,MAAM;AACZ,QAAI,CAAC,cAAc;AACf,aAAO;AAAA,IACX;AACA,UAAM,eAAe,WAAW,wBAAwB,yBAAyB;AACjF,WAAO,MAAM,aAAa,YAAY;AAAA,EAC1C,GAAG,CAAC,cAAc,UAAU,MAAM,CAAC;AAEnC,6BACK,gBAAA,EAAe,WAAU,UAAS,OAC/B,8BAAC,OAAA,EAAM,WAAU,MAAK,IAAI,UAAU,SAAS,GAAG,cAAY,MAAC,eAAa,MACtE,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW;AAAA,MACX,MAAK;AAAA,MACL,cAAY,EAAE,iBAAiB;AAAA,MAC/B,OAAO,CAAC;AAAA,QACJ,UAAU;AAAA,QACV,OAAO,CAAC,UAAU,MAAM,QAAQ,mBAAmB;AAAA,QACnD,QAAQ;AAAA,QACR,OAAO,CAAC,UAAU,OAAO,kBAAkB,oBAAoB,MAAM,QAAQ,sBAAsB,CAAC,CAAC;AAAA;AAAA,QAErG,wBAAwB;AAAA,QACxB,yBAAyB;AAAA,QACzB,UAAU;AAAA,QACV,QAAQ,CAAC,UAAU,MAAM,OAAO;AAAA,MAAA,GACjC,CAAC,UAAW,aAAa,KAAK,IAAI,cAAc,KAAK,IAAI,EAAG;AAAA,MAE/D,UAAA;AAAA,QAAA,qBAAC,mBAAgB,WAAU,OAAM,OAAO,EAAC,YAAY,UAAU,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,kBACnG,UAAA;AAAA,UAAA,oBAAC,kBAAe,SAAQ,aAAY,WAAU,MAAK,QAAM,MAAC,OAAO,EAAC,MAAM,KAAI,aAAU,UACjF,UAAA,cAAc,WAAW,CAAC,GAC/B;AAAA,UACA,oBAAC,WAAQ,OAAO,aAAa,EAAE,oBAAoB,IAAI,EAAE,kBAAkB,GACvE,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cAAW,MAAK;AAAA,cAAQ,iBAAe;AAAA,cAC5B,cAAY,aAAa,EAAE,oBAAoB,IAAI,EAAE,kBAAkB;AAAA,cACvE,SAAS,MAAM,cAAc,CAAC,gBAAgB,CAAC,WAAW;AAAA,cACjE,UAAA,iCAAc,gBAAA,EAAe,UAAS,SAAQ,IAAK,oBAAC,gBAAA,EAAe,UAAS,QAAA,CAAQ;AAAA,YAAA;AAAA,UAAA,GAE7F;AAAA,UAEC,gBAAgB,KACb,oBAAC,SAAA,EAAQ,OAAO,EAAE,iBAAiB,GAC/B,UAAA,oBAAC,YAAA,EAAW,MAAK,SAAQ,cAAY,EAAE,iBAAiB,GAAG,SAAS,wBAChE,8BAAC,WAAA,EAAU,UAAS,QAAA,CAAQ,EAAA,CAChC,EAAA,CACJ;AAAA,QAAA,GAER;AAAA,QACA,qBAAC,UAAA,EAAS,IAAI,YACV,UAAA;AAAA,UAAA,oBAAC,SAAA,EAAQ;AAAA,UAET,oBAAC,iBAAc,OAAK,MAAC,gBAAc,MAAC,OAAO,EAAC,WAAW,iBAAiB,WAAW,QAAQ,WAAW,YACjG,UAAA,UAAU,IAAI,CAAC,UAAU,UACtB,qBAAC,UAAA,EACI,UAAA;AAAA,YAAA,QAAQ,KAAK,oBAAC,SAAA,EAAQ,WAAU,MAAK;AAAA,YACtC,oBAAC,eAAY,SAAA,CAAoB;AAAA,UAAA,KAFtB,SAAS,EAGxB,CACH,EAAA,CACL;AAAA,QAAA,EAAA,CACJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAER,EAAA,CACJ;AAER;"}
|
|
@@ -10,21 +10,26 @@ import DialogActions from "@mui/material/DialogActions";
|
|
|
10
10
|
import DialogContent from "@mui/material/DialogContent";
|
|
11
11
|
import DialogContentText from "@mui/material/DialogContentText";
|
|
12
12
|
import DialogTitle from "@mui/material/DialogTitle";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
14
|
+
const THEME_NAME = "CalcitaConfirmDialog";
|
|
15
|
+
const ConfirmProblem = piecePart(THEME_NAME, "Problem", Alert);
|
|
16
|
+
const ConfirmActions = piecePart(THEME_NAME, "Actions", DialogActions);
|
|
17
|
+
function ConfirmDialog(props) {
|
|
18
|
+
const {
|
|
19
|
+
open,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
confirmLabel,
|
|
23
|
+
cancelLabel,
|
|
24
|
+
tone = "primary",
|
|
25
|
+
isPending = false,
|
|
26
|
+
isConfirmDisabled = false,
|
|
27
|
+
errorMessage = null,
|
|
28
|
+
onConfirm,
|
|
29
|
+
onClose,
|
|
30
|
+
children,
|
|
31
|
+
texts
|
|
32
|
+
} = usePieceProps(props, THEME_NAME);
|
|
28
33
|
const { t } = useToolkitTranslation("common", texts);
|
|
29
34
|
const dialogId = useId();
|
|
30
35
|
const handleClose = () => {
|
|
@@ -45,11 +50,11 @@ function ConfirmDialog({
|
|
|
45
50
|
children: [
|
|
46
51
|
/* @__PURE__ */ jsx(DialogTitle, { id: `${dialogId}-title`, children: title }),
|
|
47
52
|
/* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
48
|
-
errorMessage && /* @__PURE__ */ jsx(
|
|
53
|
+
errorMessage && /* @__PURE__ */ jsx(ConfirmProblem, { severity: "error", role: "alert", ownSx: { mb: 2 }, children: errorMessage }),
|
|
49
54
|
description && /* @__PURE__ */ jsx(DialogContentText, { id: `${dialogId}-description`, children: description }),
|
|
50
55
|
children
|
|
51
56
|
] }),
|
|
52
|
-
/* @__PURE__ */ jsxs(
|
|
57
|
+
/* @__PURE__ */ jsxs(ConfirmActions, { ownSx: { px: 3, pb: 2.5 }, children: [
|
|
53
58
|
/* @__PURE__ */ jsx(Button, { variant: "outlined", color: "inherit", onClick: handleClose, disabled: isPending, children: cancelLabel ?? t("actions.cancel") }),
|
|
54
59
|
/* @__PURE__ */ jsx(
|
|
55
60
|
Button,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmDialog.js","sources":["../../src/feedback/ConfirmDialog.jsx"],"sourcesContent":["// Diálogo de confirmación: toda acción que cambia algo, manda un correo o descarga un archivo pasa por aquí antes de\n// ejecutarse (regla de tablas). Dice en lenguaje ciudadano QUÉ va a pasar y a QUÉ renglón («¿Dar de baja a Ana López?\n// Ya no podrá entrar; se puede reactivar después»).\n//\n// Recibe el estado de la petición por props (pending, error) en vez de hacerla él: core no conoce el API. Lo normal es\n// alimentarlo con useConfirmAction (máquina confirmActionMachine), que ya garantiza que cancelar no ejecuta y que un\n// doble clic no manda dos peticiones. Mientras la petición está en curso no se puede cerrar ni volver a confirmar.\n//\n// children: lo que el diálogo necesita pedir antes de confirmar (p. ej. el motivo de una baja), debajo del texto.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\n\n/**\n * @param {object} props\n * @param {boolean} props.open\n * @param {string} props.title texto ya traducido, con el renglón («¿Dar de baja a Ana López?»)\n * @param {import('react').ReactNode} [props.description] qué va a pasar, en palabras normales\n * @param {string} [props.confirmLabel]\n * @param {string} [props.cancelLabel]\n * @param {'primary'|'error'|'warning'} [props.tone]\n * @param {boolean} [props.isPending]\n * @param {boolean} [props.isConfirmDisabled] la acción ya no aplica (p. ej. conflicto: el registro cambió)\n * @param {string|null} [props.errorMessage] mensaje ya traducido (ApiError.message)\n * @param {() => void} props.onConfirm\n * @param {() => void} props.onClose\n * @param {import('react').ReactNode} [props.children]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ConfirmDialog({\n open, title, description, confirmLabel, cancelLabel, tone = 'primary',\n isPending = false, isConfirmDisabled = false, errorMessage = null, onConfirm, onClose, children, texts,\n})
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","sources":["../../src/feedback/ConfirmDialog.jsx"],"sourcesContent":["// Diálogo de confirmación: toda acción que cambia algo, manda un correo o descarga un archivo pasa por aquí antes de\n// ejecutarse (regla de tablas). Dice en lenguaje ciudadano QUÉ va a pasar y a QUÉ renglón («¿Dar de baja a Ana López?\n// Ya no podrá entrar; se puede reactivar después»).\n//\n// Recibe el estado de la petición por props (pending, error) en vez de hacerla él: core no conoce el API. Lo normal es\n// alimentarlo con useConfirmAction (máquina confirmActionMachine), que ya garantiza que cancelar no ejecuta y que un\n// doble clic no manda dos peticiones. Mientras la petición está en curso no se puede cerrar ni volver a confirmar.\n//\n// children: lo que el diálogo necesita pedir antes de confirmar (p. ej. el motivo de una baja), debajo del texto.\n//\n// Se cambia desde el tema del proyecto, como las piezas de MUI: components.CalcitaConfirmDialog con defaultProps y\n// styleOverrides de sus partes: problem, actions.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useId} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Alert from '@mui/material/Alert';\nimport Button from '@mui/material/Button';\nimport CircularProgress from '@mui/material/CircularProgress';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogContentText from '@mui/material/DialogContentText';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport {piecePart, usePieceProps} from '@/theme/themePieces.js';\n\n// el nombre de la pieza en el tema y sus partes (theme.components.CalcitaConfirmDialog)\nconst THEME_NAME = 'CalcitaConfirmDialog';\nconst ConfirmProblem = piecePart(THEME_NAME, 'Problem', Alert);\nconst ConfirmActions = piecePart(THEME_NAME, 'Actions', DialogActions);\n\n/**\n * @param {object} props\n * @param {boolean} props.open\n * @param {string} props.title texto ya traducido, con el renglón («¿Dar de baja a Ana López?»)\n * @param {import('react').ReactNode} [props.description] qué va a pasar, en palabras normales\n * @param {string} [props.confirmLabel]\n * @param {string} [props.cancelLabel]\n * @param {'primary'|'error'|'warning'} [props.tone]\n * @param {boolean} [props.isPending]\n * @param {boolean} [props.isConfirmDisabled] la acción ya no aplica (p. ej. conflicto: el registro cambió)\n * @param {string|null} [props.errorMessage] mensaje ya traducido (ApiError.message)\n * @param {() => void} props.onConfirm\n * @param {() => void} props.onClose\n * @param {import('react').ReactNode} [props.children]\n * @param {object} [props.texts] los textos que cambian, con la forma de `common` en los textos del paquete\n */\nexport default function ConfirmDialog(props) {\n // lo que el proyecto puso en su tema (components.CalcitaConfirmDialog.defaultProps) y encima lo que llega aquí\n const {\n open, title, description, confirmLabel, cancelLabel, tone = 'primary',\n isPending = false, isConfirmDisabled = false, errorMessage = null, onConfirm, onClose, children, texts,\n} = usePieceProps(props, THEME_NAME);\n const {t} = useToolkitTranslation('common', texts);\n // ids propios: una pantalla puede tener varios diálogos montados y los aria-* no deben apuntar al de otro\n const dialogId = useId();\n\n const handleClose = () => {\n // guard clause: no se cierra a media petición\n if (isPending) {\n return;\n }\n onClose();\n };\n\n return (\n <Dialog open={open} onClose={handleClose} maxWidth=\"xs\" fullWidth\n aria-labelledby={`${dialogId}-title`} aria-describedby={`${dialogId}-description`}>\n <DialogTitle id={`${dialogId}-title`}>{title}</DialogTitle>\n <DialogContent>\n {errorMessage && <ConfirmProblem severity=\"error\" role=\"alert\" ownSx={{mb: 2}}>{errorMessage}</ConfirmProblem>}\n {description && (\n <DialogContentText id={`${dialogId}-description`}>{description}</DialogContentText>\n )}\n {children}\n </DialogContent>\n <ConfirmActions ownSx={{px: 3, pb: 2.5}}>\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose} disabled={isPending}>\n {cancelLabel ?? t('actions.cancel')}\n </Button>\n <Button variant=\"contained\" color={tone} onClick={onConfirm} disabled={isPending || isConfirmDisabled}\n aria-busy={isPending}>\n {isPending ? <CircularProgress size={20} color=\"inherit\" /> : (confirmLabel ?? t('actions.confirm'))}\n </Button>\n </ConfirmActions>\n </Dialog>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;AA2BA,MAAM,aAAa;AACnB,MAAM,iBAAiB,UAAU,YAAY,WAAW,KAAK;AAC7D,MAAM,iBAAiB,UAAU,YAAY,WAAW,aAAa;AAkBrE,SAAwB,cAAc,OAAO;AAEzC,QAAM;AAAA,IACN;AAAA,IAAM;AAAA,IAAO;AAAA,IAAa;AAAA,IAAc;AAAA,IAAa,OAAO;AAAA,IAC5D,YAAY;AAAA,IAAO,oBAAoB;AAAA,IAAO,eAAe;AAAA,IAAM;AAAA,IAAW;AAAA,IAAS;AAAA,IAAU;AAAA,EAAA,IACjG,cAAc,OAAO,UAAU;AAC/B,QAAM,EAAC,EAAA,IAAK,sBAAsB,UAAU,KAAK;AAEjD,QAAM,WAAW,MAAA;AAEjB,QAAM,cAAc,MAAM;AAEtB,QAAI,WAAW;AACX;AAAA,IACJ;AACA,YAAA;AAAA,EACJ;AAEA,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAO;AAAA,MAAY,SAAS;AAAA,MAAa,UAAS;AAAA,MAAK,WAAS;AAAA,MACzD,mBAAiB,GAAG,QAAQ;AAAA,MAAU,oBAAkB,GAAG,QAAQ;AAAA,MACvE,UAAA;AAAA,QAAA,oBAAC,aAAA,EAAY,IAAI,GAAG,QAAQ,UAAW,UAAA,OAAM;AAAA,6BAC5C,eAAA,EACI,UAAA;AAAA,UAAA,gBAAgB,oBAAC,gBAAA,EAAe,UAAS,SAAQ,MAAK,SAAQ,OAAO,EAAC,IAAI,EAAA,GAAK,UAAA,aAAA,CAAa;AAAA,UAC5F,eACG,oBAAC,mBAAA,EAAkB,IAAI,GAAG,QAAQ,gBAAiB,UAAA,aAAY;AAAA,UAElE;AAAA,QAAA,GACL;AAAA,QACA,qBAAC,kBAAe,OAAO,EAAC,IAAI,GAAG,IAAI,OAC/B,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAO,SAAQ,YAAW,OAAM,WAAU,SAAS,aAAa,UAAU,WACtE,UAAA,eAAe,EAAE,gBAAgB,GACtC;AAAA,UACA;AAAA,YAAC;AAAA,YAAA;AAAA,cAAO,SAAQ;AAAA,cAAY,OAAO;AAAA,cAAM,SAAS;AAAA,cAAW,UAAU,aAAa;AAAA,cAC5E,aAAW;AAAA,cACd,UAAA,YAAY,oBAAC,kBAAA,EAAiB,MAAM,IAAI,OAAM,UAAA,CAAU,IAAM,gBAAgB,EAAE,iBAAiB;AAAA,YAAA;AAAA,UAAA;AAAA,QACtG,EAAA,CACJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGZ;"}
|
|
@@ -7,13 +7,20 @@ import DialogContent from "@mui/material/DialogContent";
|
|
|
7
7
|
import DialogContentText from "@mui/material/DialogContentText";
|
|
8
8
|
import DialogTitle from "@mui/material/DialogTitle";
|
|
9
9
|
import Stack from "@mui/material/Stack";
|
|
10
|
-
|
|
10
|
+
import { piecePart, usePieceProps } from "../theme/themePieces.js";
|
|
11
|
+
const THEME_NAME = "CalcitaModalWaiting";
|
|
12
|
+
const WaitingRoot = piecePart(THEME_NAME, "Root", Dialog);
|
|
13
|
+
const WaitingTitle = piecePart(THEME_NAME, "Title", DialogTitle);
|
|
14
|
+
const WaitingBody = piecePart(THEME_NAME, "Body", Stack);
|
|
15
|
+
const WaitingText = piecePart(THEME_NAME, "Text", DialogContentText);
|
|
16
|
+
function ModalWaiting(props) {
|
|
17
|
+
const { open, title, text, texts } = usePieceProps(props, THEME_NAME);
|
|
11
18
|
const { t } = useToolkitTranslation("common", texts);
|
|
12
|
-
return /* @__PURE__ */ jsxs(
|
|
13
|
-
/* @__PURE__ */ jsx(
|
|
14
|
-
/* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs(
|
|
19
|
+
return /* @__PURE__ */ jsxs(WaitingRoot, { open, disableEscapeKeyDown: true, "aria-busy": "true", ownSx: { zIndex: (theme) => theme.zIndex.tooltip + 10 }, children: [
|
|
20
|
+
/* @__PURE__ */ jsx(WaitingTitle, { ownSx: { textAlign: "center" }, children: title ?? t("waiting.title") }),
|
|
21
|
+
/* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs(WaitingBody, { spacing: 2, ownSx: { alignItems: "center" }, children: [
|
|
15
22
|
/* @__PURE__ */ jsx(CircularProgress, { size: 28 }),
|
|
16
|
-
/* @__PURE__ */ jsx(
|
|
23
|
+
/* @__PURE__ */ jsx(WaitingText, { ownSx: { textAlign: "center" }, children: text ?? t("waiting.text") })
|
|
17
24
|
] }) })
|
|
18
25
|
] });
|
|
19
26
|
}
|