mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Keep typography PR focused on text preferences
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -23,7 +23,6 @@ const DEFAULT_THREAD_VIEW_MODE: ThreadViewMode = "split";
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let threadViewMode = readStoredThreadViewMode();
|
||||
let previewOverride: ThreadViewMode | null = null;
|
||||
|
||||
function parseThreadViewMode(value: string | null | undefined): ThreadViewMode {
|
||||
return value === "focus" || value === "split"
|
||||
@@ -47,7 +46,7 @@ function subscribe(listener: () => void): () => void {
|
||||
}
|
||||
|
||||
function getSnapshot(): ThreadViewMode {
|
||||
return previewOverride ?? threadViewMode;
|
||||
return threadViewMode;
|
||||
}
|
||||
|
||||
function getServerSnapshot(): ThreadViewMode {
|
||||
@@ -61,7 +60,6 @@ export function getThreadViewMode(): ThreadViewMode {
|
||||
|
||||
/** Update the thread layout preference and notify all subscribed components. */
|
||||
export function setThreadViewMode(mode: ThreadViewMode): void {
|
||||
previewOverride = null;
|
||||
threadViewMode = mode;
|
||||
|
||||
try {
|
||||
@@ -75,15 +73,6 @@ export function setThreadViewMode(mode: ThreadViewMode): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Temporarily apply a layout without changing the saved preference. */
|
||||
export function previewThreadViewMode(mode: ThreadViewMode | null): void {
|
||||
if (previewOverride === mode) return;
|
||||
previewOverride = mode;
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
/** How threads should open in a channel: as a focus drawer or a split pane. */
|
||||
export function useThreadViewMode(): ThreadViewMode {
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as React from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { Eye } from "lucide-react";
|
||||
import { ChevronDown, Eye } from "lucide-react";
|
||||
import {
|
||||
previewThreadViewMode,
|
||||
setThreadViewMode,
|
||||
useThreadViewMode,
|
||||
type ThreadViewMode,
|
||||
@@ -12,14 +10,10 @@ import { useCommunities } from "@/features/communities/useCommunities";
|
||||
import { AvatarFramingSlider } from "@/features/profile/ui/AnimatedAvatarControls";
|
||||
import { contrastColorForBackground } from "@/features/profile/ui/ProfileAvatarEditor.utils";
|
||||
import {
|
||||
previewLinkPreviewStyle,
|
||||
setLinkPreviewStyle,
|
||||
useLinkPreviewStyle,
|
||||
type LinkPreviewStyle,
|
||||
} from "@/shared/lib/linkPreviewStylePreference";
|
||||
import type { ResolvedLinkPreview } from "@/shared/lib/useResolvedLinkPreviews";
|
||||
import { LinkPreviewAttachment } from "@/shared/ui/link-preview-attachment";
|
||||
import type { LinkPreviewImageLightboxProps } from "@/shared/ui/rich-link-preview-attachment";
|
||||
import {
|
||||
previewConversationDensity,
|
||||
setConversationDensity,
|
||||
@@ -41,6 +35,14 @@ import {
|
||||
useTheme,
|
||||
} from "@/shared/theme/ThemeProvider";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import { SettingsOptionRow } from "./SettingsOptionGroup";
|
||||
import { SegmentedControl } from "@/shared/ui/segmented-control";
|
||||
@@ -254,84 +256,6 @@ export function ConversationDisplaySettings() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static sample used by the settings preview card. The thumbnail is an inline
|
||||
* SVG data URL so the preview needs no network fetch or native image pipeline.
|
||||
*/
|
||||
const LINK_PREVIEW_SAMPLE_BASE: Omit<ResolvedLinkPreview, "imageDataUrl"> = {
|
||||
kind: "generic-link",
|
||||
href: "https://example.com/product-updates",
|
||||
provider: "example.com",
|
||||
title: "Product updates — a fresh look at conversations",
|
||||
typeLabel: "link",
|
||||
description:
|
||||
"Highlights from this release: refreshed conversation layout, quicker link handling, and readability improvements.",
|
||||
imageState: "image",
|
||||
imageDomain: "example.com",
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the sample thumbnail as an SVG data URL from the Buzz gradient
|
||||
* tokens. Data-URL images cannot resolve CSS variables, so the token values
|
||||
* are read from the live stylesheet and baked in per render — if the Buzz
|
||||
* gradient ever changes in `theme.css`, this preview follows automatically.
|
||||
*/
|
||||
function buzzGradientSampleImage(isDark: boolean): string {
|
||||
const styles = globalThis.document
|
||||
? getComputedStyle(document.documentElement)
|
||||
: null;
|
||||
const readToken = (token: string, fallback: string): string =>
|
||||
styles?.getPropertyValue(token).trim() || fallback;
|
||||
const top = isDark
|
||||
? readToken("--buzz-gradient-dark-top", "#4a4616")
|
||||
: readToken("--buzz-gradient-light-top", "#e6e6b6");
|
||||
const bottom = isDark
|
||||
? readToken("--buzz-gradient-dark-bottom", "#0a1423")
|
||||
: readToken("--buzz-gradient-light-bottom", "#c4d0da");
|
||||
const shape = isDark ? "#ffffff" : "#fcfcf9";
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 382 200"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${top}"/><stop offset="1" stop-color="${bottom}"/></linearGradient></defs><rect width="382" height="200" fill="url(#g)"/><rect x="76" y="64" width="72" height="72" rx="22" fill="${shape}" opacity="0.75"/><rect x="168" y="76" width="96" height="18" rx="9" fill="${shape}" opacity="0.55"/><rect x="168" y="106" width="138" height="18" rx="9" fill="${shape}" opacity="0.4"/></svg>`;
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
/** Lightbox stand-in for the settings sample — renders the image inert. */
|
||||
function SampleImageLightbox({
|
||||
children,
|
||||
className,
|
||||
}: LinkPreviewImageLightboxProps) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
function LinkPreviewSample() {
|
||||
const { isDark } = useTheme();
|
||||
const preview = React.useMemo<ResolvedLinkPreview>(
|
||||
() => ({
|
||||
...LINK_PREVIEW_SAMPLE_BASE,
|
||||
imageDataUrl: buzzGradientSampleImage(isDark),
|
||||
}),
|
||||
[isDark],
|
||||
);
|
||||
return (
|
||||
<div className="px-4 py-3" data-testid="link-preview-sample">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative overflow-hidden rounded-xl border border-border/65 bg-transparent"
|
||||
data-testid="link-preview-sample-surface"
|
||||
>
|
||||
<span className="absolute right-3.5 top-3 inline-flex items-center gap-1 text-2xs font-medium text-muted-foreground/55">
|
||||
<Eye aria-hidden="true" className="size-3" />
|
||||
Preview
|
||||
</span>
|
||||
<div className="p-4 pr-24">
|
||||
<LinkPreviewAttachment
|
||||
ImageLightbox={SampleImageLightbox}
|
||||
preview={preview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LinkPreviewStyleSetting() {
|
||||
const style = useLinkPreviewStyle();
|
||||
const activeOption =
|
||||
@@ -339,30 +263,58 @@ export function LinkPreviewStyleSetting() {
|
||||
LINK_PREVIEW_STYLE_OPTIONS[0];
|
||||
|
||||
return (
|
||||
<div data-testid="link-preview-style-group">
|
||||
<SettingsOptionRow>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Link previews</p>
|
||||
<p
|
||||
className="text-sm font-normal text-muted-foreground/70"
|
||||
data-settings-subcopy
|
||||
<SettingsOptionRow>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">Links</p>
|
||||
<p
|
||||
className="text-sm font-normal text-muted-foreground/70"
|
||||
data-settings-subcopy
|
||||
>
|
||||
{activeOption.description}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="h-7 min-w-28 justify-between gap-1.5 rounded-md border border-border/50 bg-muted/45 px-2.5 text-xs font-medium text-foreground shadow-none hover:bg-muted/70"
|
||||
data-testid="link-preview-style-trigger"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{activeOption.description}
|
||||
</p>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
size="compact"
|
||||
legend="Link previews"
|
||||
onPreviewChange={previewLinkPreviewStyle}
|
||||
onValueChange={setLinkPreviewStyle}
|
||||
optionTestIdPrefix="link-preview-style"
|
||||
options={LINK_PREVIEW_STYLE_OPTIONS}
|
||||
testId="link-preview-style-control"
|
||||
value={style}
|
||||
/>
|
||||
</SettingsOptionRow>
|
||||
<LinkPreviewSample />
|
||||
</div>
|
||||
<span className="truncate">{activeOption.label}</span>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="min-w-72 rounded-md"
|
||||
data-testid="link-preview-style-menu"
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={(next) =>
|
||||
setLinkPreviewStyle(next as LinkPreviewStyle)
|
||||
}
|
||||
value={style}
|
||||
>
|
||||
{LINK_PREVIEW_STYLE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
data-testid={`link-preview-style-${option.value}`}
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-2xs text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SettingsOptionRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -482,119 +434,6 @@ export function GlassBackgroundSetting() {
|
||||
}
|
||||
|
||||
/** Compact thread preference row in the Appearance preferences card. */
|
||||
/**
|
||||
* Abstract diagram for the thread layout preview, in the same soft-block
|
||||
* style as the links sample: a rounded frame holding a channel surface and a
|
||||
* thread surface, with light skeleton bars. Inline SVG (not a data-URL image)
|
||||
* so fills reference theme tokens directly and follow light/dark and accent
|
||||
* changes automatically. Only the panel proportions change between modes.
|
||||
*/
|
||||
function ThreadLayoutDiagram({ mode }: { mode: ThreadViewMode }) {
|
||||
const { isDark } = useTheme();
|
||||
const gradientId = React.useId();
|
||||
// Inline SVG resolves CSS variables, so the frame gradient references the
|
||||
// Buzz gradient tokens directly and follows theme.css automatically.
|
||||
const gradientTop = isDark
|
||||
? "var(--buzz-gradient-dark-top, #4a4616)"
|
||||
: "var(--buzz-gradient-light-top, #e6e6b6)";
|
||||
const gradientBottom = isDark
|
||||
? "var(--buzz-gradient-dark-bottom, #0a1423)"
|
||||
: "var(--buzz-gradient-light-bottom, #c4d0da)";
|
||||
const shape = isDark ? "#ffffff" : "#fcfcf9";
|
||||
const channel = shape;
|
||||
const channelOpacity = isDark ? 0.3 : 0.45;
|
||||
const thread = shape;
|
||||
const threadOpacity = isDark ? 0.85 : 0.92;
|
||||
const bar = "hsl(var(--muted-foreground) / 0.18)";
|
||||
const barSoft = "hsl(var(--muted-foreground) / 0.11)";
|
||||
|
||||
const isFocus = mode === "focus";
|
||||
// Inner content area: 10..230 x 10..122 (inside the frame padding).
|
||||
// Split: channel and thread share the area side by side with a gap.
|
||||
// Focus: the thread surface dominates, leaving a narrow channel sliver.
|
||||
const gap = 6;
|
||||
const threadX = isFocus ? 42 : 124;
|
||||
const channelWidth = threadX - 10 - gap;
|
||||
const threadWidth = 230 - threadX;
|
||||
|
||||
/** Two skeleton text bars, clipped to the panel they sit in. */
|
||||
const skeleton = (x: number, y: number, width: number) => (
|
||||
<>
|
||||
<rect fill={bar} height={7} rx={3.5} width={width * 0.62} x={x} y={y} />
|
||||
<rect
|
||||
fill={barSoft}
|
||||
height={7}
|
||||
rx={3.5}
|
||||
width={width * 0.86}
|
||||
x={x}
|
||||
y={y + 13}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="block w-full max-w-60"
|
||||
data-testid={`thread-layout-diagram-${mode}`}
|
||||
role="img"
|
||||
viewBox="0 0 240 132"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0" stopColor={gradientTop} />
|
||||
<stop offset="1" stopColor={gradientBottom} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Frame */}
|
||||
<rect fill={`url(#${gradientId})`} height={132} rx={18} width={240} />
|
||||
{/* Channel surface */}
|
||||
<rect
|
||||
fill={channel}
|
||||
height={112}
|
||||
opacity={channelOpacity}
|
||||
rx={10}
|
||||
width={channelWidth}
|
||||
x={10}
|
||||
y={10}
|
||||
/>
|
||||
{channelWidth > 60 ? skeleton(22, 24, channelWidth - 24) : null}
|
||||
{/* Thread surface */}
|
||||
<rect
|
||||
fill={thread}
|
||||
height={112}
|
||||
opacity={threadOpacity}
|
||||
rx={10}
|
||||
width={threadWidth}
|
||||
x={threadX}
|
||||
y={10}
|
||||
/>
|
||||
{skeleton(threadX + 12, 24, threadWidth - 24)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadLayoutPreview() {
|
||||
const mode = useThreadViewMode();
|
||||
return (
|
||||
<div className="px-4 py-3" data-testid="thread-layout-preview">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative overflow-hidden rounded-xl border border-border/65 bg-transparent"
|
||||
data-testid="thread-layout-preview-surface"
|
||||
>
|
||||
<span className="absolute right-3.5 top-3 inline-flex items-center gap-1 text-2xs font-medium text-muted-foreground/55">
|
||||
<Eye aria-hidden="true" className="size-3" />
|
||||
Preview
|
||||
</span>
|
||||
<div className="p-4 pr-24">
|
||||
<ThreadLayoutDiagram mode={mode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThreadLayoutSetting() {
|
||||
const threadViewMode = useThreadViewMode();
|
||||
const { communities } = useCommunities();
|
||||
@@ -605,38 +444,64 @@ export function ThreadLayoutSetting() {
|
||||
) ?? THREAD_VIEW_MODE_OPTIONS[0];
|
||||
|
||||
return (
|
||||
<div data-testid="thread-layout-group">
|
||||
<SettingsOptionRow>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
Thread layout
|
||||
{showCommunityScope ? (
|
||||
<span className="font-normal text-muted-foreground">
|
||||
{" "}
|
||||
(all communities)
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm font-normal text-muted-foreground/70"
|
||||
data-settings-subcopy
|
||||
<SettingsOptionRow>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
Thread layout
|
||||
{showCommunityScope ? (
|
||||
<span className="font-normal text-muted-foreground">
|
||||
{" "}
|
||||
(all communities)
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm font-normal text-muted-foreground/70"
|
||||
data-settings-subcopy
|
||||
>
|
||||
{activeOption.description}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="h-7 min-w-28 justify-between gap-1.5 rounded-md border border-border/50 bg-muted/45 px-2.5 text-xs font-medium text-foreground shadow-none hover:bg-muted/70"
|
||||
data-testid="thread-layout-trigger"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{activeOption.description}
|
||||
</p>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
size="compact"
|
||||
legend="Thread layout"
|
||||
onPreviewChange={previewThreadViewMode}
|
||||
onValueChange={setThreadViewMode}
|
||||
optionTestIdPrefix="thread-layout"
|
||||
options={THREAD_VIEW_MODE_OPTIONS}
|
||||
testId="thread-layout-control"
|
||||
value={threadViewMode}
|
||||
/>
|
||||
</SettingsOptionRow>
|
||||
<ThreadLayoutPreview />
|
||||
</div>
|
||||
<span className="truncate">{activeOption.label}</span>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="min-w-72 rounded-md"
|
||||
data-testid="thread-layout-menu"
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={(next) => setThreadViewMode(next as ThreadViewMode)}
|
||||
value={threadViewMode}
|
||||
>
|
||||
{THREAD_VIEW_MODE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
data-testid={`thread-layout-${option.value}`}
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
<span className="text-2xs text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SettingsOptionRow>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export const DEFAULT_LINK_PREVIEW_STYLE: LinkPreviewStyle = "compact";
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
let linkPreviewStyle = readStoredLinkPreviewStyle();
|
||||
let previewOverride: LinkPreviewStyle | null = null;
|
||||
|
||||
export function parseLinkPreviewStyle(
|
||||
value: string | null | undefined,
|
||||
@@ -35,11 +34,10 @@ function subscribe(listener: () => void): () => void {
|
||||
}
|
||||
|
||||
export function getLinkPreviewStyle(): LinkPreviewStyle {
|
||||
return previewOverride ?? linkPreviewStyle;
|
||||
return linkPreviewStyle;
|
||||
}
|
||||
|
||||
export function setLinkPreviewStyle(style: LinkPreviewStyle): void {
|
||||
previewOverride = null;
|
||||
linkPreviewStyle = style;
|
||||
try {
|
||||
globalThis.localStorage?.setItem(LINK_PREVIEW_STYLE_STORAGE_KEY, style);
|
||||
@@ -49,13 +47,6 @@ export function setLinkPreviewStyle(style: LinkPreviewStyle): void {
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
/** Temporarily apply a style without changing the saved preference. */
|
||||
export function previewLinkPreviewStyle(style: LinkPreviewStyle | null): void {
|
||||
if (previewOverride === style) return;
|
||||
previewOverride = style;
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
export function useLinkPreviewStyle(): LinkPreviewStyle {
|
||||
return React.useSyncExternalStore(
|
||||
subscribe,
|
||||
|
||||
@@ -505,7 +505,7 @@ test("appearance groups theme and preferences into labeled rows", async ({
|
||||
preferencesCard.getByTestId("prominent-active-tab-toggle"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
preferencesCard.getByRole("group", { name: "Thread layout" }),
|
||||
preferencesCard.getByTestId("thread-layout-trigger"),
|
||||
).toBeVisible();
|
||||
const themeStyleTrigger = themeCard.getByTestId("theme-style-trigger");
|
||||
const themeStyleOptions = themeCard.getByTestId("theme-style-options");
|
||||
@@ -1541,14 +1541,24 @@ test("glass background keeps the content panel solid", async ({ page }) => {
|
||||
page.getByTestId("conversation-density-control"),
|
||||
page.getByTestId("conversation-density-control-indicator"),
|
||||
page.getByTestId("theme-style-trigger"),
|
||||
page.getByTestId("link-preview-style-control"),
|
||||
page.getByTestId("link-preview-style-control-indicator"),
|
||||
page.getByTestId("thread-layout-control"),
|
||||
page.getByTestId("thread-layout-control-indicator"),
|
||||
page.getByTestId("link-preview-style-trigger"),
|
||||
page.getByTestId("thread-layout-trigger"),
|
||||
];
|
||||
for (const control of matchingRadiusControls) {
|
||||
await expect(control).toHaveCSS("border-radius", "8px");
|
||||
}
|
||||
await page.getByTestId("link-preview-style-trigger").click();
|
||||
await expect(page.getByTestId("link-preview-style-menu")).toHaveCSS(
|
||||
"border-radius",
|
||||
"8px",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByTestId("thread-layout-trigger").click();
|
||||
await expect(page.getByTestId("thread-layout-menu")).toHaveCSS(
|
||||
"border-radius",
|
||||
"8px",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByTestId("glass-opacity-value")).toHaveCount(0);
|
||||
await expect(
|
||||
opacitySlider.locator(".buzz-avatar-framing-slider-handle"),
|
||||
|
||||
Reference in New Issue
Block a user