组件
本页内容
sitelo-ui 是 sitelo 的组件库。每个组件都是一个返回 HTML 字符串的函数,因此可以直接嵌进 javascript-to-html 的结构里,中间没有任何东西——没有编译器,没有运行时,没有 hydration。你写的就是最终进入 dist/ 的东西。
它随 sitelo 一起发布,入口是 sitelo/ui。
快速开始
npm install sitelo javascript-to-html把 styles() 放进 head,在 body 里调用组件。配置到此为止:
import { body, head, html, meta, title } from 'javascript-to-html'
import { styles, container, stack, heading, text, button } from 'sitelo/ui'
export default () => html({ lang: 'zh' },
head(
meta({ charset: 'utf-8' }),
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
title('我的网站'),
styles(),
),
body(
container({ size: 'md' },
stack({ gap: 'md' },
heading({ level: 1 }, '你好'),
text({ variant: 'lead' }, '一个用组件搭建的页面。'),
button({ href: '/docs' }, '查看文档'),
),
),
),
)组件名有意与它们渲染出的标签保持一致,因此其中几个——button、input、table、link、code、select、progress——会和 javascript-to-html 的元素函数重名。两边都要用时,把这个库作为命名空间导入:
import * as ui from 'sitelo/ui'
ui.card(
ui.cardHeader({ title: '路由', subtitle: '基于文件' }),
ui.cardBody(ui.text('src/about.ht.js 对应 /about。')),
ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, '了解更多')),
)调用约定
每个组件接受一个可选的 props 对象,后面跟子元素,和 javascript-to-html 的元素完全一样。组件认识的 props 会按名字取走;其余的一律作为属性落到渲染出的元素上,所以 id、data-*、aria-* 和事件属性都能用,库不必逐个列举:
button({ id: 'save', 'data-analytics': 'save-click', onclick: 'save()' }, '保存')
// <button type="button" id="save" data-analytics="save-click" onclick="save()" class="su-btn …">如果某个 props 的值组件不认识——比如 variant: 'nonsense'——它会回退到默认值而不是抛错。一个无关紧要的拼写错误不该让构建失败。
样式
styles() 返回一个指向单个文件的 <link>,浏览器会跨页面缓存它;sitelo 的插件在 dev 里提供它,构建时把它写进产物,文件名带内容哈希,可以按 immutable 提供。传 { inline: true } 则改为返回一个装着整份样式表的 <style> 元素:传输大小约 11 kB,不用多一次请求,也不会有东西从 dist/ 里丢失:
import { styles } from 'sitelo/ui'
head(
// 整份样式表就在页面里——一次请求都不用
styles({ inline: true }),
)这份样式表覆盖了库里的每一个组件,而一个站点只用其中几个。在 sitelo.config.js 里设置 pruneCss: true,就只写出构建能匹配到的规则:插件从刚写出的页面里读取 su- 类名——也从旁边的脚本里读取,以覆盖 toast 或步骤之后才加上的类名——然后丢掉选择器里提到了任何地方都没有的类名的每一条规则。依据的是输出而不是导入,所以 button({ variant: 'soft' }) 会保留 .su-btn--soft,而普通的 button() 不会。以链接引入的样式表按整个站点裁剪并得到新的哈希,每个 <link> 都会随之改写;内联的则按各自页面裁剪。十来个组件大约是 2–3 kB gzip。开发服务器仍提供完整样式表;构建看不到的标记——比如服务端区块的——需要在 keep 里列出它的类名:
export default {
pruneCss: true,
// 或列出构建看不到的类名;`*` 表示前缀
// pruneCss: { keep: ['su-card', 'su-btn*'] },
}主题
一切都由 :root 上的 CSS 自定义属性驱动:五套配色、一套间距刻度、圆角、字体和阴影。theme() 为它们写覆盖值,接受 camelCase 名称(radiusMd → --su-radius-md)、配色对象,或者直接写自定义属性:
import { styles, theme } from 'sitelo/ui'
head(
styles(),
// 放在 styles() 之后,这些值才会生效。
theme({
primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
radiusMd: '2px',
fontSans: '"Inter", system-ui, sans-serif',
}, {
dark: { primary: { base: '#8f8ff0' } },
}),
)深色模式会自己根据 prefers-color-scheme 解析。在任意祖先元素上把 data-theme 或 data-su-theme 设为 light 或 dark 就能覆盖它——themeToggle() 做的正是这件事:
import { styles, themeScript, themeToggle } from 'sitelo/ui'
head(
themeScript(), // 在首次绘制前应用已保存的选择
styles(),
)
// ……body 中的任意位置
themeToggle()也可以一次改掉整体观感:styles({ preset: 'neumorphism' }) 会在核心样式表之后紧接着链接某个预设的样式表,每个组件都会跟着变,而 theme() 依然可以叠在上面用。它在新拟态页面上实时运行。
JavaScript,以及它有多少
大多数组件根本不需要。模态框和抽屉是 <dialog> 元素,由按钮通过 command 打开,遮罩、焦点、Escape 和点击外部都由浏览器负责。折叠面板是 <details name>。菜单是 <details>。提示气泡是纯 CSS。面板就地切换的标签页是一组单选按钮:每个标签是一个 <label>,紧跟在选中单选按钮之后的那个面板就是 CSS 显示的那一个。
确实需要脚本的有三处,而且它们各自去取:
- 可关闭提示条上的关闭按钮
- 点击外部或按 Escape 关闭菜单
- 主题切换按钮
入口文件里什么都不用加——导入语句就写在事件属性里:
<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
<svg class="su-icon" …></svg>
</button>开发时 sitelo 从 /su/(使用 --base /repo/ 时为 /repo/su/)提供这些模块,构建时只复制页面真正引用到的那几个。每个都远小于 1 kB,都要等到第一次交互才会下载;在那之前每个组件也都渲染正确:菜单自己开合,关闭按钮什么都不做。
例外是 toast(),因为页面上没有东西会替你触发它:
// src/main.js
import { toast } from 'sitelo/ui/client'它约 3 kB,监听在 document 上而不是逐个元素绑定;没有它时,它所涉及的每个组件依然渲染正确——面板式标签页显示服务端标记为激活的那一个,菜单照常打开,关闭按钮只是什么都不做。
示例
表单会自己接好标签、id、帮助文本和错误信息:
import { card, cardBody, cardFooter, button, stack, textField, selectField } from 'sitelo/ui'
card(
cardBody(
stack({ gap: 'md' },
textField({ label: '邮箱', name: 'email', type: 'email', help: '绝不外传。' }),
textField({ label: '网站', name: 'site', startAdornment: 'https://', error: '这不是一个 URL。' }),
selectField({ label: '套餐', name: 'plan', options: ['免费', 'Pro'], value: 'Pro' }),
),
),
cardFooter({ divided: true }, button({ type: 'submit' }, '保存')),
)模态框就是一个 <dialog>,任何 commandfor 指向它 id 的按钮都是它的触发器:
import { button, modal } from 'sitelo/ui'
button({ commandfor: 'confirm', command: 'show-modal' }, '删除…')
modal({
id: 'confirm',
title: '删除这个页面?',
footer: button({ color: 'danger' }, '删除'),
}, '此操作无法撤销。')标签页有两种形态——链接式,或者面板式:
// 链接式标签页:一个标签一个页面,完全不需要脚本。
tabs({ items: [
{ label: '文档', href: '/docs', active: true },
{ label: 'API', href: '/api' },
] })
// 面板式标签页:就地切换,靠一组单选按钮和 CSS。
tabs({ value: 'use', items: [
{ id: 'install', label: '安装', panel: code('npm install sitelo') },
{ id: 'use', label: '使用', panel: code("import * as ui from 'sitelo/ui'") },
] })表格接受 columns 和 rows,当某个单元格需要的不只是一个值时,用 render 函数:
table({
striped: true,
columns: [
{ key: 'page', header: '页面' },
{ key: 'size', header: '大小', align: 'end' },
{ header: '状态', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : '失败') },
],
rows: pages,
})仓库里的 examples/ui 目录把每个组件都渲染在同一个页面上——这是看全整套组件最快的方式。
组件参考
按分组列出的全部导出。props 都有类型:sitelo/ui 附带 .d.ts 文件,所以编辑器在 JavaScript 里同样会补全 variant、color 和 size。
| 分组 | 组件 |
|---|---|
| 布局 | container, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio |
| 排版 | text, heading, link, code, inlineCode, kbd, visuallyHidden, prose |
| 输入 | button, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup |
| 数据展示 | avatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure |
| 反馈 | alert, progress, skeleton, toasts, empty |
| 导航 | breadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle |
| 浮层 | modal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible |
| 页面区块 | hero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup |
| 样式 | styles, stylesheet, stylesUrl, theme, themeScript |
有两个名字和预期不同:开关叫 toggle,因为 switch 是保留字,不能作为导入绑定;带样式的链接同时导出为 link 和 textLink,好与 javascript-to-html 的 link 并存。table、input、select 和 progress 也有同样的备用名:dataTable、textInput、selectField、progressBar。