How does dialkit support multiple frontend frameworks?
完美!现在我有了完整的理解。让我撰写详细的分析:
##深度解析: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:
| 框架 | 适配入口 | 响应式系统 |
|---|---|---|
| React | useDialKit() Hook | useSyncExternalStore() |
| Solid | createDialKit() Primitive | createSignal() + createMemo() |
| Vue | useDialKit() Composable | ref() + computed() + watch() |
| Svelte | createDialKit() | $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 开发然后桥接」
正确的方式 ✅:
- 分离数据层 - 框架无关的 Store(发布-订阅模式)
- 适配层 - 每个框架一个 Hook/Primitive,连接到 Store
- UI 层 - 每个框架用自己的原生 API 实现相同的组件
这种架构在大型跨框架库中非常普遍(如 TanStack Query、Zustand),是真正的"框架无关"设计,而不是"为某个框架写,然后转换"。
完美!现在我创建详细的分析文档:
#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