@inthedark/kitsui

中文文档
0.1.2发布于 2026/11/01MITTypeScriptCommonJS零依赖构建来源已验证

UI框架模板围绕可变组件包装器和显式状态转换构建。

中文使用文档
文档目录11 节
  1. 典型组件
  2. 最佳实践
  3. 可重用组件
  4. 特定组件的事件
  5. 状态值和派生值
  6. 独立所有者范围
  7. 机械手绑定
  8. 运动和放置
  9. 样式
  10. 分解
  11. 功能指数

kitsui

kitsui是一个dom优先的UI库 Component 和 State 对象。一个 Component 包裹一个真实的 HTMLElement;一个 State 是一个自有的无功值。

把喜水想象成反应,但没有那么神奇。每个可能的效果都由您显式声明和控制。直接构建和移动真正的组件;Kitsui不会在代码背后运行应用程序范围的渲染周期。

pnpm add @inthedark/kitsui

典型组件

import { Component, State, Style } from "@inthedark/kitsui"

const counterStyle = Style.Class("counter", {
    alignItems: "center",
    display: "flex",
    gap: 8,
})

function Counter (): Component {
    const root = Component("section")
        .class.add(counterStyle)

    const count = State(root, 0)

    Component("output")
        .text.set(count.map(value => `Count: ${value}`))
        .appendTo(root)

    Component("button")
        .attribute.set("type", "button")
        .text.set("Increment")
        .event.owned.on.click(() => {
            count.update(value => value + 1)
        })
        .appendTo(root)

    return root
}

Counter().appendTo(document.body)

这种风格使所有权保持简单: root 拥有状态、事件清理和子组件,因此可以删除 root 处理整个子树。

最佳实践

  • 返回 Component 来自小型工厂函数的实例。
  • 创建 State 应该处理它的组件或所有者。
  • 使用 Owner() 为了一个没有归属的活镜 Component,然后将其传递给该范围内的每个感知所有者的API。
  • 在手动之前使用派生状态、操纵符和条件放置 subscribe 加上DOM突变。
  • 在一个流畅的链中构建大多数静态结构。
  • 使用 component.class, component.style, component.attribute, component.aria, component.text, component.event 的DOM效果。
  • 使用 event.owned 对于应该与组件一起清理的处理程序。
  • 使用 Style.Class 用于可重用、可选择或共享的样式。使用 component.style 用于动态或反应性内联属性。
  • 在事件处理程序或真正的自定义行为中保持直接的DOM访问。

可重用组件

可重用组件文件应该与组件名称完全匹配,并默认导出组件函数。使用一个局部符号作为接口名和函数名,然后导出一次 export default Name.

不强制赋值 as Name 在return语句中。给函数一个显式的返回类型和let component.extend(...) 检查结果的类型。

自定义组件方法应该返回 this 除非有强烈的理由退回其他东西。

import { Component } from "@inthedark/kitsui"

interface ButtonExtensions {
    setBusy (busy: boolean): this
    setLabel (label: string): this
}

interface Button extends Component<HTMLButtonElement>, ButtonExtensions { }

function Button (this: Component<HTMLButtonElement> | void, label: string): Button {
    const button = Component(this ?? "button", Button)
        .attribute.set("type", "button")

    const labelText = Component("span")
        .text.set(label)
        .appendTo(button)

    return button.extend<ButtonExtensions>(root => ({
        setBusy (busy) {
            root.attribute.toggle("disabled", busy)
            return root
        },

        setLabel (label) {
            labelText.text.set(label)
            return root
        },
    }))
}

export default Button

可组合构造器必须将自己的函数标识传递给初始的 Component(...) 呼叫。 and 将当前组件注入 this,转发参数,并跳过已经应用于该组件的构建器。 is 和 as 检查相同的构建器标识。

const standalone = Button("Save")
const composed = Component("button").and(Button, "Save")

if (composed.is(Button)) {
    composed.setBusy(true)
}

特定组件的事件

基本组件事件仅适用于每个组件上可用的事件,例如 Mount 和 Dispose. 特定于功能的组件应该通过 Component.WithEvents<Events> 而不是增加共享组件事件映射。

自定义kitsui组件事件名称应该是 PascalCase,例如 DragStart 或者 WidgetCommit,因此它们与本地DOM事件(如 click 和 pointerdown.

import { Component } from "@inthedark/kitsui"

interface WidgetEvents {
    WidgetCommit: CustomEvent<{
        readonly value: string
    }> & {
        readonly source: "user" | "api"
    }
}

interface WidgetExtensions {
    commit (value: string): this
}

interface Widget extends Component.WithEvents<WidgetEvents>, WidgetExtensions { }

function emitWidgetCommit (widget: Widget, value: string): boolean {
    return widget.event.emit.WidgetCommit({ value }, {
        tweak: event => Object.defineProperty(event, "source", {
            value: "api",
        }),
    })
}

declare const widget: Widget

widget.event.owned.on.WidgetCommit(event => {
    event.detail.value.toUpperCase()
    event.source
})

event.emit.* 和 event.dispatch.* 构造映射的是别名吗 CustomEvent,运行可选的 tweak 回调,并返回目标的native dispatchEvent 结果。本地的 CustomEvent 默认值被保留,所以kitsui自定义事件不会冒泡,除非 bubbles: true 显式提供。冒泡不会将特性事件添加到祖先组件类型;在类型化之前,祖先仍然必须选择进入事件映射 event API公开该名称。

同步 Dispose 处理程序可以发出最终的自定义通知,而所有者同时报告这两种通知 disposed 和 disposing. 这是一个狭窄的生命周期窗口:侦听器注册和其他组件突变仍然被禁止,并且在处理完成后再次拒绝自定义事件发射。

状态值和派生值

当立即使用派生值时,使用短形式派生状态。当派生状态被重用、分组、传递或需要明显的所有者时,使用所有者显式派生状态。

保持行为和UI状态一致。被禁用的按钮不应该是唯一强制执行不变量的东西。

const root = Component("section")
const count = State(root, 0)
const atLimit = count.map(root, value => value >= 10)

Component("output")
    .text.set(count.map(value => `Count: ${value}`))
    .appendTo(root)

Component("button")
    .attribute.set("type", "button")
    .attribute.bind(atLimit, "disabled")
    .text.set(atLimit.map(value => value ? "Limit reached" : "Increment"))
    .event.owned.on.click(() => {
        count.update(value => Math.min(value + 1, 10))
    })
    .appendTo(root)

Component("p")
    .text.bind(atLimit, "The counter is capped.")
    .appendTo(root)

使用 State.Group(owner, states) 当一个派生值需要多个状态的一致快照时。使用 truthy, falsy, or(...) 而不是手工编写重复的布尔或空值映射。

独立所有者范围

使用 Owner() 当一个活的范围没有所有权 Component. 将所有者传递给 State 和其他所有者感知的api,这样整个作用域共享一个生命周期。在范围结束时处置所有者。

const session = Owner()
const status = State(session, "connecting")
const label = status.map(session, value => `Status: ${value}`)

label.subscribe(session, value => {
    console.log(value)
})

session.dispose()

当作用域属于UI子树时,使用 Component 作为它的主人。用于路由器、套接字、观察者和其他系统的集成帮助程序可以接受 Owner 和使用 owner.onCleanup(...) 在内部;普通应用程序代码通常只将所有者传递给帮助程序。

每个所有者暴露的都是终身的 signal 用于可取消的平台工作。

机械手绑定

为响应式DOM效果使用操纵符。单个状态可以驱动文本、类、原始属性、类型化ARIA属性和事件,而无需手动DOM记账。

const expandedStyle = Style.Class("panel-expanded", {
    borderColor: "#0f766e",
})

const panel = Component("section")
const expanded = State(panel, false)

Component("button")
    .attribute.set("type", "button")
    .aria.expanded(expanded)
    .text.set(expanded.map(value => value ? "Collapse" : "Expand"))
    .event.owned.on.click(() => {
        expanded.update(value => !value)
    })
    .appendTo(panel)

Component("div")
    .class.bind(expanded, expandedStyle)
    .attribute.bind(expanded, "data-expanded")
    .aria.hidden(expanded.falsy)
    .text.set(expanded.map(value => value ? "Expanded" : "Collapsed"))
    .appendTo(panel)

使用 attribute.toggle(name, enabled) 用于在命令式方法中立即切换布尔属性。使用 attribute.bind(state, name) 什么时候属性的存在应该跟随状态。

运动和放置

使用 appendTo, prependTo, insertTo 当现有组件应该相对于另一个组件、节点或标记移动时。使用 appendToWhen, prependToWhen, insertToWhen, appendWhen, prependWhen, insertWhen 用于条件放置。使用 place 当目标位置本身是被动的。

每个组件或原始节点保持一个活跃的kitsui放置流。如果通过另一个kitsui API再次放置它,则新调用将获得控制权,而旧的条件流或选择流将停止移动它。从共享选择中替换一个组件不会阻止它的兄弟组件。

条件放置使隐藏节点在dom外保持活动状态,并在它们的条件为真时恢复它们。除去它们的终身主人,它们就被处理掉了。

const root = Component("section")
const open = State(root, false)
const details = Component("p")
    .text.set("More detail")
    .appendToWhen(open, root)
const root = Component("section")
const left = Component("div").appendTo(root)
const right = Component("div").appendTo(root)
const side = State<"left" | "right">(root, "left")

const badge = Component("span")
    .text.set("moving")

badge.place(root, (Place) => {
    const leftPlace = Place().appendTo(left)
    const rightPlace = Place().appendTo(right)

    return side.map(value => value === "left" ? leftPlace : rightPlace)
})

使用 append, prepend, insert 当主人应该选择哪些孩子在场。国家支持的儿童选择被固定在适当的位置;当状态发生变化时,kitsui替换和处置旧的选择。

const root = Component("section")
const view = State<"summary" | "details">(root, "summary")

Component("button")
    .attribute.set("type", "button")
    .text.set("Summary")
    .event.owned.on.click(() => view.set("summary"))
    .appendTo(root)

Component("button")
    .attribute.set("type", "button")
    .text.set("Details")
    .event.owned.on.click(() => view.set("details"))
    .appendTo(root)

root.append(view.map(root, value => {
    if (value === "summary") {
        return Component("p").text.set("Short summary")
    }

    return Component("p").text.set("Full details")
}))

样式

使用 Style(...) 对于可重用的样式片段和 Style.Class(...) 浏览已注册的CSS类。使用 Style.after(...) 当可重用类之间的顺序很重要时。

const cardBase = Style({
    borderRadius: 8,
    padding: 12,
})

const cardStyle = Style.Class("card", {
    ...cardBase,
    $accent: "#0a7",
    border: "1px solid $accent",
})

const raisedStyle = Style.after(cardStyle).Class("card-raised", {
    ...cardBase,
    boxShadow: "0 2px 12px rgba(0, 0, 0, 0.15)",
})

const card = Component("button")
const raised = State(card, false)

card
    .attribute.set("type", "button")
    .class.add(cardStyle)
    .class.bind(raised, raisedStyle)
    .style.set({
        $panelAccent: "#0a7",
        borderColor: "$panelAccent",
    })
    .text.set("Hover for elevation")
    .event.owned.on.pointerenter(() => raised.set(true))
    .event.owned.on.pointerleave(() => raised.set(false))

字符串样式的值支持计算速记。Kitsui编译 [expression] 来 calc(expression) 并在表达式内展开变量简写。当CSS需要文本括号时,例如命名的网格线,将方括号翻倍。

const layoutStyle = Style.Class("layout", {
    $gap: "1rem",
    gridTemplateColumns: "[[content-start]] [100% - ${sidebarWidth: 18rem} - $gap] [[content-end]]",
    width: "[100% - $gap]",
})

它发出文字 [content-start] 和 [content-end] 网格线名称。括号内的表达式变成 calc(...) 值, $gap 加上 ${sidebarWidth: 18rem} 变成CSS变量引用。

分解

使用 component.breakdown(...) 当State驱动一个键列表或区域时,每个组件都应该被键重用。

interface Todo {
    id: string
    label: string
}

const list = Component("ul")
const todos = State(list, [
    { id: "first", label: "Write the README" },
])

list.breakdown(todos, (list, Part, todos) => {
    for (const todo of todos) {
        Part(todo.id, () => Component("li"))
            .text.set(todo.label)
            .appendTo(list)
    }
})

为每个逻辑项使用一个稳定的键。 Part 返回相同的组件,而该键仍然存在。它的构建器在该键需要一个新组件时运行,并且必须返回一个尚未放置的新的、活动的、无主的组件。

更新和放置每一个返回的零件在每一个过程中。 Part 永远不会为你放置组件,放置调用的顺序建立了呈现的顺序。如果一个成功的传球漏掉了一个关键,kitsui就会处理掉那个部分。

保持回调同步:不要使用异步回调, await,或延迟 Part 还有安置电话。以后的状态更新是批处理的,可以跳过中间值,因此每次都从提供给回调的值传递。

在回调中使用kitsui放置api。如果您需要检查物理DOM,请在回调返回后执行。即使你把每个部件放在别的地方,故障仍然占有它。

功能指数

版本与发布记录

@inthedark/kitsui 共发布 4 个版本,首次发布于 2026/11/01;过去 12 个月发布了 4 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.2latest2026/11/012 天前
0.1.12026/11/012 天前
0.1.02026/11/012 天前
0.0.0-stage2026/11/012 天前

关于 @inthedark/kitsui 的常见问题

@inthedark/kitsui 是什么?

UI框架模板围绕可变组件包装器和显式状态转换构建。

如何安装 @inthedark/kitsui?

在项目目录运行 npm install @inthedark/kitsui,或使用 pnpm add / yarn add / bun add。

@inthedark/kitsui 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@inthedark/kitsui 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@inthedark/kitsui 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@inthedark/kitsui 可以商用吗?

@inthedark/kitsui 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@inthedark/kitsui 还在维护吗?

最新版本 0.1.2 发布于 2026/11/01(2 天前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/11/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。