UI/update global UI (#459)

* Update loading screen to use Lottie animation

- Replace simple logo with Lottie animation from loadercalendar.lottie
- Center animation on screen with full viewport coverage
- Set animation width to 175px
- Install @lottiefiles/dotlottie-react package

* update spacing event modal

* add twake workplace logo to loading screen

* update spacing create calendar modal

* fix evenchip description color

* using variant instead of sx
This commit is contained in:
lenhanphung
2026-01-20 15:14:03 +07:00
committed by GitHub
parent 00c3c0d6f0
commit aeb8670895
21 changed files with 326 additions and 165 deletions
+4 -2
View File
@@ -88,7 +88,8 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
<TextField
disabled
fullWidth
label={t("calendar.caldav_access")}
label=""
inputProps={{ "aria-label": t("calendar.caldav_access") }}
value={calDAVLink}
size="small"
InputProps={{
@@ -109,7 +110,8 @@ export function AccessTab({ calendar }: { calendar: Calendar }) {
<TextField
disabled
fullWidth
label={t("calendar.secretUrl")}
label=""
inputProps={{ "aria-label": t("calendar.secretUrl") }}
value={secretLink}
size="small"
InputProps={{
@@ -359,3 +359,7 @@ tbody > tr.fc-scrollgrid-section:first-of-type .fc-scroller
position absolute
width 1px
whiteSpace nowrap
.date-time-group .MuiFormControl-root
margin: 0
.date-time-group.show-full-field > .MuiBox-root:first-of-type
margin-bottom: 8px
+73 -41
View File
@@ -53,27 +53,30 @@ export function ImportTab({
}, [importFile, importUrl, importMode]);
return (
<Box mt={2}>
<>
{/* Form group 1: Select file button - first group, margin top 0 */}
{importMode === "file" && (
<>
<Box mb={1}>
<Button
variant="outlined"
component="label"
size="medium"
sx={{ borderRadius: "12px" }}
>
{t("common.select_file")}
<input
type="file"
hidden
accept=".ics"
onChange={(e) => setImportFile(e.target.files?.[0] ?? null)}
/>
</Button>
</Box>
<Box mt={0}>
<Button
variant="outlined"
component="label"
size="medium"
sx={{ borderRadius: "12px" }}
>
{t("common.select_file")}
<input
type="file"
hidden
accept=".ics"
onChange={(e) => setImportFile(e.target.files?.[0] ?? null)}
/>
</Button>
{importFile && (
<Typography variant="body2" color="text.secondary">
<Typography
variant="body2"
color="text.secondary"
sx={{ marginTop: "6px" }}
>
{importFile.name}
</Typography>
)}
@@ -81,38 +84,67 @@ export function ImportTab({
variant="caption"
color="text.secondary"
display="block"
mb={2}
sx={{ marginTop: "6px" }}
>
{t("calendar.import_file_description")}
</Typography>
</>
</Box>
)}
{/* Form group 2: URL field */}
{importMode === "url" && (
<TextField
<Box mt={0}>
<TextField
fullWidth
label={t("calendar.ics_feed_url")}
value={importUrl}
onChange={(e) => setImportUrl(e.target.value)}
size="small"
margin="dense"
sx={{
"&.MuiFormControl-root.MuiFormControl-marginDense": {
marginTop: "6px",
marginBottom: 0,
},
}}
/>
</Box>
)}
{/* Form group 3: Import to */}
<Box mt={2}>
<FormControl
fullWidth
label={t("calendar.ics_feed_url")}
value={importUrl}
onChange={(e) => setImportUrl(e.target.value)}
size="small"
margin="dense"
/>
)}
<FormControl fullWidth size="small" sx={{ mt: 2 }}>
<InputLabel id="import-to-label">{t("calendar.import_to")}</InputLabel>
<Select
labelId="import-to-label"
label={t("calendar.import_to")}
value={importTarget}
onChange={(e) => setImportTarget(e.target.value)}
sx={{
"&.MuiFormControl-root.MuiFormControl-marginDense": {
marginTop: "6px",
marginBottom: 0,
},
}}
>
<MenuItem value="new">{t("calendar.new_calendar")}</MenuItem>
{CalendarItemList(personalCalendars)}
</Select>
</FormControl>
<InputLabel id="import-to-label">
{t("calendar.import_to")}
</InputLabel>
<Select
labelId="import-to-label"
label={t("calendar.import_to")}
value={importTarget}
onChange={(e) => setImportTarget(e.target.value)}
>
<MenuItem value="new">{t("calendar.new_calendar")}</MenuItem>
{CalendarItemList(personalCalendars)}
</Select>
</FormControl>
</Box>
{importTarget === "new" && <SettingsTab {...newCalParams} />}
</Box>
{/* Form group 4: SettingsTab (when importing to new calendar) */}
{importTarget === "new" && (
<Box mt={2}>
<SettingsTab {...newCalParams} />
</Box>
)}
</>
);
}
+64 -47
View File
@@ -49,62 +49,79 @@ export function SettingsTab({
}, [description]);
return (
<Box mt={2}>
<TextField
fullWidth
label=""
inputProps={{ "aria-label": t("common.name") }}
placeholder={t("common.name")}
value={name}
onChange={(e) => setName(e.target.value)}
size="small"
margin="dense"
/>
<AddDescButton
showDescription={toggleDesc}
setShowDescription={setToggleDesc}
showMore={false}
description={description}
setDescription={setDescription}
buttonVariant="contained"
buttonColor="secondary"
/>
<Box mt={2}>
<Typography variant="body2" gutterBottom>
{t("calendar.color")}
</Typography>
<ColorPicker
onChange={(color) => setColor(color)}
selectedColor={color}
<>
{/* Form group 1: Name field - first group, margin top 0 */}
<Box mt={0}>
<TextField
fullWidth
label=""
inputProps={{ "aria-label": t("common.name") }}
placeholder={t("common.name")}
value={name}
onChange={(e) => setName(e.target.value)}
size="small"
sx={{
"&.MuiFormControl-root": {
marginTop: 0,
marginBottom: 0,
},
}}
/>
</Box>
{/* Form group 2: Description */}
<Box mt={2}>
<AddDescButton
showDescription={toggleDesc}
setShowDescription={setToggleDesc}
showMore={false}
description={description}
setDescription={setDescription}
buttonVariant="contained"
buttonColor="secondary"
/>
</Box>
{/* Form group 3: Color */}
<Box mt={2}>
<Typography variant="h6" sx={{ margin: 0 }}>
{t("calendar.color")}
</Typography>
<Box sx={{ marginTop: "6px" }}>
<ColorPicker
onChange={(color) => setColor(color)}
selectedColor={color}
/>
</Box>
</Box>
{/* Form group 4: New events visibility */}
{isOwn && (
<Box mt={2}>
<Typography variant="body2" gutterBottom>
<Typography variant="h6" sx={{ margin: 0 }}>
{t("calendar.newEventsVisibility")}
</Typography>
<ToggleButtonGroup
value={visibility}
exclusive
onChange={(e, val) => val && setVisibility(val)}
size="medium"
sx={{ borderRadius: "12px" }}
>
<ToggleButton value="public" sx={{ width: "140px" }}>
<PublicIcon fontSize="small" sx={{ mr: 1 }} />
{t("common.all")}
</ToggleButton>
<Box sx={{ marginTop: "6px" }}>
<ToggleButtonGroup
value={visibility}
exclusive
onChange={(e, val) => val && setVisibility(val)}
size="medium"
sx={{ borderRadius: "12px" }}
>
<ToggleButton value="public" sx={{ width: "140px" }}>
<PublicIcon fontSize="small" sx={{ mr: 1 }} />
{t("common.all")}
</ToggleButton>
<ToggleButton value="private" sx={{ width: "140px" }}>
<LockOutlineIcon fontSize="small" sx={{ mr: 1 }} />
{t("common.you")}
</ToggleButton>
</ToggleButtonGroup>
<ToggleButton value="private" sx={{ width: "140px" }}>
<LockOutlineIcon fontSize="small" sx={{ mr: 1 }} />
{t("common.you")}
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
)}
</Box>
</>
);
}
+4 -4
View File
@@ -9,7 +9,7 @@ A highly reusable dialog component that supports both normal and expanded (fulls
-**Clean Expanded View**: No backdrop/shadow in expanded mode for seamless integration
-**Instant Transition**: No animation when expanding for immediate feedback
-**Back Navigation**: Expanded mode shows back arrow icon in header for easy collapse
-**MUI Stack Spacing**: Uses MUI Stack component with configurable spacing prop (2=16px normal, 3=24px expanded)
-**MUI Stack Spacing**: Uses MUI Stack component with configurable spacing prop (2=16px for both normal and expanded modes)
-**Fully Customizable**: Override styles with `sx`, `contentSx`, `titleSx` props
-**Container Support**: Content container with configurable max-width
-**Type Safe**: Full TypeScript support
@@ -45,7 +45,7 @@ function MyComponent() {
>
<TextField label="Name" fullWidth />
<TextField label="Email" fullWidth />
{/* Wrapped in Stack with spacing={2} (normal) or spacing={3} (expanded) */}
{/* Wrapped in Stack with spacing={2} for both normal and expanded modes */}
</ResponsiveDialog>
);
}
@@ -66,7 +66,7 @@ function MyComponent() {
| `expandedContentMaxWidth` | `string` | `"990px"` | Content container max-width in expanded mode |
| `headerHeight` | `string` | `"90px"` | App header height to preserve |
| `normalSpacing` | `number` | `2` | Stack spacing in normal mode (MUI spacing units: 1 = 8px) |
| `expandedSpacing` | `number` | `3` | Stack spacing in expanded mode (MUI spacing units: 1 = 8px) |
| `expandedSpacing` | `number` | `2` | Stack spacing in expanded mode (MUI spacing units: 1 = 8px) |
| `contentSx` | `SxProps<Theme>` | - | Custom styles for DialogContent |
| `titleSx` | `SxProps<Theme>` | - | Custom styles for DialogTitle |
| `dialogContentProps` | `DialogContentProps` | - | Additional DialogContent props |
@@ -175,7 +175,7 @@ For apps with different header heights:
- Shadow: removed (no elevation in expanded mode)
- Transition: disabled (instant expand/collapse for better UX)
- Title: replaced by back arrow IconButton (calls `onExpandToggle`)
- Children spacing: `expandedSpacing={3}` (24px via MUI Stack component)
- Children spacing: `expandedSpacing={2}` (16px via MUI Stack component)
- Actions (MuiBox): max-width = `expandedContentMaxWidth` (990px), centered container with buttons right-aligned
- padding: 0 12px
- width: 100%
+2 -2
View File
@@ -66,7 +66,7 @@ interface ResponsiveDialogProps extends Omit<
headerHeight?: string;
/** Spacing between children in normal mode (default: 2 = 16px) */
normalSpacing?: number;
/** Spacing between children in expanded mode (default: 3 = 24px) */
/** Spacing between children in expanded mode (default: 2 = 16px) */
expandedSpacing?: number;
/** Custom styles for DialogContent - merged with base styles */
contentSx?: SxProps<Theme>;
@@ -102,7 +102,7 @@ function ResponsiveDialog({
expandedContentMaxWidth = "990px",
headerHeight = "90px",
normalSpacing = 2,
expandedSpacing = 3,
expandedSpacing = 2,
contentSx,
titleSx,
dialogContentProps,
+1 -1
View File
@@ -32,7 +32,7 @@ export function AddDescButton({
<>
{!showDescription && (
<FieldWithLabel label=" " isExpanded={showMore}>
<Box display="flex" gap={1} mb={1}>
<Box display="flex" gap={1}>
<Button
startIcon={<DescriptionIcon />}
onClick={() => setShowDescription(true)}
@@ -245,6 +245,7 @@ export function EventChip({
whiteSpace: "normal",
flex: 1,
maxWidth: "75%",
color: titleStyle.color,
}}
>
{event._def.extendedProps.description}
+7 -2
View File
@@ -13,6 +13,7 @@ import {
TextField,
ToggleButtonGroup,
ToggleButton,
Typography,
} from "@linagora/twake-mui";
import {
Description as DescriptionIcon,
@@ -481,7 +482,9 @@ export default function EventFormFields({
control={
<Checkbox checked={allday} onChange={handleAllDayToggle} />
}
label={t("event.form.allDay")}
label={
<Typography variant="h6">{t("event.form.allDay")}</Typography>
}
/>
<FormControlLabel
control={
@@ -513,7 +516,9 @@ export default function EventFormFields({
}}
/>
}
label={t("event.form.repeat")}
label={
<Typography variant="h6">{t("event.form.repeat")}</Typography>
}
/>
<TimezoneAutocomplete
value={timezone}
+16 -6
View File
@@ -80,7 +80,7 @@ export default function RepeatEvent({
<Stack>
{/* Interval */}
<Box display="flex" alignItems="center" gap={2} mb={2}>
<Typography>{t("event.repeat.repeatEvery")}</Typography>
<Typography variant="h6">{t("event.repeat.repeatEvery")}</Typography>
<TextField
type="number"
value={repetition.interval ?? 1}
@@ -161,7 +161,7 @@ export default function RepeatEvent({
{/* End options */}
<Box>
<Typography variant="body2" gutterBottom>
<Typography variant="h6" gutterBottom>
{t("event.repeat.end.label")}
</Typography>
<RadioGroup
@@ -193,7 +193,11 @@ export default function RepeatEvent({
disabled={!isOwn}
value="never"
control={<Radio />}
label={t("event.repeat.end.never")}
label={
<Typography variant="h6">
{t("event.repeat.end.never")}
</Typography>
}
/>
<FormControlLabel
@@ -202,7 +206,9 @@ export default function RepeatEvent({
control={<Radio />}
label={
<Box display="flex" alignItems="center" gap={1}>
{t("event.repeat.end.after")}
<Typography variant="h6">
{t("event.repeat.end.after")}
</Typography>
<TextField
type="number"
size="small"
@@ -218,7 +224,9 @@ export default function RepeatEvent({
inputProps={{ min: 1, "data-testid": "occurrences-input" }}
disabled={!isOwn || endOption !== "after"}
/>
{t("event.repeat.end.occurrences")}
<Typography variant="h6">
{t("event.repeat.end.occurrences")}
</Typography>
</Box>
}
/>
@@ -229,7 +237,9 @@ export default function RepeatEvent({
control={<Radio />}
label={
<Box display="flex" alignItems="center" gap={1}>
{t("event.repeat.end.on")}
<Typography variant="h6">
{t("event.repeat.end.on")}
</Typography>
<TextField
type="date"
inputProps={{ "data-testid": "end-date" }}
@@ -342,6 +342,9 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
<Box
display="flex"
flexDirection="column"
className={`date-time-group ${
isExpanded || shouldShowFullFieldsInNormal ? "show-full-field" : ""
}`.trim()}
sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }}
>
{isExpanded || shouldShowFullFieldsInNormal ? (
@@ -19,19 +19,52 @@ export const FieldWithLabel = React.memo(
}) => {
if (!isExpanded) {
// Normal mode: label on top
const isEmptyLabel =
label === null ||
label === undefined ||
(typeof label === "string" && label.trim() === "");
return (
<Box sx={sx}>
<Typography
component="div"
sx={{
display: "block",
marginBottom: "4px",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
{label}
</Typography>
<Box
sx={[
{
"& > *:not(:first-of-type)": {
marginTop: isEmptyLabel ? 0 : "6px",
},
// Only apply margin to direct child MuiTextField-root
"& > .MuiFormControl-root": {
marginTop: "6px",
marginBottom: 0,
},
"& > .MuiTextField-root": {
marginTop: "6px",
marginBottom: 0,
},
// Reset margin for nested MuiTextField-root
"& .MuiFormControl-root .MuiTextField-root": {
marginTop: 0,
marginBottom: 0,
},
"& .MuiTextField-root .MuiTextField-root": {
marginTop: 0,
marginBottom: 0,
},
// Reset margin for nested Box children (DateTimeFields structure)
"& > .MuiBox-root > .MuiBox-root": {
marginTop: 0,
},
"& > .MuiBox-root > .MuiBox-root > .MuiBox-root": {
marginTop: 0,
},
},
...(Array.isArray(sx) ? sx : sx ? [sx] : []),
]}
>
{!isEmptyLabel && (
<Typography component="div" variant="h6" sx={{ display: "block" }}>
{label}
</Typography>
)}
{children}
</Box>
);
@@ -42,17 +75,35 @@ export const FieldWithLabel = React.memo(
<Box display="flex" alignItems="center" sx={sx}>
<Typography
component="div"
variant="h6"
sx={{
minWidth: "115px",
marginRight: "12px",
flexShrink: 0,
fontSize: "0.875rem",
fontWeight: 500,
}}
>
{label}
</Typography>
<Box flexGrow={1}>{children}</Box>
<Box
flexGrow={1}
sx={{
// Set margin-top: 8px for second row in DateTimeFields (4 fields layout)
"& > .MuiBox-root > .MuiBox-root:nth-of-type(2)": {
marginTop: "8px",
},
// Remove margin from MuiFormControl-root MuiFormControl-marginDense in extended mode
"& .MuiFormControl-root.MuiFormControl-marginDense": {
marginTop: 0,
marginBottom: 0,
},
"& .MuiTextField-root.MuiFormControl-marginDense": {
marginTop: 0,
marginBottom: 0,
},
}}
>
{children}
</Box>
</Box>
);
}
+37 -2
View File
@@ -1,6 +1,41 @@
import React from "react";
import logo from "../../static/images/calendar.svg";
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import { Box } from "@linagora/twake-mui";
import twakeLogo from "../../static/twake-workplace.svg";
export function Loading() {
return <img src={logo} alt="loading" />;
return (
<Box
data-testid="loading"
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100vw",
height: "100vh",
position: "fixed",
top: 0,
left: 0,
}}
>
<DotLottieReact
src="/loadercalendar.lottie"
loop
autoplay
style={{ width: "175px" }}
/>
<Box
component="img"
src={twakeLogo}
alt="twake workplace"
sx={{
position: "absolute",
bottom: "50px",
left: "50%",
transform: "translateX(-50%)",
width: "210px",
}}
/>
</Box>
);
}