mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
feat(core): linked doc visiblity setting and new sidebar layout (#12836)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a setting to control the visibility of linked document structures in the sidebar, enabled by default. - Introduced a "dense" mode for workspace selectors and cards, providing a more compact display. - **Improvements** - Refined sidebar and navigation panel layouts with updated padding, spacing, and avatar/button sizing for a cleaner and more consistent appearance. - Enhanced sidebar appearance settings UI, including new localization for the linked doc visibility option. - Updated color theming and spacing in sidebar menu items and quick search input for better usability. - Enabled collapsible behavior control for navigation panel tree nodes, improving user interaction flexibility. - **Style** - Adjusted various component styles for improved compactness and alignment across the sidebar and navigation panels. - Reduced sizes and padding of buttons and icons for a tidier interface. - Updated CSS variables and dynamic sizing for workspace cards to support dense mode. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+28
-22
@@ -6,6 +6,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import { Guard } from '@affine/core/components/guard';
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
@@ -66,10 +67,12 @@ export const NavigationPanelDocNode = ({
|
||||
FeatureFlagService,
|
||||
GuardService,
|
||||
});
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.docId.$) === docId;
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const isCollapsed = appSettings.showLinkedDocInSidebar ? collapsed : true;
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
const DocIcon = useLiveData(
|
||||
@@ -94,10 +97,10 @@ export const NavigationPanelDocNode = ({
|
||||
useMemo(
|
||||
() =>
|
||||
LiveData.from(
|
||||
!collapsed ? docsSearchService.watchRefsFrom(docId) : NEVER,
|
||||
!isCollapsed ? docsSearchService.watchRefsFrom(docId) : NEVER,
|
||||
null
|
||||
),
|
||||
[docsSearchService, docId, collapsed]
|
||||
[docsSearchService, docId, isCollapsed]
|
||||
)
|
||||
);
|
||||
const searching = children === null;
|
||||
@@ -247,8 +250,9 @@ export const NavigationPanelDocNode = ({
|
||||
onDrop={handleDropOnDoc}
|
||||
renameable
|
||||
extractEmojiAsIcon={enableEmojiIcon}
|
||||
collapsed={collapsed}
|
||||
collapsed={isCollapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
collapsible={appSettings.showLinkedDocInSidebar}
|
||||
canDrop={handleCanDrop}
|
||||
to={`/${docId}`}
|
||||
onClick={() => {
|
||||
@@ -257,7 +261,7 @@ export const NavigationPanelDocNode = ({
|
||||
active={active}
|
||||
postfix={
|
||||
referencesLoading &&
|
||||
!collapsed && (
|
||||
!isCollapsed && (
|
||||
<Tooltip
|
||||
content={t['com.affine.rootAppSidebar.docs.references-loading']()}
|
||||
>
|
||||
@@ -285,24 +289,26 @@ export const NavigationPanelDocNode = ({
|
||||
dropEffect={handleDropEffectOnDoc}
|
||||
data-testid={`navigation-panel-doc-${docId}`}
|
||||
>
|
||||
<Guard docId={docId} permission="Doc_Read">
|
||||
{canRead =>
|
||||
canRead
|
||||
? children?.map((child, index) => (
|
||||
<NavigationPanelDocNode
|
||||
key={`${child.docId}-${index}`}
|
||||
docId={child.docId}
|
||||
reorderable={false}
|
||||
location={{
|
||||
at: 'navigation-panel:doc:linked-docs',
|
||||
docId,
|
||||
}}
|
||||
isLinked
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}
|
||||
</Guard>
|
||||
{appSettings.showLinkedDocInSidebar ? (
|
||||
<Guard docId={docId} permission="Doc_Read">
|
||||
{canRead =>
|
||||
canRead
|
||||
? children?.map((child, index) => (
|
||||
<NavigationPanelDocNode
|
||||
key={`${child.docId}-${index}`}
|
||||
docId={child.docId}
|
||||
reorderable={false}
|
||||
location={{
|
||||
at: 'navigation-panel:doc:linked-docs',
|
||||
docId,
|
||||
}}
|
||||
isLinked
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}
|
||||
</Guard>
|
||||
) : null}
|
||||
</NavigationPanelTreeNode>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const itemRoot = style({
|
||||
minHeight: '30px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0 4px',
|
||||
padding: '0 6px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
position: 'relative',
|
||||
marginTop: '0px',
|
||||
@@ -44,6 +44,14 @@ export const itemMain = style({
|
||||
position: 'relative',
|
||||
gap: 12,
|
||||
});
|
||||
export const toggleIcon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
});
|
||||
export const itemRenameAnchor = style({
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
@@ -84,16 +92,26 @@ export const iconContainer = style({
|
||||
height: 20,
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: 20,
|
||||
position: 'absolute',
|
||||
selectors: {
|
||||
[`${itemRoot}[data-collapsible="true"]:hover &`]: {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const collapsedIconContainer = style({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '2px',
|
||||
transition: 'transform 0.2s',
|
||||
color: cssVarV2('icon/primary'),
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
selectors: {
|
||||
'&[data-collapsed="true"]': {
|
||||
transform: 'rotate(-90deg)',
|
||||
@@ -105,10 +123,15 @@ export const collapsedIconContainer = style({
|
||||
'&:hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
[`${itemRoot}[data-collapsible="true"]:hover &`]: {
|
||||
opacity: 1,
|
||||
pointerEvents: 'initial',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const collapsedIcon = style({
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
fontSize: 16,
|
||||
selectors: {
|
||||
'&[data-collapsed="true"]': {
|
||||
transform: 'rotate(-90deg)',
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface BaseNavigationPanelTreeNodeProps {
|
||||
extractEmojiAsIcon?: boolean;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
collapsible?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
to?: To;
|
||||
@@ -140,6 +141,7 @@ export const NavigationPanelTreeNode = ({
|
||||
collapsed,
|
||||
extractEmojiAsIcon,
|
||||
setCollapsed,
|
||||
collapsible = true,
|
||||
canDrop,
|
||||
reorderable = true,
|
||||
operations = [],
|
||||
@@ -226,7 +228,7 @@ export const NavigationPanelTreeNode = ({
|
||||
return;
|
||||
}
|
||||
onDrop?.(data);
|
||||
if (data.treeInstruction?.type === 'make-child') {
|
||||
if (data.treeInstruction?.type === 'make-child' && collapsible) {
|
||||
setCollapsed(false);
|
||||
}
|
||||
},
|
||||
@@ -242,6 +244,7 @@ export const NavigationPanelTreeNode = ({
|
||||
handleCanDrop,
|
||||
cid,
|
||||
onDrop,
|
||||
collapsible,
|
||||
setCollapsed,
|
||||
]
|
||||
);
|
||||
@@ -253,6 +256,7 @@ export const NavigationPanelTreeNode = ({
|
||||
treeInstruction?.type === 'make-child' &&
|
||||
!isSelfDraggedOver
|
||||
) {
|
||||
if (!collapsible) return;
|
||||
// auto expand when dragged over
|
||||
const timeout = setTimeout(() => {
|
||||
setCollapsed(false);
|
||||
@@ -260,7 +264,13 @@ export const NavigationPanelTreeNode = ({
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
return;
|
||||
}, [draggedOver, isSelfDraggedOver, setCollapsed, treeInstruction?.type]);
|
||||
}, [
|
||||
collapsible,
|
||||
draggedOver,
|
||||
isSelfDraggedOver,
|
||||
setCollapsed,
|
||||
treeInstruction?.type,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rootRef.current) {
|
||||
@@ -346,9 +356,10 @@ export const NavigationPanelTreeNode = ({
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // for links
|
||||
if (!collapsible) return;
|
||||
setCollapsed(!collapsed);
|
||||
},
|
||||
[collapsed, setCollapsed]
|
||||
[collapsed, collapsible, setCollapsed]
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
@@ -365,11 +376,11 @@ export const NavigationPanelTreeNode = ({
|
||||
}
|
||||
if (!clickForCollapse) {
|
||||
onClick?.();
|
||||
} else {
|
||||
} else if (collapsible) {
|
||||
setCollapsed(!collapsed);
|
||||
}
|
||||
},
|
||||
[clickForCollapse, collapsed, onClick, setCollapsed]
|
||||
[clickForCollapse, collapsed, collapsible, onClick, setCollapsed]
|
||||
);
|
||||
|
||||
const content = (
|
||||
@@ -378,20 +389,20 @@ export const NavigationPanelTreeNode = ({
|
||||
className={styles.itemRoot}
|
||||
data-active={active}
|
||||
data-disabled={disabled}
|
||||
data-collapsible={collapsible}
|
||||
>
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="navigation-panel-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemMain}>
|
||||
<div className={styles.toggleIcon}>
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="navigation-panel-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.iconContainer}>
|
||||
{emoji ??
|
||||
(Icon && (
|
||||
@@ -402,6 +413,9 @@ export const NavigationPanelTreeNode = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemMain}>
|
||||
<div className={styles.itemContent}>{name}</div>
|
||||
{postfix}
|
||||
<div
|
||||
|
||||
+36
-21
@@ -139,10 +139,10 @@ export const AppearanceSettings = () => {
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<SettingWrapper
|
||||
title={t['com.affine.appearanceSettings.sidebar.title']()}
|
||||
>
|
||||
<SettingWrapper
|
||||
title={t['com.affine.appearanceSettings.sidebar.title']()}
|
||||
>
|
||||
{BUILD_CONFIG.isElectron ? (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.noisyBackground.title']()}
|
||||
desc={t[
|
||||
@@ -156,23 +156,38 @@ export const AppearanceSettings = () => {
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
{environment.isMacOs && (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.translucentUI.title']()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.translucentUI.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.enableBlurBackground}
|
||||
onChange={checked =>
|
||||
updateSettings('enableBlurBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
) : null}
|
||||
{BUILD_CONFIG.isElectron && environment.isMacOs && (
|
||||
<SettingRow
|
||||
name={t['com.affine.appearanceSettings.translucentUI.title']()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.translucentUI.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.enableBlurBackground}
|
||||
onChange={checked =>
|
||||
updateSettings('enableBlurBackground', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
<SettingRow
|
||||
name={t[
|
||||
'com.affine.appearanceSettings.showLinkedDocInSidebar.title'
|
||||
]()}
|
||||
desc={t[
|
||||
'com.affine.appearanceSettings.showLinkedDocInSidebar.description'
|
||||
]()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.showLinkedDocInSidebar}
|
||||
onChange={checked =>
|
||||
updateSettings('showLinkedDocInSidebar', checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
|
||||
{BUILD_CONFIG.isElectron ? <MenubarSetting /> : null}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user