@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.
Files changed (82) hide show
  1. package/WORK_ORDERS.md +4 -2
  2. package/dist/auth/ReauthModal.js +3 -1
  3. package/dist/components/AccessCodeManager.js +16 -14
  4. package/dist/components/AccessCodeSingleUseToggle.js +2 -1
  5. package/dist/components/AllowedEmailDomainsManager.js +3 -1
  6. package/dist/components/AuthFactorRequirementCard.js +2 -1
  7. package/dist/components/BulkInviteCsvTab.js +7 -7
  8. package/dist/components/LoginForm.js +2 -1
  9. package/dist/components/MFAComponent.js +11 -5
  10. package/dist/components/MfaLoginComponent.js +4 -1
  11. package/dist/components/PasskeysComponent.js +4 -2
  12. package/dist/components/QrSignupManager.js +2 -1
  13. package/dist/components/QrSignupValidityManager.js +4 -1
  14. package/dist/components/RegistrationMethodsManager.js +3 -1
  15. package/dist/components/SecurityComponent.js +3 -1
  16. package/dist/components/SupportRecoveryRequestsTab.js +4 -1
  17. package/dist/components/UserInviteComponent.js +8 -8
  18. package/dist/components/UserListComponent.js +8 -4
  19. package/dist/components/charts/ChartFrame.js +5 -2
  20. package/dist/components/charts/TimeSeriesChart.js +7 -4
  21. package/dist/index.js +1 -1
  22. package/dist/layout/PageLayout.js +3 -2
  23. package/dist/messaging/AttachmentList.js +6 -1
  24. package/dist/messaging/Composer.js +2 -1
  25. package/dist/messaging/DirectMessageLauncher.js +2 -1
  26. package/dist/messaging/MessageBubble.js +29 -8
  27. package/dist/messaging/PollCard.js +6 -4
  28. package/dist/messaging/Thread.js +2 -1
  29. package/dist/notifications/NotificationSettings.js +17 -3
  30. package/dist/notifications/PopupSurface.js +2 -1
  31. package/dist/onboarding/steps/BrowserPushStep.js +2 -1
  32. package/dist/pages/AccountPage.js +2 -1
  33. package/dist/pages/LoginPage.js +2 -1
  34. package/dist/pages/SignUpPage.js +2 -1
  35. package/dist/pages/SignupConfirmPage.js +2 -1
  36. package/dist/theme/index.js +2 -1
  37. package/dist/theme/kitSxRegistry.js +187 -0
  38. package/dist/theme/themeCompleteness.js +218 -0
  39. package/dist/theme/tokens.js +1 -0
  40. package/package.json +1 -1
  41. package/src/auth/ReauthModal.jsx +5 -2
  42. package/src/components/AccessCodeManager.jsx +22 -20
  43. package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
  44. package/src/components/AllowedEmailDomainsManager.jsx +6 -3
  45. package/src/components/AuthFactorRequirementCard.jsx +4 -2
  46. package/src/components/BulkInviteCsvTab.jsx +11 -10
  47. package/src/components/LoginForm.jsx +5 -3
  48. package/src/components/MFAComponent.jsx +14 -7
  49. package/src/components/MfaLoginComponent.jsx +9 -5
  50. package/src/components/PasskeysComponent.jsx +7 -4
  51. package/src/components/QrSignupManager.jsx +6 -4
  52. package/src/components/QrSignupValidityManager.jsx +8 -4
  53. package/src/components/RegistrationMethodsManager.jsx +7 -4
  54. package/src/components/SecurityComponent.jsx +10 -7
  55. package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
  56. package/src/components/UserInviteComponent.jsx +10 -10
  57. package/src/components/UserListComponent.jsx +12 -7
  58. package/src/components/charts/ChartFrame.jsx +6 -2
  59. package/src/components/charts/TimeSeriesChart.jsx +8 -4
  60. package/src/index.js +3 -0
  61. package/src/layout/PageLayout.jsx +4 -2
  62. package/src/messaging/AttachmentList.jsx +9 -3
  63. package/src/messaging/Composer.jsx +3 -1
  64. package/src/messaging/DirectMessageLauncher.jsx +3 -1
  65. package/src/messaging/MessageBubble.jsx +31 -9
  66. package/src/messaging/PollCard.jsx +8 -6
  67. package/src/messaging/Thread.jsx +3 -1
  68. package/src/notifications/NotificationSettings.jsx +24 -9
  69. package/src/notifications/PopupSurface.jsx +3 -1
  70. package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
  71. package/src/pages/AccountPage.jsx +11 -9
  72. package/src/pages/LoginPage.jsx +6 -4
  73. package/src/pages/SignUpPage.jsx +4 -2
  74. package/src/pages/SignupConfirmPage.jsx +4 -2
  75. package/src/theme/index.js +3 -0
  76. package/src/theme/kitSxRegistry.js +246 -0
  77. package/src/theme/themeCompleteness.js +228 -0
  78. package/src/theme/tokens.js +1 -0
  79. package/tests/createAppTheme.test.js +1 -0
  80. package/tests/themeCompleteness.test.js +214 -0
  81. package/work-orders/THEME-3.md +210 -0
  82. 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: { mb: 2 }, children: message }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, 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) => {
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: actionButtonSx, children: loading ? _jsx(CircularProgress, { size: 24, color: "inherit" }) : t('Auth.INVITE_BUTTON', 'Invite') })] })] }));
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: actionButtonSx, children: typeof action.label === 'function'
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: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) }))] })),
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: { mb: 2 }, 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: { minWidth: col.minWidth, fontWeight: 600, whiteSpace: 'nowrap' }, 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: { py: 4, color: 'text.secondary' }, 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: { verticalAlign: 'top', py: 1 }, 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) => {
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: Object.assign({ p: 2 }, sx), 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: {
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: { mt: 2 }, children: t('ChartFrame.EXPORT_ERROR') }), hasMeta ? (_jsxs(Box, { 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: { mt: 4 }, children: [title && (_jsx(Typography, { variant: "h4", gutterBottom: true, children: title })), children] }));
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: { mt: 4 }, 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] }) }));
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: { bgcolor: 'background.paper', boxShadow: 1 }, children: _jsx(DownloadIcon, {}) }), _jsx(IconButton, { "aria-label": t('MessagingAttachments.CLOSE_PREVIEW'), onClick: () => setLightbox(null), sx: { bgcolor: 'background.paper', boxShadow: 1 }, 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: { m: 2 }, 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)
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: { fontSize: '1.15rem' }, 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
+ } }, 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: { mb: 1 }, 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
+ 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
- position: 'relative', p: 1.25, minWidth: 0,
132
- bgcolor: isOwn ? (theme) => alpha(theme.palette.primary.light, 0.24) : 'background.paper',
133
- borderColor: isOwn ? (theme) => alpha(theme.palette.primary.main, 0.35) : 'divider',
134
- overflowWrap: 'anywhere',
135
- '& .message-actions': { opacity: 0, pointerEvents: 'none', transition: 'opacity 120ms ease-in-out' },
136
- '&:hover .message-actions, &:focus-within .message-actions, &[data-actions-visible="true"] .message-actions': { opacity: 1, pointerEvents: 'auto' },
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: { p: 1.5, minWidth: 260 }, "aria-label": t('MessagingPoll.LABEL'), children: _jsxs(Stack, { spacing: 1.5, children: [_jsx(Typography, { variant: "subtitle2", children: poll.question }), poll.allow_multiple
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: formControlSx, label: richLabel, control: _jsx(Checkbox, { checked: selected.includes(option.id), inputProps: { 'aria-label': accessibleLabel }, onChange: () => tap(option.id), sx: { pt: 0.25 } }) }, option.id);
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: formControlSx, label: richLabel, control: _jsx(Radio, { inputProps: { 'aria-label': accessibleLabel }, onChange: () => tap(option.id), sx: { pt: 0.25 } }) }, option.id);
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
  }
@@ -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: { flex: 1 } }), _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: { flex: 1 } })] }), _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
+ 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: { mb: 2 }, onClose: () => setError(''), children: error }), conflict && _jsx(Alert, { severity: "warning", sx: { mb: 2 }, 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: { alignItems: 'flex-start', ml: 0 } }) }), _jsx(Divider, {}), _jsxs(Box, { sx: { py: 2 }, children: [iosNeedsInstall && _jsx(Alert, { severity: "info", sx: { mb: 1.5 }, children: t('NotificationSettings.IOS_HINT') }), !pushSupported && !iosNeedsInstall && _jsx(Alert, { severity: "warning", sx: { mb: 1.5 }, 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: { alignItems: 'flex-start', ml: 0 } }), _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: { alignItems: 'flex-start', ml: 0, mt: 1.5 } })] }), ((_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) => {
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: { mb: 1 }, children: [_jsx("strong", { children: notificationType.label }), " \u2014 ", reachText] }, notificationType.key));
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: { alignItems: 'center', ml: 0, display: 'flex' } }, row.category)))] })] }))] }));
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: { alignSelf: 'flex-end' }, children: t('PopupSurface.CLOSE') })] }));
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: { alignItems: 'flex-start', ml: 0 } }), 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
+ 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: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, 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: { p: 2.5, borderRadius: 2 }, 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
+ 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
  }
@@ -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: { mb: 2 }, children: t(errorKey) })), recoveryFetching && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LINK_VALIDATING', 'Validating recovery link…') })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), twoFactorRequired && !recoveryToken && !recoveryFetching && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, 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
+ 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 }) }))] }));