feat(ZoomSelector): 使用Slider组件控制字体

This commit is contained in:
肖应宇 2026-04-17 10:33:05 +08:00
parent b88fa12214
commit 830c8abcf1
6 changed files with 148 additions and 109 deletions

View File

@ -102,7 +102,7 @@ function ContextMenuContent({
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-0 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}

View File

@ -0,0 +1,63 @@
"use client"
import * as React from "react"
import { Slider as SliderPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }

View File

@ -34,6 +34,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DropdownSelector } from "@/components/ui/dropdown-selector";
import { Slider } from "@/components/ui/slider";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { CodeEditor } from "@/components/workspace/code-editor";
import { useArtifactContent } from "@/core/artifacts/hooks";
@ -457,10 +458,11 @@ export function ArtifactFileDetail({
</ToggleGroupItem>
</ToggleGroup>
)}
{/* 仅在代码视图显示缩放控制 */}
{isCodeFile && viewMode === "code" && (
{/* 代码视图显示缩放控制Markdown 预览也显示缩放控制 */}
{(isCodeFile && viewMode === "code") ||
(language === "markdown" && viewMode === "preview") ? (
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
)}
) : null}
</div>
<div className="col-span-6 flex min-w-0 items-center justify-center px-1">
<ArtifactTitle>
@ -1652,106 +1654,75 @@ export const ArtifactZoomSelector = ({
...props
}: ArtifactZoomSelectorProps) => {
const { t } = useI18n();
const handleZoomIn = () => {
const currentIndex = ZOOM_LEVELS.indexOf(value);
const nextValue = ZOOM_LEVELS[currentIndex + 1];
if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
onChange?.(nextValue);
}
};
const handleZoomOut = () => {
const currentIndex = ZOOM_LEVELS.indexOf(value);
const prevValue = ZOOM_LEVELS[currentIndex - 1];
if (currentIndex > 0 && prevValue !== undefined) {
onChange?.(prevValue);
}
};
const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
const resolvedIndex = useMemo(() => {
const exactIndex = ZOOM_LEVELS.indexOf(value);
if (exactIndex >= 0) return exactIndex;
let nearestIndex = 0;
let nearestDistance = Number.POSITIVE_INFINITY;
ZOOM_LEVELS.forEach((level, index) => {
const distance = Math.abs(level - value);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestIndex = index;
}
});
return nearestIndex;
}, [value]);
return (
<div
className={cn(
"bg-background border-border inline-flex h-[28px] items-center gap-1 rounded-[10px] border backdrop-blur-sm",
"dark:border-border dark:bg-background",
className,
)}
{...props}
>
<button
type="button"
onClick={handleZoomIn}
disabled={!canZoomIn}
className={cn(
"flex h-full w-10 items-center justify-center rounded py-1 transition-colors",
"text-muted-foreground hover:bg-muted hover:text-foreground",
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
)}
aria-label={t.artifactPreview.zoomIn}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
<path
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
fill="#666666"
<div className={cn("inline-flex", className)} {...props}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={t.artifactPreview.zoomIn}
className={cn(
"bg-background border-border text-muted-foreground hover:text-foreground inline-flex h-[28px] w-[28px] items-center justify-center rounded-[10px] border transition-colors",
"hover:bg-muted/60",
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
<path
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
fill="#666666"
/>
<path
d="M5.33325 7.5H9.7777M7.55547 5V10"
stroke="#666666"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={8} className="w-52 p-[20px] ">
<div className="mb-2 flex items-center justify-between">
<span className="text-muted-foreground text-xs">
{ZOOM_LEVELS[0]}%
</span>
<span className="text-foreground text-xs font-medium">{value}%</span>
</div>
<Slider
min={0}
max={ZOOM_LEVELS.length - 1}
step={1}
value={[resolvedIndex]}
onValueChange={(values) => {
const nextIndex = values[0];
if (nextIndex === undefined) return;
const nextValue = ZOOM_LEVELS[nextIndex];
if (nextValue !== undefined) onChange?.(nextValue);
}}
/>
<path
d="M5.33325 7.5H9.7777M7.55547 5V10"
stroke="#666666"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<span
className={cn(
"text-foreground min-w-[36px] text-center text-xs font-medium",
"dark:text-foreground",
)}
>
{value}%
</span>
<button
type="button"
onClick={handleZoomOut}
disabled={!canZoomOut}
className={cn(
"flex h-full w-10 items-center justify-center rounded transition-colors",
"text-muted-foreground hover:bg-muted hover:text-foreground",
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
)}
aria-label={t.artifactPreview.zoomOut}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
<path
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
fill="#666666"
/>
<path
d="M4.99927 7.5H9.99927"
stroke="#666666"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@ -150,7 +150,7 @@ export function ArtifactFileList({
</CardHeader>
</Card>
</ContextMenuTrigger>
<ContextMenuContent className="min-w-[120px] p-1">
<ContextMenuContent className="min-w-[120px]">
<ContextMenuItem
onSelect={() => {
dispatchMentionReference({

View File

@ -421,7 +421,7 @@ function RichFileCard({
/>
</a>
</ContextMenuTrigger>
<ContextMenuContent className="min-w-[120px] p-1">
<ContextMenuContent className="min-w-[120px]">
<ContextMenuItem
disabled={!canReference}
onSelect={() => {

View File

@ -462,12 +462,12 @@ pre{
/* 二三级标题 - 16px */
[data-streamdown="heading-2"],
[data-streamdown="heading-3"] {
[data-streamdown="heading-3"],[data-streamdown="heading-4"] {
font-size: calc(16px * var(--zoom-scale));
}
/* 代码块 - 14px */
[data-streamdown="code-block"] pre {
[data-streamdown="code-block"] pre,code {
font-size: calc(14px * var(--zoom-scale));
}
@ -481,13 +481,19 @@ pre{
[data-streamdown="table-cell"] {
background-color: transparent;
font-size: calc(14px * var(--zoom-scale));
height:calc(42px * var(--zoom-scale)) ;
}
[data-streamdown="table-header"] {
background: #9c9b9b26;
height: 50px;
height: calc(50px * var(--zoom-scale));
}
[data-streamdown="table-header"] th {
text-align: center;
font-size: calc(14px * var(--zoom-scale));
}
[data-slot="hover-card-trigger"] [data-slot="badge"]{
font-size: calc(14px * var(--zoom-scale));
}
@ -500,7 +506,7 @@ pre{
border-top-right-radius: 5px;
}
[data-streamdown="table-body"] tr:first-child td{
padding-top: 20px;
padding-top: calc(20px * var(--zoom-scale));
}
/* 行分隔线 */
[data-streamdown="table-body"] tr{
@ -515,14 +521,13 @@ pre{
}
[data-streamdown="table-body"] tr:last-child {
height: 50px;
padding-top: calc(50px * var(--zoom-scale));
}
[data-streamdown="table-row"] >[data-streamdown="table-cell"]{
line-height: 14px;
vertical-align: top;
text-align: center;
}