@micha.bigler/ui-core-micha 2.33.1 → 2.34.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 +4 -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 +11 -5
- 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/SupportRecoveryRequestsTab.js +4 -1
- package/dist/components/UserInviteComponent.js +8 -8
- package/dist/components/UserListComponent.js +8 -4
- package/dist/components/charts/ChartFrame.js +5 -2
- package/dist/components/charts/TimeSeriesChart.js +7 -4
- 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 +218 -0
- package/dist/theme/tokens.js +1 -0
- 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 +14 -7
- 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/SupportRecoveryRequestsTab.jsx +8 -4
- package/src/components/UserInviteComponent.jsx +10 -10
- package/src/components/UserListComponent.jsx +12 -7
- package/src/components/charts/ChartFrame.jsx +6 -2
- package/src/components/charts/TimeSeriesChart.jsx +8 -4
- package/src/index.js +3 -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 +3 -0
- package/src/theme/kitSxRegistry.js +246 -0
- package/src/theme/themeCompleteness.js +228 -0
- package/src/theme/tokens.js +1 -0
- package/tests/createAppTheme.test.js +1 -0
- package/tests/themeCompleteness.test.js +214 -0
- package/work-orders/THEME-3.md +210 -0
- package/work-orders/THEME-4.md +271 -0
|
@@ -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] })] }))] }));
|
|
@@ -3,6 +3,8 @@ 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
5
|
import { exportChartPng, exportChartSvg } from './exportChart';
|
|
6
|
+
export const CHART_FRAME_ROOT_SX = { p: 2 };
|
|
7
|
+
export const CHART_FRAME_ALERT_SX = { mt: 2 };
|
|
6
8
|
/**
|
|
7
9
|
* Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
|
|
8
10
|
*/
|
|
@@ -16,6 +18,7 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
|
|
|
16
18
|
const hasMeta = meta !== undefined && meta !== null && meta !== false && meta !== '';
|
|
17
19
|
const hasCustomAriaLabel = Boolean(ariaLabel);
|
|
18
20
|
const chartLabel = ariaLabel || title;
|
|
21
|
+
const callerSx = Array.isArray(sx) ? sx : [sx];
|
|
19
22
|
const runExport = async (type) => {
|
|
20
23
|
setExportError(false);
|
|
21
24
|
try {
|
|
@@ -40,14 +43,14 @@ export function ChartFrame({ title, subtitle, toolbar, controls, loading = false
|
|
|
40
43
|
else if (isEmpty) {
|
|
41
44
|
content = _jsx(Alert, { severity: "info", children: emptyMessage || t('ChartFrame.EMPTY_DEFAULT') });
|
|
42
45
|
}
|
|
43
|
-
return (_jsxs(Paper, { variant: variant, sx:
|
|
46
|
+
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
47
|
width: '100%',
|
|
45
48
|
minHeight,
|
|
46
49
|
aspectRatio: aspect,
|
|
47
50
|
display: 'flex',
|
|
48
51
|
alignItems: 'center',
|
|
49
52
|
justifyContent: 'center',
|
|
50
|
-
}, children: content }), exportError && _jsx(Alert, { severity: "error", sx:
|
|
53
|
+
}, children: content }), exportError && _jsx(Alert, { severity: "error", sx: CHART_FRAME_ALERT_SX, children: t('ChartFrame.EXPORT_ERROR') }), hasMeta ? (_jsxs(Box, { sx: {
|
|
51
54
|
display: 'flex',
|
|
52
55
|
alignItems: 'center',
|
|
53
56
|
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),
|
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, 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;
|
|
@@ -6,6 +6,7 @@ import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } fr
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
// Internal context
|
|
8
8
|
import { AuthContext } from '../auth/AuthContext';
|
|
9
|
+
export const ACCOUNT_PAGE_SECTION_PAPER_SX = { p: 2.5 };
|
|
9
10
|
// Falls die Komponenten noch lokal sind:
|
|
10
11
|
import { WidePage } from '../layout/PageLayout';
|
|
11
12
|
import { ProfileComponent } from '../components/ProfileComponent';
|
|
@@ -130,5 +131,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
130
131
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
131
132
|
? null
|
|
132
133
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
133
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx:
|
|
134
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
134
135
|
}
|
package/dist/pages/LoginPage.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loginWithPasskey, startSocialLogin } from '../utils/authService';
|
|
|
13
13
|
// Components
|
|
14
14
|
import { LoginForm } from '../components/LoginForm';
|
|
15
15
|
import { MfaLoginComponent } from '../components/MfaLoginComponent';
|
|
16
|
+
export const LOGIN_PAGE_ALERT_SX = { mb: 2 };
|
|
16
17
|
export function LoginPage() {
|
|
17
18
|
const navigate = useNavigate();
|
|
18
19
|
const location = useLocation();
|
|
@@ -244,7 +245,7 @@ export function LoginPage() {
|
|
|
244
245
|
const twoFactorRequired = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.two_factor_required)
|
|
245
246
|
|| Number((authMethods === null || authMethods === void 0 ? void 0 : authMethods.required_auth_factor_count) || 1) >= 2;
|
|
246
247
|
// --- Render ---
|
|
247
|
-
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx:
|
|
248
|
+
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: LOGIN_PAGE_ALERT_SX, children: t(errorKey) })), recoveryFetching && !errorKey && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.RECOVERY_LINK_VALIDATING', 'Validating recovery link…') })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), twoFactorRequired && !recoveryToken && !recoveryFetching && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.TWO_FACTOR_REQUIRED_HINT', 'This app requires two authentication factors for full access.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: passwordLoginEnabled ? handleSubmitCredentials : null, onForgotPassword: passwordResetEnabled ? () => navigate('/reset-request-password') : null, onSocialLogin: socialLoginEnabled ? (provider) => startSocialLogin(provider) : null, socialProviders: socialProviders, onPasskeyLogin: passkeyLoginEnabled ? handlePasskeyLoginInitial : null, onSignUp: signupEnabled ? () => navigate('/signup') : null,
|
|
248
249
|
// Block submit while the handoff is in flight so the user does not
|
|
249
250
|
// race the recovery-token fetch with a normal password POST.
|
|
250
251
|
disabled: submitting || recoveryFetching, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
|