milda 是做什么的?
Milda 是一个带图形用户界面的设计系统编译器。一个 .milda 文件就是这个设计系统:包含基础(令牌)、组件、使用规则、文档和资源。React 包、Figma 库、文档站点以及 AI 代理可读的文件都由它生成。你永远不需要编辑输出;你只需修改文件并重新生成。
本技能针对0.10版本的语言表层及0.0.2版本的预置库。你编写的每个文件都以该文件头开头。
循环:编写、检查、修复
在验证器同意之前,不要告诉用户 .milda 文件是有效的。每次编辑后,运行:
npx -y -p @mildastudio/milda-lsp milda-check --strict path/to/system/
将目录或所有文件一起传递给命令,因为一个文件中声明的令牌可能会解析另一个文件中的引用。它会输出 path:line:col severity [code] message,与编辑器扩展显示的诊断信息相同,并且在遇到任何错误时会以非零退出(使用 --strict 时,遇到任何警告也会)。修复报告的问题并再次运行,直到它报告 0 errors, 0 warnings。警告不是噪音:unknown facet "background" skipped 意味着那一行什么也没做。
要向用户展示结果,位于 https://play.milda.dev 的 Playground 可以实时渲染粘贴的文件,无需账户。
用 Milda 思考:十条规则
大多数错误源于用 CSS、DOM 树或 React 组件的思维来写 Milda 语法。这些规则解释了该语法存在的目的。
- 系统是源头;代码是输出。 所有与平台相关的东西都在生成时决定,因此文件中永远不会出现 CSS 属性、标签、框架的事件名称或伪类。
- 描述事物是什么,而不是由什么构成。 部件具有中性的 种类(
container、text、input、icon、control……),绝不用div或button。每个目标平台会选择自己的元素。 - 值有名称,名称有层级。 组件永远不会直接使用字面量。一个方面指向一个令牌;
Semantic层是Base层(字面量层)的别名。重新样式就是改变基础层。 - 语境是一个维度,而不是一份副本。 暗色模式、密度、品牌:每个都是一个
context-group,一个令牌通过alias by Group { ... }按语境给出答案。由语境组类型化的属性(variant)在每个实例上使用相同的机制。 - 状态来自行为。
hovered、pressed、focus-visible、disabled、open、selected由原型的生命周期行为发出。用state hovered { }来样式化它们,绝不要使用任何与指针或平台相关的东西。该列表是封闭的(见reference/vocabulary.md),验证器会警告任何其他词语:比如state hover会被报告为拼写错误。 - 合约即身份。
props和events是消费者编写代码的依据,也是版本化差异的依据。为它们赋予有意义的名称(如variant、checkedChange),而不是平台相关的名称。 - 从已有的东西构建。 从预置库中的
archetype开始创建组件:它会带来合约形状、必需的部件和行为(键盘、ARIA、焦点)。通过instance部件将较小的组件组合成更大的组件。 - 每个引用都通过名称。 部件使用点分隔路径(
Header.Title),令牌使用路径(space.sm表示space/sm),组件使用标识符。没有 ID。注释(#)是唯一无法在轮换中保留的东西。 - 通过接缝扩展,而不是修补输出。 当词汇表无法表达某些内容时,使用
slot、按目标选项,或者作为最后手段,使用标记了目标的foreign内容。永远不要建议编辑生成的代码。 - 说明它的预期用途。 在
props中放置接口约束(required、required when、one-of),在顶层放置组合规则(rule ... { never X within Y }),每条规则都要说明原因。
一个文件,从上到下
milda 0.10
prelude 0.0.2
meta { name "Acme" }
context-group ColorScheme { Light Dark }
foundations {
layer Base {
token violet/600 : color = hex(7c3aed)
token white : color = hex(ffffff)
token gray/900 : color = hex(0f172a)
}
layer Semantic {
token accent : color = alias by ColorScheme { default -> violet.600 }
token on-accent : color = alias by ColorScheme { default -> white; Dark -> gray.900 }
token space/sm : spacing = 8px
token radius/sm : radius = 6px
}
}
component Button "主操作按钮。" {
archetype Button
props {
label : string = "Get started"
}
events {
click
}
part Button: container {
emits { click on activate }
layout { stack row; gap space.sm; align center }
fill accent
ink on-accent
corner radius.sm
inset space.sm
state focus-visible {
ring accent
}
}
part label: text {
content bind label
}
}
顺序:文件头、meta、context-group、group(库分区)、foundations、type、component、rule,然后是 example、assets、icons、docs。根部件以组件命名;其他部件根据原型的解剖结构(label、icon、control……)命名,或者由你使用点分隔路径嵌套命名。
从 CSS 和 React 到 Milda
| 你通常写的方式 | Milda 的说法 |
|---|---|
<div>, <span>, <button> | part X: container / text / 原型的根部件 |
background | fill |
color | ink |
padding, padding-inline-start | inset, inset.inline-start |
margin | 无:使用父级的 gap、对齐或 bleed |
border, border-radius | border <color> <width>, corner |
box-shadow, outline (焦点) | elevation, ring |
font-size + line-height, font-weight | text <typography or font-size token>, weight |
display: flex; flex-direction: row | layout { stack row } |
justify-content, align-items, flex: 1 | distribute, align, size fill |
position: absolute | layout { stack layers } + 在子级上使用 position overlay |
:hover, :focus-visible, :disabled | state hovered { }, state focus-visible { }, ... |
.btn--secondary, [data-variant] | when variant = Secondary { } |
.card:hover .icon | 在图标部件上使用 when ancestor Card hovered { } |
@media (prefers-color-scheme: dark) | context-group ColorScheme + alias by ColorScheme |
--space-sm: 8px / var(--space-sm) | token space/sm : spacing = 8px / space.sm |
#7c3aed, oklch(...) | hex(7c3aed), oklch(...),仅用在令牌内部 |
color-mix(), calc() | compute <token> in oklch { darken 10% } |
onClick prop | events { click } + emits { click on activate } |
{children} | 在容纳它们的部件上使用 slot of content default |
{cond && <X/>} | 在部件上使用 present when <prop> |
z-index, text-align, box-sizing | 无:部件顺序、容器,只有一个盒子 |
部件上的方面、状态和条件
part Panel: container {
layout { stack column; gap space.sm; size fill fit }
fill surface
ink text
border line hairline
corner radius.sm
inset space.sm space.md
when tone = danger {
border danger hairline
}
state hovered {
fill surface-raised
}
state hovered when tone = danger {
fill danger-subtle
}
}
一个方面(facet)的值首先解析为一个令牌;只有在没有令牌声明该词时,才接受裸字面量(例如 opacity 50、inset 0)。优先使用令牌。when 作用于属性,state 作用于行为状态,两者可以组合。
属性、事件和约束
archetype TextField
props {
label : string required "字段要求输入的内容。同时也是它的可访问名称。"
value : string
invalid : boolean = false
error : string required when invalid "控件无效时,在控件下方显示的错误信息。"
size : enum(sm, md) = md
tone : context Tone = Neutral
}
events {
valueChange
}
类型:string、number、boolean、enum(a, b)、context <Group>、array<T>、你自定义的 type。跟在后面的字符串是该属性的文档。one-of a, b / any-of / exclusive 用于约束一组属性。
下一步查阅
- reference/syntax.md:每个语句都附有经过验证的示例(令牌和值、布局、插槽、实例、行为、规则、文档、资源、图标)。
- reference/vocabulary.md:从语言包生成。验证器接受的所有令牌类型、部件种类、方面、状态、布局词和字面量函数。对某个词有疑问时,请查阅这里,而不是猜测。
- reference/archetypes.md:从预置库生成。每个原型及其合约、解剖结构(哪些部件是必需的)和行为。
- examples/acme.milda:一个完整、干净的系统,可供复制模式。
- 规范标准:https://github.com/mildastudio/milda-lang/tree/main/spec(`FORMAT.md` 描述文件承诺什么,
GRAMMAR.ebnf是语法,PRELUDE.md是原型)。 - 对于已发布的设计系统,Milda MCP 服务器会用其确切的组件和令牌进行回答;参见 https://milda.dev/mcp。
完成前检查清单
- 文件头声明了
milda 0.10和prelude 0.0.2。 - 组件中没有字面量的颜色、长度或字体;每个值都是令牌引用。
- 任何地方都没有平台相关的词:没有标签、CSS 属性、伪类或框架事件名称。
- 每个组件都从原型开始,除非它纯粹是由实例组合而成。
- 对整个系统运行
milda-check --strict输出0 errors, 0 warnings。