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:
Cats Juice
2025-06-17 16:09:34 +08:00
committed by GitHub
parent ba718b955a
commit dfe4c22a75
23 changed files with 205 additions and 94 deletions
@@ -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
@@ -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}
</>