Add Stationery and Stationery Dark themes with notebook chrome.
Manila folders, ruled chat, post-it nav, and folder tabs; dark variant uses Catppuccin Mocha colors with muted stickies and soft glows.
This commit is contained in:
@@ -29,7 +29,20 @@ export function SidebarNav() {
|
||||
<Sidebar>
|
||||
<SidebarContent
|
||||
scrollable={
|
||||
<Scroll ref={scrollRef} variant="Background" size="0">
|
||||
<Scroll
|
||||
ref={scrollRef}
|
||||
variant="Background"
|
||||
size="0"
|
||||
data-sidebar-scroll=""
|
||||
style={{
|
||||
marginRight: -25,
|
||||
paddingRight: 25,
|
||||
width: 'calc(100% + 25px)',
|
||||
boxSizing: 'border-box',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<SidebarStack>
|
||||
{!homeHidden && <HomeTab />}
|
||||
<DirectTab />
|
||||
|
||||
@@ -249,7 +249,7 @@ export function Direct() {
|
||||
const virtualizer = useVirtualizer({
|
||||
count: sortedDirects.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
estimateSize: () => 36,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
@@ -288,7 +288,7 @@ export function Direct() {
|
||||
<NotificationsNavItem />
|
||||
<InvitesNavItem />
|
||||
</NavCategory>
|
||||
<NavCategory>
|
||||
<NavCategory data-nav-dropdown="">
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
|
||||
@@ -298,13 +298,16 @@ export function Direct() {
|
||||
Chats
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
<div
|
||||
ref={animationRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{sortedDirects.length > 0 && (
|
||||
<div data-nav-rooms="">
|
||||
<div
|
||||
ref={animationRef}
|
||||
data-nav-room-list=""
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const roomId = sortedDirects[vItem.index];
|
||||
const room = mx.getRoom(roomId);
|
||||
@@ -332,7 +335,9 @@ export function Direct() {
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NavCategory>
|
||||
</Box>
|
||||
</PageNavContent>
|
||||
|
||||
@@ -309,7 +309,7 @@ export function Home() {
|
||||
const virtualizer = useVirtualizer({
|
||||
count: listItems.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 32,
|
||||
estimateSize: () => 36,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
@@ -399,7 +399,7 @@ export function Home() {
|
||||
</NavItem>
|
||||
<PluginNavSlot location="channel-list" />
|
||||
</NavCategory>
|
||||
<NavCategory>
|
||||
<NavCategory data-nav-dropdown="">
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
|
||||
@@ -410,12 +410,15 @@ export function Home() {
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
<PluginNavSlot location="home-section" />
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{listItems.length > 0 && (
|
||||
<div data-nav-rooms="">
|
||||
<div
|
||||
data-nav-room-list=""
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const item = listItems[vItem.index];
|
||||
if (!item) return null;
|
||||
@@ -446,7 +449,16 @@ export function Home() {
|
||||
key={vItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<div style={{ paddingLeft, display: 'flex', alignItems: 'center', minHeight: item.depth > 0 ? '1.5rem' : undefined }}>
|
||||
<div
|
||||
data-nav-depth={item.depth}
|
||||
style={{
|
||||
paddingLeft,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: item.depth > 0 ? '1.5rem' : undefined,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{treeIcon && (
|
||||
<span style={{
|
||||
paddingRight: '2px',
|
||||
@@ -475,7 +487,9 @@ export function Home() {
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NavCategory>
|
||||
</Box>
|
||||
</PageNavContent>
|
||||
|
||||
@@ -154,7 +154,7 @@ export function HomeThreadsCategory({ rooms }: HomeThreadsCategoryProps) {
|
||||
if (threads.length === 0) return null;
|
||||
|
||||
return (
|
||||
<NavCategory>
|
||||
<NavCategory data-nav-dropdown="">
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
closed={closedCategories.has(THREADS_CATEGORY_ID)}
|
||||
@@ -164,15 +164,18 @@ export function HomeThreadsCategory({ rooms }: HomeThreadsCategoryProps) {
|
||||
My Threads
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
{!closedCategories.has(THREADS_CATEGORY_ID) &&
|
||||
threads.map(({ room, thread }) => (
|
||||
<ThreadNavItem
|
||||
key={`${room.roomId}:${thread.id}`}
|
||||
room={room}
|
||||
thread={thread}
|
||||
selected={false}
|
||||
/>
|
||||
))}
|
||||
{!closedCategories.has(THREADS_CATEGORY_ID) && (
|
||||
<div data-nav-rooms="">
|
||||
{threads.map(({ room, thread }) => (
|
||||
<ThreadNavItem
|
||||
key={`${room.roomId}:${thread.id}`}
|
||||
room={room}
|
||||
thread={thread}
|
||||
selected={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</NavCategory>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { Avatar, Box, Button, IconButton, Line, Menu, MenuItem, PopOut, RectCords, Spinner, Text, color, config, toRem } from 'folds';
|
||||
import { Icon, Icons } from '../../../components/icons';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { JoinRule, Room, Thread, ThreadEvent } from 'matrix-js-sdk';
|
||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
useSpaceSearchSelected,
|
||||
} from '../../../hooks/router/useSelectedSpace';
|
||||
import { useSpace } from '../../../hooks/useSpace';
|
||||
import { VirtualTile } from '../../../components/virtualizer';
|
||||
import { RoomNavCategoryButton, RoomNavItem, UnjoinedSubRoomItem } from '../../../features/room-nav';
|
||||
import { makeNavCategoryId } from '../../../state/closedNavCategories';
|
||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||
@@ -74,6 +72,119 @@ import { SpaceOptionsMenu } from '../../../features/space/SpaceOptionsMenu';
|
||||
import { ForumFeedSidebar } from '../../../features/forum/ForumFeedSidebar';
|
||||
import * as forumBoardCss from '../../../features/forum/ForumBoardView.css';
|
||||
|
||||
type SpaceHierarchyItem =
|
||||
| { type: 'hierarchy'; roomId: string }
|
||||
| { type: 'subroom'; roomId: string; room: Room; depth: number; isLast?: boolean }
|
||||
| { type: 'unjoined-subroom'; roomId: string; depth: number; isLast?: boolean };
|
||||
|
||||
type SpaceNavSectionData = {
|
||||
spaceRoomId: string;
|
||||
categoryId: string;
|
||||
label: string;
|
||||
items: SpaceHierarchyItem[];
|
||||
};
|
||||
|
||||
type SpaceNavSectionProps = {
|
||||
section: SpaceNavSectionData;
|
||||
closed: boolean;
|
||||
onCategoryClick: MouseEventHandler<HTMLButtonElement>;
|
||||
selectedRoomId?: string;
|
||||
mDirects: Set<string>;
|
||||
notificationPreferences: ReturnType<typeof useRoomsNotificationPreferencesContext>;
|
||||
getToLink: (roomId: string) => string;
|
||||
};
|
||||
|
||||
function SpaceNavSection({
|
||||
section,
|
||||
closed,
|
||||
onCategoryClick,
|
||||
selectedRoomId,
|
||||
mDirects,
|
||||
notificationPreferences,
|
||||
getToLink,
|
||||
}: SpaceNavSectionProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
return (
|
||||
<NavCategory data-nav-dropdown="">
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
data-category-id={section.categoryId}
|
||||
onClick={onCategoryClick}
|
||||
closed={closed}
|
||||
>
|
||||
{section.label}
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
{section.items.length > 0 && (
|
||||
<div data-nav-rooms="">
|
||||
<div data-nav-room-list="">
|
||||
{section.items.map((item, index) => {
|
||||
if (item.type === 'unjoined-subroom') {
|
||||
return (
|
||||
<UnjoinedSubRoomItem
|
||||
key={`${item.roomId}:${index}`}
|
||||
roomId={item.roomId}
|
||||
depth={item.depth}
|
||||
isLast={item.isLast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'subroom') {
|
||||
return (
|
||||
<div
|
||||
key={`${item.roomId}:${index}`}
|
||||
data-nav-depth={item.depth}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<RoomNavItem
|
||||
room={item.room}
|
||||
selected={selectedRoomId === item.roomId}
|
||||
hideIcon={item.depth > 0}
|
||||
showAvatar={mDirects.has(item.roomId)}
|
||||
direct={mDirects.has(item.roomId)}
|
||||
linkPath={getToLink(item.roomId)}
|
||||
notificationMode={getRoomNotificationMode(
|
||||
notificationPreferences,
|
||||
item.roomId
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const room = mx.getRoom(item.roomId);
|
||||
if (!room || isSpace(room)) return null;
|
||||
|
||||
return (
|
||||
<RoomNavItem
|
||||
key={`${item.roomId}:${index}`}
|
||||
room={room}
|
||||
selected={selectedRoomId === item.roomId}
|
||||
showAvatar={mDirects.has(item.roomId)}
|
||||
direct={mDirects.has(item.roomId)}
|
||||
linkPath={getToLink(item.roomId)}
|
||||
notificationMode={getRoomNotificationMode(
|
||||
notificationPreferences,
|
||||
room.roomId
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NavCategory>
|
||||
);
|
||||
}
|
||||
|
||||
function SpaceHeader() {
|
||||
const space = useSpace();
|
||||
const spaceName = useRoomName(space);
|
||||
@@ -272,61 +383,74 @@ export function Space() {
|
||||
return hierarchy.filter((entry) => !globalSubRoomIds.has(entry.roomId));
|
||||
}, [hierarchy, globalSubRoomIds]);
|
||||
|
||||
// Flatten hierarchy to include sub-rooms
|
||||
type HierarchyItem =
|
||||
| { type: 'hierarchy'; roomId: string }
|
||||
| { type: 'subroom'; roomId: string; room: Room; depth: number; isLast?: boolean }
|
||||
| { type: 'unjoined-subroom'; roomId: string; depth: number; isLast?: boolean };
|
||||
|
||||
const flattenedHierarchy = useMemo(() => {
|
||||
const items: HierarchyItem[] = [];
|
||||
// One paper dropdown per space: space header + its rooms/sub-rooms
|
||||
const navSections = useMemo(() => {
|
||||
const sections: SpaceNavSectionData[] = [];
|
||||
let current: SpaceNavSectionData | null = null;
|
||||
const processedSubRooms = new Set<string>();
|
||||
|
||||
const addSubRooms = (parentRoomId: string, baseDepth: number) => {
|
||||
const addSubRooms = (parentRoomId: string, baseDepth: number, into: SpaceHierarchyItem[]) => {
|
||||
const room = mx.getRoom(parentRoomId);
|
||||
if (!room || isSpace(room)) return;
|
||||
|
||||
|
||||
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
||||
const subRooms = subRoomsEvent?.getContent<{ children: string[] }>()?.children ?? [];
|
||||
|
||||
// Filter to only joined sub-rooms that haven't been processed
|
||||
const joinedSubRooms = subRooms.filter(subRoomId =>
|
||||
!processedSubRooms.has(subRoomId) && mx.getRoom(subRoomId)
|
||||
|
||||
const joinedSubRooms = subRooms.filter(
|
||||
(subRoomId) => !processedSubRooms.has(subRoomId) && mx.getRoom(subRoomId)
|
||||
);
|
||||
|
||||
|
||||
joinedSubRooms.forEach((subRoomId, index) => {
|
||||
processedSubRooms.add(subRoomId);
|
||||
|
||||
const isLast = index === joinedSubRooms.length - 1;
|
||||
const subRoom = mx.getRoom(subRoomId);
|
||||
if (subRoom) {
|
||||
// Joined sub-room
|
||||
items.push({ type: 'subroom', roomId: subRoomId, room: subRoom, depth: baseDepth + 1, isLast });
|
||||
// Recursively add sub-rooms of sub-rooms
|
||||
addSubRooms(subRoomId, baseDepth + 1);
|
||||
}
|
||||
if (!subRoom) return;
|
||||
into.push({
|
||||
type: 'subroom',
|
||||
roomId: subRoomId,
|
||||
room: subRoom,
|
||||
depth: baseDepth + 1,
|
||||
isLast,
|
||||
});
|
||||
addSubRooms(subRoomId, baseDepth + 1, into);
|
||||
});
|
||||
};
|
||||
|
||||
const startSection = (spaceRoomId: string, label: string) => {
|
||||
current = {
|
||||
spaceRoomId,
|
||||
categoryId: makeNavCategoryId(space.roomId, spaceRoomId),
|
||||
label,
|
||||
items: [],
|
||||
};
|
||||
sections.push(current);
|
||||
return current;
|
||||
};
|
||||
|
||||
filteredHierarchy.forEach((entry) => {
|
||||
items.push({ type: 'hierarchy', roomId: entry.roomId });
|
||||
|
||||
// Add sub-rooms after each non-space room
|
||||
const isSpaceEntry = 'space' in entry && entry.space === true;
|
||||
const room = mx.getRoom(entry.roomId);
|
||||
|
||||
if (isSpaceEntry || (room && isSpace(room))) {
|
||||
startSection(
|
||||
entry.roomId,
|
||||
entry.roomId === space.roomId ? 'Rooms' : room?.name ?? entry.roomId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
startSection(space.roomId, 'Rooms');
|
||||
}
|
||||
|
||||
current!.items.push({ type: 'hierarchy', roomId: entry.roomId });
|
||||
if (room && !isSpace(room)) {
|
||||
addSubRooms(entry.roomId, 0);
|
||||
addSubRooms(entry.roomId, 0, current!.items);
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [mx, filteredHierarchy]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flattenedHierarchy.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 32,
|
||||
overscan: 10,
|
||||
});
|
||||
return sections;
|
||||
}, [mx, filteredHierarchy, space.roomId]);
|
||||
|
||||
const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
|
||||
closedCategories.has(categoryId)
|
||||
@@ -410,110 +534,20 @@ export function Space() {
|
||||
</NavItem>
|
||||
</>
|
||||
)}
|
||||
<PluginNavSlot location="channel-list" />
|
||||
</NavCategory>
|
||||
<NavCategory
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const item = flattenedHierarchy[vItem.index];
|
||||
if (!item) return null;
|
||||
|
||||
// Unjoined sub-room item
|
||||
if (item.type === 'unjoined-subroom') {
|
||||
return (
|
||||
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
||||
<UnjoinedSubRoomItem roomId={item.roomId} depth={item.depth} isLast={item.isLast} />
|
||||
</VirtualTile>
|
||||
);
|
||||
}
|
||||
|
||||
// Sub-room item (nested under parent)
|
||||
if (item.type === 'subroom') {
|
||||
const paddingLeft = '30px';
|
||||
|
||||
let treeIcon = '';
|
||||
if (item.depth > 0) {
|
||||
treeIcon = item.isLast ? '╰' : '├';
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
||||
<div style={{ paddingLeft, display: 'flex', alignItems: 'center', minHeight: '1.5rem' }}>
|
||||
{treeIcon && (
|
||||
<span style={{
|
||||
paddingRight: '2px',
|
||||
opacity: 0.5,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1',
|
||||
userSelect: 'none'
|
||||
}}>
|
||||
{treeIcon}
|
||||
</span>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<RoomNavItem
|
||||
room={item.room}
|
||||
selected={selectedRoomId === item.roomId}
|
||||
hideIcon={item.depth > 0}
|
||||
showAvatar={mDirects.has(item.roomId)}
|
||||
direct={mDirects.has(item.roomId)}
|
||||
linkPath={getToLink(item.roomId)}
|
||||
notificationMode={getRoomNotificationMode(notificationPreferences, item.room.roomId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VirtualTile>
|
||||
);
|
||||
}
|
||||
|
||||
// Hierarchy item (room or space)
|
||||
const { roomId } = item;
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
if (isSpace(room)) {
|
||||
const categoryId = makeNavCategoryId(space.roomId, roomId);
|
||||
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
key={vItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<div style={{ paddingTop: vItem.index === 0 ? undefined : config.space.S400 }}>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
data-category-id={categoryId}
|
||||
onClick={handleCategoryClick}
|
||||
closed={closedCategories.has(categoryId)}
|
||||
>
|
||||
{roomId === space.roomId ? 'Rooms' : room?.name}
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
</div>
|
||||
</VirtualTile>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTile virtualItem={vItem} key={vItem.index} ref={virtualizer.measureElement}>
|
||||
<RoomNavItem
|
||||
room={room}
|
||||
selected={selectedRoomId === roomId}
|
||||
showAvatar={mDirects.has(roomId)}
|
||||
direct={mDirects.has(roomId)}
|
||||
linkPath={getToLink(roomId)}
|
||||
notificationMode={getRoomNotificationMode(notificationPreferences, room.roomId)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
<PluginNavSlot location="channel-list" />
|
||||
</NavCategory>
|
||||
{navSections.map((section) => (
|
||||
<SpaceNavSection
|
||||
key={section.categoryId}
|
||||
section={section}
|
||||
closed={closedCategories.has(section.categoryId)}
|
||||
onCategoryClick={handleCategoryClick}
|
||||
selectedRoomId={selectedRoomId}
|
||||
mDirects={mDirects}
|
||||
notificationPreferences={notificationPreferences}
|
||||
getToLink={getToLink}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</PageNavContent>
|
||||
</PageNav>
|
||||
|
||||
Reference in New Issue
Block a user