Improve stream sidebar experience and avatar affordance #284

Merged
talksik merged 5 commits from experience-nits into main 2026-06-13 18:39:29 +00:00
5 changed files with 16 additions and 52 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ function ScrollArea({
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!w-full"
>
coderabbitai[bot] commented 2026-06-13 18:29:32 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Tailwind CSS v4 important modifier syntax arbitrary variants

💡 Result:

In Tailwind CSS v4, the syntax and handling for important modifiers and arbitrary variants have been updated to align with standard CSS conventions and a CSS-first configuration approach. Important Modifier Syntax In Tailwind v4, the important modifier (!) is placed at the end of the utility class, mirroring native CSS where!important follows the declaration [1][2][3]. - Example: Instead of!text-blue-500 (v3 style), use text-blue-500! [4][2][3]. - This applies to utilities in HTML as well as those used within @apply directives in CSS [5][3]. - Global important: To apply!important to all utility classes globally, add the important keyword to the Tailwind import statement in your CSS: @import "tailwindcss" important; [6][7]. You can also wrap all utilities in a custom selector by specifying it, for example: @import "tailwindcss" important(#app); [6]. Arbitrary Variants Arbitrary variants allow for on-the-fly selector modification directly in your HTML using square bracket notation, similar to arbitrary values [8][9]. - Syntax: They are written as [selector], where the ampersand (&) represents the current element [10]. - Example: [

...
] applies styles to child list items [10]. - If a selector contains spaces, use an underscore (e.g., [&_p]:text-blue-500) [8]. - For frequently used variants, Tailwind v4 introduces the @custom-variant directive in CSS, which allows you to define reusable variant selectors without requiring JavaScript plugins [11][12]. Top Results: [4][6][2][8][3]

Citations:


Fix Tailwind v4 important modifier placement in scroll-area.tsx.

In Tailwind v4, ! must come after the utility (even inside arbitrary variants), so [&>div]:w-full! instead of [&>div]:!w-full.

🔧 Proposed fix
-        className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!w-full"
+        className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:w-full!"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/desktop/src/components/ui/scroll-area.tsx` at line 19, The Tailwind
important modifier is placed before the utility in the className string inside
the ScrollArea component; change the arbitrary variant "[&>div]:!w-full" to
place the ! after the utility like "[&>div]:w-full!" so it conforms to Tailwind
v4 syntax; update the className assignment used in the component (the string
containing "focus-visible:ring-ring/50 size-full rounded-[inherit]
transition-[color,box-shadow] outline-none focus-visible:ring-[3px]
focus-visible:outline-1 [&>div]:!w-full") to use the corrected "[&>div]:w-full!"
token.
_⚠️ Potential issue_ | _🔴 Critical_ <details> <summary>🧩 Analysis chain</summary> 🌐 Web query: `Tailwind CSS v4 important modifier syntax arbitrary variants` 💡 Result: In Tailwind CSS v4, the syntax and handling for important modifiers and arbitrary variants have been updated to align with standard CSS conventions and a CSS-first configuration approach. Important Modifier Syntax In Tailwind v4, the important modifier (!) is placed at the end of the utility class, mirroring native CSS where!important follows the declaration [1][2][3]. - Example: Instead of!text-blue-500 (v3 style), use text-blue-500! [4][2][3]. - This applies to utilities in HTML as well as those used within `@apply` directives in CSS [5][3]. - Global important: To apply!important to all utility classes globally, add the important keyword to the Tailwind import statement in your CSS: `@import` "tailwindcss" important; [6][7]. You can also wrap all utilities in a custom selector by specifying it, for example: `@import` "tailwindcss" important(`#app`); [6]. Arbitrary Variants Arbitrary variants allow for on-the-fly selector modification directly in your HTML using square bracket notation, similar to arbitrary values [8][9]. - Syntax: They are written as [selector], where the ampersand (&) represents the current element [10]. - Example: [<div class="[&>li]:py-2">...</div>] applies styles to child list items [10]. - If a selector contains spaces, use an underscore (e.g., [&_p]:text-blue-500) [8]. - For frequently used variants, Tailwind v4 introduces the `@custom-variant` directive in CSS, which allows you to define reusable variant selectors without requiring JavaScript plugins [11][12]. Top Results: [4][6][2][8][3] Citations: - 1: https://stackoverflow.com/questions/75664539/tailwind-css-class-precedence-is-not-respected - 2: https://playbooks.com/skills/fusengine/agents/tailwindcss-v4 - 3: https://tailwindcss.com/docs/upgrade-guide - 4: https://tailwindlabs-tailwindcss.mintlify.app/advanced/important-modifier - 5: https://github.com/tailwindlabs/tailwindcss/pull/14434 - 6: https://github.com/tailwindlabs/tailwindcss/pull/14448 - 7: https://github.com/tailwindlabs/tailwindcss/discussions/17994 - 8: https://tailwindcss.com/docs/hover-focus-and-other-states - 9: https://tailwindcss.com/docs/adding-custom-styles - 10: https://llmbestpractices.com/frontend/tailwind-arbitrary-values - 11: https://tailwindcss.com/docs/functions-and-directives - 12: https://deepwiki.com/tlq5l/tailwindcss-v4-skill/2.4-the-@variant-and-@custom-variant-directives --- </details> **Fix Tailwind v4 important modifier placement in `scroll-area.tsx`.** In Tailwind v4, `!` must come **after** the utility (even inside arbitrary variants), so `[&>div]:w-full!` instead of `[&>div]:!w-full`. <details> <summary>🔧 Proposed fix</summary> ```diff - className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!w-full" + className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:w-full!" ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/components/ui/scroll-area.tsx` at line 19, The Tailwind important modifier is placed before the utility in the className string inside the ScrollArea component; change the arbitrary variant "[&>div]:!w-full" to place the ! after the utility like "[&>div]:w-full!" so it conforms to Tailwind v4 syntax; update the className assignment used in the component (the string containing "focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!w-full") to use the corrected "[&>div]:w-full!" token. ``` </details> <!-- fingerprinting:phantom:poseidon:puma --> <!-- cr-comment:v1:4dccc68c6d825f79f4034232 --> <!-- This is an auto-generated comment by CodeRabbit -->
{children}
</ScrollAreaPrimitive.Viewport>
@@ -10,7 +10,6 @@ import { KeyHint } from '@/components/key-hint';
import { ScrollArea } from '@/components/ui/scroll-area';
interface StreamListSidebarProps {
streamName: string;
items: Particle[];
networkId: string;
currentIndex: number;
@@ -24,7 +23,6 @@ interface StreamListSidebarProps {
* nothing auto-advances.
*/
export function StreamListSidebar({
streamName,
items,
networkId,
currentIndex,
@@ -41,13 +39,12 @@ export function StreamListSidebar({
}, [currentIndex]);
return (
<aside className="dark flex w-96 shrink-0 flex-col border-l border-white/10 bg-zinc-950">
<aside className="dark flex max-w-60 shrink-0 flex-col border-l border-white/10 bg-zinc-950">
<div className="flex shrink-0 items-center gap-2 border-b border-white/10 px-4 py-3">
<List className="size-3.5 text-white/40" />
<span className="truncate text-sm font-medium text-white/90">
{streamName}
<span className="truncate text-sm font-medium text-white/90 mr-auto">
{items.length} messages
</span>
<span className="ml-auto text-xs text-white/40">{items.length}</span>
<KeyHint
keys="L"
onClick={onToggle}
@@ -144,7 +141,7 @@ function ChatRowContent({ particle }: { particle: Particle }) {
switch (particle.type) {
case 'text':
return (
<p className="line-clamp-3 text-xs leading-relaxed whitespace-pre-line text-white/70">
<p className="line-clamp-2 text-xs leading-relaxed whitespace-pre-line text-white/70">
{particle.properties.content}
</p>
);
@@ -212,11 +212,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
platform.autoplay.dismiss();
});
const userId = useAuthStore((s) => s.user?.id);
const { mode, toggle: toggleViewMode } = useStreamViewMode(
streamParticle,
userId,
);
const { mode, toggle: toggleViewMode } = useStreamViewMode();
const {
children,
@@ -572,7 +568,6 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
{/* Browse sidebar — a separate chat-like panel beside the stream */}
{mode === 'list' && (
<StreamListSidebar
streamName={streamParticle.properties.name}
items={children}
networkId={networkId}
currentIndex={currentIndex}
+5 -4
View File
@@ -145,13 +145,14 @@ export default function SettingsPage() {
aria-label="Change profile picture"
>
<HumanAvatar
size="lg"
className="size-16"
avatarObjectId={user?.avatar_object_id}
initials={initials}
fallbackClassName="bg-primary/10 text-primary font-medium"
fallbackClassName="bg-primary/10 text-primary text-xl font-medium"
/>
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
<Camera className="size-4 text-white" />
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100" />
<span className="bg-primary text-primary-foreground ring-background absolute bottom-0 right-0 flex size-5 items-center justify-center rounded-full ring-2">
<Camera className="size-2.5" />
</span>
</button>
<div className="min-w-0 flex-1">
+5 -34
View File
@@ -1,41 +1,12 @@
import { useCallback, useState } from 'react';
import type { Particle } from '@/api/types';
export type StreamViewMode = 'player' | 'list';
function decideMode(
streamParticle: Particle & { type: 'stream' },
userId: string | undefined,
): StreamViewMode {
const marker = userId ? streamParticle.playback_markers?.[userId] : undefined;
const lastChildAt = streamParticle.last_child_created_at;
const caughtUp =
!!marker && !!lastChildAt && lastChildAt.getTime() <= marker.getTime();
return caughtUp ? 'list' : 'player';
}
/**
* Which mode a stream opens in: the player (autoplay catch-up) when there's
* unseen content, the browsable list when the user is fully caught up.
* Decided once on entry from the playback marker vs. the stream's last
* activity — browsing afterwards advances the marker, but the mode only
* changes via the user's toggle.
*/
export function useStreamViewMode(
streamParticle: Particle & { type: 'stream' },
userId: string | undefined,
): { mode: StreamViewMode; toggle: () => void } {
const [mode, setMode] = useState<StreamViewMode>(() =>
decideMode(streamParticle, userId),
);
// Re-decide when navigating between streams without an unmount.
const [prevStreamId, setPrevStreamId] = useState(streamParticle.id);
if (prevStreamId !== streamParticle.id) {
setPrevStreamId(streamParticle.id);
setMode(decideMode(streamParticle, userId));
}
export function useStreamViewMode(): {
mode: StreamViewMode;
toggle: () => void;
} {
const [mode, setMode] = useState<StreamViewMode>('player');
const toggle = useCallback(() => {
setMode((prev) => (prev === 'player' ? 'list' : 'player'));
}, []);