- {folderNameOf(rootDir)}
+ {entryNameOf(rootDir)}
}
{menu !== null &&
}
+
diff --git a/apps/desktop/src/widgets/sidebar/ui/tree-item.tsx b/apps/desktop/src/widgets/sidebar/ui/tree-item.tsx
index f2f5a83..7b95624 100644
--- a/apps/desktop/src/widgets/sidebar/ui/tree-item.tsx
+++ b/apps/desktop/src/widgets/sidebar/ui/tree-item.tsx
@@ -1,4 +1,4 @@
-import { memo } from "react";
+import { memo, useEffect, useState } from "react";
import { css } from "styled-system/css";
import { useDocumentStore } from "@entities/document";
@@ -44,6 +44,10 @@ const rowClass = css({
cursor: "pointer",
whiteSpace: "nowrap",
userSelect: "none",
+ transitionProperty: "background-color",
+ transitionDuration: "fast",
+ transitionTimingFunction: "out",
+ _motionReduce: { transition: "none" },
_hover: { background: "bg.hover" },
});
@@ -53,12 +57,27 @@ const chevronClass = css({
height: "3.5",
transitionProperty: "transform",
transitionDuration: "fast",
+ transitionTimingFunction: "out",
+ _motionReduce: { transition: "none" },
// 자기 li의 펼침만 본다 — `>` 사슬로 조상(펼친 부모)의 셰브론까지 도는 것을 막는다.
'[aria-expanded="true"] > [data-row] > &': { transform: "rotate(90deg)" },
});
const nameClass = css({ overflow: "hidden", textOverflow: "ellipsis" });
+// 접기/펼치기 전환 규칙 → decisions/motion.md.
+const childrenCollapseClass = css({
+ display: "grid",
+ gridTemplateRows: "1fr",
+ minHeight: 0,
+ overflow: "hidden",
+ transitionProperty: "grid-template-rows, visibility",
+ transitionDuration: "fast",
+ transitionTimingFunction: "out",
+ _motionReduce: { transition: "none" },
+ '&[data-collapsed="true"]': { gridTemplateRows: "0fr", visibility: "hidden" },
+});
+
const symlinkBadgeClass = css({ flexShrink: 0, fontSize: "xs", opacity: 0.7 });
// 중첩 그룹은 왼쪽 세로 가이드 선으로 그 폴더의 자식 범위를 잇는다(시안 매칭). marginLeft가
@@ -69,6 +88,8 @@ const groupClass = css({
margin: 0,
padding: 0,
marginLeft: "19px",
+ // 0fr 트랙이 내용 최소 높이 아래로 줄 수 있어야 접힘이 닫힌다(→ childrenCollapseClass).
+ minHeight: 0,
borderLeftWidth: "1px",
borderLeftStyle: "solid",
borderLeftColor: "border.muted",
@@ -92,6 +113,18 @@ export const TreeItem = memo(function TreeItem({
const edit = useEntryEditStore((state) => state.edit);
const creatingHere = edit?.mode === "create" && edit.dir === node.path;
const renamingThis = edit?.mode === "rename" && edit.path === node.path;
+ // lazy 첫 펼침 처리 → decisions/motion.md의 예외.
+ const hasChildren = node.children !== undefined;
+ const [childrenEntered, setChildrenEntered] = useState(false);
+ useEffect(() => {
+ if (!hasChildren) {
+ setChildrenEntered(false);
+ return;
+ }
+ const timer = setTimeout(() => setChildrenEntered(true), 0);
+ return () => clearTimeout(timer);
+ }, [hasChildren]);
+ const childrenCollapsed = !expanded || !childrenEntered;
const onContextMenu = (event: React.MouseEvent): void => {
event.preventDefault();
@@ -141,13 +174,19 @@ export const TreeItem = memo(function TreeItem({
{node.name}
{symlinkBadge}
- {expanded && node.children !== undefined && (
-
- {creatingHere && edit !== null && }
- {node.children.map((child) => (
-
- ))}
-
+ {node.children !== undefined && (
+
+
+ {creatingHere && edit !== null && }
+ {node.children.map((child) => (
+
+ ))}
+
+
)}
);
diff --git a/apps/desktop/src/widgets/tab-bar/ui/tab-bar.tsx b/apps/desktop/src/widgets/tab-bar/ui/tab-bar.tsx
index aab85d0..7fe33de 100644
--- a/apps/desktop/src/widgets/tab-bar/ui/tab-bar.tsx
+++ b/apps/desktop/src/widgets/tab-bar/ui/tab-bar.tsx
@@ -46,6 +46,10 @@ const tabClass = css({
borderColor: "transparent",
borderRadius: "md",
layerStyle: "focusInside",
+ transitionProperty: "background-color, border-color, box-shadow",
+ transitionDuration: "fast",
+ transitionTimingFunction: "out",
+ _motionReduce: { transition: "none" },
_hover: { background: "bg.hover" },
'&[aria-selected="true"]': {
background: "bg.paper",
diff --git a/apps/desktop/src/widgets/tab-bar/ui/tab-hover.browser.test.tsx b/apps/desktop/src/widgets/tab-bar/ui/tab-hover.browser.test.tsx
index 29d7241..0775c2b 100644
--- a/apps/desktop/src/widgets/tab-bar/ui/tab-hover.browser.test.tsx
+++ b/apps/desktop/src/widgets/tab-bar/ui/tab-hover.browser.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, render } from "@testing-library/react";
+import { cleanup, render, waitFor } from "@testing-library/react";
import { userEvent } from "vitest/browser";
import { afterEach, beforeEach, expect, it } from "vitest";
@@ -53,5 +53,8 @@ it("비활성 탭은 호버하면 배경이 바뀐다", async () => {
await userEvent.hover(inactive);
- expect(getComputedStyle(inactive).backgroundColor).not.toBe(before);
+ // 배경은 전환된다(→ decisions/motion.md) — 즉시 값이 아니라 전환 완료를 기다린다.
+ await waitFor(() => {
+ expect(getComputedStyle(inactive).backgroundColor).not.toBe(before);
+ });
});