@micha.bigler/ui-core-micha 2.33.1 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORK_ORDERS.md +4 -2
- package/dist/auth/ReauthModal.js +3 -1
- package/dist/components/AccessCodeManager.js +16 -14
- package/dist/components/AccessCodeSingleUseToggle.js +2 -1
- package/dist/components/AllowedEmailDomainsManager.js +3 -1
- package/dist/components/AuthFactorRequirementCard.js +2 -1
- package/dist/components/BulkInviteCsvTab.js +7 -7
- package/dist/components/LoginForm.js +2 -1
- package/dist/components/MFAComponent.js +11 -5
- package/dist/components/MfaLoginComponent.js +4 -1
- package/dist/components/PasskeysComponent.js +4 -2
- package/dist/components/QrSignupManager.js +2 -1
- package/dist/components/QrSignupValidityManager.js +4 -1
- package/dist/components/RegistrationMethodsManager.js +3 -1
- package/dist/components/SecurityComponent.js +3 -1
- package/dist/components/SupportRecoveryRequestsTab.js +4 -1
- package/dist/components/UserInviteComponent.js +8 -8
- package/dist/components/UserListComponent.js +8 -4
- package/dist/components/charts/ChartFrame.js +5 -2
- package/dist/components/charts/TimeSeriesChart.js +7 -4
- package/dist/index.js +1 -1
- package/dist/layout/PageLayout.js +3 -2
- package/dist/messaging/AttachmentList.js +6 -1
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/DirectMessageLauncher.js +2 -1
- package/dist/messaging/MessageBubble.js +29 -8
- package/dist/messaging/PollCard.js +6 -4
- package/dist/messaging/Thread.js +2 -1
- package/dist/notifications/NotificationSettings.js +17 -3
- package/dist/notifications/PopupSurface.js +2 -1
- package/dist/onboarding/steps/BrowserPushStep.js +2 -1
- package/dist/pages/AccountPage.js +2 -1
- package/dist/pages/LoginPage.js +2 -1
- package/dist/pages/SignUpPage.js +2 -1
- package/dist/pages/SignupConfirmPage.js +2 -1
- package/dist/theme/index.js +2 -1
- package/dist/theme/kitSxRegistry.js +187 -0
- package/dist/theme/themeCompleteness.js +218 -0
- package/dist/theme/tokens.js +1 -0
- package/package.json +1 -1
- package/src/auth/ReauthModal.jsx +5 -2
- package/src/components/AccessCodeManager.jsx +22 -20
- package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
- package/src/components/AllowedEmailDomainsManager.jsx +6 -3
- package/src/components/AuthFactorRequirementCard.jsx +4 -2
- package/src/components/BulkInviteCsvTab.jsx +11 -10
- package/src/components/LoginForm.jsx +5 -3
- package/src/components/MFAComponent.jsx +14 -7
- package/src/components/MfaLoginComponent.jsx +9 -5
- package/src/components/PasskeysComponent.jsx +7 -4
- package/src/components/QrSignupManager.jsx +6 -4
- package/src/components/QrSignupValidityManager.jsx +8 -4
- package/src/components/RegistrationMethodsManager.jsx +7 -4
- package/src/components/SecurityComponent.jsx +10 -7
- package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
- package/src/components/UserInviteComponent.jsx +10 -10
- package/src/components/UserListComponent.jsx +12 -7
- package/src/components/charts/ChartFrame.jsx +6 -2
- package/src/components/charts/TimeSeriesChart.jsx +8 -4
- package/src/index.js +3 -0
- package/src/layout/PageLayout.jsx +4 -2
- package/src/messaging/AttachmentList.jsx +9 -3
- package/src/messaging/Composer.jsx +3 -1
- package/src/messaging/DirectMessageLauncher.jsx +3 -1
- package/src/messaging/MessageBubble.jsx +31 -9
- package/src/messaging/PollCard.jsx +8 -6
- package/src/messaging/Thread.jsx +3 -1
- package/src/notifications/NotificationSettings.jsx +24 -9
- package/src/notifications/PopupSurface.jsx +3 -1
- package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
- package/src/pages/AccountPage.jsx +11 -9
- package/src/pages/LoginPage.jsx +6 -4
- package/src/pages/SignUpPage.jsx +4 -2
- package/src/pages/SignupConfirmPage.jsx +4 -2
- package/src/theme/index.js +3 -0
- package/src/theme/kitSxRegistry.js +246 -0
- package/src/theme/themeCompleteness.js +228 -0
- package/src/theme/tokens.js +1 -0
- package/tests/createAppTheme.test.js +1 -0
- package/tests/themeCompleteness.test.js +214 -0
- package/work-orders/THEME-3.md +210 -0
- package/work-orders/THEME-4.md +271 -0
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
12
|
import { exportChartPng, exportChartSvg } from './exportChart';
|
|
13
13
|
|
|
14
|
+
export const CHART_FRAME_ROOT_SX = { p: 2 };
|
|
15
|
+
export const CHART_FRAME_ALERT_SX = { mt: 2 };
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Chart-type-agnostic chart scaffolding. Children may be an X-Charts primitive or a bespoke SVG.
|
|
16
19
|
*/
|
|
@@ -44,6 +47,7 @@ export function ChartFrame({
|
|
|
44
47
|
const hasMeta = meta !== undefined && meta !== null && meta !== false && meta !== '';
|
|
45
48
|
const hasCustomAriaLabel = Boolean(ariaLabel);
|
|
46
49
|
const chartLabel = ariaLabel || title;
|
|
50
|
+
const callerSx = Array.isArray(sx) ? sx : [sx];
|
|
47
51
|
|
|
48
52
|
const runExport = async (type) => {
|
|
49
53
|
setExportError(false);
|
|
@@ -69,7 +73,7 @@ export function ChartFrame({
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
return (
|
|
72
|
-
<Paper variant={variant} sx={
|
|
76
|
+
<Paper variant={variant} sx={[CHART_FRAME_ROOT_SX, ...callerSx]}>
|
|
73
77
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2, mb: 2 }}>
|
|
74
78
|
<Box>
|
|
75
79
|
<Typography id={titleId} variant={titleVariant}>{title}</Typography>
|
|
@@ -101,7 +105,7 @@ export function ChartFrame({
|
|
|
101
105
|
{content}
|
|
102
106
|
</Box>
|
|
103
107
|
|
|
104
|
-
{exportError && <Alert severity="error" sx={
|
|
108
|
+
{exportError && <Alert severity="error" sx={CHART_FRAME_ALERT_SX}>{t('ChartFrame.EXPORT_ERROR')}</Alert>}
|
|
105
109
|
|
|
106
110
|
{/* `meta` opts the card into the single bordered foot row (meta left, exports right).
|
|
107
111
|
Without it the markup below is byte-for-byte the pre-`meta` structure -- a bare Stack,
|
|
@@ -32,6 +32,13 @@ import { useNeutralChartPalette } from './palette';
|
|
|
32
32
|
// MUI measurement bug is actually resolved.
|
|
33
33
|
const CHART_HEIGHT = 320;
|
|
34
34
|
|
|
35
|
+
// Dynamic sx factories are outside the registry's documented top-level-object
|
|
36
|
+
// lower bound; the series colour is data-dependent at render time.
|
|
37
|
+
const seriesToggleSx = (colour) => ({
|
|
38
|
+
color: colour,
|
|
39
|
+
'&.Mui-checked': { color: colour },
|
|
40
|
+
});
|
|
41
|
+
|
|
35
42
|
// CHART-5: how many x-axis tick labels to show, evenly spread across
|
|
36
43
|
// data.xLabels, regardless of container width or label length. Replaces
|
|
37
44
|
// MUI's own collision-based tick filtering (which, given enough densely
|
|
@@ -240,10 +247,7 @@ export function TimeSeriesChart({
|
|
|
240
247
|
onChange={() => toggleSeries(series.key)}
|
|
241
248
|
icon={<SeriesToggleDot color={palette[index % palette.length]} active={false} />}
|
|
242
249
|
checkedIcon={<SeriesToggleDot color={palette[index % palette.length]} active />}
|
|
243
|
-
sx={
|
|
244
|
-
color: palette[index % palette.length],
|
|
245
|
-
'&.Mui-checked': { color: palette[index % palette.length] },
|
|
246
|
-
}}
|
|
250
|
+
sx={seriesToggleSx(palette[index % palette.length])}
|
|
247
251
|
/>
|
|
248
252
|
)}
|
|
249
253
|
label={series.label}
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Container, Box, Typography } from '@mui/material';
|
|
4
4
|
|
|
5
|
+
export const PAGE_LAYOUT_CONTAINER_SX = { mt: 4 };
|
|
6
|
+
|
|
5
7
|
// Layout for content pages with larger width (e.g. Profile, Welcome, Input)
|
|
6
8
|
export const WidePage = ({ title, children, maxWidth = 'lg' }) => (
|
|
7
|
-
<Container maxWidth={maxWidth} sx={
|
|
9
|
+
<Container maxWidth={maxWidth} sx={PAGE_LAYOUT_CONTAINER_SX}>
|
|
8
10
|
{title && (
|
|
9
11
|
<Typography variant="h4" gutterBottom>
|
|
10
12
|
{title}
|
|
@@ -16,7 +18,7 @@ export const WidePage = ({ title, children, maxWidth = 'lg' }) => (
|
|
|
16
18
|
|
|
17
19
|
// Layout for forms or narrow pages (e.g. Login, Reset, Invite)
|
|
18
20
|
export const NarrowPage = ({ title, subtitle, children }) => (
|
|
19
|
-
<Container maxWidth="md" sx={
|
|
21
|
+
<Container maxWidth="md" sx={PAGE_LAYOUT_CONTAINER_SX}>
|
|
20
22
|
<Box sx={{ maxWidth: 480, mx: 'auto' }}>
|
|
21
23
|
{title && (
|
|
22
24
|
<Typography variant="h4" gutterBottom>
|
|
@@ -7,6 +7,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
7
7
|
|
|
8
8
|
import { useMessaging } from './MessagingProvider';
|
|
9
9
|
|
|
10
|
+
export const ATTACHMENT_LIST_LIGHTBOX_ACTION_SX = {
|
|
11
|
+
bgcolor: 'background.paper',
|
|
12
|
+
boxShadow: 1,
|
|
13
|
+
};
|
|
14
|
+
export const ATTACHMENT_LIST_ALERT_SX = { m: 2 };
|
|
15
|
+
|
|
10
16
|
function isImage(attachment) { return attachment.content_type?.startsWith('image/'); }
|
|
11
17
|
// `filename` is dcm's real, sanitized upload name (MSG-12) -- `id` is the
|
|
12
18
|
// last-resort fallback for interop with a host still on a pre-MSG-12 pin.
|
|
@@ -117,15 +123,15 @@ export function AttachmentList({ attachments = [] }) {
|
|
|
117
123
|
<Dialog open={Boolean(lightbox)} onClose={() => setLightbox(null)} maxWidth="lg" aria-label={lightbox ? nameOf(lightbox.attachment) : undefined}>
|
|
118
124
|
{lightbox && <Box sx={{ position: 'relative', minWidth: 240, minHeight: lightbox.status === 'ready' ? undefined : 160, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
119
125
|
<Stack direction="row" spacing={0.5} sx={{ position: 'absolute', top: 4, right: 4 }}>
|
|
120
|
-
{lightbox.status === 'ready' && <IconButton aria-label={t('MessagingAttachments.DOWNLOAD_ACTION')} onClick={() => download(lightbox.attachment)} sx={
|
|
126
|
+
{lightbox.status === 'ready' && <IconButton aria-label={t('MessagingAttachments.DOWNLOAD_ACTION')} onClick={() => download(lightbox.attachment)} sx={ATTACHMENT_LIST_LIGHTBOX_ACTION_SX}>
|
|
121
127
|
<DownloadIcon />
|
|
122
128
|
</IconButton>}
|
|
123
|
-
<IconButton aria-label={t('MessagingAttachments.CLOSE_PREVIEW')} onClick={() => setLightbox(null)} sx={
|
|
129
|
+
<IconButton aria-label={t('MessagingAttachments.CLOSE_PREVIEW')} onClick={() => setLightbox(null)} sx={ATTACHMENT_LIST_LIGHTBOX_ACTION_SX}>
|
|
124
130
|
<CloseIcon />
|
|
125
131
|
</IconButton>
|
|
126
132
|
</Stack>
|
|
127
133
|
{lightbox.status === 'loading' && <CircularProgress aria-label={t('MessagingAttachments.LOADING_PREVIEW')} sx={{ m: 4 }} />}
|
|
128
|
-
{lightbox.status === 'error' && <Alert severity="error" role="alert" sx={
|
|
134
|
+
{lightbox.status === 'error' && <Alert severity="error" role="alert" sx={ATTACHMENT_LIST_ALERT_SX}>{t('MessagingAttachments.PREVIEW_ERROR')}</Alert>}
|
|
129
135
|
{lightbox.status === 'ready' && <Box component="img" src={lightbox.url} alt={nameOf(lightbox.attachment)} sx={{ display: 'block', maxWidth: '90vw', maxHeight: '90vh' }} />}
|
|
130
136
|
</Box>}
|
|
131
137
|
</Dialog>
|
|
@@ -10,6 +10,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
10
10
|
import { compressImageForUpload } from './composerImageCompression';
|
|
11
11
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
12
12
|
|
|
13
|
+
export const COMPOSER_EMOJI_BUTTON_SX = { fontSize: '1.15rem' };
|
|
14
|
+
|
|
13
15
|
// A wider, Composer-only set (MSG-6i) -- deliberately separate from
|
|
14
16
|
// ReactionBar's own QUICK_EMOJIS (5, curated for fast reactions). Mobile
|
|
15
17
|
// users already have a full native emoji keyboard for free-form text; this
|
|
@@ -167,7 +169,7 @@ export function Composer({ conversationId, conversation, replyTarget = null, onR
|
|
|
167
169
|
*/}
|
|
168
170
|
<Box role="group" aria-label={t('MessagingComposer.EMOJI_PICKER')} sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 0.25, p: 0.5, maxWidth: 280 }}>
|
|
169
171
|
{COMPOSER_EMOJIS.map((emoji) => (
|
|
170
|
-
<IconButton key={emoji} type="button" size="small" onClick={() => insertEmoji(emoji)} sx={
|
|
172
|
+
<IconButton key={emoji} type="button" size="small" onClick={() => insertEmoji(emoji)} sx={COMPOSER_EMOJI_BUTTON_SX}>
|
|
171
173
|
{emoji}
|
|
172
174
|
</IconButton>
|
|
173
175
|
))}
|
|
@@ -18,6 +18,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
18
18
|
|
|
19
19
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
20
20
|
|
|
21
|
+
export const DIRECT_MESSAGE_LAUNCHER_ALERT_SX = { mb: 1 };
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Starts direct conversations from host-provided people. Candidate discovery is
|
|
23
25
|
* intentionally outside messaging because memberships are host-domain data.
|
|
@@ -70,7 +72,7 @@ export function DirectMessageLauncher({ candidates = [], scope, onOpen }) {
|
|
|
70
72
|
<Dialog open={open} onClose={closePicker} fullScreen={fullScreen} aria-labelledby={titleId}>
|
|
71
73
|
<DialogTitle id={titleId}>{t('MessagingDirect.TITLE')}</DialogTitle>
|
|
72
74
|
<DialogContent>
|
|
73
|
-
{error && <Alert severity="error" sx={
|
|
75
|
+
{error && <Alert severity="error" sx={DIRECT_MESSAGE_LAUNCHER_ALERT_SX}>{error}</Alert>}
|
|
74
76
|
{starting && <Typography role="status" sx={{ mb: 1 }}><CircularProgress size={16} sx={{ mr: 1 }} />{t('MessagingDirect.STARTING')}</Typography>}
|
|
75
77
|
{candidates.length === 0 ? (
|
|
76
78
|
<Typography>{t('MessagingDirect.EMPTY')}</Typography>
|
|
@@ -8,6 +8,29 @@ import { extractApiErrorMessage, useOptionalMessaging } from './MessagingProvide
|
|
|
8
8
|
import { ReactionBar } from './ReactionBar';
|
|
9
9
|
import { PollCard } from './PollCard';
|
|
10
10
|
|
|
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
|
+
};
|
|
33
|
+
|
|
11
34
|
function senderName(message) {
|
|
12
35
|
return message.sender?.display_name || message.sender?.username || message.sender_name || message.sender?.name;
|
|
13
36
|
}
|
|
@@ -103,16 +126,15 @@ export function MessageBubble({ message, replyTo, conversation, onReply, onJumpT
|
|
|
103
126
|
data-actions-visible={actionsVisible || undefined}
|
|
104
127
|
onClick={revealActions}
|
|
105
128
|
onContextMenu={canOpenMenu ? openMenu : undefined}
|
|
106
|
-
sx={
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}}
|
|
129
|
+
sx={[
|
|
130
|
+
MESSAGE_BUBBLE_ROOT_SX,
|
|
131
|
+
{
|
|
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
|
+
},
|
|
135
|
+
]}
|
|
114
136
|
>
|
|
115
|
-
{canOpenMenu && <IconButton className="message-actions" size="small" aria-label={t('MessagingActions.MENU')} onClick={openMenu} sx={
|
|
137
|
+
{canOpenMenu && <IconButton className="message-actions" size="small" aria-label={t('MessagingActions.MENU')} onClick={openMenu} sx={MESSAGE_BUBBLE_ACTION_SX}><MoreVertIcon fontSize="small" /></IconButton>}
|
|
116
138
|
<Stack spacing={0.75}>
|
|
117
139
|
{replyTo && <ButtonBase type="button" onClick={() => 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 }}>
|
|
118
140
|
<Stack spacing={0} alignItems="flex-start">
|
|
@@ -5,6 +5,10 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
|
|
6
6
|
import { extractApiErrorMessage, useMessaging } from './MessagingProvider';
|
|
7
7
|
|
|
8
|
+
export const POLL_CARD_ROOT_SX = { p: 1.5, minWidth: 260 };
|
|
9
|
+
export const POLL_CARD_FORM_CONTROL_SX = { width: '100%', m: 0, alignItems: 'flex-start' };
|
|
10
|
+
export const POLL_CARD_CHECKBOX_SX = { pt: 0.25 };
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* Independently mountable live poll view. Server authorization remains authoritative.
|
|
10
14
|
*
|
|
@@ -124,19 +128,17 @@ export function PollCard({ message, canClose: canCloseProp }) {
|
|
|
124
128
|
</Stack>;
|
|
125
129
|
return { accessibleLabel, richLabel };
|
|
126
130
|
};
|
|
127
|
-
const formControlSx = { width: '100%', m: 0, alignItems: 'flex-start' };
|
|
128
|
-
|
|
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 <Paper variant="outlined" sx={
|
|
135
|
+
return <Paper variant="outlined" sx={POLL_CARD_ROOT_SX} aria-label={t('MessagingPoll.LABEL')}><Stack spacing={1.5}>
|
|
134
136
|
<Typography variant="subtitle2">{poll.question}</Typography>
|
|
135
137
|
{poll.allow_multiple
|
|
136
138
|
? (poll.options || []).map((option) => {
|
|
137
139
|
const { accessibleLabel, richLabel } = optionLabel(option);
|
|
138
|
-
return <FormControlLabel key={option.id} disabled={closed} sx={
|
|
139
|
-
control={<Checkbox checked={selected.includes(option.id)} inputProps={{ 'aria-label': accessibleLabel }} onChange={() => tap(option.id)} sx={
|
|
140
|
+
return <FormControlLabel key={option.id} disabled={closed} sx={POLL_CARD_FORM_CONTROL_SX} label={richLabel}
|
|
141
|
+
control={<Checkbox checked={selected.includes(option.id)} inputProps={{ 'aria-label': accessibleLabel }} onChange={() => tap(option.id)} sx={POLL_CARD_CHECKBOX_SX} />} />;
|
|
140
142
|
})
|
|
141
143
|
// No RadioGroup-level onChange: it would only ever hand back
|
|
142
144
|
// `event.target.value`, a DOM string -- coercing a non-string option id
|
|
@@ -148,7 +150,7 @@ export function PollCard({ message, canClose: canCloseProp }) {
|
|
|
148
150
|
: <RadioGroup value={selected[0] ?? ''} name={`poll-${poll.id}-options`}>
|
|
149
151
|
{(poll.options || []).map((option) => {
|
|
150
152
|
const { accessibleLabel, richLabel } = optionLabel(option);
|
|
151
|
-
return <FormControlLabel key={option.id} value={option.id} disabled={closed} sx={
|
|
153
|
+
return <FormControlLabel key={option.id} value={option.id} disabled={closed} sx={POLL_CARD_FORM_CONTROL_SX} label={richLabel}
|
|
152
154
|
control={<Radio inputProps={{ 'aria-label': accessibleLabel }} onChange={() => tap(option.id)} sx={{ pt: 0.25 }} />} />;
|
|
153
155
|
})}
|
|
154
156
|
</RadioGroup>}
|
package/src/messaging/Thread.jsx
CHANGED
|
@@ -8,6 +8,8 @@ import { MESSAGING_ENVELOPE, useMessaging } from './MessagingProvider';
|
|
|
8
8
|
import { ReadTicks } from './ReadTicks';
|
|
9
9
|
import { useRealtime } from '../notifications/realtime';
|
|
10
10
|
|
|
11
|
+
export const THREAD_DAY_SEPARATOR_SX = { flex: 1 };
|
|
12
|
+
|
|
11
13
|
function chronological(messages) { return [...messages].sort((left, right) => new Date(left.created_at || 0) - new Date(right.created_at || 0)); }
|
|
12
14
|
function replyToId(message) { return message.reply_to_id ?? message.reply_to; }
|
|
13
15
|
// last_reply_at/reply_count are viewer-independent (always on serialize_message);
|
|
@@ -174,7 +176,7 @@ export function Thread({ conversationId, onReplyTargetChange, canModerateMessage
|
|
|
174
176
|
no vertical overflow). */}
|
|
175
177
|
<Stack spacing={1} sx={{ pr: 2 }}>{datedRoots.map(({ message, showDateSeparator }) => {
|
|
176
178
|
const replies = chronological(Object.values(cache.messages).filter((item) => String(replyToId(item)) === String(message.id)));
|
|
177
|
-
return <Stack key={message.id} spacing={0.75}>{showDateSeparator && <Stack data-testid="day-separator" direction="row" alignItems="center" spacing={1}><Divider sx={
|
|
179
|
+
return <Stack key={message.id} spacing={0.75}>{showDateSeparator && <Stack data-testid="day-separator" direction="row" alignItems="center" spacing={1}><Divider sx={THREAD_DAY_SEPARATOR_SX} /><Typography variant="caption" color="text.secondary">{new Date(message.created_at).toLocaleDateString(i18n?.language)}</Typography><Divider sx={THREAD_DAY_SEPARATOR_SX} /></Stack>}<MessageBubble message={message} conversation={conversation} onReply={reply} onJumpToMessage={jumpToMessage} onAnnouncementLink={onAnnouncementLink} canModerateMessages={canModerateMessages}>{canShowReadTicks(message, user) && <ReadTicks messageId={message.id} conversation={conversation} />}</MessageBubble>
|
|
178
180
|
{(message.reply_count || replies.length) > 0 && (() => {
|
|
179
181
|
const unread = hasUnreadReplies(message, cache.receipts, user?.id);
|
|
180
182
|
const label = openThreads[message.id] ? t('MessagingThread.HIDE_REPLIES') : t('MessagingThread.SHOW_REPLIES', { count: message.reply_count || replies.length });
|
|
@@ -18,6 +18,21 @@ import {
|
|
|
18
18
|
urlBase64ToUint8Array,
|
|
19
19
|
} from './api';
|
|
20
20
|
|
|
21
|
+
export const NOTIFICATION_SETTINGS_ALERT_SX = { mb: 2 };
|
|
22
|
+
export const NOTIFICATION_SETTINGS_HINT_ALERT_SX = { mb: 1.5 };
|
|
23
|
+
export const NOTIFICATION_SETTINGS_REACH_ALERT_SX = { mb: 1 };
|
|
24
|
+
export const NOTIFICATION_SETTINGS_FORM_CONTROL_SX = { alignItems: 'flex-start', ml: 0 };
|
|
25
|
+
export const NOTIFICATION_SETTINGS_PUSH_CONTROL_SX = {
|
|
26
|
+
alignItems: 'flex-start',
|
|
27
|
+
ml: 0,
|
|
28
|
+
mt: 1.5,
|
|
29
|
+
};
|
|
30
|
+
export const NOTIFICATION_SETTINGS_CATEGORY_CONTROL_SX = {
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
ml: 0,
|
|
33
|
+
display: 'flex',
|
|
34
|
+
};
|
|
35
|
+
|
|
21
36
|
function reachLabelKey(notificationType) {
|
|
22
37
|
const noActiveChannel = notificationType.active && notificationType.has_active_channel === false;
|
|
23
38
|
if (notificationType.active && notificationType.passive) {
|
|
@@ -210,23 +225,23 @@ export function NotificationSettings() {
|
|
|
210
225
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
211
226
|
{t('NotificationSettings.SUBTITLE')}
|
|
212
227
|
</Typography>
|
|
213
|
-
{error && <Alert severity="error" sx={
|
|
214
|
-
{conflict && <Alert severity="warning" sx={
|
|
228
|
+
{error && <Alert severity="error" sx={NOTIFICATION_SETTINGS_ALERT_SX} onClose={() => setError('')}>{error}</Alert>}
|
|
229
|
+
{conflict && <Alert severity="warning" sx={NOTIFICATION_SETTINGS_ALERT_SX} onClose={() => setConflict('')}>{conflict}</Alert>}
|
|
215
230
|
|
|
216
231
|
<Box sx={{ py: 2 }}>
|
|
217
232
|
<FormControlLabel
|
|
218
233
|
control={<Switch checked={Boolean(preferences?.email_opt_in)} onChange={handleEmailToggle} disabled={savingEmail} />}
|
|
219
234
|
label={<Box><Typography variant="body1">{t('NotificationSettings.EMAIL_LABEL')}</Typography><Typography variant="caption" color="text.secondary">{t('NotificationSettings.EMAIL_HINT')}</Typography></Box>}
|
|
220
235
|
labelPlacement="end"
|
|
221
|
-
sx={
|
|
236
|
+
sx={NOTIFICATION_SETTINGS_FORM_CONTROL_SX}
|
|
222
237
|
/>
|
|
223
238
|
</Box>
|
|
224
239
|
|
|
225
240
|
<Divider />
|
|
226
241
|
|
|
227
242
|
<Box sx={{ py: 2 }}>
|
|
228
|
-
{iosNeedsInstall && <Alert severity="info" sx={
|
|
229
|
-
{!pushSupported && !iosNeedsInstall && <Alert severity="warning" sx={
|
|
243
|
+
{iosNeedsInstall && <Alert severity="info" sx={NOTIFICATION_SETTINGS_HINT_ALERT_SX}>{t('NotificationSettings.IOS_HINT')}</Alert>}
|
|
244
|
+
{!pushSupported && !iosNeedsInstall && <Alert severity="warning" sx={NOTIFICATION_SETTINGS_HINT_ALERT_SX}>{t('NotificationSettings.PUSH_NOT_SUPPORTED')}</Alert>}
|
|
230
245
|
<FormControlLabel
|
|
231
246
|
control={(
|
|
232
247
|
<Switch
|
|
@@ -237,7 +252,7 @@ export function NotificationSettings() {
|
|
|
237
252
|
)}
|
|
238
253
|
label={<Box><Typography variant="body1">{t('NotificationSettings.PUSH_LABEL')}</Typography><Typography variant="caption" color="text.secondary">{t('NotificationSettings.PUSH_HINT')}</Typography></Box>}
|
|
239
254
|
labelPlacement="end"
|
|
240
|
-
sx={
|
|
255
|
+
sx={NOTIFICATION_SETTINGS_FORM_CONTROL_SX}
|
|
241
256
|
/>
|
|
242
257
|
<FormControlLabel
|
|
243
258
|
control={(
|
|
@@ -249,7 +264,7 @@ export function NotificationSettings() {
|
|
|
249
264
|
)}
|
|
250
265
|
label={<Box><Typography variant="body1">{t('NotificationSettings.PUSH_PREVIEW_LABEL')}</Typography><Typography variant="caption" color="text.secondary">{t('NotificationSettings.PUSH_PREVIEW_HINT')}</Typography></Box>}
|
|
251
266
|
labelPlacement="end"
|
|
252
|
-
sx={
|
|
267
|
+
sx={NOTIFICATION_SETTINGS_PUSH_CONTROL_SX}
|
|
253
268
|
/>
|
|
254
269
|
</Box>
|
|
255
270
|
|
|
@@ -266,7 +281,7 @@ export function NotificationSettings() {
|
|
|
266
281
|
// reach description is static settings copy, not a status message.
|
|
267
282
|
if (labelKey === 'NotificationSettings.REACH_NO_ACTIVE_CHANNEL') {
|
|
268
283
|
return (
|
|
269
|
-
<Alert key={notificationType.key} severity="warning" sx={
|
|
284
|
+
<Alert key={notificationType.key} severity="warning" sx={NOTIFICATION_SETTINGS_REACH_ALERT_SX}>
|
|
270
285
|
<strong>{notificationType.label}</strong> — {reachText}
|
|
271
286
|
</Alert>
|
|
272
287
|
);
|
|
@@ -302,7 +317,7 @@ export function NotificationSettings() {
|
|
|
302
317
|
)}
|
|
303
318
|
label={row.label}
|
|
304
319
|
labelPlacement="end"
|
|
305
|
-
sx={
|
|
320
|
+
sx={NOTIFICATION_SETTINGS_CATEGORY_CONTROL_SX}
|
|
306
321
|
/>
|
|
307
322
|
))}
|
|
308
323
|
</Box>
|
|
@@ -8,6 +8,8 @@ import { WizardDialogShell } from '../onboarding/WizardDialogShell';
|
|
|
8
8
|
import { useNotifications } from './NotificationsProvider';
|
|
9
9
|
import { DEFAULT_ENVELOPE, useRealtime } from './realtime';
|
|
10
10
|
|
|
11
|
+
export const POPUP_SURFACE_DISMISS_BUTTON_SX = { alignSelf: 'flex-end' };
|
|
12
|
+
|
|
11
13
|
function PopupStepComponent({ onDismiss, ctx }) {
|
|
12
14
|
const { t } = useTranslation();
|
|
13
15
|
const content = ctx?.content || {};
|
|
@@ -17,7 +19,7 @@ function PopupStepComponent({ onDismiss, ctx }) {
|
|
|
17
19
|
{content.body_key && (
|
|
18
20
|
<Typography variant="body1">{t(content.body_key, content.params || {})}</Typography>
|
|
19
21
|
)}
|
|
20
|
-
<Button onClick={onDismiss} sx={
|
|
22
|
+
<Button onClick={onDismiss} sx={POPUP_SURFACE_DISMISS_BUTTON_SX}>
|
|
21
23
|
{t('PopupSurface.CLOSE')}
|
|
22
24
|
</Button>
|
|
23
25
|
</Stack>
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
urlBase64ToUint8Array,
|
|
17
17
|
} from '../../notifications/api';
|
|
18
18
|
|
|
19
|
+
export const BROWSER_PUSH_FORM_CONTROL_LABEL_SX = { alignItems: 'flex-start', ml: 0 };
|
|
20
|
+
|
|
19
21
|
export function BrowserPushStep({ onComplete, onDismiss, ctx }) {
|
|
20
22
|
const { t } = useTranslation();
|
|
21
23
|
const [loading, setLoading] = useState(false);
|
|
@@ -92,7 +94,7 @@ export function BrowserPushStep({ onComplete, onDismiss, ctx }) {
|
|
|
92
94
|
control={<Switch checked={emailOptedIn} onChange={handleEmailToggle} disabled={savingEmail} />}
|
|
93
95
|
label={<Box><Typography variant="body1">{t('Onboarding.EMAIL_NOTIFICATIONS_LABEL')}</Typography><Typography variant="caption" color="text.secondary">{t('Onboarding.EMAIL_NOTIFICATIONS_HINT')}</Typography></Box>}
|
|
94
96
|
labelPlacement="end"
|
|
95
|
-
sx={
|
|
97
|
+
sx={BROWSER_PUSH_FORM_CONTROL_LABEL_SX}
|
|
96
98
|
/>
|
|
97
99
|
{pushSupported && (
|
|
98
100
|
<Box>
|
|
@@ -16,6 +16,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
16
16
|
// Internal context
|
|
17
17
|
import { AuthContext } from '../auth/AuthContext';
|
|
18
18
|
|
|
19
|
+
export const ACCOUNT_PAGE_SECTION_PAPER_SX = { p: 2.5 };
|
|
20
|
+
|
|
19
21
|
// Falls die Komponenten noch lokal sind:
|
|
20
22
|
import { WidePage } from '../layout/PageLayout';
|
|
21
23
|
import { ProfileComponent } from '../components/ProfileComponent';
|
|
@@ -250,13 +252,13 @@ export function AccountPage({
|
|
|
250
252
|
<Box sx={{ mt: 2 }}>
|
|
251
253
|
<Stack spacing={2.5}>
|
|
252
254
|
{canViewAuthPolicy && (
|
|
253
|
-
<Paper variant="outlined" sx={
|
|
255
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
254
256
|
<AuthFactorRequirementCard canEdit={canWriteAuthPolicy} policy={authPolicy} />
|
|
255
257
|
</Paper>
|
|
256
258
|
)}
|
|
257
259
|
|
|
258
260
|
{canViewAuthPolicy && (
|
|
259
|
-
<Paper variant="outlined" sx={
|
|
261
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
260
262
|
<RegistrationMethodsManager
|
|
261
263
|
policy={authPolicy}
|
|
262
264
|
error={authPolicyError}
|
|
@@ -267,13 +269,13 @@ export function AccountPage({
|
|
|
267
269
|
)}
|
|
268
270
|
|
|
269
271
|
{canSendInvites && Boolean(authPolicy?.allow_admin_invite) && (
|
|
270
|
-
<Paper variant="outlined" sx={
|
|
272
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
271
273
|
<UserInviteComponent />
|
|
272
274
|
</Paper>
|
|
273
275
|
)}
|
|
274
276
|
|
|
275
277
|
{canManageAccessCodes && Boolean(authPolicy?.allow_self_signup_access_code) && (
|
|
276
|
-
<Paper variant="outlined" sx={
|
|
278
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
277
279
|
<Typography variant="h6" gutterBottom>
|
|
278
280
|
{t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes')}
|
|
279
281
|
</Typography>
|
|
@@ -285,7 +287,7 @@ export function AccountPage({
|
|
|
285
287
|
)}
|
|
286
288
|
|
|
287
289
|
{canManageAccessCodes && Boolean(authPolicy?.allow_self_signup_access_code) && (
|
|
288
|
-
<Paper variant="outlined" sx={
|
|
290
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
289
291
|
<AccessCodeSingleUseToggle
|
|
290
292
|
canEdit={canWriteAuthPolicy}
|
|
291
293
|
policy={authPolicy}
|
|
@@ -295,13 +297,13 @@ export function AccountPage({
|
|
|
295
297
|
)}
|
|
296
298
|
|
|
297
299
|
{canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy?.allow_admin_invite) && (
|
|
298
|
-
<Paper variant="outlined" sx={
|
|
300
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
299
301
|
<BulkInviteCsvTab {...bulkInviteCsvProps} />
|
|
300
302
|
</Paper>
|
|
301
303
|
)}
|
|
302
304
|
|
|
303
305
|
{canViewAuthPolicy && Boolean(authPolicy?.allow_self_signup_email_domain) && (
|
|
304
|
-
<Paper variant="outlined" sx={
|
|
306
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
305
307
|
<AllowedEmailDomainsManager
|
|
306
308
|
enabled={Boolean(authPolicy?.allow_self_signup_email_domain)}
|
|
307
309
|
domains={authPolicy?.allowed_email_domains || []}
|
|
@@ -312,7 +314,7 @@ export function AccountPage({
|
|
|
312
314
|
)}
|
|
313
315
|
|
|
314
316
|
{canViewAuthPolicy && Boolean(authPolicy?.allow_self_signup_qr) && (
|
|
315
|
-
<Paper variant="outlined" sx={
|
|
317
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
316
318
|
<QrSignupValidityManager
|
|
317
319
|
enabled={Boolean(authPolicy?.allow_self_signup_qr)}
|
|
318
320
|
expiryDays={authPolicy?.signup_qr_expiry_days}
|
|
@@ -323,7 +325,7 @@ export function AccountPage({
|
|
|
323
325
|
)}
|
|
324
326
|
|
|
325
327
|
{canManageSignupQr && Boolean(authPolicy?.allow_self_signup_qr) && (
|
|
326
|
-
<Paper variant="outlined" sx={
|
|
328
|
+
<Paper variant="outlined" sx={ACCOUNT_PAGE_SECTION_PAPER_SX}>
|
|
327
329
|
<QrSignupManager
|
|
328
330
|
enabled={Boolean(authPolicy?.allow_self_signup_qr)}
|
|
329
331
|
expiryDays={authPolicy?.signup_qr_expiry_days}
|
package/src/pages/LoginPage.jsx
CHANGED
|
@@ -20,6 +20,8 @@ import { loginWithPasskey, startSocialLogin } from '../utils/authService';
|
|
|
20
20
|
import { LoginForm } from '../components/LoginForm';
|
|
21
21
|
import { MfaLoginComponent } from '../components/MfaLoginComponent';
|
|
22
22
|
|
|
23
|
+
export const LOGIN_PAGE_ALERT_SX = { mb: 2 };
|
|
24
|
+
|
|
23
25
|
export function LoginPage() {
|
|
24
26
|
const navigate = useNavigate();
|
|
25
27
|
const location = useLocation();
|
|
@@ -284,25 +286,25 @@ export function LoginPage() {
|
|
|
284
286
|
</Helmet>
|
|
285
287
|
|
|
286
288
|
{errorKey && (
|
|
287
|
-
<Alert severity="error" sx={
|
|
289
|
+
<Alert severity="error" sx={LOGIN_PAGE_ALERT_SX}>
|
|
288
290
|
{t(errorKey)}
|
|
289
291
|
</Alert>
|
|
290
292
|
)}
|
|
291
293
|
|
|
292
294
|
{recoveryFetching && !errorKey && (
|
|
293
|
-
<Alert severity="info" sx={
|
|
295
|
+
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
294
296
|
{t('Auth.RECOVERY_LINK_VALIDATING', 'Validating recovery link…')}
|
|
295
297
|
</Alert>
|
|
296
298
|
)}
|
|
297
299
|
|
|
298
300
|
{recoveryToken && !errorKey && (
|
|
299
|
-
<Alert severity="info" sx={
|
|
301
|
+
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
300
302
|
{t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.')}
|
|
301
303
|
</Alert>
|
|
302
304
|
)}
|
|
303
305
|
|
|
304
306
|
{twoFactorRequired && !recoveryToken && !recoveryFetching && (
|
|
305
|
-
<Alert severity="info" sx={
|
|
307
|
+
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
306
308
|
{t('Auth.TWO_FACTOR_REQUIRED_HINT', 'This app requires two authentication factors for full access.')}
|
|
307
309
|
</Alert>
|
|
308
310
|
)}
|
package/src/pages/SignUpPage.jsx
CHANGED
|
@@ -21,6 +21,8 @@ import { AuthContext } from '../auth/AuthContext';
|
|
|
21
21
|
import { submitRegistrationRequest } from '../auth/authApi';
|
|
22
22
|
import { TurnstileWidget } from '../components/TurnstileWidget';
|
|
23
23
|
|
|
24
|
+
export const SIGN_UP_PAGE_ALERT_SX = { mb: 2 };
|
|
25
|
+
|
|
24
26
|
const TURNSTILE_SIGNUP_MODES = new Set([
|
|
25
27
|
'self_signup_open',
|
|
26
28
|
'self_signup_email_domain',
|
|
@@ -162,13 +164,13 @@ export function SignUpPage() {
|
|
|
162
164
|
</Helmet>
|
|
163
165
|
|
|
164
166
|
{successKey && (
|
|
165
|
-
<Alert severity="success" sx={
|
|
167
|
+
<Alert severity="success" sx={SIGN_UP_PAGE_ALERT_SX}>
|
|
166
168
|
{t(successKey, { email })}
|
|
167
169
|
</Alert>
|
|
168
170
|
)}
|
|
169
171
|
|
|
170
172
|
{errorKey && (
|
|
171
|
-
<Alert severity="error" sx={
|
|
173
|
+
<Alert severity="error" sx={SIGN_UP_PAGE_ALERT_SX}>
|
|
172
174
|
{t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.'))}
|
|
173
175
|
</Alert>
|
|
174
176
|
)}
|
|
@@ -20,6 +20,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
20
20
|
import { NarrowPage } from '../layout/PageLayout';
|
|
21
21
|
import { confirmRegistration } from '../auth/authApi';
|
|
22
22
|
|
|
23
|
+
export const SIGNUP_CONFIRM_PAGE_ALERT_SX = { mb: 2 };
|
|
24
|
+
|
|
23
25
|
export function SignupConfirmPage() {
|
|
24
26
|
const { t } = useTranslation();
|
|
25
27
|
const location = useLocation();
|
|
@@ -85,7 +87,7 @@ export function SignupConfirmPage() {
|
|
|
85
87
|
|
|
86
88
|
{tokenMissing ? (
|
|
87
89
|
<>
|
|
88
|
-
<Alert severity="error" sx={
|
|
90
|
+
<Alert severity="error" sx={SIGNUP_CONFIRM_PAGE_ALERT_SX}>
|
|
89
91
|
{t('Auth.PENDING_TOKEN_INVALID', 'This confirmation link is invalid or expired.')}
|
|
90
92
|
</Alert>
|
|
91
93
|
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
|
|
@@ -100,7 +102,7 @@ export function SignupConfirmPage() {
|
|
|
100
102
|
) : (
|
|
101
103
|
<>
|
|
102
104
|
{errorKey && (
|
|
103
|
-
<Alert severity="error" sx={
|
|
105
|
+
<Alert severity="error" sx={SIGNUP_CONFIRM_PAGE_ALERT_SX}>
|
|
104
106
|
{t(errorKey, t('Auth.PENDING_TOKEN_INVALID', 'Could not confirm registration.'))}
|
|
105
107
|
</Alert>
|
|
106
108
|
)}
|
package/src/theme/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { createAppTheme } from './createAppTheme';
|
|
2
2
|
export {
|
|
3
3
|
assertThemeComplete,
|
|
4
|
+
assertKitSxDisjoint,
|
|
4
5
|
calculateContrastRatio,
|
|
6
|
+
reportKitSxBypasses,
|
|
5
7
|
reportThemeAdoption,
|
|
6
8
|
THEME_COMPLETENESS_SURFACES,
|
|
7
9
|
} from './themeCompleteness';
|
|
10
|
+
export { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
|
8
11
|
export {
|
|
9
12
|
formatShortTime,
|
|
10
13
|
formatShortDate,
|