@astralume/keyboard
v0.1.0
Published
A scalable Vue 3 visual keyboard component with themes, key states, holding hints and animation APIs.
Maintainers
Readme
@astralume/keyboard
English documentation is available in the second half of this README.
@astralume/keyboard 是一个 Vue 3 可视化键盘组件。它可以展示高保真键盘布局,监听真实键盘和鼠标输入,显示按下、成功、失败、教学提示、锁定按下、时间轴动画和波纹动画等状态。
组件源码位于 src/lib。本仓库的演示页面位于 src/demo,不会进入 npm 包。npm 包只发布 dist 内的组件产物。
功能特性
- Vue 3 组件,支持 TypeScript 类型。
- 支持紧凑键盘、合并全键盘、分区全键盘三种布局。
- 支持整体缩放,通过
width、keySize、gapX、gapY控制比例。 - 支持真实键盘输入、鼠标/触摸输入和按键事件抛出。
- 支持普通全面接管和增强接管。增强接管会尝试使用 Fullscreen API 与 Keyboard Lock API。
- 支持 holding、locked、success、error、自定义状态。
- 支持 timeline 脚本动画、蛇形波纹动画、typed-array 高频帧渲染。
- 支持主题、CSS 变量、自定义 class、自定义单键样式。
安装
pnpm add @astralume/keyboard也可以使用 npm 或 yarn:
npm install @astralume/keyboard
yarn add @astralume/keyboard快速开始
<script setup lang="ts">
import VisualKeyboard from '@astralume/keyboard'
import '@astralume/keyboard/style.css'
</script>
<template>
<VisualKeyboard />
</template>推荐用法
大多数情况下,只需要传入少量 options。组件会把缺失项与默认配置合并。
<script setup lang="ts">
import { computed } from 'vue'
import VisualKeyboard, { type PartialDeep, type KeyboardOptions } from '@astralume/keyboard'
import '@astralume/keyboard/style.css'
const keyboardOptions = computed<PartialDeep<KeyboardOptions>>(() => ({
layout: {
mode: 'compact',
width: 960,
keySize: 56,
gapX: 4,
gapY: 4,
},
appearance: {
theme: 'demo2',
},
behavior: {
defaultPressState: 'success',
pointerBehavior: 'press',
},
}))
</script>
<template>
<VisualKeyboard :options="keyboardOptions" />
</template>完整示例
<script setup lang="ts">
import { ref } from 'vue'
import VisualKeyboard, {
type KeyEventPayload,
type KeyboardOptions,
type StateDefinitionMap,
type TimelineItem,
} from '@astralume/keyboard'
import '@astralume/keyboard/style.css'
const keyboardRef = ref<InstanceType<typeof VisualKeyboard> | null>(null)
const options: KeyboardOptions = {
layout: {
mode: 'compact',
width: 960,
keySize: 56,
gapX: 4,
gapY: 4,
blockGapScale: 4,
paddingScale: 2.5,
topRowHeight: 'compact',
splitRatios: [0.5, 0.44, 0.4],
},
appearance: {
theme: 'demo2',
keyRadiusPercent: 25,
topKeyRadiusPercent: 25,
boardClass: '',
keyClass: '',
boardStyleVars: {},
keyStyleVars: {},
keyClassMap: {},
keyStyleVarsMap: {},
},
behavior: {
captureAllKeys: false,
enhancedCapture: false,
pointerBehavior: 'press',
defaultPressState: 'success',
renderMode: 'vue',
},
states: {
holdingState: 'holding',
lockedState: 'default',
},
}
const stateDefinitions: StateDefinitionMap = {
teaching: {
styleVars: {
'--vk-key-bg': '#e9ddff',
'--vk-key-color': '#4c2c85',
},
},
}
const timeline: TimelineItem[] = [
{ keyId: 'KeyH', at: 0, duration: 120, visualState: 'success', pressed: true },
{ keyId: 'KeyI', at: 150, duration: 120, visualState: 'success', pressed: true },
]
function handleKeyEvent(payload: KeyEventPayload) {
console.log(payload.keyId, payload.phase)
}
function playWave() {
keyboardRef.value?.playWave({
tailLength: 7,
speed: 1,
maxActiveWaves: 8,
})
}
</script>
<template>
<VisualKeyboard
ref="keyboardRef"
:options="options"
:holding-keys="['KeyA', 'KeyS']"
:locked-keys="['Escape']"
:state-definitions="stateDefinitions"
:timeline="timeline"
:timeline-playing="false"
@key-event="handleKeyEvent"
/>
<button type="button" @click="playWave">播放波纹</button>
</template>Props
| Prop | 类型 | 默认值 | 说明 |
| ------------------ | ------------------------------- | ------- | --------------------------------------------------------------------------- |
| options | PartialDeep<KeyboardOptions> | {} | 键盘配置。可以只传局部配置,组件会与默认配置合并。 |
| holdingKeys | string[] | [] | 教学提示按键。可以传 keyId,例如 KeyA,也可以传部分显示文本,例如 A。 |
| lockedKeys | string[] | [] | 持续按下的按键。适合展示“某个键保持按下”的状态。 |
| keyStateMap | Record<string, KeyStateInput> | {} | 外部直接指定某些按键的状态。key 可以是 keyId 或显示文本。 |
| stateDefinitions | StateDefinitionMap | {} | 自定义状态定义。用于把状态名映射成 class、CSS 变量和 pressed 覆盖。 |
| resolveKeyView | (payload) => KeyStateInput | null | 渲染前的状态解析钩子。适合把业务判断映射成状态。 |
| plugins | VisualKeyboardPlugin[] | [] | 插件列表。插件可以处理输入事件或参与状态解析。 |
| timeline | TimelineItem[] | [] | 声明式时间轴动画。 |
| timelinePlaying | boolean | false | 是否播放 timeline。 |
KeyboardOptions
KeyboardOptions 分为四组:layout、appearance、behavior、states。
layout
| 字段 | 类型 | 默认值 | 作用 |
| --------------- | ------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------- |
| mode | 'compact' \| 'full' \| 'separated' | 'compact' | 布局模式。compact 是主键盘,full 是主键盘加右侧合并区域,separated 是主键盘、导航区和数字区分离显示。 |
| width | number | 960 | 键盘最终显示宽度,单位 px。组件会按这个宽度整体缩放。 |
| keySize | number | 56 | 标准按键边长,单位 px。布局内部以它作为基准计算。 |
| gapX | number | 4 | 按键横向间隙,单位 px。 |
| gapY | number | 4 | 按键纵向间隙,单位 px。 |
| blockGapScale | number | 4 | 全键盘模式下,不同键盘区之间的横向间距倍数。实际间距为 gapX * blockGapScale。 |
| paddingScale | number | 2.5 | 键盘背景内边距倍数。实际内边距为 gapX/gapY * paddingScale。 |
| topRowHeight | 'compact' \| 'full' | 'compact' | 顶部功能键高度。compact 约为标准键高的三分之二,full 等于标准键高。 |
| splitRatios | [number, number, number] | [0.5, 0.44, 0.4] | 动态宽度按键比例。依次控制 Tab/反斜杠、Caps/Enter、左 Shift/右 Shift。值会限制在 0 到 1。 |
appearance
| 字段 | 类型 | 默认值 | 作用 |
| --------------------- | ---------------------------------------- | --------- | --------------------------------------------------------------------- |
| theme | string | 'demo2' | 主题名称。内置主题包括 demo1、demo2、demo1-dark、demo2-dark。 |
| keyRadiusPercent | number | 25 | 普通按键圆角百分比。0 表示直角,50 表示最大圆角。 |
| topKeyRadiusPercent | number | 25 | 顶部功能键圆角百分比。 |
| boardClass | string | '' | 追加到键盘背景上的 class。 |
| keyClass | string | '' | 追加到所有按键上的 class。 |
| boardStyleVars | Record<string, string> | {} | 写到键盘背景上的 CSS 变量或样式。 |
| keyStyleVars | Record<string, string> | {} | 写到所有按键上的 CSS 变量或样式。 |
| keyClassMap | Record<string, string \| string[]> | {} | 给单个按键追加 class。key 可以是 keyId 或显示文本。 |
| keyStyleVarsMap | Record<string, Record<string, string>> | {} | 给单个按键追加 CSS 变量或样式。key 可以是 keyId 或显示文本。 |
behavior
| 字段 | 类型 | 默认值 | 作用 |
| ------------------- | ------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| captureAllKeys | boolean | false | 是否阻止可捕获键盘事件的默认行为和传播。浏览器保留快捷键不一定能被普通页面拦截。 |
| enhancedCapture | boolean | false | 是否启用增强接管。开启后会在用户输入时尝试 Fullscreen API 和 Keyboard Lock API。 |
| pointerBehavior | 'press' \| 'emit' \| 'none' \| 'toggle' \| 'lock' \| 'custom' \| string | 'press' | 鼠标/触摸行为。press 临时按下,emit 只抛事件,none 忽略状态,toggle 点击切换,lock 点击后保持按下,custom 留给外部逻辑处理。 |
| defaultPressState | string | 'success' | 真实键盘或鼠标按下时默认触发的视觉状态。可设为 none 表示不显示状态色,设为 default 表示只显示主题原本按下态。 |
| renderMode | 'vue' \| 'hybrid' \| 'frame' | 'vue' | 渲染模式。vue 适合普通使用,hybrid 和 frame 适合更高频动画。 |
states
| 字段 | 类型 | 默认值 | 作用 |
| -------------- | -------- | ----------- | -------------------------------- |
| holdingState | string | 'holding' | holdingKeys 使用的视觉状态名。 |
| lockedState | string | 'default' | lockedKeys 使用的视觉状态名。 |
状态系统
状态可以是字符串,也可以是对象。
type KeyStateInput = string | KeyLayerState | null | undefinedKeyLayerState 字段:
| 字段 | 类型 | 说明 |
| ------------- | ------------------------ | ------------------------------------------------------------------- |
| pressed | boolean | 是否显示按下形态。 |
| visualState | string | 视觉状态名,例如 success、error、holding、wave。 |
| priority | number | 状态优先级。数值越大越晚合成,越容易覆盖低优先级状态。 |
| className | string \| string[] | 附加 class。 |
| styleVars | Record<string, string> | 附加 CSS 变量或样式。它会略高于同层默认状态定义,适合逐键动画颜色。 |
内置状态
| 状态名 | 说明 |
| --------- | ------------------------------------ |
| success | 成功状态,默认是绿色系。 |
| error | 失败状态,默认是红色系。 |
| holding | 教学提示状态。 |
| wave | 波纹动画状态。 |
| default | 不追加视觉状态,但可以保留按下形态。 |
| none | 不渲染视觉状态。 |
自定义状态
const stateDefinitions = {
warning: {
styleVars: {
'--vk-key-bg': '#fff1b8',
'--vk-key-color': '#705600',
},
},
pressedBlue: {
pressed: true,
styleVars: {
'--vk-key-bg': '#dbeafe',
'--vk-key-color': '#1d4ed8',
},
},
}事件
组件会抛出 key-event。
<VisualKeyboard @key-event="handleKeyEvent" />KeyEventPayload 字段:
| 字段 | 类型 | 说明 |
| ------------- | --------------- | --------------------------------------------- |
| keyId | string | 标准按键 id,例如 KeyA、Escape、Space。 |
| label | string | 按键显示文本。 |
| codes | number[] | 兼容旧 keyCode 的数字列表。 |
| source | string | 输入来源,例如 physical 或 pointer。 |
| phase | string | 输入阶段,例如 down、up、cancel。 |
| nativeEvent | Event \| null | 原生事件。 |
| repeat | boolean | 是否是键盘 repeat。 |
| timestamp | number | 事件时间戳。 |
命令式 API
通过 ref 获取组件实例:
const keyboardRef = ref<InstanceType<typeof VisualKeyboard> | null>(null)
keyboardRef.value?.playWave()| 方法 | 说明 |
| ----------------------------------------- | ------------------------------------------ |
| requestEnhancedCapture() | 手动请求增强接管。浏览器通常需要用户手势。 |
| releaseEnhancedCapture() | 释放 Keyboard Lock 等增强接管状态。 |
| getVisibleKeys() | 获取当前可见交互键列表。 |
| setLayerState(layerName, keyRef, state) | 设置某一层中的单个按键状态。 |
| setLayerStates(layerName, changes) | 批量设置某一层中的按键状态。 |
| clearLayer(layerName?) | 清理指定状态层;不传时清空所有状态层。 |
| playTimeline(items, options?) | 播放脚本化时间轴。 |
| pauseTimeline(target?) | 暂停时间轴。 |
| resumeTimeline(target?) | 恢复时间轴。 |
| seekTimeline(time, target?) | 跳转时间轴到指定毫秒。 |
| stopTimeline(target?, options?) | 停止时间轴。 |
| playWave(options?) | 播放蛇形波纹动画。 |
| startFrameRenderer(source, options?) | 启动 typed-array 高频帧渲染器。 |
| stopFrameRenderer(clearFrameLayer?) | 停止 typed-array 高频帧渲染器。 |
| clearTransientActiveKeys() | 清理临时按下态,适合页面失焦后手动兜底。 |
时间轴动画
keyboardRef.value?.playTimeline(
[
{ keyId: 'KeyH', at: 0, duration: 120, visualState: 'success', pressed: true },
{ keyId: 'KeyI', at: 150, duration: 120, visualState: 'success', pressed: true },
],
{
layerName: 'typing',
autoClear: true,
loop: false,
},
)TimelineItem 字段:
| 字段 | 类型 | 说明 |
| ------------- | ------------------------- | -------------------------------- |
| keyId | string | 目标按键 id。 |
| key | string | keyId 的兼容别名。 |
| at | number | 事件开始时间,单位毫秒。 |
| start | number | at 的兼容别名。 |
| action | string | set 或 clear。默认是 set。 |
| state | string \| KeyLayerState | 要写入的状态。 |
| pressed | boolean | 是否显示按下形态。 |
| visualState | string | 视觉状态名。 |
| priority | number | 事件优先级。 |
| className | string \| string[] | 附加 class。 |
| styleVars | Record<string, string> | 附加 CSS 变量或样式。 |
| duration | number | 持续时间,结束后会自动 clear。 |
波纹动画
playWave 默认按视觉行蛇形播放:第一行从左到右,第二行从右到左,依次交替。蛇头颜色较深,尾巴逐渐变浅。
keyboardRef.value?.playWave({
speed: 1,
tailLength: 7,
maxActiveWaves: 8,
headColor: '#0a2644',
tailColor: '#f4faff',
})WaveOptions 常用字段:
| 字段 | 类型 | 默认值 | 说明 |
| ---------------- | --------------------------------- | --------- | -------------------------------------------- |
| speed | number | 1 | 播放速度倍率。越大越快。 |
| step | number | 34 | 每步间隔,单位毫秒,会受 speed 影响。 |
| tailLength | number | 7 | 蛇身长度,也就是同一时间最多显示的尾巴段数。 |
| headColor | string | #0a2644 | 蛇头背景色。 |
| tailColor | string | #f4faff | 尾巴末端背景色。 |
| headTextColor | string | #f6fbff | 蛇头文字色。 |
| tailTextColor | string | #1d4a70 | 尾巴文字色。 |
| maxActiveWaves | number | 6 | 最大并发波数量。重复点击时会复用波纹层。 |
| loop | boolean \| 'infinite' \| number | false | 是否循环播放。 |
| repeatDelay | number | 0 | 每轮循环之间的延迟。 |
高频帧渲染
如果你需要播放非常长、非常密集的动画,可以使用 startFrameRenderer。它使用 typed array 存储每一帧状态,并只提交发生变化的按键。
const startedAt = performance.now()
keyboardRef.value?.startFrameRenderer((frame, now) => {
const elapsed = now - startedAt
if (elapsed > 3000) {
return false
}
frame.setKey('KeyA', {
pressed: true,
stateId: 1,
intensity: 1,
})
})主题与样式扩展
必须引入组件样式:
import '@astralume/keyboard/style.css'内置主题:
demo1亮色立体主题。demo2亮色简洁主题。demo1-dark暗色立体主题。demo2-dark暗色简洁主题。
可以通过 CSS 变量覆盖主题:
<VisualKeyboard
:options="{
appearance: {
boardClass: 'my-keyboard',
keyStyleVars: {
'--vk-key-bg': '#111827',
'--vk-key-color': '#f9fafb',
},
},
}"
/>.my-keyboard {
--vk-board-bg: #f8fafc;
}全面接管与增强接管
captureAllKeys 会尽可能阻止页面内可捕获键盘事件的默认行为和传播。
const options = {
behavior: {
captureAllKeys: true,
},
}enhancedCapture 会尝试使用 Fullscreen API 和 Keyboard Lock API。
const options = {
behavior: {
enhancedCapture: true,
},
}浏览器仍可能保留系统级快捷键或部分浏览器快捷键,例如某些关闭标签、切换应用、系统截图快捷键。这不是组件可以完全绕过的限制。
发布包边界
组件代码:
src/lib用于 npm 包构建。src/lib/index.ts是包入口。
演示代码:
src/demo只用于本仓库调试和展示。dist-demo是演示页构建产物,不会发布到 npm。
npm 包内容通过 files 限制,只发布:
dist/index.jsdist/index.cjsdist/index.d.tsdist/style.css- 相关类型声明文件
本地开发
pnpm install
pnpm dev构建
pnpm buildpnpm build 会生成库模式产物:
dist/index.js是 ESM 入口。dist/index.cjs是 CommonJS 入口。dist/index.d.ts是类型入口。dist/style.css是组件样式。
演示页单独构建:
pnpm build:demo发布前检查
pnpm lint
pnpm format:check
pnpm build
pnpm pack --dry-run正式发布:
npm publish --access public@astralume/keyboard
@astralume/keyboard is a Vue 3 visual keyboard component. It renders a high-fidelity keyboard, listens to physical keyboard and pointer input, and supports pressed states, success/error states, teaching hints, locked keys, timeline animations, wave animations, and high-frequency frame rendering.
The component source lives in src/lib. The demo app lives in src/demo and is not published to npm. The npm package only publishes compiled files from dist.
Features
- Vue 3 component with TypeScript types.
- Compact keyboard, merged full keyboard, and separated full keyboard layouts.
- Scalable layout controlled by
width,keySize,gapX, andgapY. - Physical keyboard input, pointer input, and normalized key events.
- Basic key capture and optional enhanced capture with Fullscreen API and Keyboard Lock API.
- Built-in and custom visual states.
- Timeline animation, snake-like wave animation, and typed-array frame rendering.
- Theme presets, CSS variables, custom classes, and per-key style overrides.
Installation
pnpm add @astralume/keyboardOr:
npm install @astralume/keyboard
yarn add @astralume/keyboardQuick Start
<script setup lang="ts">
import VisualKeyboard from '@astralume/keyboard'
import '@astralume/keyboard/style.css'
</script>
<template>
<VisualKeyboard />
</template>Recommended Usage
You can pass only the options you need. Missing fields are merged with the default options.
<script setup lang="ts">
import { computed } from 'vue'
import VisualKeyboard, { type PartialDeep, type KeyboardOptions } from '@astralume/keyboard'
import '@astralume/keyboard/style.css'
const keyboardOptions = computed<PartialDeep<KeyboardOptions>>(() => ({
layout: {
mode: 'compact',
width: 960,
keySize: 56,
gapX: 4,
gapY: 4,
},
appearance: {
theme: 'demo2',
},
behavior: {
defaultPressState: 'success',
pointerBehavior: 'press',
},
}))
</script>
<template>
<VisualKeyboard :options="keyboardOptions" />
</template>Props
| Prop | Type | Default | Description |
| ------------------ | ------------------------------- | ------- | ----------------------------------------------------------------------------------------------- |
| options | PartialDeep<KeyboardOptions> | {} | Keyboard layout, appearance, behavior, and state options. |
| holdingKeys | string[] | [] | Keys highlighted as teaching hints. Values can be key ids such as KeyA or labels such as A. |
| lockedKeys | string[] | [] | Keys that should stay visually pressed. |
| keyStateMap | Record<string, KeyStateInput> | {} | External per-key state map. Keys can be key ids or labels. |
| stateDefinitions | StateDefinitionMap | {} | Custom state definitions. |
| resolveKeyView | (payload) => KeyStateInput | null | Hook for resolving per-key visual state before rendering. |
| plugins | VisualKeyboardPlugin[] | [] | Plugin list for input and rendering extensions. |
| timeline | TimelineItem[] | [] | Declarative timeline animation. |
| timelinePlaying | boolean | false | Whether to play the declarative timeline. |
KeyboardOptions
layout
| Field | Type | Default | Description |
| --------------- | ------------------------------------ | ------------------ | ------------------------------------------------------------------------- |
| mode | 'compact' \| 'full' \| 'separated' | 'compact' | Layout mode. |
| width | number | 960 | Final rendered keyboard width in pixels. |
| keySize | number | 56 | Base key size in pixels. |
| gapX | number | 4 | Horizontal gap between keys. |
| gapY | number | 4 | Vertical gap between rows. |
| blockGapScale | number | 4 | Gap multiplier between keyboard blocks. |
| paddingScale | number | 2.5 | Board padding multiplier. |
| topRowHeight | 'compact' \| 'full' | 'compact' | Function-key row height mode. |
| splitRatios | [number, number, number] | [0.5, 0.44, 0.4] | Dynamic split ratios for Tab/Backslash, Caps/Enter, and left/right Shift. |
appearance
| Field | Type | Default | Description |
| --------------------- | ---------------------------------------- | --------- | -------------------------------------------------------------------------- |
| theme | string | 'demo2' | Theme name. Built-in themes: demo1, demo2, demo1-dark, demo2-dark. |
| keyRadiusPercent | number | 25 | Regular key radius percentage. |
| topKeyRadiusPercent | number | 25 | Top-row key radius percentage. |
| boardClass | string | '' | Extra class added to the keyboard board. |
| keyClass | string | '' | Extra class added to all keys. |
| boardStyleVars | Record<string, string> | {} | Inline CSS variables or styles for the board. |
| keyStyleVars | Record<string, string> | {} | Inline CSS variables or styles for all keys. |
| keyClassMap | Record<string, string \| string[]> | {} | Extra classes for specific keys. |
| keyStyleVarsMap | Record<string, Record<string, string>> | {} | Inline CSS variables or styles for specific keys. |
behavior
| Field | Type | Default | Description |
| ------------------- | ------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- |
| captureAllKeys | boolean | false | Prevent default behavior and propagation for capturable keyboard events. |
| enhancedCapture | boolean | false | Try to use Fullscreen API and Keyboard Lock API. |
| pointerBehavior | 'press' \| 'emit' \| 'none' \| 'toggle' \| 'lock' \| 'custom' \| string | 'press' | Pointer input behavior. |
| defaultPressState | string | 'success' | Visual state used when a key is pressed. |
| renderMode | 'vue' \| 'hybrid' \| 'frame' | 'vue' | Rendering mode. Use vue for normal usage and hybrid/frame for high-frequency animation. |
states
| Field | Type | Default | Description |
| -------------- | -------- | ----------- | ----------------------------------- |
| holdingState | string | 'holding' | Visual state used by holdingKeys. |
| lockedState | string | 'default' | Visual state used by lockedKeys. |
Imperative API
| Method | Description |
| ----------------------------------------- | ------------------------------------------------------ |
| requestEnhancedCapture() | Request enhanced keyboard capture. |
| releaseEnhancedCapture() | Release enhanced keyboard capture. |
| getVisibleKeys() | Get currently visible interactive keys. |
| setLayerState(layerName, keyRef, state) | Set one key state in a layer. |
| setLayerStates(layerName, changes) | Set multiple key states in a layer. |
| clearLayer(layerName?) | Clear a state layer, or clear all layers when omitted. |
| playTimeline(items, options?) | Play a scripted timeline. |
| pauseTimeline(target?) | Pause timelines. |
| resumeTimeline(target?) | Resume timelines. |
| seekTimeline(time, target?) | Seek a timeline to a specific time. |
| stopTimeline(target?, options?) | Stop timelines. |
| playWave(options?) | Play the snake-like wave animation. |
| startFrameRenderer(source, options?) | Start the typed-array frame renderer. |
| stopFrameRenderer(clearFrameLayer?) | Stop the typed-array frame renderer. |
| clearTransientActiveKeys() | Clear temporary pressed states. |
Wave Animation
playWave moves in a snake-like path: first row left to right, second row right to left, and so on. The head is darker and the tail fades to a lighter color.
keyboardRef.value?.playWave({
speed: 1,
tailLength: 7,
maxActiveWaves: 8,
headColor: '#0a2644',
tailColor: '#f4faff',
})Local Development
pnpm install
pnpm devBuild
pnpm buildDemo build:
pnpm build:demoPrepublish Checklist
pnpm lint
pnpm format:check
pnpm build
pnpm pack --dry-runPublish:
npm publish --access public