ente/web/packages/base/components/RowButton.tsx
Neeraj Gupta 7621041ce0 Revert "[mob] Fix exif time parsing (#4985)"
This reverts commit 5b17711b55e9e8995454121e3a209ee5e8c9b3c1, reversing
changes made to 85bf3eebcb5ac197ce6a5901cbc8db1d03be0113.
2025-02-11 18:27:17 +05:30

417 lines
13 KiB
TypeScript

import { EnteSwitch } from "@/base/components/EnteSwitch";
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import {
Box,
Divider,
Stack,
styled,
Typography,
type ButtonProps,
type TypographyProps,
} from "@mui/material";
import React from "react";
interface RowButtonGroupTitleProps {
/**
* An optional leading SvgIcon before the title.
*/
icon?: React.ReactNode;
}
/**
* A section title, meant to precede a {@link RowButtonGroup}.
*/
export const RowButtonGroupTitle: React.FC<
React.PropsWithChildren<RowButtonGroupTitleProps>
> = ({ children, icon }) => (
<Stack
direction="row"
sx={{
px: "8px",
py: "6px",
gap: "8px",
"& > svg": {
fontSize: "17px",
color: "stroke.muted",
},
}}
>
{icon && icon}
<Typography variant="small" sx={{ color: "text.muted" }}>
{children}
</Typography>
</Stack>
);
/**
* A short description text meant to come after a {@link RowButtonGroup}.
*/
export const RowButtonGroupHint: React.FC<React.PropsWithChildren> = ({
children,
}) => (
<Typography
variant="small"
sx={{ color: "text.faint", px: "16px", py: "6px" }}
>
{children}
</Typography>
);
/**
* A divider for buttons in a {@link RowButtonGroup}.
*/
export const RowButtonDivider = () => (
<Divider sx={{ "&&&": { mttty: 0 }, opacity: 0.4 }} />
);
/**
* A group of {@link RowButton}s that visually look together as a single
* section.
*
* {@link RowButtonGroupTitle} can be used to provide a title for the entire
* group, and {@link RowButtonGroupHint} can be used to provide a hint text that
* follows the group.
*
* {@link RowButtonDivider} can be used to to separate the individual buttons in
* the group.
*
* ---
*
* Note: {@link RowButtonGroup} is not designed to work with the "secondary"
* variant of {@link RowButton}.
*/
export const RowButtonGroup = styled("div")(
({ theme }) => `
background-color: ${theme.vars.palette.fill.faint};
border-radius: 8px;
/* Modify the RowButton style when it is placed inside a RowButtonGroup. */
& > button {
border-radius: 8px;
background-color: transparent;
}
/* Need to retarget the disabled state with increased specificity. */
& > button.Mui-disabled {
background-color: transparent;
}
& > button:not(:last-of-type) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
& > button:not(:first-of-type) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
& > button:hover {
/* These fills are translucent and additive, and the RowButtonGroup
already has a background, so pick a (transparent) color that gives us
a similar outcome of the hover state as for a RowButton that is not
inside a RowButtonGroup */
background-color: ${theme.vars.palette.fill.faintHover};
}
/* Reset the border radius when showing focus outlines that will be added
by the styles attached to FocusVisibleButton. */
& > button.Mui-focusVisible {
border-radius: 8px;
}
/* Modify the outline offset added by FocusVisibleButton for a visual
effect that fits better with these row buttons. */
& > button:active {
outline-offset: 0;
}
`,
);
interface RowButtonProps {
/**
* Variants:
*
* - "primary" (default): A row button with a filled in background color.
*
* - "secondary": A row button without a fill.
*/
variant?: "primary" | "secondary";
/**
* Color of the row button.
*
* Semantically, this is similar to the "color" props for a MUI button,
* except we only support two cases for this row button component:
*
* - "primary" (default): A row button that uses "text.base" as the color of
* the text (and an approprite background color, if needed, based on the
* value of the {@link variant}).
*
* - "critical": A row button that uses "critical.main" as the color of the
* text. The background fill, if any, will be the same as what "primary"
* would've entailed.
*/
color?: "primary" | "critical";
/**
* Modify the font weight of the {@link label}, when label is a string.
*
* Default: "medium".
*/
fontWeight?: TypographyProps["fontWeight"];
/**
* If true, then the row button will be disabled.
*/
disabled?: boolean;
/**
* Called when the row button is activated (e.g. by a click).
*/
onClick: () => void;
/**
* Optional icon shown at the leading edge of the row button.
*
* This is usually an icon like an {@link SvgIcon}, but it can be any
* arbitrary component, the row button does not make any assumptions as to
* what this is.
*
* If it is an {@link SvgIcon}, the row button will size it appropriately.
*/
startIcon?: React.ReactNode;
/**
* Optional icon shown at the trailing edge of the row button.
*
* Similar to {@link startIcon} this can be any arbitrary component, though
* usually it is an {@link SvgIcon} whose size the row button will set.
*/
endIcon?: React.ReactNode;
/**
* The label for the component.
*
* Usually this is expected to be a string, in which case it is wrapped up
* in an appropriate {@link Typography} and shown on the row button. But it
* can be an arbitrary react component too, to allow customizing its
* appearance or otherwise modifying it in one off cases (it will be used as
* it is and not wrapped in a {@link Typography} if it is not a string).
*/
label: React.ReactNode;
/**
* The text (or icon) to show alongside {@link label}, separated from it by
* a bullet.
*
* This is usually expected to be a string, in which case it is wrapped in a
* {@link Typography} component and styled with a muted text color before
* being rendered.
*
* However, it can also be an {@link SvgIcon} (or any an arbitrary
* component), though in terms of styling, only an {@link SvgIcon} usually
* makes sense.
*
* Similar to {@link label}, it will not be wrapped in a {@link Typography}
* if it is not a string.
*/
caption?: React.ReactNode;
}
/**
* A button that looks like a row in a group of options / choices.
*
* It can be used both standalone, or as part of a {@link RowButtonGroup}.
*/
export const RowButton: React.FC<RowButtonProps> = ({
variant = "primary",
color = "primary",
fontWeight = "medium",
disabled = false,
startIcon,
endIcon,
label,
caption,
onClick,
}) => (
<RowButtonRoot rbVariant={variant} fullWidth {...{ disabled, onClick }}>
<Stack
direction="row"
sx={[
{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
px: "16px",
pr: "12px",
color: "text.base",
},
color == "critical" && { color: "critical.main" },
disabled && { color: "text.muted" },
]}
>
<Stack direction="row" sx={{ py: "14px", gap: "10px" }}>
{startIcon && startIcon}
<Box sx={{ px: "2px" }}>
{typeof label !== "string" ? (
label
) : caption ? (
<Stack
direction="row"
sx={{ gap: "4px", alignItems: "center" }}
>
<Typography>{label}</Typography>
<CaptionTypography color={color}>
{"•"}
</CaptionTypography>
<CaptionTypography color={color}>
{caption}
</CaptionTypography>
</Stack>
) : (
<Typography fontWeight={fontWeight}>{label}</Typography>
)}
</Box>
</Stack>
{endIcon && endIcon}
</Stack>
</RowButtonRoot>
);
type RowButtonRootProps = ButtonProps & {
// Prefix with "rb" to differentiate this from the "variant" prop of the MUI
// button that we're styling.
rbVariant: RowButtonProps["variant"];
};
const RowButtonRoot = styled(FocusVisibleButton, {
shouldForwardProp: (prop) => prop != "rbVariant",
// name: "MuiMenuItem",
})<React.PropsWithChildren<RowButtonRootProps>>(({ theme }) => ({
// Remove button's default padding.
padding: 0,
// Set the size of the any icons (SvgIcon instances) provided to us to make
// them fit with the Typography within the button's content.
"& .MuiSvgIcon-root": {
fontSize: "20px",
},
variants: [
{
props: { rbVariant: "primary" },
style: {
backgroundColor: theme.vars.palette.fill.faint,
"&:hover": {
backgroundColor: theme.vars.palette.fill.muted,
},
},
},
{
props: { rbVariant: "secondary" },
style: {
backgroundColor: "transparent",
color: "white",
"&:hover": { backgroundColor: theme.vars.palette.fill.faint },
},
},
],
}));
const CaptionTypography: React.FC<
React.PropsWithChildren<{ color: RowButtonProps["color"] }>
> = ({ color, children }) => (
<Typography
variant="small"
sx={[
color == "critical"
? { color: "critical.main" }
: { color: "text.faint" },
]}
>
{children}
</Typography>
);
interface RowSwitchProps {
/**
* The state of the contained {@link EnteSwitch}.
*/
checked?: boolean;
/**
* Called when the user activates the contained {@link EnteSwitch}.
*/
onClick: () => void;
/**
* The label for the component.
*
* Similar to the {@link label} prop for {@link RowButton}, but can only be
* a string instead of an arbitrary component.
*/
label: string;
}
/**
* A row that visually looks similar to a {@link RowButton}, but instead of a
* button is a normal {@link Typography} with a {@link EnteSwitch} at its
* trailing edge.
*
* It only works (visually) when placed within a {@link RowButtonGroup} since
* that is where it gets its background color from.
*/
export const RowSwitch: React.FC<RowSwitchProps> = ({
checked,
label,
onClick,
}) => (
<Stack
direction="row"
sx={{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
px: "16px",
pr: "12px",
}}
>
<Typography sx={{ py: "14px", px: "2px", fontWeight: "medium" }}>
{label}
</Typography>
<EnteSwitch {...{ checked, onClick }} />
</Stack>
);
interface RowLabelProps {
/**
* Optional icon shown at the leading edge of the row button.
*
* This is usually an icon like an {@link SvgIcon}, but it can be any
* arbitrary component, the row button does not make any assumptions as to
* what this is.
*
* Unlike a {@link RowButton}, there is not sizing applied to it.
*/
startIcon?: React.ReactNode;
/**
* The label for the component.
*
* Similar to the {@link label} prop for {@link RowButton}, but can only be
* a string instead of an arbitrary component.
*/
label: string;
}
/**
* A row that visually looks similar to a {@link RowButton}, but instead of a
* button is a normal {@link Typography}.
*
* This is useful for creating non-interactive, static, labels.
*
* It only works (visually) when placed within a {@link RowButtonGroup} since
* that is where it gets its background color from.
*/
export const RowLabel: React.FC<RowLabelProps> = ({ startIcon, label }) => (
<Stack
direction="row"
sx={{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
px: "16px",
pr: "12px",
}}
>
<Stack direction="row" sx={{ py: "14px", gap: "10px" }}>
{startIcon && startIcon}
<Box sx={{ px: "2px" }}>
{/* Nb: Unlike the button, this has "regular" fontWeight. */}
<Typography>{label}</Typography>
</Box>
</Stack>
</Stack>
);