From ac76a827e0049f17f8932a71b8afae71677bcbcb Mon Sep 17 00:00:00 2001
From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com>
Date: Sat, 3 Oct 2026 22:31:57 -0300
Subject: [PATCH 1/3] perf(app): slide the side panels and the bots list with
transforms
Both animated their width, so the chat was laid out again on every
frame. With a long chat that took 4-7 ms a frame, over the 5.5 ms a
180 Hz screen gives, and frames came unevenly. Now the room changes at
once and only a transform moves: the list and the panels slide over the
chat, at 180 fps and about 1.8 ms a frame on the same chat.
Spec 15.1 said the chat narrowed along with the panel; it now says the
chat takes its new width at once.
---
app/src/features/bots/BotView.tsx | 3 ++-
app/src/panels.css | 25 +++++++++++++------------
app/src/shell/sidebarToggle.tsx | 9 ++-------
app/src/sidebar.css | 28 ++++++++++++++++++++++++----
app/src/ui/SidePanel.tsx | 10 +---------
docs/spec.md | 4 ++--
6 files changed, 44 insertions(+), 35 deletions(-)
diff --git a/app/src/features/bots/BotView.tsx b/app/src/features/bots/BotView.tsx
index feb305a5..382a76f8 100644
--- a/app/src/features/bots/BotView.tsx
+++ b/app/src/features/bots/BotView.tsx
@@ -181,7 +181,8 @@ export function BotView({ bot, crew }: { bot: Bot; crew: Crew }) {
/>
label={bot.name} tabs={tabs} value={pane} onChange={setPane} />
-
+ {/* A panel slides out over the chat, and is cut at the window's edge. */}
+
{pane === "chat" ? (
diff --git a/app/src/panels.css b/app/src/panels.css
index bfdfe473..84342bdb 100644
--- a/app/src/panels.css
+++ b/app/src/panels.css
@@ -1,31 +1,32 @@
-/* Side panels slide (spec 15.1, panelMotion.ts): the panel widens from the
- window's right edge, or from the width of the panel it replaces, while
- its content, already at its full width, comes in with it, and the chat
- narrows smoothly instead of jumping. Closing runs it back to the edge.
- Reduced motion makes both instant (motion.css). */
+/* Side panels slide (spec 15.1, panelMotion.ts): the chat takes its new
+ width at once, and the panel, already at its full width, comes in from
+ the window's right edge, or from the width of the panel it replaces.
+ Closing, it leaves the chat its room at once and slides back out over
+ it. Only a transform moves, so the chat is laid out once, not on every
+ frame. Reduced motion makes both instant (motion.css). */
@keyframes panel-open {
from {
- width: var(--panel-from, 0px);
- min-width: var(--panel-from, 0px);
+ transform: translateX(max(0px, calc(100% - var(--panel-from, 0px))));
}
}
@keyframes panel-close {
to {
- width: 0;
- min-width: 0;
- border-left-width: 0;
+ transform: translateX(100%);
}
}
.panel-opening {
- overflow: hidden;
animation: panel-open 460ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
+/* Out of the flow, on the right edge of the chat and its panel. */
.panel-closing {
- overflow: hidden;
+ position: absolute;
+ inset-block: 0;
+ right: 0;
+ z-index: 20;
pointer-events: none;
animation: panel-close 360ms cubic-bezier(0.4, 0, 0.6, 1) both;
}
diff --git a/app/src/shell/sidebarToggle.tsx b/app/src/shell/sidebarToggle.tsx
index 1db82bd3..92d0625a 100644
--- a/app/src/shell/sidebarToggle.tsx
+++ b/app/src/shell/sidebarToggle.tsx
@@ -49,13 +49,8 @@ export function SidebarSlot({ children }: { children: ReactNode }) {
const open = usePref(prefs.sidebar);
useSidebarKey();
return (
-
- {children}
+
+
{children}
);
}
diff --git a/app/src/sidebar.css b/app/src/sidebar.css
index 08f0c168..656624f5 100644
--- a/app/src/sidebar.css
+++ b/app/src/sidebar.css
@@ -1,12 +1,32 @@
-/* Hiding the crews and bots (spec 15.1, sidebarToggle.tsx): the list keeps
- its width and slides out under the left edge while the chat widens.
- Reduced motion makes it instant (motion.css). */
+/* Hiding the crews and bots (spec 15.1, sidebarToggle.tsx): the room for
+ the list changes at once, and the list slides out under the left edge
+ (or back in) above the chat. Only a transform moves, so the chat is laid
+ out once, not on every frame. Reduced motion makes it instant
+ (motion.css). */
.sidebar-slot {
+ position: relative;
width: 18rem;
- transition: width 320ms cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-slot[data-open="false"] {
width: 0;
}
+
+.sidebar-slide {
+ position: absolute;
+ inset-block: 0;
+ left: 0;
+ z-index: 20;
+ display: flex;
+ transition: transform 320ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Hidden once it is out of sight, so it is not drawn off screen. */
+.sidebar-slot[data-open="false"] > .sidebar-slide {
+ transform: translateX(-100%);
+ visibility: hidden;
+ transition:
+ transform 320ms cubic-bezier(0.4, 0, 0.2, 1),
+ visibility 0s 320ms;
+}
diff --git a/app/src/ui/SidePanel.tsx b/app/src/ui/SidePanel.tsx
index dc8fc775..51990430 100644
--- a/app/src/ui/SidePanel.tsx
+++ b/app/src/ui/SidePanel.tsx
@@ -60,9 +60,6 @@ export function SidePanel({
const panel = useRef(null);
const drag = useRef<{ x: number; width: number; scale: number } | null>(null);
const { motion, from, ended } = usePanelMotion(panel, width);
- // While it slides, the content keeps its full width and is cut at the
- // moving edge, so it comes and goes without reflowing.
- const sliding = motion !== "open";
const set = (next: number, keep = false) => {
const clamped = clamp(next);
@@ -149,12 +146,7 @@ export function SidePanel({
>