marko-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

marko-ui

marko-ui

marko-ui is shadcn/ui for Marko 6: accessible components whose behavior comes from Zag.js state machines and whose styling is Tailwind v4. Use the
marko-ui
CLI for everything below — do not hand-write a component that the registry already ships, and do not guess a component's props.
Run the CLI with the project's package runner (
bunx marko-ui
,
npx marko-ui
,
pnpm dlx marko-ui
). Every command is non-interactive when stdin is not a TTY: confirmations are taken as yes,
init
uses its defaults, and
add
with no component names exits 2 instead of opening a picker.
marko-ui 是面向 Marko 6 的 shadcn/ui:可访问组件,其行为由 Zag.js 状态机驱动,样式基于 Tailwind v4。对于以下所有操作,请使用
marko-ui
CLI——不要手写 registry 已经提供的组件,也不要猜测组件的 props。
使用项目的 package runner 运行 CLI(
bunx marko-ui
、
npx marko-ui
、
pnpm dlx marko-ui
)。当 stdin 不是 TTY 时,所有命令都会以非交互方式运行:确认操作一律视为“是”,
init
使用默认配置,
add
未指定组件名称时会以退出码 2 退出,而不是打开选择器。

First: which distribution is this project on?

首先:该项目使用的是哪种分发方式?

Read
distribution
in
components.json
(absent means
copy
). It changes how components are obtained and customized.
copy
(default)
import
Components live inthe project, under the
ui
alias (
src/components/ui/<name>/
)
node_modules/@marko-ui/shadcn
Get a component
marko-ui add <name> -y
nothing to add — import
@marko-ui/shadcn/ui/<name>/<name>.marko
Customizeedit the component file (it is the project's code)override
mu-*
hook classes from the project stylesheet
No
components.json
means marko-ui is not set up: run
marko-ui init
(add
--agents
to also write the agent docs).
读取
components.json
中的
distribution
(不存在则默认为
copy
)。它决定了组件的获取和自定义方式。
copy
(默认)
import
组件存放位置项目中
ui
alias 下(
src/components/ui/<name>/
)
node_modules/@marko-ui/shadcn
获取组件
marko-ui add <name> -y
无需添加——import
@marko-ui/shadcn/ui/<name>/<name>.marko
自定义编辑组件文件(它属于项目代码)从项目 stylesheet 中覆盖
mu-*
hook classes
如果不存在
components.json
,说明 marko-ui 尚未配置:运行
marko-ui init
(添加
--agents
还会写入 Agent 文档)。

Workflow

工作流

  1. marko-ui search -q <query>
    — find a component (
    --json
    for machine output). The installed ones are listed in the project's
    AGENTS.md
    .
  2. marko-ui docs <name>
    — usage, props, keyboard contract, and examples as markdown. Read this before writing markup for a component you have not used in this session.
    marko-ui docs --list
    prints the index.
  3. marko-ui show <name>
    — the registry item as JSON;
    --files
    lists what would be written,
    --deps
    lists npm and registry dependencies.
  4. marko-ui add <name> -y
    — install (copy distribution).
    --dry-run
    previews the file changes;
    --overwrite
    replaces local files.
  5. marko-ui diff <name>
    — compare local edits against the registry version.
  6. marko-ui doctor --json
    — health checks; exit code 3 means something is broken and each failed check names its fix. Run it after
    init
    /
    add
    and before reporting the work done.
  7. marko-ui agents sync
    — refresh the component list in
    AGENTS.md
    after adding or removing components.
marko-ui manifest
prints every command, flag, exit code, and error code as JSON when you need the exact surface.
  1. marko-ui search -q <query>
    ——查找组件(
    --json
    用于机器可读输出)。已安装的组件列在项目的
    AGENTS.md
    中。
  2. marko-ui docs <name>
    ——以 markdown 形式提供用法、props、键盘交互约定和示例。在本次会话中尚未使用过的组件编写 markup 前,请务必先阅读此文档。
    marko-ui docs --list
    可打印索引。
  3. marko-ui show <name>
    ——以 JSON 格式显示 registry 条目;
    --files
    列出将要写入的文件,
    --deps
    列出 npm 和 registry 依赖项。
  4. marko-ui add <name> -y
    ——安装组件(copy 分发方式)。
    --dry-run
    可预览文件变更;
    --overwrite
    可替换本地文件。
  5. marko-ui diff <name>
    ——将本地修改与 registry 版本进行比较。
  6. marko-ui doctor --json
    ——执行健康检查;退出码 3 表示存在损坏项,每项失败的检查都会指出对应的修复方法。应在
    init
    /
    add
    后以及汇报工作完成前运行。
  7. marko-ui agents sync
    ——添加或移除组件后,刷新
    AGENTS.md
    中的组件列表。
需要准确了解完整的命令接口时,可运行
marko-ui manifest
,它会以 JSON 格式输出每个命令、flag、退出码和错误码。

Using components

使用组件

marko
import Dialog from "../components/ui/dialog/dialog.marko";
import Button from "../components/ui/button/button.marko";

<let/open=false/>

<Dialog open:=open>
  <@trigger|props|>
    <Button ...props>Edit profile</Button>
  </@trigger>
  <@title>Edit profile</@title>
  <@description>Make changes to your profile here.</@description>
  <p>Form fields go here.</p>
</Dialog>
  • Parts are attribute tags (
    <@trigger>
    ,
    <@title>
    ), not separately imported subcomponents.
    marko-ui docs <name>
    lists each component's parts.
  • <@trigger|props|>
    hands you the machine props
    to spread onto whatever element you render. It is the equivalent of Radix's
    asChild
    .
  • State binds with
    :=
    (
    open:=open
    ,
    value:=value
    ,
    checked:=checked
    ), or pass the value plus its
    xChange
    handler (
    open=open openChange=(v) => { open = v }
    ).
  • On the copy distribution
    add
    also generates a taglib
    (
    marko.json
    ), unless the project already has its own
    marko.json
    , so installed components are usable as tags without an import (
    <Button>
    ,
    <CardHeader>
    ). An explicit import always works too.
  • Never import a component as
    Input
    .
    It shadows Marko's reserved props type. The input component is imported as
    TextInput
    .
marko
import Dialog from "../components/ui/dialog/dialog.marko";
import Button from "../components/ui/button/button.marko";

<let/open=false/>

<Dialog open:=open>
  <@trigger|props|>
    <Button ...props>Edit profile</Button>
  </@trigger>
  <@title>Edit profile</@title>
  <@description>Make changes to your profile here.</@description>
  <p>Form fields go here.</p>
</Dialog>
  • 组成部分是 attribute tags(
    <@trigger>
    、
    <@title>
    ),而不是单独导入的子组件。
    marko-ui docs <name>
    会列出每个组件的组成部分。
  • <@trigger|props|>
    会将 machine props 交给你,以便 spread 到所渲染的任何元素上。它相当于 Radix 的
    asChild
    。
  • 状态使用
    :=
    绑定(
    open:=open
    、
    value:=value
    、
    checked:=checked
    ),或者传入该值及其
    xChange
    handler(
    open=open openChange=(v) => { open = v }
    )。
  • 在 copy 分发方式下,
    add
    还会生成一个 taglib(
    marko.json
    ),除非项目已有自己的
    marko.json
    ;这样已安装的组件无需 import 即可作为 tag 使用(
    <Button>
    、
    <CardHeader>
    )。显式 import 也始终有效。
  • 绝不将组件导入为
    Input
    。
    这会遮蔽 Marko 的保留 props 类型。input 组件应导入为
    TextInput
    。

Pitfalls that break components silently

导致组件静默失效的陷阱

Each of these has caused a real bug. Check your code against the list before finishing.
  1. Controlled props pin the machine.
    open=
    /
    value=
    /
    checked=
    WITHOUT the matching
    openChange
    /
    valueChange
    /
    checkedChange
    (or the
    :=
    bind) freezes the component in that state. For an initial value only, use
    defaultOpen=
    /
    defaultValue=
    /
    defaultChecked=
    .
  2. Boolean
    aria-*
    /
    data-*
    attributes serialize wrong.
    Marko renders boolean
    true
    as a bare attribute:
    aria-selected=(bool)
    emits
    aria-selected=""
    and
    data-active=(bool)
    never matches a
    data-[active=true]:
    Tailwind variant. Wrap in
    String(...)
    .
  3. Nothing from Zag crosses a tag boundary or lives in state. Machine services and
    api
    objects contain functions and cannot be serialized for resume. Pass plain data, or a closure written in the template (
    parentApi=() => api()
    ), and call
    api()
    at the use site.
  4. Do not read a parent component's
    api()
    inside a child's attribute-tag body.
    Route it through the child's input as a closure instead. The symptom is
    TypeError: ... is not a function
    after an overlay opens.
  5. Overrides that do nothing (import distribution). The style layer must be imported with
    layer(components)
    and the matching
    @custom-variant style-<name>
    line must exist in the project's stylesheet. Both fail silently.
  6. Unstyled components (copy distribution). The theme stylesheet is not loading. Check that the file recorded as
    tailwind.css
    in
    components.json
    is imported by the root layout and that
    @tailwindcss/vite
    is in the Vite config.
    marko-ui init
    wires both; a hand-rolled setup often misses one.
  7. "Cannot find module @zag-js/..."
    or
    marko-zag
    .
    A component's npm dependencies are missing.
    marko-ui add
    installs them;
    marko-ui show <name> --deps
    lists them.
For Marko 6 language rules (
<let>
,
<const>
,
<for>
, event handlers, TypeScript in templates) use the
marko6
skill; for
@marko/run
routing use
marko-run
.
以下每种情况都曾引发过真实的 bug。完成前请对照清单检查代码。
  1. 受控 props 会锁定 machine。 传入
    open=
    /
    value=
    /
    checked=
    ,但未提供对应的
    openChange
    /
    valueChange
    /
    checkedChange
    (也未使用
    :=
    绑定),会将组件冻结在该状态。如果只需设置初始值,请使用
    defaultOpen=
    /
    defaultValue=
    /
    defaultChecked=
    。
  2. 布尔
    aria-*
    /
    data-*
    attributes 的序列化结果错误。
    Marko 会将布尔值
    true
    渲染为无值 attribute:
    aria-selected=(bool)
    会输出
    aria-selected=""
    ,而
    data-active=(bool)
    永远无法匹配
    data-[active=true]:
    Tailwind variant。请使用
    String(...)
    包装。
  3. Zag 中的任何内容都无法跨越 tag 边界或存在于 state 中。 machine services 和
    api
    对象包含函数,无法序列化后用于 resume。请传入纯数据,或传入在 template 中编写的 closure(
    parentApi=() => api()
    ),并在使用位置调用
    api()
    。
  4. 不要在子组件的 attribute-tag body 中读取父组件的
    api()
    。
    应改为通过子组件的 input 以 closure 形式传入。否则,overlay 打开后会出现
    TypeError: ... is not a function
    。
  5. 覆盖样式不起作用(import 分发方式)。 样式层必须使用
    layer(components)
    导入,并且项目 stylesheet 中必须存在匹配的
    @custom-variant style-<name>
    行。两者缺失时都会静默失败。
  6. 组件没有样式(copy 分发方式)。 theme stylesheet 未加载。检查
    components.json
    中记录为
    tailwind.css
    的文件是否已由 root layout 导入,以及 Vite config 中是否包含
    @tailwindcss/vite
    。
    marko-ui init
    会配置好这两项;手动搭建时经常会漏掉其中一项。
  7. 出现
    Cannot find module @zag-js/...
    或
    marko-zag
    。
    组件缺少 npm dependencies。
    marko-ui add
    会安装这些依赖;
    marko-ui show <name> --deps
    会列出它们。
对于 Marko 6 语言规则(
<let>
、
<const>
、
<for>
、event handlers、模板中的 TypeScript),请使用
marko6
skill;对于
@marko/run
路由,请使用
marko-run
。

Editing a component (copy distribution)

编辑组件(copy 分发方式)

The file is project code. Each
.marko
file starts with a
static const styles = { ... }
block holding the Tailwind classes per part: change classes there. Interactive components wire their machine with one tag,
<zag/api=() => machine from=input/>
, which picks the machine's props out of
input
and returns the
api
getter; keyboard handling, focus management, and ARIA come from the machine, so restyling cannot break accessibility. After editing,
marko-ui diff <name>
shows what diverged from the registry.
该文件属于项目代码。每个
.marko
文件都以
static const styles = { ... }
代码块开头,其中按组成部分保存 Tailwind classes:请在该处修改 classes。交互式组件通过单个 tag
<zag/api=() => machine from=input/>
绑定 machine,它会从
input
中提取 machine 的 props,并返回
api
getter;键盘处理、焦点管理和 ARIA 均来自 machine,因此调整样式不会破坏可访问性。编辑后,
marko-ui diff <name>
会显示与 registry 版本之间的差异。

More

更多信息