组件

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' }, '查看文档'),
      ),
    ),
  ),
)

组件名有意与它们渲染出的标签保持一致,因此其中几个——buttoninputtablelinkcodeselectprogress——会和 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 会按名字取走;其余的一律作为属性落到渲染出的元素上,所以 iddata-*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-themedata-su-theme 设为 lightdark 就能覆盖它——themeToggle() 做的正是这件事:

import { styles, themeScript, themeToggle } from 'sitelo/ui'

head(
  themeScript(), // 在首次绘制前应用已保存的选择
  styles(),
)

// ……body 中的任意位置
themeToggle()

JavaScript,以及它有多少

大多数组件根本不需要。模态框和抽屉是 popover 元素,打开、遮罩、点击外部和 Escape 都由浏览器负责。折叠面板是 <details name>。菜单是 <details>。提示气泡是纯 CSS。

确实需要脚本的有四处,而且它们各自去取:

入口文件里什么都不用加——导入语句就写在事件属性里:

<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
        onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
  &times;
</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'") },
] })

表格接受 columnsrows,当某个单元格需要的不只是一个值时,用 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 里同样会补全 variantcolorsize

分组组件
布局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 是保留字,不能作为导入绑定;带样式的链接同时导出为 linktextLink,好与 javascript-to-html 的 link 并存。tableinputselectprogress 也有同样的备用名:dataTabletextInputselectFieldprogressBar