@micha.bigler/ui-core-micha 2.33.2 → 2.35.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/WORK_ORDERS.md +9 -2
- package/dist/auth/ReauthModal.js +3 -1
- package/dist/components/AccessCodeManager.js +16 -14
- package/dist/components/AccessCodeSingleUseToggle.js +2 -1
- package/dist/components/AllowedEmailDomainsManager.js +3 -1
- package/dist/components/AuthFactorRequirementCard.js +2 -1
- package/dist/components/BulkInviteCsvTab.js +7 -7
- package/dist/components/LoginForm.js +2 -1
- package/dist/components/MFAComponent.js +15 -8
- package/dist/components/MfaLoginComponent.js +4 -1
- package/dist/components/PasskeysComponent.js +4 -2
- package/dist/components/QrSignupManager.js +2 -1
- package/dist/components/QrSignupValidityManager.js +4 -1
- package/dist/components/RegistrationMethodsManager.js +3 -1
- package/dist/components/SecurityComponent.js +3 -1
- package/dist/components/SocialLoginButtons.js +2 -1
- package/dist/components/SupportRecoveryRequestsTab.js +4 -1
- package/dist/components/UserInviteComponent.js +8 -8
- package/dist/components/UserListComponent.js +8 -4
- package/dist/components/charts/ChartFrame.js +8 -3
- package/dist/components/charts/TimeSeriesChart.js +7 -4
- package/dist/components/charts/exportChart.js +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/PageLayout.js +3 -2
- package/dist/messaging/AttachmentList.js +6 -1
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/DirectMessageLauncher.js +2 -1
- package/dist/messaging/MessageBubble.js +29 -8
- package/dist/messaging/PollCard.js +6 -4
- package/dist/messaging/Thread.js +2 -1
- package/dist/notifications/NotificationSettings.js +17 -3
- package/dist/notifications/PopupSurface.js +2 -1
- package/dist/onboarding/steps/BrowserPushStep.js +2 -1
- package/dist/pages/AccountPage.js +2 -1
- package/dist/pages/LoginPage.js +2 -1
- package/dist/pages/SignUpPage.js +2 -1
- package/dist/pages/SignupConfirmPage.js +2 -1
- package/dist/theme/index.js +2 -1
- package/dist/theme/kitSxRegistry.js +187 -0
- package/dist/theme/themeCompleteness.js +422 -1
- package/package.json +1 -1
- package/src/auth/ReauthModal.jsx +5 -2
- package/src/components/AccessCodeManager.jsx +22 -20
- package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
- package/src/components/AllowedEmailDomainsManager.jsx +6 -3
- package/src/components/AuthFactorRequirementCard.jsx +4 -2
- package/src/components/BulkInviteCsvTab.jsx +11 -10
- package/src/components/LoginForm.jsx +5 -3
- package/src/components/MFAComponent.jsx +20 -12
- package/src/components/MfaLoginComponent.jsx +9 -5
- package/src/components/PasskeysComponent.jsx +7 -4
- package/src/components/QrSignupManager.jsx +6 -4
- package/src/components/QrSignupValidityManager.jsx +8 -4
- package/src/components/RegistrationMethodsManager.jsx +7 -4
- package/src/components/SecurityComponent.jsx +10 -7
- package/src/components/SocialLoginButtons.jsx +2 -1
- package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
- package/src/components/UserInviteComponent.jsx +10 -10
- package/src/components/UserListComponent.jsx +12 -7
- package/src/components/charts/ChartFrame.jsx +13 -3
- package/src/components/charts/TimeSeriesChart.jsx +8 -4
- package/src/components/charts/exportChart.js +2 -2
- package/src/index.js +4 -0
- package/src/layout/PageLayout.jsx +4 -2
- package/src/messaging/AttachmentList.jsx +9 -3
- package/src/messaging/Composer.jsx +3 -1
- package/src/messaging/DirectMessageLauncher.jsx +3 -1
- package/src/messaging/MessageBubble.jsx +31 -9
- package/src/messaging/PollCard.jsx +8 -6
- package/src/messaging/Thread.jsx +3 -1
- package/src/notifications/NotificationSettings.jsx +24 -9
- package/src/notifications/PopupSurface.jsx +3 -1
- package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
- package/src/pages/AccountPage.jsx +11 -9
- package/src/pages/LoginPage.jsx +6 -4
- package/src/pages/SignUpPage.jsx +4 -2
- package/src/pages/SignupConfirmPage.jsx +4 -2
- package/src/theme/index.js +4 -0
- package/src/theme/kitSxRegistry.js +246 -0
- package/src/theme/themeCompleteness.js +451 -1
- package/tests/ChartFrame.test.jsx +5 -0
- package/tests/themeCompleteness.test.js +351 -0
- package/work-orders/THEME-4.md +86 -0
- package/work-orders/THEME-5.md +341 -0
|
@@ -6,6 +6,9 @@ import CloseIcon from '@mui/icons-material/Close';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { AuthContext } from '../auth/AuthContext';
|
|
8
8
|
import { approveRecoveryRequest, fetchRecoveryRequests, fetchUsersList, rejectRecoveryRequest, updateUserSupportStatus, } from '../auth/authApi';
|
|
9
|
+
export const SUPPORT_RECOVERY_AGENT_PAPER_SX = { p: 2, mb: 3 };
|
|
10
|
+
export const SUPPORT_RECOVERY_ALERT_SX = { mb: 2 };
|
|
11
|
+
export const SUPPORT_RECOVERY_AGENT_BUTTON_SX = { minWidth: 160 };
|
|
9
12
|
export function SupportRecoveryRequestsTab() {
|
|
10
13
|
const { t, i18n } = useTranslation();
|
|
11
14
|
const { user } = useContext(AuthContext);
|
|
@@ -160,7 +163,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
160
163
|
if (loading) {
|
|
161
164
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 4 }, children: _jsx(CircularProgress, {}) }));
|
|
162
165
|
}
|
|
163
|
-
return (_jsxs(Box, { children: [canManageAgents && (_jsxs(Paper, { variant: "outlined", sx:
|
|
166
|
+
return (_jsxs(Box, { children: [canManageAgents && (_jsxs(Paper, { variant: "outlined", sx: SUPPORT_RECOVERY_AGENT_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.AGENTS_TITLE', 'Support agents') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.AGENTS_DESCRIPTION', 'Assign which users are allowed to handle support requests.') }), agentErrorKey && (_jsx(Alert, { severity: "error", sx: SUPPORT_RECOVERY_ALERT_SX, children: t(agentErrorKey) })), _jsxs(Stack, { direction: { xs: 'column', md: 'row' }, spacing: 2, sx: { mb: 2, alignItems: { md: 'flex-start' } }, children: [_jsx(Autocomplete, { fullWidth: true, options: availableAgentCandidates, value: selectedAgentCandidate, loading: agentLoading, onChange: (_event, value) => setSelectedAgentCandidate(value), isOptionEqualToValue: (option, value) => option.id === value.id, getOptionLabel: getUserOptionLabel, renderInput: (params) => (_jsx(TextField, Object.assign({}, params, { label: t('Support.AGENTS_SELECT_LABEL', 'Select user'), placeholder: t('Support.AGENTS_SELECT_PLACEHOLDER', 'Choose a user') }))) }), _jsx(Button, { variant: "contained", onClick: handleAddAgent, disabled: !selectedAgentCandidate || agentLoading || Boolean(agentActionUserId), sx: SUPPORT_RECOVERY_AGENT_BUTTON_SX, children: t('Support.AGENTS_ADD', 'Add as agent') })] }), agentLoading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 3 }, children: _jsx(CircularProgress, { size: 24 }) })) : supportAgents.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.AGENTS_EMPTY', 'No support agents assigned yet.') })) : (_jsx(TableContainer, { component: Paper, variant: "outlined", children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Profile.NAME_LABEL', 'Name') }), _jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { align: "right", children: t('Common.ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: supportAgents.map((agentUser) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: getUserDisplayName(agentUser) || '-' }), _jsx(TableCell, { children: agentUser.email || '-' }), _jsx(TableCell, { align: "right", children: _jsx(IconButton, { size: "small", color: "error", onClick: () => handleRemoveAgent(agentUser), disabled: agentActionUserId === agentUser.id, "aria-label": t('Support.AGENTS_REMOVE', 'Remove support agent'), children: _jsx(CloseIcon, { fontSize: "small" }) }) })] }, agentUser.id))) })] }) }))] })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_REQUESTS_DESCRIPTION', 'Users who cannot complete MFA can request support. You can review their request and approve or reject it.') }), errorKey && (_jsx(Alert, { severity: "error", sx: SUPPORT_RECOVERY_ALERT_SX, children: t(errorKey) })), _jsxs(Stack, { direction: "row", spacing: 2, sx: { mb: 2 }, children: [_jsx(Button, { variant: statusFilter === 'pending' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('pending'), children: t('Support.RECOVERY_FILTER_PENDING', 'Open') }), _jsx(Button, { variant: statusFilter === 'approved' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('approved'), children: t('Support.RECOVERY_FILTER_APPROVED', 'Approved') }), _jsx(Button, { variant: statusFilter === 'rejected' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('rejected'), children: t('Support.RECOVERY_FILTER_REJECTED', 'Rejected') })] }), requests.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.RECOVERY_REQUESTS_EMPTY', 'No recovery requests for this filter.') })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Support.RECOVERY_COL_CREATED', 'Created') }), _jsx(TableCell, { children: t('Support.RECOVERY_USER', 'User') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_STATUS', 'Status') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: requests.map((req) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: req.created_at
|
|
164
167
|
? new Date(req.created_at).toLocaleString(i18n === null || i18n === void 0 ? void 0 : i18n.language)
|
|
165
168
|
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: t(`Support.RECOVERY_STATUS_${req.status}`, req.status) }), _jsx(TableCell, { children: _jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REVIEW', 'Review') }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request') }), _jsxs(DialogContent, { dividers: true, children: [selectedRequest && (_jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_USER', 'User'), ":"] }), ' ', selectedRequest.user_email || selectedRequest.user] }), _jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_CREATED', 'Requested at'), ":"] }), ' ', selectedRequest.created_at
|
|
166
169
|
? new Date(selectedRequest.created_at).toLocaleString(i18n === null || i18n === void 0 ? void 0 : i18n.language)
|
|
@@ -3,18 +3,18 @@ import React, { useState } from 'react';
|
|
|
3
3
|
import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
|
4
4
|
import { sendAdminInvite } from '../auth/authApi';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
export const USER_INVITE_ACTION_SX = {
|
|
7
|
+
minWidth: 120,
|
|
8
|
+
height: 40,
|
|
9
|
+
whiteSpace: 'nowrap',
|
|
10
|
+
};
|
|
11
|
+
export const USER_INVITE_ALERT_SX = { mb: 2 };
|
|
6
12
|
export function UserInviteComponent() {
|
|
7
13
|
const { t } = useTranslation();
|
|
8
14
|
const [inviteEmail, setInviteEmail] = useState('');
|
|
9
15
|
const [message, setMessage] = useState('');
|
|
10
16
|
const [error, setError] = useState('');
|
|
11
17
|
const [loading, setLoading] = useState(false);
|
|
12
|
-
const actionButtonSx = {
|
|
13
|
-
minWidth: 120,
|
|
14
|
-
height: 40,
|
|
15
|
-
textTransform: 'none',
|
|
16
|
-
whiteSpace: 'nowrap',
|
|
17
|
-
};
|
|
18
18
|
const inviteUser = async () => {
|
|
19
19
|
setMessage('');
|
|
20
20
|
setError('');
|
|
@@ -36,8 +36,8 @@ export function UserInviteComponent() {
|
|
|
36
36
|
setLoading(false);
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
return (_jsxs(Box, { sx: { maxWidth: 600 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.INVITE_TITLE', 'Invite a new user') }), message && _jsx(Alert, { severity: "success", sx:
|
|
39
|
+
return (_jsxs(Box, { sx: { maxWidth: 600 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.INVITE_TITLE', 'Invite a new user') }), message && _jsx(Alert, { severity: "success", sx: USER_INVITE_ALERT_SX, children: message }), error && _jsx(Alert, { severity: "error", sx: USER_INVITE_ALERT_SX, children: error }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL', 'Email address'), type: "email", variant: "outlined", fullWidth: true, size: "small", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), disabled: loading, onKeyPress: (e) => {
|
|
40
40
|
if (e.key === 'Enter')
|
|
41
41
|
inviteUser();
|
|
42
|
-
} }), _jsx(Button, { variant: "contained", size: "small", onClick: inviteUser, disabled: loading || !inviteEmail, sx:
|
|
42
|
+
} }), _jsx(Button, { variant: "contained", size: "small", onClick: inviteUser, disabled: loading || !inviteEmail, sx: USER_INVITE_ACTION_SX, children: loading ? _jsx(CircularProgress, { size: 24, color: "inherit" }) : t('Auth.INVITE_BUTTON', 'Invite') })] })] }));
|
|
43
43
|
}
|
|
@@ -7,6 +7,11 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
9
9
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
10
|
+
export const USER_LIST_ACTION_SX = { minWidth: 90 };
|
|
11
|
+
export const USER_LIST_ALERT_SX = { mb: 2 };
|
|
12
|
+
export const USER_LIST_HEADER_CELL_SX = { fontWeight: 600, whiteSpace: 'nowrap' };
|
|
13
|
+
export const USER_LIST_EMPTY_CELL_SX = { py: 4, color: 'text.secondary' };
|
|
14
|
+
export const USER_LIST_BODY_CELL_SX = { verticalAlign: 'top', py: 1 };
|
|
10
15
|
export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, showNewColumn = true, showSuccessfulLoginColumn = true, showRoleColumn = true, onChangeRole = null, showDeleteAction = true, canDeleteUser = null, onDeleteUser = null, }) {
|
|
11
16
|
const { t } = useTranslation();
|
|
12
17
|
const [users, setUsers] = useState([]);
|
|
@@ -19,7 +24,6 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
19
24
|
const [page, setPage] = useState(0);
|
|
20
25
|
const [pageSize, setPageSize] = useState(25);
|
|
21
26
|
const controlSx = { minWidth: 140 };
|
|
22
|
-
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
23
27
|
const loadUsers = useCallback(async () => {
|
|
24
28
|
setLoading(true);
|
|
25
29
|
setError(null);
|
|
@@ -324,10 +328,10 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
324
328
|
t,
|
|
325
329
|
})
|
|
326
330
|
: false;
|
|
327
|
-
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, row), disabled: isBusy || isDisabled, sx:
|
|
331
|
+
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, row), disabled: isBusy || isDisabled, sx: USER_LIST_ACTION_SX, children: typeof action.label === 'function'
|
|
328
332
|
? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
|
|
329
333
|
: action.label }, `${action.key}-${row.id}`));
|
|
330
|
-
}), showDeleteAction && (_jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(row.id), disabled: !canDelete(row), sx:
|
|
334
|
+
}), showDeleteAction && (_jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(row.id), disabled: !canDelete(row), sx: USER_LIST_ACTION_SX, children: t('Common.DELETE', 'Delete') }) }) }))] })),
|
|
331
335
|
};
|
|
332
336
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
333
337
|
}, [
|
|
@@ -368,7 +372,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
368
372
|
}
|
|
369
373
|
setPage(0);
|
|
370
374
|
};
|
|
371
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx:
|
|
375
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: USER_LIST_ALERT_SX, children: t(error) }), loading && (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })), !loading && (_jsxs(Box, { sx: { width: '100%' }, children: [_jsx(TableContainer, { sx: { border: 1, borderColor: 'divider', borderRadius: 1 }, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { sx: { bgcolor: 'action.hover' }, children: _jsx(TableRow, { children: columns.map((col) => (_jsx(TableCell, { align: col.headerAlign || col.align || 'left', sortDirection: sortField === col.field ? sortDir : false, sx: [USER_LIST_HEADER_CELL_SX, { minWidth: col.minWidth }], children: col.sortable !== false ? (_jsx(TableSortLabel, { active: sortField === col.field, direction: sortField === col.field ? sortDir : 'asc', onClick: () => handleSortClick(col.field), children: col.headerName })) : (col.headerName) }, col.field))) }) }), _jsx(TableBody, { children: pagedUsers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length, align: "center", sx: USER_LIST_EMPTY_CELL_SX, children: t('UserList.NO_USERS', 'No users found.') }) })) : (pagedUsers.map((row) => (_jsx(TableRow, { hover: true, children: columns.map((col) => (_jsx(TableCell, { align: col.align || 'left', sx: USER_LIST_BODY_CELL_SX, children: col.renderCell(row) }, col.field))) }, row.id)))) })] }) }), _jsx(TablePagination, { component: "div", count: sortedUsers.length, page: page, onPageChange: (_, newPage) => setPage(newPage), rowsPerPage: pageSize, onRowsPerPageChange: (event) => {
|
|
372
376
|
setPageSize(Number(event.target.value));
|
|
373
377
|
setPage(0);
|
|
374
378
|
}, rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));
|
|
@@ -2,12 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useId, useRef, useState } from 'react';
|
|
3
3
|
import { Alert, Box, Button, CircularProgress, Paper, Stack, Typography, } from '@mui/material';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useTheme } from '@mui/material/styles';
|
|
5
6
|
import { exportChartPng, exportChartSvg } from './exportChart';
|
|
7
|
+
export const CHART_FRAME_ROOT_SX = { p: 2 };
|
|
8
|
+
export const CHART_FRAME_ALERT_SX = { mt: 2 };
|
|
6
9
|
/**
|
|
7
10
|
* Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
|
|
8
11
|
*/
|
|
9
12
|
export function ChartFrame({ title, subtitle, toolbar, controls, loading = false, error = false, isEmpty = false, emptyMessage, minHeight, aspect, exportOptions = false, onExportSvg, onExportPng, meta, ariaLabel, children, variant = 'outlined', titleVariant = 'h6', sx, }) {
|
|
10
13
|
const { t } = useTranslation();
|
|
14
|
+
const theme = useTheme();
|
|
11
15
|
const titleId = useId();
|
|
12
16
|
const chartRef = useRef(null);
|
|
13
17
|
const [exportError, setExportError] = useState(false);
|
|
@@ -16,13 +20,14 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
|
|
|
16
20
|
const hasMeta = meta !== undefined && meta !== null && meta !== false && meta !== '';
|
|
17
21
|
const hasCustomAriaLabel = Boolean(ariaLabel);
|
|
18
22
|
const chartLabel = ariaLabel || title;
|
|
23
|
+
const callerSx = Array.isArray(sx) ? sx : [sx];
|
|
19
24
|
const runExport = async (type) => {
|
|
20
25
|
setExportError(false);
|
|
21
26
|
try {
|
|
22
27
|
const filename = options.filename || 'chart';
|
|
23
28
|
const result = type === 'svg'
|
|
24
29
|
? exportChartSvg(chartRef.current, `${filename}.svg`)
|
|
25
|
-
: await exportChartPng(chartRef.current, `${filename}.png
|
|
30
|
+
: await exportChartPng(chartRef.current, `${filename}.png`, theme.palette.background.paper);
|
|
26
31
|
const callback = type === 'svg' ? onExportSvg : onExportPng;
|
|
27
32
|
callback === null || callback === void 0 ? void 0 : callback(result);
|
|
28
33
|
}
|
|
@@ -40,14 +45,14 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
|
|
|
40
45
|
else if (isEmpty) {
|
|
41
46
|
content = _jsx(Alert, { severity: "info", children: emptyMessage || t('ChartFrame.EMPTY_DEFAULT') });
|
|
42
47
|
}
|
|
43
|
-
return (_jsxs(Paper, { variant: variant, sx:
|
|
48
|
+
return (_jsxs(Paper, { variant: variant, sx: [CHART_FRAME_ROOT_SX, ...callerSx], children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 2 }, children: [_jsxs(Box, { children: [_jsx(Typography, { id: titleId, variant: titleVariant, children: title }), subtitle && _jsx(Typography, { variant: "body2", color: "text.secondary", children: subtitle })] }), toolbar && _jsx(Box, { children: toolbar })] }), controls && _jsx(Box, { sx: { mb: 2 }, children: controls }), _jsx(Box, { ref: chartRef, role: "img", "aria-label": chartLabel, "aria-labelledby": hasCustomAriaLabel ? undefined : titleId, sx: {
|
|
44
49
|
width: '100%',
|
|
45
50
|
minHeight,
|
|
46
51
|
aspectRatio: aspect,
|
|
47
52
|
display: 'flex',
|
|
48
53
|
alignItems: 'center',
|
|
49
54
|
justifyContent: 'center',
|
|
50
|
-
}, children: content }), exportError && _jsx(Alert, { severity: "error", sx:
|
|
55
|
+
}, children: content }), exportError && _jsx(Alert, { severity: "error", sx: CHART_FRAME_ALERT_SX, children: t('ChartFrame.EXPORT_ERROR') }), hasMeta ? (_jsxs(Box, { sx: {
|
|
51
56
|
display: 'flex',
|
|
52
57
|
alignItems: 'center',
|
|
53
58
|
justifyContent: 'space-between',
|
|
@@ -24,6 +24,12 @@ import { useNeutralChartPalette } from './palette';
|
|
|
24
24
|
// to a responsive/aspect-ratio height without re-verifying the underlying
|
|
25
25
|
// MUI measurement bug is actually resolved.
|
|
26
26
|
const CHART_HEIGHT = 320;
|
|
27
|
+
// Dynamic sx factories are outside the registry's documented top-level-object
|
|
28
|
+
// lower bound; the series colour is data-dependent at render time.
|
|
29
|
+
const seriesToggleSx = (colour) => ({
|
|
30
|
+
color: colour,
|
|
31
|
+
'&.Mui-checked': { color: colour },
|
|
32
|
+
});
|
|
27
33
|
// CHART-5: how many x-axis tick labels to show, evenly spread across
|
|
28
34
|
// data.xLabels, regardless of container width or label length. Replaces
|
|
29
35
|
// MUI's own collision-based tick filtering (which, given enough densely
|
|
@@ -162,10 +168,7 @@ export function TimeSeriesChart({ title, subtitle, xAxisLabel, yAxisLabel, secon
|
|
|
162
168
|
Object.assign({ id: 'secondary', label: secondaryYAxisLabel, position: 'right' }, (isIntegerAxis(secondaryVisibleSeries) ? { valueFormatter: integerTickFormatter } : {})),
|
|
163
169
|
]
|
|
164
170
|
: (isIntegerAxis(primaryVisibleSeries) ? [{ valueFormatter: integerTickFormatter }] : undefined);
|
|
165
|
-
const toolbar = (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }, children: [_jsx(RadioGroup, { row: true, value: range, onChange: handleRangeChange, "aria-label": t('TimeSeriesChart.RANGE_LABEL'), children: RANGE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.key, control: _jsx(Radio, { size: "small" }), label: t(option.labelKey) }, option.key))) }), seriesConfig.length > 0 && (_jsx(FormGroup, { row: true, "aria-label": t('TimeSeriesChart.SERIES_LABEL'), children: seriesConfig.map((series, index) => (_jsx(FormControlLabel, { control: (_jsx(Checkbox, { size: "small", checked: visibleKeys.has(series.key), onChange: () => toggleSeries(series.key), icon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: false }), checkedIcon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: true }), sx:
|
|
166
|
-
color: palette[index % palette.length],
|
|
167
|
-
'&.Mui-checked': { color: palette[index % palette.length] },
|
|
168
|
-
} })), label: series.label }, series.key))) }))] }));
|
|
171
|
+
const toolbar = (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }, children: [_jsx(RadioGroup, { row: true, value: range, onChange: handleRangeChange, "aria-label": t('TimeSeriesChart.RANGE_LABEL'), children: RANGE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.key, control: _jsx(Radio, { size: "small" }), label: t(option.labelKey) }, option.key))) }), seriesConfig.length > 0 && (_jsx(FormGroup, { row: true, "aria-label": t('TimeSeriesChart.SERIES_LABEL'), children: seriesConfig.map((series, index) => (_jsx(FormControlLabel, { control: (_jsx(Checkbox, { size: "small", checked: visibleKeys.has(series.key), onChange: () => toggleSeries(series.key), icon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: false }), checkedIcon: _jsx(SeriesToggleDot, { color: palette[index % palette.length], active: true }), sx: seriesToggleSx(palette[index % palette.length]) })), label: series.label }, series.key))) }))] }));
|
|
169
172
|
return (_jsx(ChartFrame, { title: title, subtitle: subtitle, toolbar: toolbar, loading: loading, error: error, isEmpty: isDataEmpty, minHeight: CHART_HEIGHT, children: !isDataEmpty && (_jsx(BarChart, { xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xAxis: [{
|
|
170
173
|
data: (data === null || data === void 0 ? void 0 : data.xLabels) || [],
|
|
171
174
|
tickLabelInterval: makeTickLabelInterval(((data === null || data === void 0 ? void 0 : data.xLabels) || []).length),
|
|
@@ -26,7 +26,7 @@ export function exportChartSvg(chartContainer, filename = 'chart.svg') {
|
|
|
26
26
|
return blob;
|
|
27
27
|
}
|
|
28
28
|
/** Rasterizes the first SVG rendered inside a chart container at two-times scale and downloads it. */
|
|
29
|
-
export function exportChartPng(chartContainer, filename = 'chart.png') {
|
|
29
|
+
export function exportChartPng(chartContainer, filename = 'chart.png', backgroundColour) {
|
|
30
30
|
const source = svgBlob(chartContainer);
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
32
|
const url = URL.createObjectURL(source);
|
|
@@ -40,7 +40,7 @@ export function exportChartPng(chartContainer, filename = 'chart.png') {
|
|
|
40
40
|
canvas.width = width * 2;
|
|
41
41
|
canvas.height = height * 2;
|
|
42
42
|
const context = canvas.getContext('2d');
|
|
43
|
-
context.fillStyle =
|
|
43
|
+
context.fillStyle = backgroundColour;
|
|
44
44
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
45
45
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
46
46
|
canvas.toBlob((blob) => {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// index.js (Entry Point deiner Library)
|
|
2
2
|
// --- 0. Theme ---
|
|
3
|
-
export { createAppTheme, assertThemeComplete, calculateContrastRatio, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
|
|
3
|
+
export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
|
|
4
4
|
// --- 1. Auth Context (Essentiell für den Wrapper) ---
|
|
5
5
|
export { AuthContext, AuthProvider } from './auth/AuthContext';
|
|
6
6
|
export { UserMenu } from './auth/UserMenu';
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
// src/components/layout/PageLayout.jsx
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Container, Box, Typography } from '@mui/material';
|
|
5
|
+
export const PAGE_LAYOUT_CONTAINER_SX = { mt: 4 };
|
|
5
6
|
// Layout for content pages with larger width (e.g. Profile, Welcome, Input)
|
|
6
|
-
export const WidePage = ({ title, children, maxWidth = 'lg' }) => (_jsxs(Container, { maxWidth: maxWidth, sx:
|
|
7
|
+
export const WidePage = ({ title, children, maxWidth = 'lg' }) => (_jsxs(Container, { maxWidth: maxWidth, sx: PAGE_LAYOUT_CONTAINER_SX, children: [title && (_jsx(Typography, { variant: "h4", gutterBottom: true, children: title })), children] }));
|
|
7
8
|
// Layout for forms or narrow pages (e.g. Login, Reset, Invite)
|
|
8
|
-
export const NarrowPage = ({ title, subtitle, children }) => (_jsx(Container, { maxWidth: "md", sx:
|
|
9
|
+
export const NarrowPage = ({ title, subtitle, children }) => (_jsx(Container, { maxWidth: "md", sx: PAGE_LAYOUT_CONTAINER_SX, children: _jsxs(Box, { sx: { maxWidth: 480, mx: 'auto' }, children: [title && (_jsx(Typography, { variant: "h4", gutterBottom: true, children: title })), subtitle && (_jsx(Typography, { paragraph: true, children: subtitle })), children] }) }));
|
|
@@ -6,6 +6,11 @@ import { Alert, Box, ButtonBase, CircularProgress, Dialog, IconButton, Menu, Men
|
|
|
6
6
|
import { useEffect, useRef, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { useMessaging } from './MessagingProvider';
|
|
9
|
+
export const ATTACHMENT_LIST_LIGHTBOX_ACTION_SX = {
|
|
10
|
+
bgcolor: 'background.paper',
|
|
11
|
+
boxShadow: 1,
|
|
12
|
+
};
|
|
13
|
+
export const ATTACHMENT_LIST_ALERT_SX = { m: 2 };
|
|
9
14
|
function isImage(attachment) { var _a; return (_a = attachment.content_type) === null || _a === void 0 ? void 0 : _a.startsWith('image/'); }
|
|
10
15
|
// `filename` is dcm's real, sanitized upload name (MSG-12) -- `id` is the
|
|
11
16
|
// last-resort fallback for interop with a host still on a pre-MSG-12 pin.
|
|
@@ -98,6 +103,6 @@ export function AttachmentList({ attachments = [] }) {
|
|
|
98
103
|
return (_jsx(ButtonBase, { type: "button", title: nameOf(attachment), onClick: () => (image ? openLightbox(attachment) : download(attachment)), onContextMenu: (event) => openMenu(event, attachment), "aria-label": label, sx: { width: 120, height: 120, borderRadius: 1, overflow: 'hidden', border: '1px solid', borderColor: 'divider', display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'action.hover' }, children: image && preview
|
|
99
104
|
? _jsx(Box, { component: "img", src: preview, alt: "", sx: { width: '100%', height: '100%', objectFit: 'cover' } })
|
|
100
105
|
: _jsxs(Stack, { alignItems: "center", spacing: 0.75, sx: { px: 1, maxWidth: '100%' }, children: [_jsx(InsertDriveFileOutlinedIcon, { fontSize: "large" }), _jsx(Typography, { variant: "caption", noWrap: true, sx: { maxWidth: '100%' }, children: nameOf(attachment) })] }) }, attachment.id));
|
|
101
|
-
}) }), _jsx(Dialog, { open: Boolean(lightbox), onClose: () => setLightbox(null), maxWidth: "lg", "aria-label": lightbox ? nameOf(lightbox.attachment) : undefined, children: lightbox && _jsxs(Box, { sx: { position: 'relative', minWidth: 240, minHeight: lightbox.status === 'ready' ? undefined : 160, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: [_jsxs(Stack, { direction: "row", spacing: 0.5, sx: { position: 'absolute', top: 4, right: 4 }, children: [lightbox.status === 'ready' && _jsx(IconButton, { "aria-label": t('MessagingAttachments.DOWNLOAD_ACTION'), onClick: () => download(lightbox.attachment), sx:
|
|
106
|
+
}) }), _jsx(Dialog, { open: Boolean(lightbox), onClose: () => setLightbox(null), maxWidth: "lg", "aria-label": lightbox ? nameOf(lightbox.attachment) : undefined, children: lightbox && _jsxs(Box, { sx: { position: 'relative', minWidth: 240, minHeight: lightbox.status === 'ready' ? undefined : 160, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: [_jsxs(Stack, { direction: "row", spacing: 0.5, sx: { position: 'absolute', top: 4, right: 4 }, children: [lightbox.status === 'ready' && _jsx(IconButton, { "aria-label": t('MessagingAttachments.DOWNLOAD_ACTION'), onClick: () => download(lightbox.attachment), sx: ATTACHMENT_LIST_LIGHTBOX_ACTION_SX, children: _jsx(DownloadIcon, {}) }), _jsx(IconButton, { "aria-label": t('MessagingAttachments.CLOSE_PREVIEW'), onClick: () => setLightbox(null), sx: ATTACHMENT_LIST_LIGHTBOX_ACTION_SX, children: _jsx(CloseIcon, {}) })] }), lightbox.status === 'loading' && _jsx(CircularProgress, { "aria-label": t('MessagingAttachments.LOADING_PREVIEW'), sx: { m: 4 } }), lightbox.status === 'error' && _jsx(Alert, { severity: "error", role: "alert", sx: ATTACHMENT_LIST_ALERT_SX, children: t('MessagingAttachments.PREVIEW_ERROR') }), lightbox.status === 'ready' && _jsx(Box, { component: "img", src: lightbox.url, alt: nameOf(lightbox.attachment), sx: { display: 'block', maxWidth: '90vw', maxHeight: '90vh' } })] }) }), _jsx(Menu, { open: Boolean(menu), onClose: closeMenu, anchorReference: "anchorPosition", anchorPosition: menu === null || menu === void 0 ? void 0 : menu.anchorPosition, children: _jsxs(MenuItem, { onClick: () => { const attachment = menu === null || menu === void 0 ? void 0 : menu.attachment; closeMenu(); if (attachment)
|
|
102
107
|
download(attachment); }, children: [_jsx(DownloadIcon, { fontSize: "small", sx: { mr: 1 } }), t('MessagingAttachments.DOWNLOAD_ACTION')] }) })] });
|
|
103
108
|
}
|
|
@@ -9,6 +9,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { compressImageForUpload } from './composerImageCompression';
|
|
11
11
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
12
|
+
export const COMPOSER_EMOJI_BUTTON_SX = { fontSize: '1.15rem' };
|
|
12
13
|
// A wider, Composer-only set (MSG-6i) -- deliberately separate from
|
|
13
14
|
// ReactionBar's own QUICK_EMOJIS (5, curated for fast reactions). Mobile
|
|
14
15
|
// users already have a full native emoji keyboard for free-form text; this
|
|
@@ -167,5 +168,5 @@ export function Composer({ conversationId, conversation, replyTarget = null, onR
|
|
|
167
168
|
return _jsxs(Stack, { component: "form", spacing: 1, onSubmit: (event) => { event.preventDefault(); submit(); }, "aria-label": t('MessagingComposer.LABEL'), children: [replyTarget && _jsx(Alert, { severity: "info", onClose: () => onReplyTargetChange === null || onReplyTargetChange === void 0 ? void 0 : onReplyTargetChange(null), children: t('MessagingComposer.REPLYING_TO', { sender: ((_a = replyTarget.sender) === null || _a === void 0 ? void 0 : _a.display_name) || t('MessagingThread.UNKNOWN_SENDER') }) }), error && _jsx(Alert, { severity: "error", role: "alert", children: error }), files.length > 0 && _jsx(Stack, { direction: "row", spacing: 0.5, flexWrap: "wrap", "aria-label": t('MessagingComposer.STAGED_FILES'), children: files.map((file, index) => _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.25, sx: { border: 1, borderColor: 'divider', borderRadius: 1, p: 0.25 }, children: [previews[index] && _jsx(Box, { component: "img", src: previews[index], alt: file.name, width: 36, height: 36, sx: { objectFit: 'cover' } }), _jsx(Typography, { variant: "caption", children: file.name }), _jsx(IconButton, { type: "button", size: "small", "aria-label": t('MessagingComposer.REMOVE_ATTACHMENT', { name: file.name }), onClick: () => removeFile(index), children: _jsx(CloseIcon, { fontSize: "inherit" }) })] }, `${file.name}-${index}`)) }), files.length > 0 && _jsx(Typography, { variant: "caption", children: t('MessagingComposer.FILES_SELECTED', { count: files.length }) }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "flex-end", children: [_jsx("input", { ref: inputRef, hidden: true, type: "file", multiple: true, onChange: (event) => setFiles(Array.from(event.target.files || [])) }), _jsx(IconButton, { type: "button", "aria-label": t('MessagingComposer.ADD_ATTACHMENT'), onClick: () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: _jsx(AttachFileIcon, {}) }), _jsx(IconButton, { type: "button", "aria-label": t('MessagingComposer.ADD_EMOJI'), onClick: (event) => setEmojiAnchor(event.currentTarget), children: _jsx(EmojiEmotionsOutlinedIcon, {}) }), _jsx(IconButton, { type: "button", "aria-label": t('MessagingPoll.CREATE'), disabled: pollOpen || files.some(isImage), onClick: () => setPollOpen(true), children: _jsx(PollOutlinedIcon, {}) }), canAnnounce && _jsx(IconButton, { type: "button", "aria-label": t('MessagingAnnouncement.CREATE'), onClick: () => setAnnouncementOpen(true), children: _jsx(CampaignOutlinedIcon, {}) }), _jsx(TextField, { inputRef: messageInputRef, fullWidth: true, multiline: true, minRows: 2, value: body, onChange: (event) => setBody(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
168
169
|
event.preventDefault();
|
|
169
170
|
submit();
|
|
170
|
-
} }, label: t('MessagingComposer.MESSAGE'), disabled: sending }), _jsx(Button, { type: "submit", variant: "contained", disabled: sending || (!body.trim() && !files.length), children: t('MessagingComposer.SEND') })] }), sending && files.length > 0 && _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [uploadProgress == null ? _jsx(CircularProgress, { size: 16 }) : _jsx(CircularProgress, { size: 16, variant: "determinate", value: uploadProgress }), _jsx(Typography, { variant: "caption", children: t('MessagingComposer.UPLOADING') })] }), uploadProgress != null && _jsx(LinearProgress, { variant: "determinate", value: uploadProgress })] }), failed && _jsx(Button, { type: "button", onClick: () => submit(failed.requestId, true), disabled: sending, children: t('MessagingComposer.RETRY') }), _jsx(Popover, { anchorEl: emojiAnchor, open: Boolean(emojiAnchor), onClose: () => setEmojiAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, children: _jsx(Box, { role: "group", "aria-label": t('MessagingComposer.EMOJI_PICKER'), sx: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 0.25, p: 0.5, maxWidth: 280 }, children: COMPOSER_EMOJIS.map((emoji) => (_jsx(IconButton, { type: "button", size: "small", onClick: () => insertEmoji(emoji), sx:
|
|
171
|
+
} }, label: t('MessagingComposer.MESSAGE'), disabled: sending }), _jsx(Button, { type: "submit", variant: "contained", disabled: sending || (!body.trim() && !files.length), children: t('MessagingComposer.SEND') })] }), sending && files.length > 0 && _jsxs(Stack, { spacing: 0.5, role: "status", children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [uploadProgress == null ? _jsx(CircularProgress, { size: 16 }) : _jsx(CircularProgress, { size: 16, variant: "determinate", value: uploadProgress }), _jsx(Typography, { variant: "caption", children: t('MessagingComposer.UPLOADING') })] }), uploadProgress != null && _jsx(LinearProgress, { variant: "determinate", value: uploadProgress })] }), failed && _jsx(Button, { type: "button", onClick: () => submit(failed.requestId, true), disabled: sending, children: t('MessagingComposer.RETRY') }), _jsx(Popover, { anchorEl: emojiAnchor, open: Boolean(emojiAnchor), onClose: () => setEmojiAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, children: _jsx(Box, { role: "group", "aria-label": t('MessagingComposer.EMOJI_PICKER'), sx: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 0.25, p: 0.5, maxWidth: 280 }, children: COMPOSER_EMOJIS.map((emoji) => (_jsx(IconButton, { type: "button", size: "small", onClick: () => insertEmoji(emoji), sx: COMPOSER_EMOJI_BUTTON_SX, children: emoji }, emoji))) }) }), _jsxs(Dialog, { open: pollOpen, onClose: () => !sending && setPollOpen(false), fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('MessagingPoll.CREATE') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 1, sx: { pt: 1 }, children: [_jsx(TextField, { label: t('MessagingPoll.QUESTION'), value: pollQuestion, onChange: (event) => setPollQuestion(event.target.value), autoFocus: true }), pollOptions.map((option, index) => _jsx(TextField, { label: t('MessagingPoll.OPTION', { count: index + 1 }), value: option, onChange: (event) => setPollOptions((current) => current.map((value, optionIndex) => optionIndex === index ? event.target.value : value)) }, index)), _jsx(Button, { type: "button", disabled: pollOptions.length >= 10, onClick: () => setPollOptions((current) => [...current, '']), children: t('MessagingPoll.ADD_OPTION') }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: allowMultiple, onChange: (event) => setAllowMultiple(event.target.checked) }), label: t('MessagingPoll.ALLOW_MULTIPLE') })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setPollOpen(false), children: t('MessagingPoll.CANCEL') }), _jsx(Button, { variant: "contained", onClick: submitPoll, disabled: sending, children: t('MessagingPoll.CREATE') })] })] }), canAnnounce && _jsxs(Dialog, { open: announcementOpen, onClose: () => !sending && setAnnouncementOpen(false), fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('MessagingAnnouncement.CREATE') }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 1, sx: { pt: 1 }, children: [_jsx(TextField, { label: t('MessagingAnnouncement.TITLE'), value: announcementTitle, onChange: (event) => setAnnouncementTitle(event.target.value), autoFocus: true }), _jsx(TextField, { label: t('MessagingAnnouncement.BODY'), value: announcementBody, onChange: (event) => setAnnouncementBody(event.target.value), multiline: true, minRows: 2 })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setAnnouncementOpen(false), children: t('MessagingAnnouncement.CANCEL') }), _jsx(Button, { variant: "contained", onClick: submitAnnouncement, disabled: sending, children: t('MessagingAnnouncement.SEND') })] })] })] });
|
|
171
172
|
}
|
|
@@ -4,6 +4,7 @@ import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent,
|
|
|
4
4
|
import { useTheme } from '@mui/material/styles';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
7
|
+
export const DIRECT_MESSAGE_LAUNCHER_ALERT_SX = { mb: 1 };
|
|
7
8
|
/**
|
|
8
9
|
* Starts direct conversations from host-provided people. Candidate discovery is
|
|
9
10
|
* intentionally outside messaging because memberships are host-domain data.
|
|
@@ -47,5 +48,5 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
47
48
|
setStarting(false);
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
|
-
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", onClick: showPicker, children: t('MessagingDirect.LAUNCH') }), _jsxs(Dialog, { open: open, onClose: closePicker, fullScreen: fullScreen, "aria-labelledby": titleId, children: [_jsx(DialogTitle, { id: titleId, children: t('MessagingDirect.TITLE') }), _jsxs(DialogContent, { children: [error && _jsx(Alert, { severity: "error", sx:
|
|
51
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", onClick: showPicker, children: t('MessagingDirect.LAUNCH') }), _jsxs(Dialog, { open: open, onClose: closePicker, fullScreen: fullScreen, "aria-labelledby": titleId, children: [_jsx(DialogTitle, { id: titleId, children: t('MessagingDirect.TITLE') }), _jsxs(DialogContent, { children: [error && _jsx(Alert, { severity: "error", sx: DIRECT_MESSAGE_LAUNCHER_ALERT_SX, children: error }), starting && _jsxs(Typography, { role: "status", sx: { mb: 1 }, children: [_jsx(CircularProgress, { size: 16, sx: { mr: 1 } }), t('MessagingDirect.STARTING')] }), candidates.length === 0 ? (_jsx(Typography, { children: t('MessagingDirect.EMPTY') })) : (_jsx(List, { "aria-label": t('MessagingDirect.CANDIDATES'), children: candidates.map((candidate) => (_jsx(ListItemButton, { selected: candidate.id === selectedId, disabled: starting, onClick: () => setSelectedId(candidate.id), children: _jsx(ListItemText, { primary: candidate.display_name || candidate.name || candidate.label || candidate.id }) }, candidate.id))) }))] }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: closePicker, disabled: starting, children: t('MessagingDirect.CANCEL') }), _jsx(Button, { variant: "contained", onClick: start, disabled: selectedId == null || starting, children: t('MessagingDirect.START') })] })] })] }));
|
|
51
52
|
}
|
|
@@ -8,6 +8,28 @@ import { AttachmentList } from './AttachmentList';
|
|
|
8
8
|
import { extractApiErrorMessage, useOptionalMessaging } from './MessagingProvider';
|
|
9
9
|
import { ReactionBar } from './ReactionBar';
|
|
10
10
|
import { PollCard } from './PollCard';
|
|
11
|
+
export const MESSAGE_BUBBLE_ROOT_SX = {
|
|
12
|
+
position: 'relative',
|
|
13
|
+
p: 1.25,
|
|
14
|
+
minWidth: 0,
|
|
15
|
+
overflowWrap: 'anywhere',
|
|
16
|
+
'& .message-actions': {
|
|
17
|
+
opacity: 0,
|
|
18
|
+
pointerEvents: 'none',
|
|
19
|
+
transition: 'opacity 120ms ease-in-out',
|
|
20
|
+
},
|
|
21
|
+
'&:hover .message-actions, &:focus-within .message-actions, &[data-actions-visible="true"] .message-actions': {
|
|
22
|
+
opacity: 1,
|
|
23
|
+
pointerEvents: 'auto',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const MESSAGE_BUBBLE_ACTION_SX = {
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
top: -12,
|
|
29
|
+
right: -12,
|
|
30
|
+
bgcolor: 'background.paper',
|
|
31
|
+
boxShadow: 1,
|
|
32
|
+
};
|
|
11
33
|
function senderName(message) {
|
|
12
34
|
var _a, _b, _c;
|
|
13
35
|
return ((_a = message.sender) === null || _a === void 0 ? void 0 : _a.display_name) || ((_b = message.sender) === null || _b === void 0 ? void 0 : _b.username) || message.sender_name || ((_c = message.sender) === null || _c === void 0 ? void 0 : _c.name);
|
|
@@ -127,12 +149,11 @@ export function MessageBubble({ message, replyTo, conversation, onReply, onJumpT
|
|
|
127
149
|
setError(t('MessagingActions.COPY_ERROR', { message: extractApiErrorMessage(copyError) }));
|
|
128
150
|
}
|
|
129
151
|
};
|
|
130
|
-
return (_jsxs(Box, { ref: bubbleRef, component: "article", "data-message-id": message.id, "data-message-side": isOwn ? 'own' : 'incoming', "aria-label": t('MessagingThread.MESSAGE'), sx: { alignSelf: isOwn ? 'flex-end' : 'flex-start', width: 'fit-content', maxWidth: 'min(75%, 680px)', minWidth: 0 }, children: [_jsxs(Paper, { variant: "outlined", "data-actions-visible": actionsVisible || undefined, onClick: revealActions, onContextMenu: canOpenMenu ? openMenu : undefined, sx:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}, children: [canOpenMenu && _jsx(IconButton, { className: "message-actions", size: "small", "aria-label": t('MessagingActions.MENU'), onClick: openMenu, sx: { position: 'absolute', top: -12, right: -12, bgcolor: 'background.paper', boxShadow: 1 }, children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsxs(Stack, { spacing: 0.75, children: [replyTo && _jsx(ButtonBase, { type: "button", onClick: () => onJumpToMessage === null || onJumpToMessage === void 0 ? void 0 : onJumpToMessage(replyTo.id), sx: { display: 'block', width: '100%', px: 0.75, py: 0.5, textAlign: 'left', borderLeft: 3, borderColor: 'primary.main', bgcolor: 'action.hover', borderRadius: 0.5 }, children: _jsxs(Stack, { spacing: 0, alignItems: "flex-start", children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: t('MessagingThread.REPLY_TO', { sender: senderName(replyTo) || t('MessagingThread.UNKNOWN_SENDER') }) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: quoteLabel(replyTo, t) })] }) }), (conversation === null || conversation === void 0 ? void 0 : conversation.kind) !== 'direct' && _jsx(Typography, { variant: "subtitle2", children: senderName(message) || t('MessagingThread.UNKNOWN_SENDER') }), !deleted && message.kind === 'announcement' && message.title && _jsx(Typography, { variant: "subtitle1", fontWeight: 700, children: message.title }), !deleted && message.kind === 'announcement' && message.link_target && onAnnouncementLink && _jsx(Button, { type: "button", size: "small", onClick: () => onAnnouncementLink(message.link_target), children: t('MessagingAnnouncement.OPEN_LINK') }), error && _jsx(Alert, { severity: "error", role: "alert", children: error }), editing ? _jsxs(Stack, { spacing: 0.75, children: [_jsx(TextField, { label: t('MessagingActions.EDIT'), value: draft, onChange: (event) => setDraft(event.target.value), multiline: true, minRows: 2, autoFocus: true, disabled: saving }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { type: "button", onClick: saveEdit, disabled: saving, children: t('MessagingActions.SAVE') }), _jsx(Button, { type: "button", onClick: () => setEditing(false), disabled: saving, children: t('MessagingActions.CANCEL') })] })] }) : _jsx(Typography, { sx: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: deleted ? t('MessagingThread.DELETED') : (message.kind === 'announcement' ? message.body : (message.body || message.title)) || '' }), !deleted && _jsxs(Stack, { spacing: 0.75, children: [attachments.length > 0 && _jsx(AttachmentList, { attachments: attachments }), message.poll && _jsx(PollCard, { message: message })] }), hasMeta && _jsxs(Stack, { "data-testid": "message-meta", direction: "row", spacing: 0.5, justifyContent: "flex-end", alignItems: "center", flexWrap: "wrap", sx: { color: 'text.disabled', minWidth: 0, '& .MuiSvgIcon-root': { fontSize: '1rem' } }, children: [message.created_at && _jsx(Typography, { component: "span", variant: "caption", color: "inherit", sx: { whiteSpace: 'nowrap' }, children: new Date(message.created_at).toLocaleTimeString(i18n === null || i18n === void 0 ? void 0 : i18n.language, { hour: '2-digit', minute: '2-digit' }) }), message.edited_at && !deleted && _jsx(Typography, { component: "span", variant: "caption", color: "inherit", children: t('MessagingThread.EDITED') }), children] })] })] }), !deleted && (reactions.length > 0 || reactionPickerVisible) && _jsx(ReactionBar, { message: message, expanded: reactionPickerVisible, onExpandedChange: setReactionPickerVisible, sx: { mt: 0.5, ml: 0.75 } }), _jsxs(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: closeMenu, children: [_jsx(MenuItem, { onClick: startReply, children: t('MessagingThread.REPLY') }), _jsx(MenuItem, { onClick: startReact, children: t('MessagingReactions.ADD') }), canAct && _jsx(MenuItem, { onClick: startEdit, children: t('MessagingActions.EDIT') }), canAct && _jsx(MenuItem, { onClick: () => { setConfirmingDelete(true); closeMenu(); }, children: t('MessagingActions.DELETE') }), _jsx(MenuItem, { onClick: copyMessage, children: t('MessagingActions.COPY') })] }), _jsxs(Dialog, { open: confirmingDelete, onClose: () => !saving && setConfirmingDelete(false), children: [_jsx(DialogTitle, { children: t('MessagingActions.DELETE_CONFIRM_TITLE') }), _jsx(DialogContent, { children: t('MessagingActions.DELETE_CONFIRM_BODY') }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setConfirmingDelete(false), disabled: saving, children: t('MessagingActions.CANCEL') }), _jsx(Button, { onClick: confirmDelete, disabled: saving, color: "error", children: t('MessagingActions.DELETE') })] })] })] }));
|
|
152
|
+
return (_jsxs(Box, { ref: bubbleRef, component: "article", "data-message-id": message.id, "data-message-side": isOwn ? 'own' : 'incoming', "aria-label": t('MessagingThread.MESSAGE'), sx: { alignSelf: isOwn ? 'flex-end' : 'flex-start', width: 'fit-content', maxWidth: 'min(75%, 680px)', minWidth: 0 }, children: [_jsxs(Paper, { variant: "outlined", "data-actions-visible": actionsVisible || undefined, onClick: revealActions, onContextMenu: canOpenMenu ? openMenu : undefined, sx: [
|
|
153
|
+
MESSAGE_BUBBLE_ROOT_SX,
|
|
154
|
+
{
|
|
155
|
+
bgcolor: isOwn ? (theme) => alpha(theme.palette.primary.light, 0.24) : 'background.paper',
|
|
156
|
+
borderColor: isOwn ? (theme) => alpha(theme.palette.primary.main, 0.35) : 'divider',
|
|
157
|
+
},
|
|
158
|
+
], children: [canOpenMenu && _jsx(IconButton, { className: "message-actions", size: "small", "aria-label": t('MessagingActions.MENU'), onClick: openMenu, sx: MESSAGE_BUBBLE_ACTION_SX, children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsxs(Stack, { spacing: 0.75, children: [replyTo && _jsx(ButtonBase, { type: "button", onClick: () => onJumpToMessage === null || onJumpToMessage === void 0 ? void 0 : onJumpToMessage(replyTo.id), sx: { display: 'block', width: '100%', px: 0.75, py: 0.5, textAlign: 'left', borderLeft: 3, borderColor: 'primary.main', bgcolor: 'action.hover', borderRadius: 0.5 }, children: _jsxs(Stack, { spacing: 0, alignItems: "flex-start", children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: t('MessagingThread.REPLY_TO', { sender: senderName(replyTo) || t('MessagingThread.UNKNOWN_SENDER') }) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: quoteLabel(replyTo, t) })] }) }), (conversation === null || conversation === void 0 ? void 0 : conversation.kind) !== 'direct' && _jsx(Typography, { variant: "subtitle2", children: senderName(message) || t('MessagingThread.UNKNOWN_SENDER') }), !deleted && message.kind === 'announcement' && message.title && _jsx(Typography, { variant: "subtitle1", fontWeight: 700, children: message.title }), !deleted && message.kind === 'announcement' && message.link_target && onAnnouncementLink && _jsx(Button, { type: "button", size: "small", onClick: () => onAnnouncementLink(message.link_target), children: t('MessagingAnnouncement.OPEN_LINK') }), error && _jsx(Alert, { severity: "error", role: "alert", children: error }), editing ? _jsxs(Stack, { spacing: 0.75, children: [_jsx(TextField, { label: t('MessagingActions.EDIT'), value: draft, onChange: (event) => setDraft(event.target.value), multiline: true, minRows: 2, autoFocus: true, disabled: saving }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { type: "button", onClick: saveEdit, disabled: saving, children: t('MessagingActions.SAVE') }), _jsx(Button, { type: "button", onClick: () => setEditing(false), disabled: saving, children: t('MessagingActions.CANCEL') })] })] }) : _jsx(Typography, { sx: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: deleted ? t('MessagingThread.DELETED') : (message.kind === 'announcement' ? message.body : (message.body || message.title)) || '' }), !deleted && _jsxs(Stack, { spacing: 0.75, children: [attachments.length > 0 && _jsx(AttachmentList, { attachments: attachments }), message.poll && _jsx(PollCard, { message: message })] }), hasMeta && _jsxs(Stack, { "data-testid": "message-meta", direction: "row", spacing: 0.5, justifyContent: "flex-end", alignItems: "center", flexWrap: "wrap", sx: { color: 'text.disabled', minWidth: 0, '& .MuiSvgIcon-root': { fontSize: '1rem' } }, children: [message.created_at && _jsx(Typography, { component: "span", variant: "caption", color: "inherit", sx: { whiteSpace: 'nowrap' }, children: new Date(message.created_at).toLocaleTimeString(i18n === null || i18n === void 0 ? void 0 : i18n.language, { hour: '2-digit', minute: '2-digit' }) }), message.edited_at && !deleted && _jsx(Typography, { component: "span", variant: "caption", color: "inherit", children: t('MessagingThread.EDITED') }), children] })] })] }), !deleted && (reactions.length > 0 || reactionPickerVisible) && _jsx(ReactionBar, { message: message, expanded: reactionPickerVisible, onExpandedChange: setReactionPickerVisible, sx: { mt: 0.5, ml: 0.75 } }), _jsxs(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: closeMenu, children: [_jsx(MenuItem, { onClick: startReply, children: t('MessagingThread.REPLY') }), _jsx(MenuItem, { onClick: startReact, children: t('MessagingReactions.ADD') }), canAct && _jsx(MenuItem, { onClick: startEdit, children: t('MessagingActions.EDIT') }), canAct && _jsx(MenuItem, { onClick: () => { setConfirmingDelete(true); closeMenu(); }, children: t('MessagingActions.DELETE') }), _jsx(MenuItem, { onClick: copyMessage, children: t('MessagingActions.COPY') })] }), _jsxs(Dialog, { open: confirmingDelete, onClose: () => !saving && setConfirmingDelete(false), children: [_jsx(DialogTitle, { children: t('MessagingActions.DELETE_CONFIRM_TITLE') }), _jsx(DialogContent, { children: t('MessagingActions.DELETE_CONFIRM_BODY') }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setConfirmingDelete(false), disabled: saving, children: t('MessagingActions.CANCEL') }), _jsx(Button, { onClick: confirmDelete, disabled: saving, color: "error", children: t('MessagingActions.DELETE') })] })] })] }));
|
|
138
159
|
}
|
|
@@ -4,6 +4,9 @@ import { Alert, Button, Checkbox, Chip, FormControlLabel, LinearProgress, Paper,
|
|
|
4
4
|
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
7
|
+
export const POLL_CARD_ROOT_SX = { p: 1.5, minWidth: 260 };
|
|
8
|
+
export const POLL_CARD_FORM_CONTROL_SX = { width: '100%', m: 0, alignItems: 'flex-start' };
|
|
9
|
+
export const POLL_CARD_CHECKBOX_SX = { pt: 0.25 };
|
|
7
10
|
/**
|
|
8
11
|
* Independently mountable live poll view. Server authorization remains authoritative.
|
|
9
12
|
*
|
|
@@ -125,15 +128,14 @@ export function PollCard({ message, canClose: canCloseProp }) {
|
|
|
125
128
|
const richLabel = _jsxs(Stack, { sx: { flex: 1, minWidth: 0, opacity: pending.has(option.id) ? 0.6 : 1 }, spacing: 0.75, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", spacing: 1, children: [_jsx(Typography, { variant: "body2", noWrap: true, children: option.text || option.label }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: resultText })] }), _jsx(LinearProgress, { variant: "determinate", value: percent, sx: { borderRadius: 1, height: 8 }, color: votedByMe ? 'primary' : 'inherit' })] });
|
|
126
129
|
return { accessibleLabel, richLabel };
|
|
127
130
|
};
|
|
128
|
-
const formControlSx = { width: '100%', m: 0, alignItems: 'flex-start' };
|
|
129
131
|
// MSG-6j: a short question/option set could otherwise pull the whole `MessageBubble`
|
|
130
132
|
// (`width: fit-content`) in tighter than comfortable to read. 260px stays under the
|
|
131
133
|
// bubble's own `maxWidth: min(75%, 680px)` ceiling even at a 375px viewport (75% = 281px)
|
|
132
134
|
// -- verified at that width, not just desktop.
|
|
133
|
-
return _jsx(Paper, { variant: "outlined", sx:
|
|
135
|
+
return _jsx(Paper, { variant: "outlined", sx: POLL_CARD_ROOT_SX, "aria-label": t('MessagingPoll.LABEL'), children: _jsxs(Stack, { spacing: 1.5, children: [_jsx(Typography, { variant: "subtitle2", children: poll.question }), poll.allow_multiple
|
|
134
136
|
? (poll.options || []).map((option) => {
|
|
135
137
|
const { accessibleLabel, richLabel } = optionLabel(option);
|
|
136
|
-
return _jsx(FormControlLabel, { disabled: closed, sx:
|
|
138
|
+
return _jsx(FormControlLabel, { disabled: closed, sx: POLL_CARD_FORM_CONTROL_SX, label: richLabel, control: _jsx(Checkbox, { checked: selected.includes(option.id), inputProps: { 'aria-label': accessibleLabel }, onChange: () => tap(option.id), sx: POLL_CARD_CHECKBOX_SX }) }, option.id);
|
|
137
139
|
})
|
|
138
140
|
// No RadioGroup-level onChange: it would only ever hand back
|
|
139
141
|
// `event.target.value`, a DOM string -- coercing a non-string option id
|
|
@@ -144,6 +146,6 @@ export function PollCard({ message, canClose: canCloseProp }) {
|
|
|
144
146
|
// Checkbox branch above exactly.
|
|
145
147
|
: _jsx(RadioGroup, { value: (_h = selected[0]) !== null && _h !== void 0 ? _h : '', name: `poll-${poll.id}-options`, children: (poll.options || []).map((option) => {
|
|
146
148
|
const { accessibleLabel, richLabel } = optionLabel(option);
|
|
147
|
-
return _jsx(FormControlLabel, { value: option.id, disabled: closed, sx:
|
|
149
|
+
return _jsx(FormControlLabel, { value: option.id, disabled: closed, sx: POLL_CARD_FORM_CONTROL_SX, label: richLabel, control: _jsx(Radio, { inputProps: { 'aria-label': accessibleLabel }, onChange: () => tap(option.id), sx: { pt: 0.25 } }) }, option.id);
|
|
148
150
|
}) }), error && _jsx(Alert, { severity: "error", role: "alert", children: error }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [canClose && !closed && _jsx(Button, { size: "small", color: "warning", onClick: close, disabled: busy, children: t('MessagingPoll.CLOSE') }), closed && _jsx(Chip, { size: "small", icon: _jsx(LockOutlinedIcon, { fontSize: "small" }), label: t('MessagingPoll.CLOSED') })] })] }) });
|
|
149
151
|
}
|
package/dist/messaging/Thread.js
CHANGED
|
@@ -7,6 +7,7 @@ import { MessageBubble } from './MessageBubble';
|
|
|
7
7
|
import { MESSAGING_ENVELOPE, useMessaging } from './MessagingProvider';
|
|
8
8
|
import { ReadTicks } from './ReadTicks';
|
|
9
9
|
import { useRealtime } from '../notifications/realtime';
|
|
10
|
+
export const THREAD_DAY_SEPARATOR_SX = { flex: 1 };
|
|
10
11
|
function chronological(messages) { return [...messages].sort((left, right) => new Date(left.created_at || 0) - new Date(right.created_at || 0)); }
|
|
11
12
|
function replyToId(message) { var _a; return (_a = message.reply_to_id) !== null && _a !== void 0 ? _a : message.reply_to; }
|
|
12
13
|
// last_reply_at/reply_count are viewer-independent (always on serialize_message);
|
|
@@ -172,7 +173,7 @@ export function Thread({ conversationId, onReplyTargetChange, canModerateMessage
|
|
|
172
173
|
};
|
|
173
174
|
return (_jsxs(Stack, { spacing: 1, sx: { minWidth: 0, flex: 1, minHeight: 0 }, children: [error && _jsx(Alert, { severity: "error", role: "alert", children: error }), replyingTo && _jsx(Alert, { severity: "info", onClose: () => { setReplyingTo(null); onReplyTargetChange === null || onReplyTargetChange === void 0 ? void 0 : onReplyTargetChange(null); }, children: t('MessagingThread.REPLYING_TO', { sender: ((_a = replyingTo.sender) === null || _a === void 0 ? void 0 : _a.display_name) || t('MessagingThread.UNKNOWN_SENDER') }) }), _jsxs(Box, { ref: scrollRef, onScroll: onScroll, sx: { flex: 1, minHeight: 0, maxHeight: 560, overflowY: 'auto', overflowX: 'hidden', px: 0.5 }, "aria-label": t('MessagingThread.TIMELINE'), children: [cache.cursors.messages[conversationId] && _jsx(Box, { textAlign: "center", py: 1, children: _jsx(Button, { onClick: loadOlder, disabled: loadingOlder, children: loadingOlder ? _jsx(CircularProgress, { size: 18 }) : t('MessagingThread.LOAD_OLDER') }) }), loadingInitial && !roots.length ? _jsx(Box, { textAlign: "center", py: 4, children: _jsx(CircularProgress, { size: 20 }) }) : (!roots.length && _jsx(Typography, { color: "text.secondary", textAlign: "center", py: 4, children: t('MessagingThread.EMPTY') })), _jsx(Stack, { spacing: 1, sx: { pr: 2 }, children: datedRoots.map(({ message, showDateSeparator }) => {
|
|
174
175
|
const replies = chronological(Object.values(cache.messages).filter((item) => String(replyToId(item)) === String(message.id)));
|
|
175
|
-
return _jsxs(Stack, { spacing: 0.75, children: [showDateSeparator && _jsxs(Stack, { "data-testid": "day-separator", direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Divider, { sx:
|
|
176
|
+
return _jsxs(Stack, { spacing: 0.75, children: [showDateSeparator && _jsxs(Stack, { "data-testid": "day-separator", direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Divider, { sx: THREAD_DAY_SEPARATOR_SX }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: new Date(message.created_at).toLocaleDateString(i18n === null || i18n === void 0 ? void 0 : i18n.language) }), _jsx(Divider, { sx: THREAD_DAY_SEPARATOR_SX })] }), _jsx(MessageBubble, { message: message, conversation: conversation, onReply: reply, onJumpToMessage: jumpToMessage, onAnnouncementLink: onAnnouncementLink, canModerateMessages: canModerateMessages, children: canShowReadTicks(message, user) && _jsx(ReadTicks, { messageId: message.id, conversation: conversation }) }), (message.reply_count || replies.length) > 0 && (() => {
|
|
176
177
|
const unread = hasUnreadReplies(message, cache.receipts, user === null || user === void 0 ? void 0 : user.id);
|
|
177
178
|
const label = openThreads[message.id] ? t('MessagingThread.HIDE_REPLIES') : t('MessagingThread.SHOW_REPLIES', { count: message.reply_count || replies.length });
|
|
178
179
|
// MUI's Badge spreads unrecognized props (incl. aria-label) onto
|
|
@@ -10,6 +10,20 @@ import Switch from '@mui/material/Switch';
|
|
|
10
10
|
import Typography from '@mui/material/Typography';
|
|
11
11
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
|
12
12
|
import { getNotificationPreferences, getVapidPublicKey, patchNotificationPreferences, removePushSubscription, savePushSubscription, setNotificationCategorySubscription, urlBase64ToUint8Array, } from './api';
|
|
13
|
+
export const NOTIFICATION_SETTINGS_ALERT_SX = { mb: 2 };
|
|
14
|
+
export const NOTIFICATION_SETTINGS_HINT_ALERT_SX = { mb: 1.5 };
|
|
15
|
+
export const NOTIFICATION_SETTINGS_REACH_ALERT_SX = { mb: 1 };
|
|
16
|
+
export const NOTIFICATION_SETTINGS_FORM_CONTROL_SX = { alignItems: 'flex-start', ml: 0 };
|
|
17
|
+
export const NOTIFICATION_SETTINGS_PUSH_CONTROL_SX = {
|
|
18
|
+
alignItems: 'flex-start',
|
|
19
|
+
ml: 0,
|
|
20
|
+
mt: 1.5,
|
|
21
|
+
};
|
|
22
|
+
export const NOTIFICATION_SETTINGS_CATEGORY_CONTROL_SX = {
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
ml: 0,
|
|
25
|
+
display: 'flex',
|
|
26
|
+
};
|
|
13
27
|
function reachLabelKey(notificationType) {
|
|
14
28
|
const noActiveChannel = notificationType.active && notificationType.has_active_channel === false;
|
|
15
29
|
if (notificationType.active && notificationType.passive) {
|
|
@@ -193,16 +207,16 @@ export function NotificationSettings() {
|
|
|
193
207
|
if (loading) {
|
|
194
208
|
return _jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 4 }, children: _jsx(CircularProgress, {}) });
|
|
195
209
|
}
|
|
196
|
-
return (_jsxs(Box, { sx: { maxWidth: 520 }, children: [_jsxs(Typography, { variant: "h6", gutterBottom: true, sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(NotificationsIcon, {}), t('NotificationSettings.TITLE')] }), _jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: t('NotificationSettings.SUBTITLE') }), error && _jsx(Alert, { severity: "error", sx:
|
|
210
|
+
return (_jsxs(Box, { sx: { maxWidth: 520 }, children: [_jsxs(Typography, { variant: "h6", gutterBottom: true, sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(NotificationsIcon, {}), t('NotificationSettings.TITLE')] }), _jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: t('NotificationSettings.SUBTITLE') }), error && _jsx(Alert, { severity: "error", sx: NOTIFICATION_SETTINGS_ALERT_SX, onClose: () => setError(''), children: error }), conflict && _jsx(Alert, { severity: "warning", sx: NOTIFICATION_SETTINGS_ALERT_SX, onClose: () => setConflict(''), children: conflict }), _jsx(Box, { sx: { py: 2 }, children: _jsx(FormControlLabel, { control: _jsx(Switch, { checked: Boolean(preferences === null || preferences === void 0 ? void 0 : preferences.email_opt_in), onChange: handleEmailToggle, disabled: savingEmail }), label: _jsxs(Box, { children: [_jsx(Typography, { variant: "body1", children: t('NotificationSettings.EMAIL_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('NotificationSettings.EMAIL_HINT') })] }), labelPlacement: "end", sx: NOTIFICATION_SETTINGS_FORM_CONTROL_SX }) }), _jsx(Divider, {}), _jsxs(Box, { sx: { py: 2 }, children: [iosNeedsInstall && _jsx(Alert, { severity: "info", sx: NOTIFICATION_SETTINGS_HINT_ALERT_SX, children: t('NotificationSettings.IOS_HINT') }), !pushSupported && !iosNeedsInstall && _jsx(Alert, { severity: "warning", sx: NOTIFICATION_SETTINGS_HINT_ALERT_SX, children: t('NotificationSettings.PUSH_NOT_SUPPORTED') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: pushSubscribed, onChange: (event) => (event.target.checked ? handleEnablePush() : handleDisablePush()), disabled: savingPush || !pushSupported || iosNeedsInstall })), label: _jsxs(Box, { children: [_jsx(Typography, { variant: "body1", children: t('NotificationSettings.PUSH_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('NotificationSettings.PUSH_HINT') })] }), labelPlacement: "end", sx: NOTIFICATION_SETTINGS_FORM_CONTROL_SX }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: (preferences === null || preferences === void 0 ? void 0 : preferences.push_preview_opt_in) !== false, onChange: handlePreviewToggle, disabled: savingPreview || !(preferences === null || preferences === void 0 ? void 0 : preferences.push_opt_in) })), label: _jsxs(Box, { children: [_jsx(Typography, { variant: "body1", children: t('NotificationSettings.PUSH_PREVIEW_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('NotificationSettings.PUSH_PREVIEW_HINT') })] }), labelPlacement: "end", sx: NOTIFICATION_SETTINGS_PUSH_CONTROL_SX })] }), ((_a = preferences === null || preferences === void 0 ? void 0 : preferences.notification_types) === null || _a === void 0 ? void 0 : _a.length) > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Box, { sx: { py: 2 }, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('NotificationSettings.REACH_TITLE') }), preferences.notification_types.map((notificationType) => {
|
|
197
211
|
const labelKey = reachLabelKey(notificationType);
|
|
198
212
|
const reachText = t(labelKey);
|
|
199
213
|
// Reserve Alert (a colored banner) for the one case that is actually a
|
|
200
214
|
// warning -- an active-only type nobody can currently reach. Routine
|
|
201
215
|
// reach description is static settings copy, not a status message.
|
|
202
216
|
if (labelKey === 'NotificationSettings.REACH_NO_ACTIVE_CHANNEL') {
|
|
203
|
-
return (_jsxs(Alert, { severity: "warning", sx:
|
|
217
|
+
return (_jsxs(Alert, { severity: "warning", sx: NOTIFICATION_SETTINGS_REACH_ALERT_SX, children: [_jsx("strong", { children: notificationType.label }), " \u2014 ", reachText] }, notificationType.key));
|
|
204
218
|
}
|
|
205
219
|
return (_jsxs(Box, { sx: { py: 0.5 }, children: [_jsx(Typography, { variant: "body2", children: notificationType.label }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: reachText })] }, notificationType.key));
|
|
206
|
-
})] })] })), ((_b = preferences === null || preferences === void 0 ? void 0 : preferences.subscribable_categories) === null || _b === void 0 ? void 0 : _b.length) > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Box, { sx: { py: 2 }, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('NotificationSettings.SUBSCRIPTIONS_TITLE') }), _jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: t('NotificationSettings.SUBSCRIPTIONS_SUBTITLE') }), preferences.subscribable_categories.map((row) => (_jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(row.subscribed), onChange: (event) => handleSubscriptionToggle(row.category, event), disabled: savingSubscription === row.category })), label: row.label, labelPlacement: "end", sx:
|
|
220
|
+
})] })] })), ((_b = preferences === null || preferences === void 0 ? void 0 : preferences.subscribable_categories) === null || _b === void 0 ? void 0 : _b.length) > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Box, { sx: { py: 2 }, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('NotificationSettings.SUBSCRIPTIONS_TITLE') }), _jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: t('NotificationSettings.SUBSCRIPTIONS_SUBTITLE') }), preferences.subscribable_categories.map((row) => (_jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(row.subscribed), onChange: (event) => handleSubscriptionToggle(row.category, event), disabled: savingSubscription === row.category })), label: row.label, labelPlacement: "end", sx: NOTIFICATION_SETTINGS_CATEGORY_CONTROL_SX }, row.category)))] })] }))] }));
|
|
207
221
|
}
|
|
208
222
|
export default NotificationSettings;
|
|
@@ -8,10 +8,11 @@ import { useOnboarding } from '../onboarding/OnboardingProvider';
|
|
|
8
8
|
import { WizardDialogShell } from '../onboarding/WizardDialogShell';
|
|
9
9
|
import { useNotifications } from './NotificationsProvider';
|
|
10
10
|
import { DEFAULT_ENVELOPE, useRealtime } from './realtime';
|
|
11
|
+
export const POPUP_SURFACE_DISMISS_BUTTON_SX = { alignSelf: 'flex-end' };
|
|
11
12
|
function PopupStepComponent({ onDismiss, ctx }) {
|
|
12
13
|
const { t } = useTranslation();
|
|
13
14
|
const content = (ctx === null || ctx === void 0 ? void 0 : ctx.content) || {};
|
|
14
|
-
return (_jsxs(Stack, { spacing: 2, children: [content.body_key && (_jsx(Typography, { variant: "body1", children: t(content.body_key, content.params || {}) })), _jsx(Button, { onClick: onDismiss, sx:
|
|
15
|
+
return (_jsxs(Stack, { spacing: 2, children: [content.body_key && (_jsx(Typography, { variant: "body1", children: t(content.body_key, content.params || {}) })), _jsx(Button, { onClick: onDismiss, sx: POPUP_SURFACE_DISMISS_BUTTON_SX, children: t('PopupSurface.CLOSE') })] }));
|
|
15
16
|
}
|
|
16
17
|
const POPUP_STEP = { blocking: false, Component: PopupStepComponent };
|
|
17
18
|
/**
|
|
@@ -11,6 +11,7 @@ import Typography from '@mui/material/Typography';
|
|
|
11
11
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
12
12
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
|
13
13
|
import { getVapidPublicKey, patchNotificationPreferences, savePushSubscription, urlBase64ToUint8Array, } from '../../notifications/api';
|
|
14
|
+
export const BROWSER_PUSH_FORM_CONTROL_LABEL_SX = { alignItems: 'flex-start', ml: 0 };
|
|
14
15
|
export function BrowserPushStep({ onComplete, onDismiss, ctx }) {
|
|
15
16
|
var _a, _b, _c;
|
|
16
17
|
const { t } = useTranslation();
|
|
@@ -81,6 +82,6 @@ export function BrowserPushStep({ onComplete, onDismiss, ctx }) {
|
|
|
81
82
|
setLoading(false);
|
|
82
83
|
}
|
|
83
84
|
};
|
|
84
|
-
return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(NotificationsIcon, { color: "primary" }), _jsx(Typography, { variant: "h6", children: t('Onboarding.NOTIFICATIONS_TITLE') })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Onboarding.NOTIFICATIONS_BODY') }), error && _jsx(Alert, { severity: "warning", onClose: () => setError(''), children: error }), _jsx(FormControlLabel, { control: _jsx(Switch, { checked: emailOptedIn, onChange: handleEmailToggle, disabled: savingEmail }), label: _jsxs(Box, { children: [_jsx(Typography, { variant: "body1", children: t('Onboarding.EMAIL_NOTIFICATIONS_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('Onboarding.EMAIL_NOTIFICATIONS_HINT') })] }), labelPlacement: "end", sx:
|
|
85
|
+
return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(NotificationsIcon, { color: "primary" }), _jsx(Typography, { variant: "h6", children: t('Onboarding.NOTIFICATIONS_TITLE') })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Onboarding.NOTIFICATIONS_BODY') }), error && _jsx(Alert, { severity: "warning", onClose: () => setError(''), children: error }), _jsx(FormControlLabel, { control: _jsx(Switch, { checked: emailOptedIn, onChange: handleEmailToggle, disabled: savingEmail }), label: _jsxs(Box, { children: [_jsx(Typography, { variant: "body1", children: t('Onboarding.EMAIL_NOTIFICATIONS_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('Onboarding.EMAIL_NOTIFICATIONS_HINT') })] }), labelPlacement: "end", sx: BROWSER_PUSH_FORM_CONTROL_LABEL_SX }), pushSupported && (_jsxs(Box, { children: [_jsx(Typography, { variant: "body1", gutterBottom: true, children: t('Onboarding.PUSH_NOTIFICATIONS_LABEL') }), _jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", sx: { mb: 1.5 }, children: t('Onboarding.PUSH_NOTIFICATIONS_HINT') }), pushSubscribed ? (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(CheckCircleIcon, { color: "success", fontSize: "small" }), _jsx(Typography, { variant: "body2", children: t('Onboarding.PUSH_ENABLED') })] })) : (_jsx(Button, { variant: "contained", onClick: enable, disabled: loading, startIcon: loading ? _jsx(CircularProgress, { size: 16 }) : undefined, children: t('NotificationSettings.PUSH_ENABLE') }))] })), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1 }, children: [_jsx(Button, { variant: "text", onClick: onDismiss, disabled: loading || savingEmail, children: t('Onboarding.SKIP') }), _jsx(Button, { variant: "contained", onClick: onComplete, disabled: loading || savingEmail, children: t('Onboarding.CONTINUE') })] })] }));
|
|
85
86
|
}
|
|
86
87
|
export default BrowserPushStep;
|