+
(null);
const projectSidebarPanelRef = useRef(null);
const applyingSidebarLayoutRef = useRef(false);
+ const sidebarLayoutAnimationFrameRef = useRef(null);
+ const hasInitializedSidebarLayoutRef = useRef(false);
/** Expanded sidebar width in px; only user drag (or stored value) changes this — window resize adjusts % to keep this width. */
const sidebarWidthPxRef = useRef(readStoredSidebarWidthPx());
const persistSidebarWidthTimeoutRef = useRef {
+ const group = shellPanelGroupImperativeRef.current;
+ if (!group) return;
+
+ const target = layout.map(clampPct);
+
+ if (sidebarLayoutAnimationFrameRef.current != null) {
+ cancelAnimationFrame(sidebarLayoutAnimationFrameRef.current);
+ sidebarLayoutAnimationFrameRef.current = null;
+ }
+
+ const applyFinalLayout = () => {
+ group.setLayout(target);
+ requestAnimationFrame(() => {
+ applyingSidebarLayoutRef.current = false;
+ });
+ };
+
+ const current = group.getLayout();
+ const shouldAnimate =
+ animate &&
+ current.length === target.length &&
+ current.some((value, index) => Math.abs(value - target[index]) > 0.1);
+
+ applyingSidebarLayoutRef.current = true;
+
+ if (!shouldAnimate) {
+ applyFinalLayout();
+ return;
+ }
+
+ const from = [...current];
+ const durationMs = 260;
+ const start = performance.now();
+
+ const tick = (now: number) => {
+ const progress = Math.min(1, (now - start) / durationMs);
+ const eased = 1 - Math.pow(1 - progress, 3);
+ group.setLayout(
+ from.map((value, index) => value + (target[index] - value) * eased)
+ );
+
+ if (progress < 1) {
+ sidebarLayoutAnimationFrameRef.current = requestAnimationFrame(tick);
+ return;
+ }
+
+ sidebarLayoutAnimationFrameRef.current = null;
+ applyFinalLayout();
+ };
+
+ sidebarLayoutAnimationFrameRef.current = requestAnimationFrame(tick);
+ },
+ []
+ );
+
/** Recompute sidebar % from fixed px so the rail does not grow/shrink when the window resizes. */
- const applyExpandedSidebarLayout = useCallback(() => {
- const shell = shellPanelGroupRef.current;
- const group = shellPanelGroupImperativeRef.current;
- if (!shell || !group) return;
- if (usePageTabStore.getState().projectSidebarFolded) return;
- const w = shell.getBoundingClientRect().width;
- if (w <= 0) return;
- const minPct = clampPct((SIDEBAR_MIN_PX / w) * 100);
- const maxPct = clampPct((SIDEBAR_MAX_PX / w) * 100);
- const px = Math.min(
- SIDEBAR_MAX_PX,
- Math.max(SIDEBAR_MIN_PX, sidebarWidthPxRef.current)
- );
- let pct = (px / w) * 100;
- pct = Math.min(maxPct, Math.max(minPct, pct));
- applyingSidebarLayoutRef.current = true;
- group.setLayout([pct, 100 - pct]);
- requestAnimationFrame(() => {
- applyingSidebarLayoutRef.current = false;
- });
- }, []);
+ const applyExpandedSidebarLayout = useCallback(
+ (animate: boolean = false) => {
+ const shell = shellPanelGroupRef.current;
+ if (!shell) return;
+ if (usePageTabStore.getState().projectSidebarFolded) return;
+ const w = shell.getBoundingClientRect().width;
+ if (w <= 0) return;
+ const minPct = clampPct((SIDEBAR_MIN_PX / w) * 100);
+ const maxPct = clampPct((SIDEBAR_MAX_PX / w) * 100);
+ const px = Math.min(
+ SIDEBAR_MAX_PX,
+ Math.max(SIDEBAR_MIN_PX, sidebarWidthPxRef.current)
+ );
+ let pct = (px / w) * 100;
+ pct = Math.min(maxPct, Math.max(minPct, pct));
+ setShellPanelLayout([pct, 100 - pct], animate);
+ },
+ [setShellPanelLayout]
+ );
const handleShellPanelLayout = useCallback(
(sizes: number[]) => {
@@ -240,26 +297,21 @@ export default function Home() {
/** Expanded: apply stored px width when leaving folded or on first paint. */
useLayoutEffect(() => {
if (projectSidebarFolded) return;
- applyExpandedSidebarLayout();
+ applyExpandedSidebarLayout(hasInitializedSidebarLayoutRef.current);
+ hasInitializedSidebarLayoutRef.current = true;
}, [projectSidebarFolded, applyExpandedSidebarLayout]);
/** Folded: exact rail + main split (`setLayout`); update when shell width changes rail %. */
useLayoutEffect(() => {
if (!projectSidebarFolded) return;
- const shell = shellPanelGroupRef.current;
- const group = shellPanelGroupImperativeRef.current;
- if (!shell || !group) return;
- const w = shell.getBoundingClientRect().width;
- if (w <= 0) return;
-
- applyingSidebarLayoutRef.current = true;
const rail = sidebarPct.rail;
const main = Math.min(99, Math.max(0, 100 - rail));
- group.setLayout([rail, main]);
- requestAnimationFrame(() => {
- applyingSidebarLayoutRef.current = false;
- });
- }, [projectSidebarFolded, sidebarPct.rail]);
+ setShellPanelLayout(
+ [rail, main],
+ hasInitializedSidebarLayoutRef.current && sidebarWidthPxRef.current > 0
+ );
+ hasInitializedSidebarLayoutRef.current = true;
+ }, [projectSidebarFolded, sidebarPct.rail, setShellPanelLayout]);
useEffect(() => {
const el = shellPanelGroupRef.current;
@@ -296,6 +348,9 @@ export default function Home() {
useEffect(() => {
return () => {
+ if (sidebarLayoutAnimationFrameRef.current != null) {
+ cancelAnimationFrame(sidebarLayoutAnimationFrameRef.current);
+ }
if (persistSidebarWidthTimeoutRef.current) {
clearTimeout(persistSidebarWidthTimeoutRef.current);
}
diff --git a/src/style/tokens/base.color.json b/src/style/tokens/base.color.json
index 0cb33161..b9fea6f0 100644
--- a/src/style/tokens/base.color.json
+++ b/src/style/tokens/base.color.json
@@ -60,7 +60,7 @@
"information": "#2563eb",
"status-running": "#2563eb",
"status-splitting": "#0284c7",
- "status-pending": "#d97706",
+ "status-pending": "#4a69af",
"status-error": "#dc2626",
"status-reassigning": "#ea580c",
"status-completed": "#16a34a",
@@ -82,7 +82,7 @@
"information": "#2563eb",
"status-running": "#2563eb",
"status-splitting": "#0284c7",
- "status-pending": "#d97706",
+ "status-pending": "#4a69af",
"status-error": "#dc2626",
"status-reassigning": "#ea580c",
"status-completed": "#16a34a",
@@ -172,17 +172,17 @@
"950": "#082f49"
},
"status-pending": {
- "50": "#fffbeb",
- "100": "#fef3c7",
- "200": "#fde68a",
- "300": "#fcd34d",
- "400": "#fbbf24",
- "500": "#f59e0b",
- "600": "#d97706",
- "700": "#b45309",
- "800": "#92400e",
- "900": "#78350f",
- "950": "#451a03"
+ "50": "#e0e8f6",
+ "100": "#d0dcf1",
+ "200": "#b3c5e8",
+ "300": "#92abdc",
+ "400": "#7291cf",
+ "500": "#5778c0",
+ "600": "#4a69af",
+ "700": "#3f598f",
+ "800": "#374d79",
+ "900": "#314163",
+ "950": "#222d45"
},
"status-error": {
"50": "#fef2f2",
diff --git a/test/unit/lib/themeTokens/engine.v2.test.ts b/test/unit/lib/themeTokens/engine.v2.test.ts
index fa53112e..a4a2e2f4 100644
--- a/test/unit/lib/themeTokens/engine.v2.test.ts
+++ b/test/unit/lib/themeTokens/engine.v2.test.ts
@@ -314,24 +314,46 @@ describe('themeTokens v2 engine', () => {
expect(successInverse).toBe('#ffffff');
});
- it('maps system status background emphases to 50/300/600/900 shades', () => {
- for (const mode of ['light', 'dark'] as const) {
- const theme = buildThemeV2(
- createDefaultThemeContractV2(mode, {
- themeId: 'eigent',
- contrast: 50,
- }),
- DEFAULT_THEME_CATALOG
+ it('maps system status background emphases to fixed shade steps (light vs dark)', () => {
+ const lightTheme = buildThemeV2(
+ createDefaultThemeContractV2('light', {
+ themeId: 'eigent',
+ contrast: 50,
+ }),
+ DEFAULT_THEME_CATALOG
+ );
+ const darkTheme = buildThemeV2(
+ createDefaultThemeContractV2('dark', {
+ themeId: 'eigent',
+ contrast: 50,
+ }),
+ DEFAULT_THEME_CATALOG
+ );
+
+ for (const tone of SYSTEM_STATUS_TONES) {
+ const scale = FIXED_SHADE_SCALES[tone];
+ expect(scale).toBeDefined();
+ expect(lightTheme.tokens[`bg.${tone}.subtle.default`]).toBe(
+ scale?.['50']
+ );
+ expect(lightTheme.tokens[`bg.${tone}.muted.default`]).toBe(
+ scale?.['300']
+ );
+ expect(lightTheme.tokens[`bg.${tone}.default.default`]).toBe(
+ scale?.['600']
+ );
+ expect(lightTheme.tokens[`bg.${tone}.strong.default`]).toBe(
+ scale?.['900']
);
- for (const tone of SYSTEM_STATUS_TONES) {
- const scale = FIXED_SHADE_SCALES[tone];
- expect(scale).toBeDefined();
- expect(theme.tokens[`bg.${tone}.subtle.default`]).toBe(scale?.['50']);
- expect(theme.tokens[`bg.${tone}.muted.default`]).toBe(scale?.['300']);
- expect(theme.tokens[`bg.${tone}.default.default`]).toBe(scale?.['600']);
- expect(theme.tokens[`bg.${tone}.strong.default`]).toBe(scale?.['900']);
- }
+ expect(darkTheme.tokens[`bg.${tone}.subtle.default`]).toBe(
+ scale?.['950']
+ );
+ expect(darkTheme.tokens[`bg.${tone}.muted.default`]).toBe(scale?.['600']);
+ expect(darkTheme.tokens[`bg.${tone}.default.default`]).toBe(
+ scale?.['300']
+ );
+ expect(darkTheme.tokens[`bg.${tone}.strong.default`]).toBe(scale?.['50']);
}
});