组件
本页内容
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() 返回一个 <style> 元素,里面是压缩后的整份样式表。传输大小约 7 kB,而且不可能从 dist/ 里丢失——这正是它作为默认方式的原因。如果你更希望只链接一次、让浏览器跨页面缓存它,那就从被打包的入口文件里导入这份 CSS,Vite 会把它输出出来:
// src/main.js —— 由 Vite 打包,跨页面缓存
import 'sitelo/ui/styles.css'两种方式选一种,不要同时用。
主题
一切都由 :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()JavaScript,以及它有多少
大多数组件根本不需要。模态框和抽屉是 popover 元素,打开、遮罩、点击外部和 Escape 都由浏览器负责。折叠面板是 <details name>。菜单是 <details>。提示气泡是纯 CSS。
确实需要脚本的有四处,而且它们各自去取:
- 面板就地切换的标签页
- 可关闭提示条上的关闭按钮
- 点击外部或按 Escape 关闭菜单
- 主题切换按钮
入口文件里什么都不用加——导入语句就写在事件属性里:
<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
×
</button>开发时 sitelo 从 /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' }, '保存')),
)模态框就是一个 popover,任何指向它 id 的按钮都是它的触发器:
import { button, modal } from 'sitelo/ui'
button({ popovertarget: 'confirm' }, '删除…')
modal({
id: 'confirm',
title: '删除这个页面?',
footer: button({ color: 'danger' }, '删除'),
}, '此操作无法撤销。')标签页有两种形态——链接式,或者面板式:
// 链接式标签页:一个标签一个页面,完全不需要脚本。
tabs({ items: [
{ label: '文档', href: '/docs', active: true },
{ label: 'API', href: '/api' },
] })
// 面板式标签页:就地切换,所需代码由它们自己加载。
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, spinner, 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, theme, themeScript |
有两个名字和预期不同:开关叫 toggle,因为 switch 是保留字,不能作为导入绑定;带样式的链接同时导出为 link 和 textLink,好与 javascript-to-html 的 link 并存。table、input、select 和 progress 也有同样的备用名:dataTable、textInput、selectField、progressBar。