How does dialkit support multiple frontend frameworks?

HEAD
146503dupdate readme
Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

完美!现在我有了完整的理解。让我撰写详细的分析:

##深度解析:dialkit 的多框架实现架构

你的理解方向正确,但不完全是「基于 React 开发然后桥接」,而是更优雅的设计模式。让我详细解析:


##🏗️ 核心架构模式

###第一层:框架无关的数据层(Store)

dialkit 的关键设计是将状态管理和业务逻辑完全分离成一个框架无关的层:

src/store/DialStore.ts  ← 唯一的框架无关代码

DialStore 的职责:

  • 管理所有参数的状态(值、默认值、快捷键等)
  • 提供订阅/发布机制(观察者模式)
  • 管理面板的生命周期
  • 支持预设的保存和加载
class DialStoreClass {
  private panels: Map<string, PanelConfig> = new Map();
  private listeners: Map<string, Set<Listener>> = new Map();
  private snapshots: Map<string, Record<string, DialValue>> = new Map();

  // 核心接口
  registerPanel(id: string, name: string, config: DialConfig, shortcuts?: ShortcutConfig): void
  updateValue(panelId: string, path: string, value: DialValue): void
  subscribe(panelId: string, listener: Listener): () => void  // 返回取消订阅函数
  getValues(panelId: string): Record<string, DialValue>
}

export const DialStore = new DialStoreClass();

###第二层:框架适配层(Hook/Primitive)

每个框架都实现一个薄适配层,只负责连接框架的响应式系统与 DialStore:

框架适配入口响应式系统
ReactuseDialKit() HookuseSyncExternalStore()
SolidcreateDialKit() PrimitivecreateSignal() + createMemo()
VueuseDialKit() Composableref() + computed() + watch()
SveltecreateDialKit()$state + $effect

###第三层:UI 组件层

所有 UI 组件都是框架特定的实现,使用该框架的原生 API。


##📊 具体实现对比

###React 实现 - src/hooks/useDialKit.ts

export function useDialKit<T extends DialConfig>(
  name: string,
  config: T,
  options?: UseDialOptions
): ResolvedValues<T> {
  const instanceId = useId();
  const panelId = `${name}-${instanceId}`;

  // 1. 挂载时注册
  useEffect(() => {
    DialStore.registerPanel(panelId, name, config, options?.shortcuts);
    return () => DialStore.unregisterPanel(panelId);
  }, [panelId, name]);

  // 2. 使用 React 的外部存储订阅
  const values = useSyncExternalStore(
    (callback) => DialStore.subscribe(panelId, callback),  // 订阅
    () => DialStore.getValues(panelId),                    // 获取快照
    () => DialStore.getValues(panelId)                     // SSR 快照
  );

  // 3. 构建返回值对象
  return buildResolvedValues(config, values, '') as ResolvedValues<T>;
}

关键点:

  • React 的 useSyncExternalStore() 是与 DialStore 的桥梁
  • 每次 DialStore 值改变时,Hook 自动重新计算
  • 依赖于 DialStore 返回新的引用new snapshot)以触发更新

###Solid 实现 - src/solid/createDialKit.ts

export function createDialKit<T extends DialConfig>(
  name: string,
  config: T,
  options?: CreateDialOptions
): Accessor<ResolvedValues<T>> {
  const id = createUniqueId();
  const panelId = `${name}-${id}`;

  // 1. 初始化 Solid 信号
  const [values, setValues] = createSignal<Record<string, DialValue>>(
    DialStore.getValues(panelId)
  );

  // 2. 在 onMount 中注册并订阅
  onMount(() => {
    DialStore.registerPanel(panelId, name, config, options?.shortcuts);
    setValues(DialStore.getValues(panelId));

    const unsubValues = DialStore.subscribe(panelId, () => {
      setValues(DialStore.getValues(panelId));  // 更新 Solid 信号
    });

    onCleanup(() => {
      unsubValues();
      DialStore.unregisterPanel(panelId);
    });
  });

  // 3. 返回 Accessor(计算派生值)
  return createMemo(() => 
    buildResolvedValues(config, values(), '') as ResolvedValues<T>
  );
}

关键点:

  • Solid 的 createSignal() 与 DialStore 的值保持同步
  • createMemo() 提供细粒度的响应式性能优化
  • 返回 Accessor<T> 而不是直接值(Solid 的模式)

###Vue 实现 - src/vue/useDialKit.ts

export function useDialKit<T extends DialConfig>(
  name: string,
  config: T,
  options?: UseDialOptions
): ComputedRef<ResolvedValues<T>> {
  const panelId = `${name}-${++dialKitInstance}`;

  // 1. Vue 的响应式引用
  const values = ref<Record<string, DialValue>>(DialStore.getValues(panelId));
  const mounted = ref(false);

  // 2. 在 onMounted 时注册
  onMounted(() => {
    DialStore.registerPanel(panelId, name, configRef.value, shortcutsRef.value);
    
    // 订阅 DialStore
    unsubscribeValues = DialStore.subscribe(panelId, () => {
      values.value = DialStore.getValues(panelId);  // 更新 ref
    });
  });

  // 3. 返回计算属性(自动追踪依赖)
  return computed(() => 
    buildResolvedValues(configRef.value, values.value, '') as ResolvedValues<T>
  );
}

关键点:

  • Vue 的 ref() 与 DialStore 同步
  • computed() 自动追踪依赖,值变化时自动重算
  • watch() 用于监听配置变化

###Svelte 实现 - src/svelte/createDialKit.svelte.ts

export function createDialKit<T extends DialConfig>(
  name: string,
  config: T,
  options?: CreateDialOptions
): DialKitValues<ResolvedValues<T>> {
  const panelId = `${name}-${++dialKitInstance}`;
  
  // Svelte 5 的响应式状态
  let values = $state<ResolvedValues<T>>(resolve());

  // 使用 $effect(Svelte 5 的自动追踪)
  $effect(() => {
    DialStore.registerPanel(panelId, name, config, options?.shortcuts);
    values = resolve();

    const unsubValues = DialStore.subscribe(panelId, () => {
      values = resolve();
    });

    return () => {
      unsubValues();
      DialStore.unregisterPanel(panelId);
    };
  });

  return values;  // 返回响应式值的直接引用
}

关键点:

  • Svelte 5 的 $state$effect 是最直接的(反应式编程)
  • 直接返回状态引用而不是函数

##🎨 UI 组件实现对比

所有框架都实现了相同的 UI 组件(如 Slider),但使用框架原生 API:

###React Slider - 使用 React Hooks

export function Slider({ label, value, onChange, ... }: SliderProps) {
  const [isInteracting, setIsInteracting] = useState(false);
  const fillPercent = useMotionValue(percentage);
  
  useEffect(() => {
    if (!isInteracting && !animRef.current) {
      fillPercent.jump(percentage);
    }
  }, [percentage, isInteracting, fillPercent]);
  
  return (/* JSX UI */)
}

###Solid Slider - 使用 Solid Primitives

export function Slider(props: SliderProps) {
  const [isInteracting, setIsInteracting] = createSignal(false);
  const fillPercent = motionValue(percentage());
  
  createEffect(() => {
    if (!isInteracting() && !snapAnim) {
      fillPercent.jump(((props.value - min()) / (max() - min())) * 100);
    }
  });
  
  return (/* JSX UI */)
}

###Svelte Slider - 使用 Svelte Runes

<script lang="ts">
  let isInteracting = $state(false);
  let fillPercent = new Spring(percentage, { stiffness: 0.25 });
  
  $effect(() => {
    if (!isInteracting && !snapAnim) {
      fillPercent.jump(percentage);
    }
  });
</script>

<!-- Svelte markup -->

##🔌 关键的适配设计模式

###1. 统一的订阅机制

// 所有框架使用同样的订阅 API
DialStore.subscribe(panelId, () => {
  // 在各自框架中更新响应式状态
});

###2. 不可变快照

// DialStore 返回新的引用来通知变化(React 的 useSyncExternalStore 需要)
getValues(panelId): Record<string, DialValue> {
  return this.snapshots.get(panelId) ?? EMPTY_VALUES;
}

updateValue(panelId: string, path: string, value: DialValue) {
  // ...
  this.snapshots.set(panelId, { ...panel.values });  // 新引用!
  this.notify(panelId);
}

###3. 框架特定的"解析"函数

所有框架都实现相同的 buildResolvedValues() 函数,将扁平化的值对象转换为结构化的返回对象:

// React/Solid/Vue/Svelte 中都有这个相同的函数
function buildResolvedValues(
  config: DialConfig,
  flatValues: Record<string, DialValue>,
  prefix: string
): Record<string, unknown> {
  // 递归构建结构化对象
}

##📦 编译和分发

###tsup 多入口构建 - tsup.config.ts

export default defineConfig([
  // Store 构建(所有框架共享)
  { entry: 'src/store/DialStore.ts', outDir: 'dist/store', ... },
  
  // React 构建
  { entry: 'src/index.ts', external: ['react', 'react-dom', 'motion'], ... },
  
  // Solid 构建
  { entry: 'src/solid/index.ts', outDir: 'dist/solid', external: ['solid-js', 'motion'], ... },
  
  // Vue 构建
  { entry: 'src/vue/index.ts', outDir: 'dist/vue', external: ['vue', 'motion-v'], ... },
]);

###导出结构 - package.json

{
  "exports": {
    ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" },      // React
    "./solid": { "import": "./dist/solid/index.js", ... },                     // Solid
    "./vue": { "import": "./dist/vue/index.js", ... },                         // Vue
    "./store": { "import": "./dist/store/index.js", ... },                     // 共享 Store
  }
}

使用方式:

// React
import { useDialKit } from 'dialkit';

// Solid
import { createDialKit } from 'dialkit/solid';

// Vue
import { useDialKit } from 'dialkit/vue';

// 任何框架
import { DialStore } from 'dialkit/store';

##🎯 架构优势

优势说明
DRY 原则Store 逻辑只写一次,所有框架共用
框架解耦添加新框架只需实现 Hook/Primitive + UI 组件
一致性用户体验完全相同,无论使用哪个框架
最小化不需要"虚拟 React 层"或中间层转换
性能每个框架用其最优的响应式系统
可维护性框架特定代码独立,便于版本升级

##📋 总结:不同于你的预想

你的理解 ❌:「基于 React 开发然后桥接」

正确的方式 ✅:

  1. 分离数据层 - 框架无关的 Store(发布-订阅模式)
  2. 适配层 - 每个框架一个 Hook/Primitive,连接到 Store
  3. UI 层 - 每个框架用自己的原生 API 实现相同的组件

这种架构在大型跨框架库中非常普遍(如 TanStack Query、Zustand),是真正的"框架无关"设计,而不是"为某个框架写,然后转换"。

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

完美!现在我创建详细的分析文档:

#dialkit 样式方案深度解析

##核心结论:不是 Tailwind,而是 CSS Variables + BEM

您问"是不是 Tailwind",答案是 绝对不是。dialkit 采用的是完全不同的轻量级方案:

  • 纯 CSS - 无任何预处理器(SCSS/Less)
  • CSS Variables - 用于主题切换和定制
  • BEM 命名规范 - .dialkit-block-element--modifier
  • 单一 CSS 文件 - src/styles/theme.css(1326 行,27KB)
  • 框架无关 - 相同的 CSS 对 React/Solid/Vue/Svelte 都适用

这个选择非常聪明,因为 dialkit 需要支持多个框架,而 Tailwind 会增加复杂性而没有额外收益。


##1. 整体架构

dialkit/
├── src/
│   ├── styles/
│   │   └── theme.css          ← 唯一的核心样式文件
│   ├── components/            ← React 组件 + 类名
│   ├── solid/components/      ← Solid 组件 + 类名
│   ├── vue/components/        ← Vue 组件 + 类名
│   └── svelte/
│       ├── components/        ← Svelte 组件 + 类名
│       └── theme-css.ts       ← 自动生成(编译时)
│
└── dist/
    ├── styles.css             ← theme.css 的编译产物
    ├── index.js / index.cjs    ← React
    ├── solid/
    ├── vue/
    └── svelte/                ← 包含注入的 CSS 字符串

##2. CSS Variables 系统(主题引擎)

###2.1 色彩体系

深色主题(默认) - src/styles/theme.css 第 1-50 行

.dialkit-root {
  /* ═══════════ 表面层(背景)═══════════ */
  --dial-surface: rgba(255, 255, 255, 0.05);        /* 基础背景 */
  --dial-surface-hover: rgba(255, 255, 255, 0.1);   /* 悬停状态 */
  --dial-surface-active: rgba(255, 255, 255, 0.11); /* 点击/聚焦 */
  --dial-surface-subtle: rgba(255, 255, 255, 0.06); /* 分隔线 */

  /* ═══════════ 文本层 - 3 级层次 ═══════════ */
  --dial-text-root: #FFFFFF;                        /* 面板标题 */
  --dial-text-section: rgba(255, 255, 255, 0.7);    /* 区域标题、脱字符 */
  --dial-text-label: rgba(255, 255, 255, 0.7);      /* 控制标签 */
  --dial-text-focus: #ffffff;                       /* 激活状态 */
  
  /* 兼容性别名 */
  --dial-text-primary: rgba(255, 255, 255, 0.95);
  --dial-text-secondary: rgba(255, 255, 255, 0.6);
  --dial-text-tertiary: rgba(255, 255, 255, 0.4);

  /* ═══════════ 边框层 ═══════════ */
  --dial-border: rgba(255, 255, 255, 0.1);
  --dial-border-hover: rgba(255, 255, 255, 0.15);

  /* ═══════════ Glassmorphic 设计令牌 ═══════════ */
  --dial-glass-bg: #212121;                         /* 玻璃背景 */
  --dial-dropdown-bg: #2a2a2a;                      /* 下拉菜单 */
  --dial-backdrop-blur: 20px;                       /* 毛玻璃模糊 */

  /* ═══════════ 几何和间距 ═══════════ */
  --dial-radius: 8px;                               /* 圆角 */
  --dial-row-height: 36px;                          /* 标准行高 */

  /* ═══════════ 阴影 ═══════════ */
  --dial-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  --dial-shadow-collapsed: 0 4px 16px rgba(0, 0, 0, 0.25);
  --dial-shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.4);

  /* 字体 */
  font-family: system-ui, -apple-system, 'SF Pro Display', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

###2.2 主题切换实现

####浅色主题

.dialkit-root[data-theme="light"] {
  --dial-surface: rgba(0, 0, 0, 0.04);           /* 黑色 4% */
  --dial-surface-hover: rgba(0, 0, 0, 0.08);     /* 黑色 8% */
  --dial-surface-active: rgba(0, 0, 0, 0.1);     /* 黑色 10% */

  --dial-text-root: #000000;
  --dial-text-section: rgba(0, 0, 0, 0.65);
  --dial-text-label: rgba(0, 0, 0, 0.6);

  --dial-glass-bg: #fafafa;                      /* 浅灰 */
  --dial-dropdown-bg: #ffffff;                   /* 白色 */
}

####系统偏好(响应式)

@media (prefers-color-scheme: light) {
  .dialkit-root[data-theme="system"] {
    /* 应用浅色主题变量 */
  }
}

使用方式

// React
<DialRoot theme="dark" />      // 强制深色
<DialRoot theme="light" />     // 强制浅色
<DialRoot theme="system" />    // 跟随系统(默认)

##3. BEM 命名规范

###命名模式

.dialkit-{BLOCK}[-{ELEMENT}][--{MODIFIER}]

###实际例子

/* Block - 最顶级容器 */
.dialkit-slider { }
.dialkit-toggle { }
.dialkit-panel { }

/* Block + Element - 子组件 */
.dialkit-slider-label { }       /* slider 中的标签 */
.dialkit-slider-value { }       /* slider 中的值 */
.dialkit-slider-handle { }      /* slider 中的滑块 */
.dialkit-slider-fill { }        /* slider 中的填充条 */

.dialkit-panel-inner { }        /* panel 内部容器 */
.dialkit-panel-header { }       /* panel 头部 */
.dialkit-panel-toolbar { }      /* panel 工具栏 */

/* Element + Modifier(修饰符)*/
.dialkit-slider-value-editable { }      /* 可编辑的值 */
.dialkit-slider[data-active="true"] { } /* 活跃状态 */

##4. 组件样式实现示例

###4.1 Slider 组件(完整解析)

样式定义(theme.css 第 260-330 行):

.dialkit-slider-wrapper {
  position: relative;
  height: var(--dial-row-height);  /* 36px */
}

.dialkit-slider {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
  user-select: none;
  overflow: hidden;
  background: var(--dial-surface);
  border-radius: var(--dial-radius);  /* 8px */
  touch-action: none;
}

/* 填充条 - 从左到右 */
.dialkit-slider-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background: var(--dial-surface-active);
  transition: background 0.15s;
  pointer-events: none;
}

/* 鼠标悬停/活跃时填充条变亮 */
.dialkit-slider-active .dialkit-slider-fill {
  background: var(--dial-border-hover);
}

/* 标签(左侧)*/
.dialkit-slider-label {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(calc(-50% - 0.5px));
  font-size: 13px;
  font-weight: 500;
  color: var(--dial-text-label);
  transition: color 0.15s;
}

/* 值(右侧)*/
.dialkit-slider-value {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(calc(-50% + 0.5px));
  font-size: 13px;
  font-weight: 500;
  font-family: 'Geist Mono', monospace;  /* 等宽字体 */
  color: var(--dial-text-label);
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}

/* 可编辑值 - 显示下划线 */
.dialkit-slider-value-editable {
  border-bottom-color: var(--dial-text-label);
}

/* 滑块把手 */
.dialkit-slider-handle {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 20px;
  border-radius: 999px;
  background: var(--dial-text-primary);
  pointer-events: none;
}

React 实现 - 如何驱动这些样式:

export function Slider({ 
  label, value, onChange, 
  min = 0, max = 1, step = 0.01,
  shortcut, shortcutActive 
}) {
  const [isInteracting, setIsInteracting] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [isHovered, setIsHovered] = useState(false);
  const [isValueEditable, setIsValueEditable] = useState(false);

  // Motion 值 - 驱动动画
  const fillPercent = useMotionValue(percentage);
  const fillWidth = useTransform(fillPercent, (pct) => `${pct}%`);
  const handleLeft = useTransform(fillPercent, (pct) => 
    `max(5px, calc(${pct}% - 9px))`
  );

  const percentage = ((value - min) / (max - min)) * 100;
  const isActive = isInteracting || isHovered;
  const displayValue = value.toFixed(decimalsForStep(step));

  return (
    <div className="dialkit-slider-wrapper">
      <div 
        className={`dialkit-slider ${isActive ? 'dialkit-slider-active' : ''}`}
        onMouseEnter={() => setIsHovered(true)}
        onMouseLeave={() => setIsHovered(false)}
        onPointerDown={(e) => {
          setIsInteracting(true);
          setIsDragging(true);
          // 处理拖动逻辑
        }}
      >
        {/* 填充条 - Motion 控制宽度 */}
        <motion.div 
          className="dialkit-slider-fill"
          style={{ width: fillWidth }}
        />

        {/* 标签 */}
        <span className="dialkit-slider-label">
          {label}
        </span>

        {/* 值显示 */}
        <span 
          className={`dialkit-slider-value ${
            isValueEditable ? 'dialkit-slider-value-editable' : ''
          }`}
          onDoubleClick={() => setIsValueEditable(true)}
        >
          {displayValue}
        </span>

        {/* 滑块把手 - Motion 控制位置 */}
        <motion.div 
          className="dialkit-slider-handle"
          style={{ left: handleLeft }}
        />
      </div>
    </div>
  );
}

关键点

  • CSS 类 定义所有样式和布局
  • React 状态 控制何时添加额外的类(如 dialkit-slider-active
  • Motion.dev 驱动特定的动画值(宽度、位置)

##5. 跨框架样式注入

###5.1 React / Solid / Vue

标准导入

// React main.tsx
import 'dialkit/styles.css';
// Solid main.ts
import 'dialkit/styles.css';
// Vue main.ts
import 'dialkit/styles.css';

打包后位置:dist/styles.css(直接复制自 src/styles/theme.css

###5.2 Svelte(特殊编译时注入)

Svelte 的 svelte-package 工具有特殊的打包方式,CSS 文件不会自动被包含,所以 dialkit 在编译时将 CSS 转换为 TypeScript 字符串。

步骤 1:编译脚本 - scripts/gen-theme-css.js

import { readFileSync, writeFileSync } from 'fs';

// 读取原始 CSS
const css = readFileSync('src/styles/theme.css', 'utf8');

// 转义特殊字符(反斜杠、反引号、美元符号)
const escaped = css
  .replace(/\\/g, '\\\\')    // \ -> \\
  .replace(/`/g, '\\`')      // ` -> \`
  .replace(/\$/g, '\\$');    // $ -> \$

// 生成 TypeScript 文件
writeFileSync(
  'src/svelte/theme-css.ts',
  `// Auto-generated from src/styles/theme.css — do not edit
export const themeCSS = \`${escaped}\`;
`
);

构建命令(package.json):

{
  "scripts": {
    "build": "rm -rf dist && tsup && npm run build:svelte",
    "build:svelte": "node scripts/gen-theme-css.js && svelte-package -i src/svelte -o dist/svelte"
  }
}

步骤 2:生成的文件 - src/svelte/theme-css.ts(自动生成)

// Auto-generated from src/styles/theme.css — do not edit
export const themeCSS = `
  @import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');

  /* Dialkit Theme - Dark glassmorphic design */
  .dialkit-root {
    /* Surfaces */
    --dial-surface: rgba(255, 255, 255, 0.05);
    ...
  }
  
  /* 完整的 1326 行 CSS... */
`;

步骤 3:Svelte 组件中使用 - src/svelte/components/DialRoot.svelte

<script lang="ts">
  import { themeCSS } from '../theme-css';
  
  export let theme = 'system';

  // 在组件挂载时注入 CSS
  $effect(() => {
    if (typeof document === 'undefined') return;
    
    const styleId = 'dialkit-theme';
    
    // 避免重复注入
    if (!document.getElementById(styleId)) {
      const style = document.createElement('style');
      style.id = styleId;
      style.textContent = themeCSS;  // 注入 CSS 字符串
      document.head.appendChild(style);
    }
  });
</script>

<div class="dialkit-root" data-theme={theme}>
  {/* ... 组件内容 */}
</div>

为什么这样做?

  • Svelte 的 svelte-package 不自动打包 CSS 文件
  • 将 CSS 作为 TypeScript 字符串,确保它与组件一起打包
  • 在运行时注入到 <head> 中,用户不需要手动导入 CSS

##6. Glassmorphism 设计实现

.dialkit-panel-inner {
  /* 半透明背景 */
  background: var(--dial-glass-bg);              /* 深色主题: #212121 */
  border: 1px solid var(--dial-border);
  border-radius: 14px;
  
  /* 毛玻璃效果三要素 */
  backdrop-filter: blur(var(--dial-backdrop-blur));        /* 20px */
  -webkit-backdrop-filter: blur(var(--dial-backdrop-blur)); /* Safari */
  
  /* GPU 加速 */
  transform: translateZ(0);
  transform-origin: top right;
  
  /* 阴影增加深度 */
  box-shadow: var(--dial-shadow);                /* 0 8px 32px ... */
}

视觉效果

  • 背后的内容通过模糊显示,但仍然可见
  • 类似于 iOS/macOS 的 UI 风格
  • 需要 backdrop-filter 支持(现代浏览器都支持)

##7. 响应式设计

###7.1 位置定位

/* 四个角位置 */
.dialkit-panel[data-position="top-right"] {
  top: 16px;
  right: 16px;
}

.dialkit-panel[data-position="top-left"] {
  top: 16px;
  left: 16px;
}

.dialkit-panel[data-position="bottom-right"] {
  bottom: 16px;
  right: 16px;
}

.dialkit-panel[data-position="bottom-left"] {
  bottom: 16px;
  left: 16px;
}

###7.2 内联模式(铺满页面)

.dialkit-panel[data-mode="inline"] {
  position: static;           /* 不浮动 */
  z-index: auto;
  max-height: 100%;
  height: 100%;
  overflow: hidden;
}

.dialkit-panel[data-mode="inline"] .dialkit-panel-wrapper {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

###7.3 可折叠气泡(collapsed 状态)

.dialkit-panel-inner[data-collapsed="true"] {
  border-radius: 50%;                             /* 变成圆形 */
  padding: 12px;
  box-sizing: border-box;
  box-shadow: var(--dial-shadow-collapsed);       /* 较小阴影 */
}

/* 隐藏标题和工具栏 */
.dialkit-panel-inner[data-collapsed="true"] .dialkit-panel-header {
  display: none;
}

.dialkit-panel-inner[data-collapsed="true"] .dialkit-folder-title-row {
  display: none;
}

/* 中央对齐(收起时只显示气泡) */
.dialkit-panel-inner[data-collapsed="true"] .dialkit-folder-header-top {
  justify-content: center;
  padding: 0;
}

JavaScript 侦测 - src/components/DialRoot.tsx

const observer = new MutationObserver(() => {
  const inner = panelRef.current?.querySelector('.dialkit-panel-inner');
  if (!inner) return;
  
  const collapsed = inner.getAttribute('data-collapsed') === 'true';

  if (!collapsed) {
    // 打开:保存拖动位置,靠齐到角落
    if (dragOffset) {
      lastDragOffset.current = dragOffset;
      const bubbleCenterX = dragOffset.x + 21;
      const midX = window.innerWidth / 2;
      setActivePosition(bubbleCenterX < midX ? 'top-left' : 'top-right');
    }
    setDragOffset(null);
  } else if (lastDragOffset.current) {
    // 关闭:恢复之前的拖动位置
    setDragOffset(lastDragOffset.current);
  }
});

observer.observe(panelRef.current, { 
  subtree: true, 
  attributes: true, 
  attributeFilter: ['data-collapsed'] 
});

##8. 样式与动画的协作

###8.1 CSS Transitions(用户反馈)

/* 颜色过渡 */
.dialkit-slider {
  transition: background 0.15s;
}

.dialkit-slider-fill {
  transition: background 0.15s;
}

.dialkit-toggle-track {
  transition: background 0.2s;
}

.dialkit-folder-title {
  transition: color 0.15s;
}

###8.2 Motion.dev 驱动的动画(复杂交互)

// React Slider 使用 Motion 驱动 fill width
const fillPercent = useMotionValue(percentage);
const fillWidth = useTransform(fillPercent, (pct) => `${pct}%`);

return (
  <motion.div 
    className="dialkit-slider-fill"     // CSS 类定义基础样式
    style={{ width: fillWidth }}        // Motion 值驱动宽度
  />
);

###8.3 Spring 动画(Svelte)

<script lang="ts">
  import { Spring } from 'svelte/motion';

  let fillPercent = new Spring(
    ((value - min) / (max - min)) * 100,
    { stiffness: 0.25, damping: 0.7 }
  );
</script>

<div 
  class="dialkit-slider-fill"
  style:width={`${fillPercent.current}%`}
></div>

##9. 文件大小和性能

###9.1 大小统计

src/styles/theme.css:    1326 行,27 KB(未压缩)
dist/styles.css:         ~8 KB(gzip 压缩)

包含内容

  • CSS Variables 定义(颜色、间距、阴影)
  • 所有 UI 组件样式
  • 响应式设计(4 个位置、内联模式、可折叠)
  • 完整的 Light/Dark/System 主题
  • 媒体查询

###9.2 性能优化

1. GPU 加速

.dialkit-panel-inner {
  transform: translateZ(0);  /* 创建新的层叠上下文 */
}

2. Will-change 提示

.dialkit-folder-content {
  will-change: transform;    /* 浏览器准备动画 */
}

3. 样式隔离

  • 每个组件的样式都是独立的
  • 浏览器可以并行优化渲染

4. 无阻塞渲染

.dialkit-slider-fill {
  pointer-events: none;      /* 不参与事件处理 */
}

.dialkit-slider-handle {
  pointer-events: none;      /* 只有父容器处理事件 */
}

##10. 自定义主题示例

###10.1 覆盖 CSS Variables

/* 在你的应用中 */
.dialkit-root.custom-indigo {
  /* 颜色 */
  --dial-surface: rgba(99, 102, 241, 0.1);        /* Indigo */
  --dial-surface-hover: rgba(99, 102, 241, 0.15);
  --dial-text-root: #4F46E5;
  --dial-text-primary: #6366F1;
  
  /* 几何 */
  --dial-radius: 12px;                            /* 更圆 */
  --dial-backdrop-blur: 10px;                     /* 更清晰 */
}

###10.2 使用自定义主题

<DialRoot theme="dark" />
<style>{`
  .dialkit-root {
    --dial-surface: rgba(99, 102, 241, 0.1);
  }
`}</style>

###10.3 动态主题切换

const [theme, setTheme] = useState('dark');

return (
  <>
    <DialRoot theme={theme} />
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      Toggle Theme
    </button>
  </>
);

##11. BEM 类名参考

组件主类名元素修饰符
Panel.dialkit-panel-inner, -header, -wrapper[data-position], [data-mode], [data-collapsed]
Slider.dialkit-slider-label, -value, -handle, -fill[data-active], -value-editable
Toggle.dialkit-toggle-label, -track, -thumb[data-checked]
Folder.dialkit-folder-header, -title, -content, -icon[data-open], -root
Select.dialkit-select-*-trigger, -dropdown, -option[data-open], [data-selected]
Color.dialkit-color-*-swatch, -input, -label
Button.dialkit-button:hover, :active
Segmented.dialkit-segmented-button, -pill[data-active]

##总结表

维度实现细节
样式引擎纯 CSS(无 SCSS/Less/Tailwind)
主题系统CSS Variables + [data-theme] 属性
命名规范BEM(Block-Element-Modifier)
文件结构单一 src/styles/theme.css
框架支持相同 CSS 对所有框架适用
Svelte 处理编译时转换为 TS 字符串,运行时注入
动画方案CSS Transitions + Motion.dev API
文件大小27 KB(源)→ 8 KB(gzipped)
主题支持Dark(默认)/ Light / System(响应式)
响应式位置定位 + 内联模式 + 可折叠设计
定制性完全可定制 - 覆盖 CSS 变量即可
浏览器兼容现代浏览器(Chrome/Firefox/Safari/Edge)

这个设计非常优雅 - 避免了 Tailwind 的复杂性,同时提供了跨框架支持和完全的定制能力!


END OF POST

How does dialkit support multiple frontend frameworks? — joshpuckett/dialkit