跳转到正文

PATTERN

媒体工具栏可读配方

可运行的 media-toolbar 组合,与真实源码和组件契约对应。

这些组合由真实组件实现,演示状态保存在当前页面。设置与账户示例不连接业务后端,AI 工作台使用静态消息 fixture。

媒体工具栏可读配方

正在准备交互组合…

组合使用的组件

媒体背景为样式化占位(无真实照片依赖);接入时用应用实际媒体验证对比度。

Clear 仅在媒体与主题方向一致且文字/必要图标有实色底面时使用;混合亮度请用 Thick/Solid。

查看组合源码
recipes/media-toolbar.tsx
"use client";

import { useState } from "react";
import { Button } from "@cwa-design/react";
import { IconButton } from "@cwa-design/react";
import { Surface } from "@cwa-design/react";
import { Text } from "@cwa-design/react";
import { RecipeScope as CwaProvider } from "./recipe-scope";

/**
 * F08:媒体工具栏可读配方。按媒体亮/暗/混合选择前景保护策略:
 * - Regular(默认):正文/控件直接可读,适合大多数媒体;
 * - Clear(显式启用):仅当媒体与主题方向一致且文字/必要图标有实色底面时使用;
 * - Thick/Solid:复杂混合亮度或要求稳定阅读底面时的回退。
 * 前景保护通过局部实色底面(图标配色 + 局部 Surface)实现,不要求切换站点主题;
 * 文字/图标保持不透明;blur 固定;动效只用 transform/opacity。
 */
export type MediaToolbarTone = "regular" | "clear" | "thick" | "solid";

export interface MediaToolbarRecipeProps {
  /** 初始材质;默认 regular(可读性最稳)。 */
  initialMaterial?: MediaToolbarTone;
  /** 媒体亮度:bright/dark/mixed;用于文案与默认策略说明。 */
  media?: "bright" | "dark" | "mixed";
}

const MATERIAL_BY_TONE = {
  regular: "glass",
  clear: "glass-clear",
  thick: "frosted",
  solid: "solid",
} as const;

/** 媒体工具栏可读配方(F08)。 */
export function MediaToolbarRecipe({
  initialMaterial = "regular",
  media = "mixed",
}: MediaToolbarRecipeProps) {
  const [material, setMaterial] = useState<MediaToolbarTone>(initialMaterial);
  const surfaceMaterial = MATERIAL_BY_TONE[material];
  return (
    <CwaProvider>
      <div
        style={{
          position: "relative",
          minHeight: 280,
          borderRadius: 24,
          overflow: "hidden",
          display: "grid",
          alignContent: "center",
          justifyItems: "center",
          padding: 24,
          background:
            media === "bright"
              ? "linear-gradient(135deg, #bfe0f2, #f3ead8)"
              : media === "dark"
                ? "linear-gradient(135deg, #101b2c, #1d2c46)"
                : "linear-gradient(110deg, #172434 50%, #ebeddf 50%)",
        }}
      >
        <div aria-hidden="true" style={{ position: "absolute", inset: 0 }}>
          <div
            style={{
              position: "absolute",
              top: "18%",
              left: "6%",
              width: 200,
              height: 120,
              borderRadius: 999,
              background: media === "dark" ? "#0c1526" : "#ffffff",
              opacity: 0.6,
            }}
          />
          <div
            style={{
              position: "absolute",
              bottom: "12%",
              right: "8%",
              width: 260,
              height: 140,
              borderRadius: 24,
              background: media === "dark" ? "#22354f" : "#9fc3d8",
              opacity: 0.55,
            }}
          />
        </div>
        <Surface
          material={surfaceMaterial}
          style={{
            position: "relative",
            borderRadius: 20,
            padding: "10px 14px",
            display: "flex",
            gap: 12,
            alignItems: "center",
            maxWidth: "100%",
          }}
        >
          <span
            aria-hidden="true"
            style={{
              width: 39,
              height: 39,
              flexShrink: 0,
              display: "grid",
              placeItems: "center",
              borderRadius: 9,
              fontSize: 18,
              // F08:图标使用实色底面(局部保护),非文本对比按相邻底色测量。
              color: "#f6ead6",
              background: "linear-gradient(135deg, #346b7f, #1f3f40)",
            }}
          >
            ▶
          </span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <strong style={{ fontSize: 14 }}>
              {media === "dark" ? "夜色山谷" : media === "bright" ? "高山晨光" : "明暗之间"}
            </strong>
            <Text variant="caption" tone="muted" as="p" style={{ margin: 0 }}>
              {material === "clear"
                ? "Clear:媒体方向一致 + 图标实色底面"
                : material === "thick"
                  ? "Thick:混合亮度的稳妥回退"
                  : material === "solid"
                    ? "Solid:最长阅读时间的底面"
                    : "Regular:默认可读"}
            </Text>
          </div>
          {/* 必要图标:♡ 有实色底面(局部 Surface 保护),非文本对比 ≥3:1。 */}
          <Surface
            material="frosted"
            style={{ borderRadius: 999, display: "grid", placeItems: "center" }}
          >
            <IconButton label="收藏" variant="ghost" size="sm">
              ♡
            </IconButton>
          </Surface>
          <Button
            variant="secondary"
            size="sm"
            onClick={() =>
              setMaterial((m) =>
                m === "regular"
                  ? "clear"
                  : m === "clear"
                    ? "thick"
                    : m === "thick"
                      ? "solid"
                      : "regular",
              )
            }
          >
            切换材质
          </Button>
        </Surface>
        <Text
          variant="caption"
          tone="muted"
          as="p"
          style={{
            position: "relative",
            margin: "14px 0 0",
            textAlign: "center",
            background: "rgba(255,255,255,0.72)",
            borderRadius: 8,
            padding: "2px 10px",
          }}
        >
          F08 配方:媒体亮暗与前景角色决定材质与保护,不需要切换站点主题。
        </Text>
      </div>
    </CwaProvider>
  );
}
recipes/recipe-scope.tsx
import type { ReactNode } from "react";
import { CwaProvider, useOptionalCwaContext } from "@cwa-design/react";

export function RecipeScope({ children }: { children: ReactNode }) {
  const parent = useOptionalCwaContext();
  return parent ? children : <CwaProvider>{children}</CwaProvider>;
}