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:
2026-07-23 00:24:10 +10:00
parent e7777f42d8
commit 61d41900cc
43 changed files with 2765 additions and 421 deletions

View File

@@ -26,6 +26,7 @@ import {
MATRIX_SPOILER_PROPERTY_NAME,
MATRIX_SPOILER_REASON_PROPERTY_NAME,
} from '../../../types/matrix/common';
import { StationeryMedia, hashStationerySeed, stationeryMediaRot } from './StationeryMedia';
import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes';
import { parseGeoUri, scaleYDimension } from '../../utils/common';
import { resolveAttachmentBoxSize } from '../../state/mediaDimensionCache';
@@ -207,6 +208,13 @@ const resolveMediaBoxSize = (
h?: number
): { width: number; height: number } => resolveAttachmentBoxSize(mxcUrl, w, h);
/** Extra space after media so following text stays on the 24px ruled grid — applied outside the photo chrome */
const STATIONERY_LINE_PX = 24;
const snapStationeryGridPad = (height: number): number => {
const rem = height % STATIONERY_LINE_PX;
return rem === 0 ? 0 : STATIONERY_LINE_PX - rem;
};
export function MImage({ content, renderImageContent, outlined }: MImageProps) {
const imgInfo = content?.info;
const mxcUrl = content.file?.url ?? content.url;
@@ -215,11 +223,21 @@ export function MImage({ content, renderImageContent, outlined }: MImageProps) {
}
const { width, height } = resolveMediaBoxSize(mxcUrl, imgInfo?.w, imgInfo?.h);
const gridPad = snapStationeryGridPad(height);
return (
<Attachment outlined={outlined} transparent>
<StationeryMedia
outlined={outlined}
transparent
tiltSeed={mxcUrl}
gridPad={gridPad}
>
<AttachmentBox
style={{ width: toRem(width), height: toRem(height) }}
style={{
width: toRem(width),
height: toRem(height),
['--media-h' as string]: `${height}px`,
}}
data-paarrot-media-height={height}
>
{renderImageContent({
@@ -232,7 +250,7 @@ export function MImage({ content, renderImageContent, outlined }: MImageProps) {
spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
})}
</AttachmentBox>
</Attachment>
</StationeryMedia>
);
}
@@ -269,9 +287,15 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
videoInfo.w ?? videoInfo.thumbnail_info?.w,
videoInfo.h ?? videoInfo.thumbnail_info?.h
);
const gridPad = snapStationeryGridPad(height);
return (
<Attachment outlined={outlined} transparent>
<StationeryMedia
outlined={outlined}
transparent
tiltSeed={mxcUrl}
gridPad={gridPad}
>
<AttachmentHeader>
<FileHeader
body={filename}
@@ -287,7 +311,10 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
/>
</AttachmentHeader>
<AttachmentBox
style={{ width: toRem(width), height: toRem(height) }}
style={{
width: toRem(width),
height: toRem(height),
}}
data-paarrot-media-height={height}
>
{renderVideoContent({
@@ -300,7 +327,7 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
})}
</AttachmentBox>
</Attachment>
</StationeryMedia>
);
}
@@ -439,12 +466,16 @@ export function MSticker({ content, renderImageContent }: MStickerProps) {
return <MessageBrokenContent />;
}
const height = scaleYDimension(imgInfo?.w || 152, 152, imgInfo?.h || 152);
const tapeAlt = hashStationerySeed(mxcUrl) % 2 === 1;
return (
<AttachmentBox
data-stationery-media=""
data-tape-alt={tapeAlt ? '' : undefined}
style={{
height: toRem(height < 48 ? 48 : height),
width: toRem(152),
['--media-rot' as string]: stationeryMediaRot(mxcUrl),
}}
>
{renderImageContent({

View File

@@ -53,6 +53,19 @@ export const Reaction = style([
},
]);
export const ReactionStack = style([
DefaultReset,
{
position: 'relative',
display: 'inline-grid',
placeItems: 'center',
gridTemplateColumns: '1fr',
gridTemplateRows: '1fr',
lineHeight: toRem(20),
minWidth: 0,
},
]);
export const ReactionText = style([
DefaultReset,
{
@@ -60,7 +73,15 @@ export const ReactionText = style([
maxWidth: toRem(150),
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: toRem(20),
gridArea: '1 / 1',
transformOrigin: 'center center',
// Fan each stacked copy so count>1 reads as multiple stickers
transform: `translate(
calc(var(--sticker-i, 0) * 7px - 2px),
calc(var(--sticker-i, 0) * -5px)
) rotate(calc((var(--sticker-i, 0) - 1) * 12deg))`,
},
]);

View File

@@ -7,6 +7,8 @@ import { getHexcodeForEmoji, getShortcodeFor } from '../../plugins/emoji';
import { getMemberDisplayName } from '../../utils/room';
import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
const MAX_STICKER_STACK = 4;
export const Reaction = as<
'button',
{
@@ -15,35 +17,67 @@ export const Reaction = as<
reaction: string;
useAuthentication?: boolean;
}
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => (
<Box
as="button"
className={classNames(css.Reaction, className)}
alignItems="Center"
shrink="No"
gap="200"
{...props}
ref={ref}
>
<Text className={css.ReactionText} as="span" size="T400">
{reaction.startsWith('mxc://') ? (
<img
className={css.ReactionImg}
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
}
alt={reaction}
/>
) : (
<Text as="span" size="Inherit" truncate>
{reaction}
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => {
const stackCount = Math.min(Math.max(count, 1), MAX_STICKER_STACK);
const showCount = count > 2;
const isCustomEmoji = reaction.startsWith('mxc://');
const customSrc = isCustomEmoji
? mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
: undefined;
return (
<Box
as="button"
className={classNames(css.Reaction, className)}
alignItems="Center"
shrink="No"
gap="200"
data-reaction=""
data-reaction-stack={stackCount}
aria-label={`${reaction}, ${count}`}
{...props}
ref={ref}
>
<span className={css.ReactionStack} data-reaction-stack-layer="">
{Array.from({ length: stackCount }, (_, i) => {
// Draw back-to-front so the top sticker is the last layer
const layer = stackCount - 1 - i;
return (
<Text
key={layer}
className={css.ReactionText}
as="span"
size="T400"
data-reaction-sticker=""
style={{
['--sticker-i' as string]: layer,
zIndex: i + 1,
}}
aria-hidden={i < stackCount - 1 ? true : undefined}
>
{isCustomEmoji ? (
<img
className={css.ReactionImg}
src={customSrc}
alt={i === stackCount - 1 ? reaction : ''}
/>
) : (
<Text as="span" size="Inherit" truncate>
{reaction}
</Text>
)}
</Text>
);
})}
</span>
{showCount && (
<Text as="span" size="T300" data-reaction-count="">
{count}
</Text>
)}
</Text>
<Text as="span" size="T300">
{count}
</Text>
</Box>
));
</Box>
);
});
type ReactionTooltipMsgProps = {
room: Room;

View File

@@ -14,29 +14,54 @@ import { useRoomEvent } from '../../hooks/useRoomEvent';
import colorMXID from '../../../util/colorMXID';
import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useOtherUserColor } from '../../hooks/useUserColor';
import { isStationeryTheme, useTheme } from '../../hooks/useTheme';
import { STATIONERY_NAME_INK } from '../../utils/paperSafeInk';
type ReplyLayoutProps = {
userColor?: string;
username?: ReactNode;
};
export const ReplyLayout = as<'div', ReplyLayoutProps>(
({ username, userColor, className, children, ...props }, ref) => (
<Box
className={classNames(css.Reply, className)}
alignItems="Center"
gap="100"
{...props}
ref={ref}
>
<Box style={{ color: userColor, maxWidth: toRem(200) }} alignItems="Center" shrink="No">
<Icon size="100" src={Icons.ReplyArrow} />
{username}
({ username, userColor, className, children, ...props }, ref) => {
const theme = useTheme();
const isStationery = isStationeryTheme(theme);
return (
<Box
className={classNames(css.Reply, className)}
alignItems="Center"
gap="100"
{...props}
ref={ref}
>
<Box
style={
isStationery
? {
color: STATIONERY_NAME_INK,
maxWidth: toRem(200),
...(userColor
? {
['--username-accent' as string]: userColor,
textDecorationColor: userColor,
}
: null),
}
: { color: userColor, maxWidth: toRem(200) }
}
data-username-accent={isStationery && userColor ? '' : undefined}
alignItems="Center"
shrink="No"
>
<Icon size="100" src={Icons.ReplyArrow} />
{username}
</Box>
<Box grow="Yes" className={css.ReplyContent}>
{children}
</Box>
</Box>
<Box grow="Yes" className={css.ReplyContent}>
{children}
</Box>
</Box>
)
);
}
);
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (

View File

@@ -0,0 +1,293 @@
import React, {
ComponentProps,
CSSProperties,
MouseEventHandler,
ReactNode,
useCallback,
useMemo,
useRef,
} from 'react';
import { Attachment } from './attachment';
import { isStationeryTheme, useTheme } from '../../hooks/useTheme';
type Corner = 'tl' | 'tr' | 'bl' | 'br';
type StationeryMediaProps = ComponentProps<typeof Attachment> & {
children: ReactNode;
/** Stable id (e.g. mxc URL) so tilt/tape don't reshuffle on timeline re-renders */
tiltSeed?: string;
/** Invisible bottom spacer (px) so following text stays on the ruled grid */
gridPad?: number;
};
const CORNERS: { id: Corner; x: 0 | 1; y: 0 | 1 }[] = [
{ id: 'tl', x: 0, y: 0 },
{ id: 'tr', x: 1, y: 0 },
{ id: 'bl', x: 0, y: 1 },
{ id: 'br', x: 1, y: 1 },
];
const MEDIA_ROTS = [-0.9, 0.7, -0.5, 1.0, -1.1, 0.8, -0.4, 0.9] as const;
export function hashStationerySeed(seed: string): number {
let h = 2166136261;
for (let i = 0; i < seed.length; i += 1) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
export function stationeryMediaRot(seed: string): string {
return `${MEDIA_ROTS[hashStationerySeed(seed) % MEDIA_ROTS.length]}deg`;
}
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
/**
* Fold sits halfway between corner and cursor; flap hinges on that fold.
* `keep` = remaining photo; `flap` = lifted triangle.
*/
function cornerPeelPaths(
id: Corner,
w: number,
h: number,
cx: number,
cy: number,
ux: number,
uy: number,
size: number
): {
keep: string;
flap: string;
origin: string;
axisX: number;
axisY: number;
hingeSign: number;
} {
const eps = 0.0001;
// Fold line through the midpoint on corner→cursor, ⊥ to that ray
const px = cx + ux * size;
const py = cy + uy * size;
let ax = 0;
let ay = 0;
let bx = 0;
let by = 0;
if (id === 'tr') {
ax = clamp(Math.abs(ux) > eps ? px + (py * uy) / ux : px, 0, w);
ay = 0;
bx = w;
by = clamp(Math.abs(uy) > eps ? py - ((w - px) * ux) / uy : py, 0, h);
} else if (id === 'tl') {
ax = clamp(Math.abs(ux) > eps ? px + (py * uy) / ux : px, 0, w);
ay = 0;
bx = 0;
by = clamp(Math.abs(uy) > eps ? py - ((0 - px) * ux) / uy : py, 0, h);
} else if (id === 'br') {
ax = clamp(Math.abs(ux) > eps ? px + ((py - h) * uy) / ux : px, 0, w);
ay = h;
bx = w;
by = clamp(Math.abs(uy) > eps ? py - ((w - px) * ux) / uy : py, 0, h);
} else {
ax = clamp(Math.abs(ux) > eps ? px + ((py - h) * uy) / ux : px, 0, w);
ay = h;
bx = 0;
by = clamp(Math.abs(uy) > eps ? py - ((0 - px) * ux) / uy : py, 0, h);
}
let keep: string;
if (id === 'tr') {
keep = `polygon(0 0, ${ax}px 0, ${w}px ${by}px, ${w}px ${h}px, 0 ${h}px)`;
} else if (id === 'tl') {
keep = `polygon(${ax}px 0, ${w}px 0, ${w}px ${h}px, 0 ${h}px, 0 ${by}px)`;
} else if (id === 'br') {
keep = `polygon(0 0, ${w}px 0, ${w}px ${by}px, ${ax}px ${h}px, 0 ${h}px)`;
} else {
keep = `polygon(0 0, ${w}px 0, ${w}px ${h}px, ${ax}px ${h}px, 0 ${by}px)`;
}
const flap = `polygon(${cx}px ${cy}px, ${ax}px ${ay}px, ${bx}px ${by}px)`;
// Hinge on the fold (midpoint), axis along the fold edge
const midX = (ax + bx) / 2;
const midY = (ay + by) / 2;
const fdx = bx - ax;
const fdy = by - ay;
const flen = Math.hypot(fdx, fdy) || 1;
const axisX = fdx / flen;
const axisY = fdy / flen;
// Sign so the corner swings up over the fold (right-hand rule)
const cross = fdx * (cy - midY) - fdy * (cx - midX);
const hingeSign = cross >= 0 ? 1 : -1;
return {
keep,
flap,
origin: `${midX}px ${midY}px`,
axisX,
axisY,
hingeSign,
};
}
/**
* Stationery-theme image chrome: clear corner tape + a mouse-driven peel
* that shows a duplicated slice of the photo lifting toward the cursor.
*/
export function StationeryMedia({
children,
tiltSeed = 'stationery',
gridPad = 0,
style,
...attachmentProps
}: StationeryMediaProps) {
const theme = useTheme();
const isStationery = isStationeryTheme(theme);
const peelRef = useRef<HTMLSpanElement>(null);
const tiltStyle = useMemo((): CSSProperties => {
return {
...(style as CSSProperties | undefined),
// Margin sits outside the paper chrome — snaps the grid without a white bar
...(isStationery && gridPad > 0 ? { marginBottom: `${gridPad}px` } : null),
['--media-rot' as string]: stationeryMediaRot(tiltSeed),
};
}, [tiltSeed, style, isStationery, gridPad]);
const tapeAlt = useMemo(() => hashStationerySeed(tiltSeed) % 2 === 1, [tiltSeed]);
// Default tape covers TL; alt covers TR — peel only the free top corner
const peelCorner = useMemo(
() => (tapeAlt ? CORNERS.find((c) => c.id === 'tl')! : CORNERS.find((c) => c.id === 'tr')!),
[tapeAlt]
);
const clearPeel = useCallback((host: HTMLElement) => {
const peel = peelRef.current;
const target = host.querySelector('[data-paarrot-media-height]') as HTMLElement | null;
if (peel) {
// Hide flap first (no opacity tween) so restoring the photo can't double-draw
peel.style.transition = 'none';
peel.style.opacity = '0';
peel.style.setProperty('--peel-hinge', '0deg');
peel.style.clipPath = '';
}
if (target) target.style.clipPath = '';
}, []);
const syncPeel = useCallback(
(host: HTMLElement, clientX: number, clientY: number) => {
const peel = peelRef.current;
if (!peel) return;
const rect = host.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
const w = rect.width;
const h = rect.height;
const best = peelCorner;
const cx = best.x * w;
const cy = best.y * h;
const dist = Math.hypot(cx - x, cy - y);
const maxReach = Math.min(w, h) * 0.55;
// Fold halfway between corner and cursor; fade when leaving the corner zone
let size = 0;
let amount = 0;
if (dist > 8 && dist <= maxReach) {
size = dist * 0.5;
amount = Math.min(1, size / (Math.min(w, h) * 0.22));
} else if (dist > maxReach && dist < maxReach * 1.35) {
const fade = 1 - (dist - maxReach) / (maxReach * 0.35);
size = maxReach * 0.5 * Math.max(0, fade);
amount = Math.max(0, fade);
}
const img = host.querySelector('img');
const url = img?.currentSrc || img?.src || '';
// Unit vector from corner → cursor (triangle aims this way)
let ux = x - cx;
let uy = y - cy;
if (best.id === 'tr') {
ux = Math.min(-0.05, ux);
uy = Math.max(0.05, uy);
} else {
ux = Math.max(0.05, ux);
uy = Math.max(0.05, uy);
}
const len = Math.hypot(ux, uy) || 1;
ux /= len;
uy /= len;
peel.dataset.corner = best.id;
if (url) {
peel.style.setProperty('--peel-img', `url(${JSON.stringify(url)})`);
}
const target = host.querySelector('[data-paarrot-media-height]') as HTMLElement | null;
if (target && amount > 0.06 && size > 6) {
const { keep, flap, origin, axisX, axisY, hingeSign } = cornerPeelPaths(
best.id,
w,
h,
cx,
cy,
ux,
uy,
size
);
target.style.clipPath = keep;
peel.style.clipPath = flap;
peel.style.transformOrigin = origin;
peel.style.setProperty('--peel-axis-x', String(axisX));
peel.style.setProperty('--peel-axis-y', String(axisY));
peel.style.setProperty('--peel-hinge', `${hingeSign * amount * 52}deg`);
peel.style.setProperty('--peel-tip-x', `${cx}px`);
peel.style.setProperty('--peel-tip-y', `${cy}px`);
peel.style.setProperty('--peel-grad-r', `${Math.max(size * 1.15, 12)}px`);
peel.style.opacity = '1';
} else {
clearPeel(host);
}
},
[peelCorner, clearPeel]
);
const handleMove: MouseEventHandler<HTMLElement> = (evt) => {
syncPeel(evt.currentTarget, evt.clientX, evt.clientY);
};
const handleEnter: MouseEventHandler<HTMLElement> = (evt) => {
syncPeel(evt.currentTarget, evt.clientX, evt.clientY);
};
const handleLeave: MouseEventHandler<HTMLElement> = (evt) => {
clearPeel(evt.currentTarget);
};
if (!isStationery) {
return (
<Attachment style={style} {...attachmentProps}>
{children}
</Attachment>
);
}
return (
<Attachment
{...attachmentProps}
style={tiltStyle}
data-stationery-media=""
data-tape-alt={tapeAlt ? '' : undefined}
onMouseEnter={handleEnter}
onMouseMove={handleMove}
onMouseLeave={handleLeave}
>
<span ref={peelRef} data-stationery-peel="" aria-hidden="true" />
{children}
</Attachment>
);
}

View File

@@ -1,7 +1,9 @@
import React from 'react';
import React, { CSSProperties, useMemo } from 'react';
import { Text, as } from 'folds';
import classNames from 'classnames';
import * as css from './layout.css';
import { isStationeryTheme, useTheme } from '../../../hooks/useTheme';
import { STATIONERY_NAME_INK } from '../../../utils/paperSafeInk';
export const MessageBase = as<'div', css.MessageBaseVariants>(
({ className, highlight, selected, collapse, autoCollapse, space, newMessage, ...props }, ref) => (
@@ -10,6 +12,7 @@ export const MessageBase = as<'div', css.MessageBaseVariants>(
css.MessageBase({ highlight, selected, collapse, autoCollapse, space, newMessage }),
className
)}
data-message-collapsed={collapse ? '' : undefined}
{...props}
ref={ref}
/>
@@ -17,12 +20,45 @@ export const MessageBase = as<'div', css.MessageBaseVariants>(
);
export const AvatarBase = as<'span'>(({ className, ...props }, ref) => (
<span className={classNames(css.AvatarBase, className)} {...props} ref={ref} />
<span
className={classNames(css.AvatarBase, className)}
data-message-avatar=""
{...props}
ref={ref}
/>
));
export const Username = as<'span'>(({ as: AsUsername = 'span', className, ...props }, ref) => (
<AsUsername className={classNames(css.Username, className)} {...props} ref={ref} />
));
export const Username = as<'span'>(({ as: AsUsername = 'span', className, style, ...props }, ref) => {
const theme = useTheme();
const isStationery = isStationeryTheme(theme);
const userColor =
typeof (style as CSSProperties | undefined)?.color === 'string'
? ((style as CSSProperties).color as string)
: undefined;
const safeStyle = useMemo((): CSSProperties | undefined => {
if (!isStationery) return style as CSSProperties | undefined;
return {
...(style as CSSProperties | undefined),
// Name in ink; keep their color as an accent underline
color: STATIONERY_NAME_INK,
...(userColor
? { ['--username-accent' as string]: userColor, textDecorationColor: userColor }
: null),
};
}, [style, isStationery, userColor]);
return (
<AsUsername
className={classNames(css.Username, className)}
data-message-username=""
data-username-accent={isStationery && userColor ? '' : undefined}
style={safeStyle}
{...props}
ref={ref}
/>
);
});
export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...props }, ref) => (
<AsUsernameBold className={classNames(css.UsernameBold, className)} {...props} ref={ref} />
@@ -36,6 +72,8 @@ export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?
priority={notice ? '300' : '400'}
className={classNames(css.MessageTextBody({ preWrap, jumboEmoji, emote }), className)}
data-allow-text-selection="true"
data-message-body=""
data-jumbo-emoji={jumboEmoji ? '' : undefined}
{...props}
ref={ref}
/>

View File

@@ -204,6 +204,14 @@ export const Username = style({
'button&:hover, button&:focus-visible': {
textDecoration: 'underline',
},
// Stationery uses a colored border-bottom accent instead
'html.stationery &, body.stationery &': {
overflow: 'visible',
overflowY: 'visible',
},
'body.stationery button&:hover, body.stationery button&:focus-visible': {
textDecoration: 'none',
},
},
});

View File

@@ -7,5 +7,10 @@ type NavCategoryProps = {
children: ReactNode;
};
export const NavCategory = as<'div', NavCategoryProps>(({ className, ...props }, ref) => (
<div className={classNames(css.NavCategory, className)} {...props} ref={ref} />
<div
className={classNames(css.NavCategory, className)}
data-nav-category=""
{...props}
ref={ref}
/>
));

View File

@@ -13,6 +13,7 @@ export const NavItem = as<
<AsNavItem
className={classNames(css.NavItem({ variant, radii }), className)}
data-highlight={highlight}
data-nav-item=""
{...props}
ref={ref}
>

View File

@@ -6,6 +6,7 @@ import * as css from './style.css';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
import { useShowCompactMasterView } from '../../hooks/useCompactNav';
import { isStationeryTheme, useTheme } from '../../hooks/useTheme';
type PageRootProps = {
nav: ReactNode;
@@ -43,6 +44,7 @@ export function PageNav({
grow={isMobile ? 'Yes' : undefined}
className={classNames(css.PageNav({ size }), className)}
shrink={isMobile ? 'Yes' : 'No'}
data-page-nav=""
>
<Box grow="Yes" direction="Column">
{children}
@@ -58,6 +60,7 @@ export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
className={classNames(css.PageNavHeader({ outlined }), className)}
variant="Background"
size="600"
data-folder-tab="nav"
{...props}
ref={ref}
/>
@@ -73,13 +76,16 @@ export function PageNavContent({
scrollRef?: MutableRefObject<HTMLDivElement | null>;
scrollProps?: React.ComponentProps<typeof Scroll>;
}) {
const theme = useTheme();
const hideScrollbar = isStationeryTheme(theme);
return (
<Box grow="Yes" direction="Column">
<Scroll
ref={scrollRef}
variant="Background"
direction="Vertical"
size="300"
size={hideScrollbar ? '0' : '300'}
hideTrack
visibility="Hover"
{...scrollProps}
@@ -95,6 +101,7 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
grow="Yes"
direction="Column"
className={classNames(ContainerColor({ variant: 'Surface' }), className)}
data-page=""
{...props}
ref={ref}
/>
@@ -106,6 +113,7 @@ export const PageHeader = as<'div', css.PageHeaderVariants>(
as="header"
size="600"
className={classNames(css.PageHeader({ balance, outlined }), className)}
data-folder-tab-bar=""
{...props}
ref={ref}
/>

View File

@@ -4,5 +4,10 @@ import React from 'react';
import * as css from './Sidebar.css';
export const Sidebar = as<'div'>(({ as: AsSidebar = 'div', className, ...props }, ref) => (
<AsSidebar className={classNames(css.Sidebar, className)} {...props} ref={ref} />
<AsSidebar
className={classNames(css.Sidebar, className)}
data-sidebar=""
{...props}
ref={ref}
/>
));

View File

@@ -8,10 +8,10 @@ type SidebarContentProps = {
export function SidebarContent({ scrollable, sticky }: SidebarContentProps) {
return (
<>
<Box direction="Column" grow="Yes">
<Box direction="Column" grow="Yes" data-sidebar-scroll-region="" style={{ overflow: 'visible', minWidth: 0 }}>
{scrollable}
</Box>
<Box direction="Column" shrink="No">
<Box direction="Column" shrink="No" data-sidebar-sticky="" style={{ overflow: 'visible' }}>
{sticky}
</Box>
</>

View File

@@ -7,6 +7,7 @@ export const SidebarItem = as<'div', css.SidebarItemVariants>(
({ as: AsSidebarAvatarBox = 'div', className, active, ...props }, ref) => (
<AsSidebarAvatarBox
className={classNames(css.SidebarItem({ active }), className)}
data-sidebar-item=""
{...props}
ref={ref}
/>
@@ -54,6 +55,7 @@ export const SidebarAvatar = as<'div', css.SidebarAvatarVariants & ComponentProp
<Avatar
className={classNames(css.SidebarAvatar({ size, outlined }), className)}
radii={radii}
data-sidebar-avatar=""
{...props}
ref={ref}
/>
@@ -64,6 +66,7 @@ export const SidebarFolder = as<'div', css.SidebarFolderVariants>(
({ as: AsSidebarFolder = 'div', className, state, ...props }, ref) => (
<AsSidebarFolder
className={classNames(css.SidebarFolder({ state }), className)}
data-sidebar-folder=""
{...props}
ref={ref}
/>

View File

@@ -11,7 +11,7 @@ export const VirtualTile = as<'div', VirtualTileProps>(
({ className, virtualItem, style, ...props }, ref) => (
<div
className={classNames(css.VirtualTile, className)}
style={{ top: virtualItem.start, ...style }}
style={{ top: virtualItem.start, zIndex: virtualItem.index, ...style }}
data-index={virtualItem.index}
{...props}
ref={ref}