Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/src/features/channels/ui/QuickBotBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function QuickBotBar({ personas, pending, onAdd }: QuickBotBarProps) {
>
<div
className={cn(
"flex items-center gap-1 overflow-hidden transition-all duration-200 ease-out",
"flex items-center gap-1 overflow-hidden transition-[max-width,margin-right,opacity] duration-motion-state ease-motion-standard",
"max-w-0 opacity-0 group-hover/quick:mr-1 group-hover/quick:max-w-[120px] group-hover/quick:opacity-100",
)}
>
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/messages/ui/SystemMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
const reactionPicker = canToggleReactions ? (
<div
className={cn(
"overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-xs backdrop-blur-sm supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
"overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-xs backdrop-blur-sm supports-[backdrop-filter]:bg-background/85 transition-[max-width,border-color,border-width,box-shadow,opacity,transform] duration-motion-feedback ease-motion-standard",
"max-w-0 border-0 shadow-none translate-y-1 opacity-0",
"group-hover/message:max-w-9 group-hover/message:border group-hover/message:border-border/70 group-hover/message:shadow-xs group-hover/message:translate-y-0 group-hover/message:opacity-100",
"group-focus-within/message:max-w-9 group-focus-within/message:border group-focus-within/message:border-border/70 group-focus-within/message:shadow-xs group-focus-within/message:translate-y-0 group-focus-within/message:opacity-100",
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/profile/ui/UserProfilePanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ function ActivityCarouselDots({
<span
aria-hidden="true"
className={cn(
"relative z-10 block rounded-full bg-foreground transition-all",
"relative z-10 block rounded-full bg-foreground transition-[width,opacity] duration-motion-feedback ease-motion-standard",
isSelected
? "h-1 w-4"
: "h-1 w-1 opacity-30 group-hover:opacity-60",
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/projects/ui/ProjectCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export function ProjectActivityBar({
// can receive hover events. Fixed h-2 wrapper keeps layout stable
// while the inner bar grows on hover.
<div className="group/activity-bar relative z-10 flex h-2 w-full items-center">
<div className="flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-muted/60 transition-all duration-150 group-hover/activity-bar:h-2">
<div className="flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-muted/60 transition-[height] duration-motion-feedback ease-motion-standard group-hover/activity-bar:h-2">
{total > 0
? items
.filter((item) => item.count > 0)
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/sidebar/ui/CommunityRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export function CommunityRail({
<TooltipTrigger asChild>
<button
aria-label="Add community"
className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70 outline-hidden transition-all hover:rounded-xl hover:bg-primary/80 hover:text-primary-foreground focus:outline-none focus-visible:outline-none"
className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70 outline-hidden transition-[border-radius,background-color,color] duration-motion-feedback ease-motion-standard hover:rounded-xl hover:bg-primary/80 hover:text-primary-foreground focus:outline-none focus-visible:outline-none"
data-testid="community-rail-add"
onClick={onAddCommunity}
type="button"
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/sidebar/ui/SidebarDnd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function DroppableSectionBody({
<div
ref={setNodeRef}
className={cn(
"rounded-md transition-all",
"rounded-md transition-shadow duration-motion-feedback ease-motion-standard",
isOver && "ring-2 ring-primary/30",
className,
)}
Expand All @@ -96,7 +96,7 @@ export function DroppableUngroupedBody({
<div
ref={setNodeRef}
className={cn(
"rounded-md transition-all",
"rounded-md transition-shadow duration-motion-feedback ease-motion-standard",
isOver && "ring-2 ring-primary/30",
className,
)}
Expand Down
8 changes: 8 additions & 0 deletions desktop/src/shared/styles/globals/motion.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
}

@media (prefers-reduced-motion: reduce) {
:root {
/* Semantic transition utilities inherit the user's motion preference. */
--motion-duration-instant: 1ms;
--motion-duration-fast: 1ms;
--motion-duration-standard: 1ms;
--motion-duration-arrival: 1ms;
}

.motion-enter-conversation {
animation-duration: 1ms;
}
Expand Down
61 changes: 60 additions & 1 deletion desktop/src/shared/styles/globals/motion.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

const motionCss = readFileSync(
new URL("./motion.css", import.meta.url),
"utf8",
);
const tailwindConfig = readFileSync(
new URL("../../../../tailwind.config.js", import.meta.url),
"utf8",
);

function sourceFiles(directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
if (entry.isDirectory()) return sourceFiles(path);
return [".css", ".ts", ".tsx"].includes(extname(path)) ? [path] : [];
});
}

test("conversation arrival uses shared motion tokens", () => {
assert.match(motionCss, /--motion-duration-arrival:\s*500ms/);
Expand All @@ -22,3 +36,48 @@ test("conversation arrival has a reduced-motion treatment", () => {
/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.motion-enter-conversation/,
);
});

test("shared state-transition utilities resolve through motion tokens", () => {
assert.match(
tailwindConfig,
/"motion-feedback":\s*"var\(--motion-duration-instant\)"/,
);
assert.match(
tailwindConfig,
/"motion-state":\s*"var\(--motion-duration-fast\)"/,
);
assert.match(
tailwindConfig,
/"motion-standard":\s*"var\(--motion-ease-standard\)"/,
);
});

test("semantic motion durations collapse under reduced motion", () => {
const reducedMotionBlock = motionCss.match(
/@media \(prefers-reduced-motion: reduce\)\s*\{([\s\S]*)\}\s*\/\*/,
)?.[1];

assert.ok(reducedMotionBlock, "expected a reduced-motion media block");
for (const token of ["instant", "fast", "standard", "arrival"]) {
assert.match(
reducedMotionBlock,
new RegExp(`--motion-duration-${token}:\\s*1ms`),
);
}
});

test("interactive styles never transition every property", () => {
const sourceRoot = fileURLToPath(new URL("../../../", import.meta.url));
const violations = sourceFiles(sourceRoot)
.flatMap((path) => {
const source = readFileSync(path, "utf8");
return (
source
.match(/transition-all|transition(?:-property)?\s*:\s*all/g)
?.map(() => path) ?? []
);
})
.sort();

assert.deepEqual(violations, []);
});
2 changes: 1 addition & 1 deletion desktop/src/shared/ui/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ function VolumeControl({
<input
aria-label="Volume"
className={cn(
"video-volume-slider h-3 cursor-pointer transition-all duration-200",
"video-volume-slider h-3 cursor-pointer transition-[width,margin-left,opacity] duration-motion-state ease-motion-standard",
expanded
? "ml-1 w-16"
: "pointer-events-none w-0 opacity-0 focus-visible:pointer-events-auto focus-visible:ml-1 focus-visible:w-16 focus-visible:opacity-100 group-hover/volume:pointer-events-auto group-hover/volume:ml-1 group-hover/volume:w-16 group-hover/volume:opacity-100",
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/shared/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ const SidebarRail = React.forwardRef<
}}
title="Drag to resize sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-[left,right] duration-motion-state ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"cursor-col-resize",
"after:absolute after:bottom-0 after:left-1/2 after:top-6 after:z-10 after:w-px after:-translate-x-1/2 after:bg-transparent after:content-['']",
"disabled:pointer-events-none disabled:hidden",
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/shared/ui/step-progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function StepProgress({
<span
aria-hidden="true"
className={cn(
"h-1.5 rounded-full transition-all duration-200 ease-out",
"h-1.5 rounded-full transition-[width,background-color] duration-motion-state ease-motion-standard",
isActive && cn("w-6 bg-primary", activeSegmentClassName),
!isActive &&
isComplete &&
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/shared/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-[background-color,color,box-shadow] duration-motion-feedback ease-motion-standard focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className,
)}
{...props}
Expand Down
10 changes: 10 additions & 0 deletions desktop/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export default {
spacing: {
4.5: "1.125rem",
},
transitionDuration: {
"motion-feedback": "var(--motion-duration-instant)",
"motion-state": "var(--motion-duration-fast)",
"motion-standard": "var(--motion-duration-standard)",
"motion-arrival": "var(--motion-duration-arrival)",
},
transitionTimingFunction: {
"motion-standard": "var(--motion-ease-standard)",
"motion-arrival": "var(--motion-ease-arrival)",
},
fontFamily: {
sans: [
'"Inter Variable"',
Expand Down