-
Notifications
You must be signed in to change notification settings - Fork 18
new-log-viewer: Add tabbed Sidebar with resizable panel; Rearrange open and settings buttons. #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 88 commits
96de362
c6ab307
a0a618a
3c35843
b0948b6
2666f95
3605209
db82737
b9ebe1d
3ac708c
1474245
ef55b61
0418f1f
f87fc9f
1707ccf
1ed3de9
25af90b
6efc0e4
85d112d
c032ce4
7b00600
5f43d9a
9bbfc0b
54bb3c8
ea6b38e
98b55f5
128ab5f
8a3e9b6
f535eeb
bd3c94c
fd1431f
8e1e54e
d5c9fa4
f4baacd
4e046fe
74328ca
0b1b33f
832763d
203f4f2
748a159
6784f3b
f5d6a6f
e0f1c93
27cc339
69d0aa3
c19ae6b
dbe858c
38a31b7
3aa0e0a
ed64bcf
8c1d6dc
f1e1d58
db8a443
80b203a
6e27725
68adc0c
e9ff7a9
9dfdef3
f53a2e2
d1c40de
81bc4ae
591a55b
1fbe903
c3165a1
561d7ed
20efbb5
2204868
7abfdf9
030036e
03e3de1
6f4613e
7146619
9e078b4
f7c38d7
caf32a3
b3d3e03
7e1000d
d36a73b
cae668b
374ac14
1fa0953
530b352
02a9eb0
703cea2
6ad338e
22f97aa
0b66e00
efe3a3d
9d9901e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
.resize-handle { | ||
cursor: ew-resize; | ||
|
||
z-index: var(--ylv-resize-handle-z-index); | ||
|
||
box-sizing: border-box; | ||
width: var(--ylv-panel-resize-handle-width); | ||
height: 100%; | ||
|
||
/* stylelint-disable-next-line custom-property-pattern */ | ||
background-color: var(--joy-palette-background-surface, #fbfcfe); | ||
/* stylelint-disable-next-line custom-property-pattern */ | ||
border-right: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1); | ||
} | ||
|
||
.resize-handle-holding, | ||
.resize-handle:hover { | ||
box-sizing: initial; | ||
|
||
/* stylelint-disable-next-line custom-property-pattern */ | ||
background-color: var(--joy-palette-primary-solidHoverBg, #0258a8); | ||
border-right: initial; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React, { | ||
useCallback, | ||
useEffect, | ||
useState, | ||
} from "react"; | ||
|
||
import "./ResizeHandle.css"; | ||
|
||
|
||
interface ResizeHandleProps { | ||
onHandleRelease: () => void, | ||
|
||
/** | ||
* Gets triggered when a resize event occurs. | ||
* | ||
* @param resizeHandlePosition The horizontal distance, in pixels, between the mouse pointer | ||
* and the left edge of the viewport. | ||
*/ | ||
onResize: (resizeHandlePosition: number) => void, | ||
} | ||
|
||
/** | ||
* A vertical handle for resizing an object. | ||
* | ||
* @param props | ||
* @param props.onResize The method to call when a resize occurs. | ||
* @param props.onHandleRelease | ||
* @return | ||
*/ | ||
const ResizeHandle = ({ | ||
onResize, | ||
onHandleRelease, | ||
}: ResizeHandleProps) => { | ||
const [isMouseDown, setIsMouseDown] = useState<boolean>(false); | ||
|
||
const handleMouseDown = (ev: React.MouseEvent) => { | ||
ev.preventDefault(); | ||
setIsMouseDown(true); | ||
}; | ||
|
||
const handleMouseMove = useCallback((ev: MouseEvent) => { | ||
ev.preventDefault(); | ||
onResize(ev.clientX); | ||
}, [onResize]); | ||
|
||
const handleMouseUp = useCallback((ev: MouseEvent) => { | ||
ev.preventDefault(); | ||
setIsMouseDown(false); | ||
onHandleRelease(); | ||
}, [onHandleRelease]); | ||
|
||
// On `isMouseDown` change, add / remove event listeners. | ||
useEffect(() => { | ||
if (isMouseDown) { | ||
window.addEventListener("mousemove", handleMouseMove); | ||
window.addEventListener("mouseup", handleMouseUp); | ||
} | ||
|
||
return () => { | ||
// Always clean up the event listeners before the hook is re-run due to `isMouseDown` | ||
// changes / when the component is unmounted. | ||
window.removeEventListener("mousemove", handleMouseMove); | ||
window.removeEventListener("mouseup", handleMouseUp); | ||
}; | ||
}, [ | ||
handleMouseMove, | ||
handleMouseUp, | ||
isMouseDown, | ||
]); | ||
|
||
return ( | ||
<div | ||
className={`resize-handle ${isMouseDown ? | ||
"resize-handle-holding" : | ||
""}`} | ||
onMouseDown={handleMouseDown}/> | ||
); | ||
}; | ||
|
||
|
||
export default ResizeHandle; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from "react"; | ||
|
||
import { | ||
ListItem, | ||
ListItemContent, | ||
ListItemDecorator, | ||
Typography, | ||
TypographyProps, | ||
} from "@mui/joy"; | ||
|
||
|
||
interface CustomListItemProps { | ||
content: string, | ||
icon: React.ReactNode, | ||
slotProps?: { | ||
content?: TypographyProps | ||
}, | ||
title: string | ||
} | ||
|
||
/** | ||
* Renders a custom list item with an icon, a title and a context text. | ||
* | ||
* @param props | ||
* @param props.content | ||
* @param props.icon | ||
* @param props.slotProps | ||
* @param props.title | ||
* @return | ||
*/ | ||
const CustomListItem = ({content, icon, slotProps, title}: CustomListItemProps) => ( | ||
<ListItem> | ||
<ListItemDecorator> | ||
{icon} | ||
</ListItemDecorator> | ||
<ListItemContent> | ||
<Typography level={"title-sm"}> | ||
{title} | ||
</Typography> | ||
<Typography | ||
{...slotProps?.content} | ||
level={"body-sm"} | ||
> | ||
{content} | ||
</Typography> | ||
</ListItemContent> | ||
</ListItem> | ||
); | ||
|
||
export default CustomListItem; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
.sidebar-tab-panel { | ||
padding: 0.75rem; | ||
} | ||
|
||
.sidebar-tab-panel-title-container { | ||
user-select: none; | ||
margin-bottom: 0.5rem !important; | ||
} | ||
|
||
.sidebar-tab-panel-title { | ||
font-size: 0.875rem !important; | ||
font-weight: 400 !important; | ||
text-transform: uppercase; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from "react"; | ||
|
||
import { | ||
DialogContent, | ||
DialogTitle, | ||
TabPanel, | ||
Typography, | ||
} from "@mui/joy"; | ||
|
||
import "./CustomTabPanel.css"; | ||
|
||
|
||
interface CustomTabPanelProps { | ||
children: React.ReactNode, | ||
tabName: string, | ||
title: string, | ||
} | ||
|
||
/** | ||
* Renders a customized tab panel to be extended for displaying extra information in the sidebar. | ||
* | ||
* @param props | ||
* @param props.children | ||
* @param props.tabName | ||
* @param props.title | ||
* @return | ||
*/ | ||
const CustomTabPanel = ({children, tabName, title}: CustomTabPanelProps) => { | ||
return ( | ||
<TabPanel | ||
className={"sidebar-tab-panel"} | ||
value={tabName} | ||
> | ||
<DialogTitle className={"sidebar-tab-panel-title-container"}> | ||
<Typography | ||
className={"sidebar-tab-panel-title"} | ||
level={"body-md"} | ||
> | ||
{title} | ||
</Typography> | ||
</DialogTitle> | ||
<DialogContent> | ||
{children} | ||
</DialogContent> | ||
</TabPanel> | ||
); | ||
}; | ||
|
||
|
||
export default CustomTabPanel; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { | ||
useContext, | ||
useMemo, | ||
} from "react"; | ||
|
||
import { | ||
Divider, | ||
List, | ||
} from "@mui/joy"; | ||
|
||
import AbcIcon from "@mui/icons-material/Abc"; | ||
import StorageIcon from "@mui/icons-material/Storage"; | ||
|
||
import {StateContext} from "../../../../contexts/StateContextProvider"; | ||
import { | ||
TAB_DISPLAY_NAMES, | ||
TAB_NAME, | ||
} from "../../../../typings/tab"; | ||
import {formatSizeInBytes} from "../../../../utils/units"; | ||
import CustomListItem from "./CustomListItem"; | ||
import CustomTabPanel from "./CustomTabPanel"; | ||
|
||
|
||
/** | ||
* Displays a panel containing the file name and on-disk size of the selected file. | ||
* | ||
* @return | ||
*/ | ||
const FileInfoTabPanel = () => { | ||
const {fileName, onDiskFileSizeInBytes} = useContext(StateContext); | ||
|
||
const isFileUnloaded = 0 === fileName.length; | ||
const formattedOriginalSize = useMemo( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. formattedOnDiskSize There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry fixed |
||
() => formatSizeInBytes(onDiskFileSizeInBytes, false), | ||
[onDiskFileSizeInBytes] | ||
); | ||
|
||
return ( | ||
<CustomTabPanel | ||
tabName={TAB_NAME.FILE_INFO} | ||
title={TAB_DISPLAY_NAMES[TAB_NAME.FILE_INFO]} | ||
> | ||
{isFileUnloaded ? | ||
"No file is open." : | ||
<List> | ||
<CustomListItem | ||
content={fileName} | ||
icon={<AbcIcon/>} | ||
slotProps={{content: {sx: {wordBreak: "break-word"}}}} | ||
title={"Name"}/> | ||
<Divider/> | ||
<CustomListItem | ||
content={formattedOriginalSize} | ||
icon={<StorageIcon/>} | ||
title={"Original Size"}/> | ||
davemarco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</List>} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider internationalizing the hardcoded strings. To improve the component's readiness for internationalization, consider extracting the hardcoded English strings into a translation file or constant. This will make it easier to support multiple languages in the future. Example using a hypothetical import { useTranslation } from '../path/to/translation/hook';
const FileInfoTabPanel = () => {
const { t } = useTranslation();
// ...
return (
// ...
{isFileUnloaded ?
t('noFileOpen') :
<List>
<CustomListItem
content={fileName}
icon={<AbcIcon />}
title={t('name')}
/>
<Divider />
<CustomListItem
content={formattedOriginalSize}
icon={<StorageIcon />}
title={t('originalSize')}
/>
</List>
}
// ...
);
}; This change will make it easier to add support for multiple languages in the future without modifying the component's logic. |
||
</CustomTabPanel> | ||
); | ||
}; | ||
|
||
export default FileInfoTabPanel; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
.sidebar-tab-button { | ||
justify-content: center !important; | ||
width: 48px; | ||
height: 48px; | ||
padding: 0 !important; | ||
} | ||
|
||
.sidebar-tab-button-icon { | ||
font-size: 32px !important; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { | ||
Tab, | ||
Tooltip, | ||
} from "@mui/joy"; | ||
|
||
import {SvgIconComponent} from "@mui/icons-material"; | ||
|
||
import { | ||
TAB_DISPLAY_NAMES, | ||
TAB_NAME, | ||
} from "../../../../typings/tab"; | ||
|
||
import "./TabButton.css"; | ||
|
||
|
||
interface TabButtonProps { | ||
tabName: TAB_NAME, | ||
Icon: SvgIconComponent, | ||
onTabButtonClick: (tabName: TAB_NAME) => void | ||
} | ||
|
||
/** | ||
* Renders a tooltip-wrapped tab button. | ||
* | ||
* @param props | ||
* @param props.tabName | ||
* @param props.Icon | ||
* @param props.onTabButtonClick | ||
* @return | ||
*/ | ||
const TabButton = ({tabName, Icon, onTabButtonClick}: TabButtonProps) => { | ||
const handleClick = () => { | ||
onTabButtonClick(tabName); | ||
}; | ||
davemarco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<Tooltip | ||
arrow={true} | ||
key={tabName} | ||
placement={"right"} | ||
title={TAB_DISPLAY_NAMES[tabName]} | ||
variant={"outlined"} | ||
> | ||
<Tab | ||
className={"sidebar-tab-button"} | ||
color={"neutral"} | ||
indicatorPlacement={"left"} | ||
slotProps={{root: {onClick: handleClick}}} | ||
value={tabName} | ||
> | ||
<Icon className={"sidebar-tab-button-icon"}/> | ||
</Tab> | ||
</Tooltip> | ||
); | ||
}; | ||
|
||
export default TabButton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.sidebar-tabs { | ||
flex-grow: 1; | ||
width: calc(100% - var(--ylv-panel-resize-handle-width)); | ||
height: 100%; | ||
} | ||
|
||
.sidebar-tab-list-spacing { | ||
flex-grow: 1; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.