@endfield-ui/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +768 -2
- package/dist/index.d.ts +3243 -0
- package/dist/index.js +8182 -0
- package/dist/styles.css +2 -0
- package/package.json +83 -3
- package/src/components/README.md +172 -0
- package/src/components/accordion/Accordion.tsx +168 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/Alert.tsx +106 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/Avatar.tsx +89 -0
- package/src/components/avatar/AvatarSwitcher.tsx +261 -0
- package/src/components/avatar/index.ts +8 -0
- package/src/components/back-to-top/BackToTop.tsx +124 -0
- package/src/components/back-to-top/index.ts +5 -0
- package/src/components/badge/Badge.tsx +75 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bracket-title/BracketTitle.tsx +40 -0
- package/src/components/bracket-title/index.ts +1 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +161 -0
- package/src/components/breadcrumb/index.ts +6 -0
- package/src/components/button/Button.tsx +357 -0
- package/src/components/button/ButtonGroup.tsx +55 -0
- package/src/components/button/index.ts +13 -0
- package/src/components/calendar/Calendar.tsx +443 -0
- package/src/components/calendar/index.ts +7 -0
- package/src/components/carousel/Carousel.tsx +292 -0
- package/src/components/carousel/index.ts +7 -0
- package/src/components/checkbox/Checkbox.tsx +94 -0
- package/src/components/checkbox/choice-style.ts +30 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/chip/FilterChip.tsx +68 -0
- package/src/components/chip/ResourceChip.tsx +63 -0
- package/src/components/chip/capsule-style.ts +22 -0
- package/src/components/chip/index.ts +6 -0
- package/src/components/combobox/Combobox.tsx +450 -0
- package/src/components/combobox/index.ts +10 -0
- package/src/components/completion-banner/CompletionBanner.tsx +72 -0
- package/src/components/completion-banner/index.ts +4 -0
- package/src/components/context-menu/ContextMenu.tsx +70 -0
- package/src/components/context-menu/index.ts +5 -0
- package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
- package/src/components/corner-brackets/index.ts +5 -0
- package/src/components/countdown/Countdown.tsx +145 -0
- package/src/components/countdown/index.ts +6 -0
- package/src/components/dash-indicator/DashIndicator.tsx +44 -0
- package/src/components/dash-indicator/index.ts +1 -0
- package/src/components/data-row/DataRow.tsx +282 -0
- package/src/components/data-row/index.ts +8 -0
- package/src/components/date-picker/DateField.tsx +159 -0
- package/src/components/date-picker/DatePicker.tsx +182 -0
- package/src/components/date-picker/DateRangePicker.tsx +203 -0
- package/src/components/date-picker/index.ts +7 -0
- package/src/components/dialog/Dialog.tsx +203 -0
- package/src/components/dialog/index.ts +7 -0
- package/src/components/dialog/initial-focus.ts +19 -0
- package/src/components/dialog/overlay-style.ts +28 -0
- package/src/components/drawer/Drawer.tsx +213 -0
- package/src/components/drawer/index.ts +6 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
- package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
- package/src/components/dropdown-menu/index.ts +22 -0
- package/src/components/dropdown-menu/menu-style.ts +47 -0
- package/src/components/empty-state/EmptyState.tsx +79 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/field/Field.tsx +203 -0
- package/src/components/field/index.ts +6 -0
- package/src/components/file-upload/FileItem.tsx +201 -0
- package/src/components/file-upload/FileUpload.tsx +425 -0
- package/src/components/file-upload/index.ts +9 -0
- package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
- package/src/components/flyout-bar/index.ts +7 -0
- package/src/components/ghost-text/GhostText.tsx +47 -0
- package/src/components/ghost-text/index.ts +5 -0
- package/src/components/hatch/Hatch.tsx +35 -0
- package/src/components/hatch/index.ts +1 -0
- package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
- package/src/components/hazard-stripe/index.ts +5 -0
- package/src/components/icon-button/IconButton.tsx +95 -0
- package/src/components/icon-button/index.ts +6 -0
- package/src/components/input/Input.tsx +109 -0
- package/src/components/input/control-box.ts +71 -0
- package/src/components/input/index.ts +6 -0
- package/src/components/input/value-chip.ts +34 -0
- package/src/components/item-slot/ItemGrid.tsx +207 -0
- package/src/components/item-slot/ItemSlot.tsx +283 -0
- package/src/components/item-slot/index.ts +7 -0
- package/src/components/kbd/Kbd.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/link-element/LinkElement.tsx +25 -0
- package/src/components/list/List.tsx +229 -0
- package/src/components/list/band-style.ts +18 -0
- package/src/components/list/index.ts +8 -0
- package/src/components/loader/Loader.tsx +154 -0
- package/src/components/loader/index.ts +1 -0
- package/src/components/marquee/Marquee.tsx +96 -0
- package/src/components/marquee/index.ts +1 -0
- package/src/components/media-card/MediaCard.tsx +139 -0
- package/src/components/media-card/index.ts +5 -0
- package/src/components/nav-action/NavAction.tsx +142 -0
- package/src/components/nav-action/index.ts +5 -0
- package/src/components/nav-menu/NavMenu.tsx +288 -0
- package/src/components/nav-menu/index.ts +6 -0
- package/src/components/navigator/Navigator.tsx +136 -0
- package/src/components/navigator/index.ts +5 -0
- package/src/components/pagination/Pagination.tsx +181 -0
- package/src/components/pagination/index.ts +5 -0
- package/src/components/panel/Panel.tsx +114 -0
- package/src/components/panel/index.ts +13 -0
- package/src/components/play-button/PlayButton.tsx +92 -0
- package/src/components/play-button/index.ts +7 -0
- package/src/components/popover/Popover.tsx +105 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/progress/Progress.tsx +133 -0
- package/src/components/progress/ProgressRing.tsx +123 -0
- package/src/components/progress/index.ts +2 -0
- package/src/components/radio/Radio.tsx +182 -0
- package/src/components/radio/index.ts +7 -0
- package/src/components/rec-indicator/RecIndicator.tsx +47 -0
- package/src/components/rec-indicator/index.ts +1 -0
- package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
- package/src/components/registration-strip/index.ts +5 -0
- package/src/components/schedule/Schedule.tsx +457 -0
- package/src/components/schedule/index.ts +9 -0
- package/src/components/scroll-hint/ScrollHint.tsx +78 -0
- package/src/components/scroll-hint/index.ts +5 -0
- package/src/components/section-title/SectionTitle.tsx +202 -0
- package/src/components/section-title/index.ts +5 -0
- package/src/components/segmented-control/SegmentedControl.tsx +220 -0
- package/src/components/segmented-control/index.ts +8 -0
- package/src/components/select/Select.tsx +390 -0
- package/src/components/select/index.ts +15 -0
- package/src/components/side-rail/SideRail.tsx +317 -0
- package/src/components/side-rail/SideRailSub.tsx +298 -0
- package/src/components/side-rail/index.ts +14 -0
- package/src/components/skeleton/Skeleton.tsx +70 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/slider/Slider.tsx +219 -0
- package/src/components/slider/index.ts +8 -0
- package/src/components/sparkline/Sparkline.tsx +122 -0
- package/src/components/sparkline/index.ts +6 -0
- package/src/components/spinner/Spinner.tsx +64 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/stat/Stat.tsx +79 -0
- package/src/components/stat/index.ts +1 -0
- package/src/components/stepper/Stepper.tsx +316 -0
- package/src/components/stepper/index.ts +1 -0
- package/src/components/steps/Steps.tsx +302 -0
- package/src/components/steps/index.ts +10 -0
- package/src/components/switch/Switch.tsx +120 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/table/Table.tsx +559 -0
- package/src/components/table/index.ts +20 -0
- package/src/components/tabs/Tabs.tsx +313 -0
- package/src/components/tabs/index.ts +12 -0
- package/src/components/tag/Tag.tsx +66 -0
- package/src/components/tag/TagPair.tsx +34 -0
- package/src/components/tag/index.ts +2 -0
- package/src/components/tag-input/TagInput.tsx +501 -0
- package/src/components/tag-input/index.ts +8 -0
- package/src/components/term/Term.tsx +53 -0
- package/src/components/term/index.ts +1 -0
- package/src/components/textarea/Textarea.tsx +99 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/texture/Texture.tsx +45 -0
- package/src/components/texture/index.ts +1 -0
- package/src/components/tick-ring/TickRing.tsx +77 -0
- package/src/components/tick-ring/index.ts +1 -0
- package/src/components/timeline/Timeline.tsx +84 -0
- package/src/components/timeline/index.ts +7 -0
- package/src/components/toast/Toast.tsx +257 -0
- package/src/components/toast/index.ts +10 -0
- package/src/components/toc/Toc.tsx +262 -0
- package/src/components/toc/index.ts +7 -0
- package/src/components/toolbar/Toolbar.tsx +389 -0
- package/src/components/toolbar/index.ts +17 -0
- package/src/components/tooltip/Tooltip.tsx +175 -0
- package/src/components/tooltip/index.ts +8 -0
- package/src/components/top-bar/TopBar.tsx +47 -0
- package/src/components/top-bar/index.ts +1 -0
- package/src/components/viewfinder/Viewfinder.tsx +97 -0
- package/src/components/viewfinder/index.ts +6 -0
- package/src/hooks/README.md +14 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useControllableState.ts +30 -0
- package/src/hooks/useInView.ts +44 -0
- package/src/hooks/useOverflowing.ts +28 -0
- package/src/hooks/usePortalScope.ts +68 -0
- package/src/hooks/useScrollPosition.ts +57 -0
- package/src/hooks/useTheme.ts +102 -0
- package/src/icons/ArrowCorner.tsx +7 -0
- package/src/icons/ArrowLeft.tsx +6 -0
- package/src/icons/ArrowRight.tsx +6 -0
- package/src/icons/CalendarIcon.tsx +10 -0
- package/src/icons/Check.tsx +4 -0
- package/src/icons/ChevronDown.tsx +3 -0
- package/src/icons/ChevronLeft.tsx +6 -0
- package/src/icons/ChevronRight.tsx +6 -0
- package/src/icons/Close.tsx +3 -0
- package/src/icons/Crosshair.tsx +7 -0
- package/src/icons/Lock.tsx +6 -0
- package/src/icons/Menu.tsx +4 -0
- package/src/icons/Minus.tsx +4 -0
- package/src/icons/Plus.tsx +3 -0
- package/src/icons/README.md +31 -0
- package/src/icons/StatusDanger.tsx +7 -0
- package/src/icons/StatusInfo.tsx +7 -0
- package/src/icons/StatusSuccess.tsx +10 -0
- package/src/icons/StatusWarning.tsx +7 -0
- package/src/icons/TriangleRight.tsx +7 -0
- package/src/icons/Upload.tsx +7 -0
- package/src/icons/createIcon.tsx +34 -0
- package/src/icons/index.ts +21 -0
- package/src/index.ts +82 -0
- package/src/lib/README.md +17 -0
- package/src/lib/cn.ts +34 -0
- package/src/lib/date.ts +125 -0
- package/src/lib/decor.ts +6 -0
- package/src/lib/defined.ts +9 -0
- package/src/lib/file.ts +61 -0
- package/src/lib/focus-ring.ts +11 -0
- package/src/lib/merge-refs.ts +11 -0
- package/src/styles/README.md +70 -0
- package/src/styles/index.css +25 -0
- package/src/styles/theme.css +568 -0
- package/src/styles/utilities.css +241 -0
package/README.md
CHANGED
|
@@ -1,3 +1,769 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @endfield-ui/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
终末地风格的 React 基础控件库。非官方,详见 [NOTICE](https://github.com/icaruszezen/endfield-ui/blob/main/NOTICE.md)。
|
|
4
|
+
|
|
5
|
+
已实现的控件,全部适配亮 / 暗主题与主题色接管:
|
|
6
|
+
|
|
7
|
+
| 类别 | 控件 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 基础 | Button / ButtonGroup、IconButton、Toolbar、Tag / TagPair、Badge、Kbd、SectionTitle、BracketTitle、Tabs、Panel |
|
|
10
|
+
| 表单 | Field、Input、Textarea、Select、Combobox、TagInput、DatePicker / DateRangePicker / Calendar、FileUpload / FileItem、Checkbox、Radio / RadioGroup、SegmentedControl、Switch、Stepper、Slider、FilterChip |
|
|
11
|
+
| 反馈 | Alert、Toast、Progress / ProgressRing、Spinner、Skeleton、EmptyState、Loader、CompletionBanner、RecIndicator |
|
|
12
|
+
| 浮层 | Tooltip、Popover、Dialog、Drawer、DropdownMenu、ContextMenu、FlyoutBar |
|
|
13
|
+
| 展示 | Table(行能展开)、Stat、Sparkline、DataRowList / DataRow、List / ListRow、Accordion、MediaCard、PlayButton / PlayMark、Carousel、ItemSlot / ItemGrid、Avatar、Timeline、Schedule、Term、ResourceChip、Countdown、Marquee、ScrollHint |
|
|
14
|
+
| 导航 | SideRail(含二级)、TopBar、NavMenu、NavAction、Breadcrumb、Pagination、Navigator、AvatarSwitcher、DashIndicator、Steps、Toc、BackToTop |
|
|
15
|
+
| 母题 | CornerBrackets、Viewfinder、GhostText、Hatch、Texture、RegistrationStrip、TickRing、HazardStripe |
|
|
16
|
+
|
|
17
|
+
每个控件的变体与状态见[在线预览](https://icaruszezen.github.io/endfield-ui/)。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @endfield-ui/react
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
需要 React 19。走 `tailwind.css` 入口的项目还要有 Tailwind CSS v4,用预编译的 `styles.css` 则不需要。包只出 ESM。
|
|
26
|
+
|
|
27
|
+
## 技术栈
|
|
28
|
+
|
|
29
|
+
| 项 | 选择 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 框架 | React 19 + TypeScript |
|
|
32
|
+
| 样式 | Tailwind CSS v4(CSS-first,`@theme` 令牌,不使用 `tailwind.config.js`) |
|
|
33
|
+
| 变体 | 手写的 `Record<Variant, string>` + `cn()`(`clsx` + `tailwind-merge`) |
|
|
34
|
+
| 无障碍基元 | [Base UI](https://base-ui.com)(`@base-ui/react`),用在浮层和几个行为复杂的控件上:文字提示、气泡卡片、弹窗、抽屉、全屏菜单、轻提示、下拉选择、组合框、下拉菜单、右键菜单、展开条、折叠面板、滑块、头像。其余控件建立在原生元素上(月历是手写的:Base UI 还没有公开的日历基元) |
|
|
35
|
+
| 构建 | tsdown(ESM + 类型声明)+ `@tailwindcss/cli`(预编译 CSS) |
|
|
36
|
+
| 测试 | Vitest + Testing Library(jsdom) |
|
|
37
|
+
|
|
38
|
+
## 使用
|
|
39
|
+
|
|
40
|
+
### 样式
|
|
41
|
+
|
|
42
|
+
已经在用 Tailwind CSS v4 的项目,用本包的入口代替 `@import "tailwindcss"`。它依次引入 Tailwind、令牌、母题工具类,并让 Tailwind 扫到本包组件用到的类名:
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
@import "@endfield-ui/react/tailwind.css";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
不用 Tailwind 的项目,引入预编译好的样式:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import "@endfield-ui/react/styles.css";
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
只想要令牌:`@endfield-ui/react/theme.css`(用法见 [styles/README.md](src/styles/README.md))。
|
|
55
|
+
|
|
56
|
+
两个入口都会清掉 Tailwind 的默认色板、圆角、阴影与字阶,并给 `body` 设上主题的底色与字色。字体只声明字体栈、不负责加载,见 [字体规范](https://github.com/icaruszezen/endfield-ui/blob/main/docs/design/foundations/typography.md)。
|
|
57
|
+
|
|
58
|
+
### 组件
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Button, SectionTitle, Tab, TabList, TabPanel, Tabs } from "@endfield-ui/react";
|
|
62
|
+
|
|
63
|
+
export function Bulletin() {
|
|
64
|
+
return (
|
|
65
|
+
<section>
|
|
66
|
+
<SectionTitle latin="Bulletin">最新情报</SectionTitle>
|
|
67
|
+
<Tabs defaultValue="news">
|
|
68
|
+
<TabList aria-label="情报分类">
|
|
69
|
+
<Tab value="news">新闻</Tab>
|
|
70
|
+
<Tab value="notice">公告</Tab>
|
|
71
|
+
</TabList>
|
|
72
|
+
<TabPanel value="news">…</TabPanel>
|
|
73
|
+
<TabPanel value="notice">…</TabPanel>
|
|
74
|
+
</Tabs>
|
|
75
|
+
<Button variant="text" href="/news">查看全部</Button>
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 链接与路由
|
|
82
|
+
|
|
83
|
+
能当链接用的控件(按钮、列表行、媒体卡、物品格、面包屑项、菜单项、侧轨项…)传 `href` 渲染成 `<a>`。用路由库时,把它的链接组件传给 `render`:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Link } from "react-router";
|
|
87
|
+
|
|
88
|
+
<Button render={<Link to="/archive" />}>查看档案</Button>
|
|
89
|
+
<ListRow render={<Link to={`/records/${id}`} />} selected>
|
|
90
|
+
排水泵检修记录
|
|
91
|
+
</ListRow>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
控件算好的类名、状态属性(`aria-current`…)和事件会合并到那个元素上。禁用时它被标上 `aria-disabled` 并拦下点击;地址去不掉,所以它仍然能被聚焦——这一点和 `href` 形态不同。
|
|
95
|
+
|
|
96
|
+
### 表单
|
|
97
|
+
|
|
98
|
+
标签、帮助文字、错误说明由 `Field` 统一关联到里面的控件;一组复选或一个单选组用 `group`,渲染成 `<fieldset>` + `<legend>`。
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { Checkbox, Field, Input, Radio, RadioGroup, Switch } from "@endfield-ui/react";
|
|
102
|
+
|
|
103
|
+
<Field label="代号" required help="两到十二个字母。" error={error}>
|
|
104
|
+
<Input value={codename} onChange={(event) => setCodename(event.target.value)} />
|
|
105
|
+
</Field>
|
|
106
|
+
|
|
107
|
+
<Field group label="测绘精度">
|
|
108
|
+
<RadioGroup defaultValue="standard" onValueChange={setPrecision}>
|
|
109
|
+
<Radio value="draft">草图</Radio>
|
|
110
|
+
<Radio value="standard">标准</Radio>
|
|
111
|
+
</RadioGroup>
|
|
112
|
+
</Field>
|
|
113
|
+
|
|
114
|
+
<Checkbox defaultChecked>归档时保存原始读数</Checkbox>
|
|
115
|
+
<Switch onCheckedChange={setSync}>自动同步</Switch>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
表单控件都是原生 `<input>` / `<textarea>` 套样式:`name`、`value`、`required`、`ref` 这些属性直接落在原生元素上,可以照常放进 `<form>` 提交。`className` 给的是外层(输入框的外框、复选框的整行)。
|
|
119
|
+
|
|
120
|
+
几个短选项要一眼看全时用分段选择。它的语义也是单选组(方向键换值、带 `name` 随表单提交),只是并排摆在一条轨道里:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { Field, Segment, SegmentedControl } from "@endfield-ui/react";
|
|
124
|
+
|
|
125
|
+
<Field label="时间显示">
|
|
126
|
+
<SegmentedControl name="clock" value={clock} onValueChange={setClock}>
|
|
127
|
+
<Segment value="24">24 小时</Segment>
|
|
128
|
+
<Segment value="12">12 小时</Segment>
|
|
129
|
+
</SegmentedControl>
|
|
130
|
+
</Field>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- 高度和输入框同三档,并排时对得齐;放在凹陷底色的工具条里用 `variant="outline"`。
|
|
134
|
+
- 各段等宽;要撑满一栏加 `className="w-full"`。放不下时文字截断,所以每段两到四个字为好。
|
|
135
|
+
- 切换的是下面显示哪一块内容时用 `Tabs`,不是它。
|
|
136
|
+
|
|
137
|
+
滑块和日期选择同样放进 `Field`,带 `name` 时值随表单提交:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { DatePicker, DateRangePicker, Field, Slider } from "@endfield-ui/react";
|
|
141
|
+
|
|
142
|
+
<Field label="告警音量">
|
|
143
|
+
<Slider name="volume" value={volume} onValueChange={setVolume} step={5} showValue />
|
|
144
|
+
</Field>
|
|
145
|
+
|
|
146
|
+
<Field label="载重(吨)">
|
|
147
|
+
{/* value 传两个数就是范围滑块 */}
|
|
148
|
+
<Slider value={range} onValueChange={setRange} max={120} showValue />
|
|
149
|
+
</Field>
|
|
150
|
+
|
|
151
|
+
<Field label="发车日期">
|
|
152
|
+
<DatePicker name="depart" value={date} onValueChange={setDate} min="2026-10-01" />
|
|
153
|
+
</Field>
|
|
154
|
+
|
|
155
|
+
<Field label="检修窗口">
|
|
156
|
+
{/* 值是 [起, 止],两头都包含 */}
|
|
157
|
+
<DateRangePicker startName="from" endName="to" value={range} onValueChange={setRange} />
|
|
158
|
+
</Field>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- 日期一律是 `YYYY-MM-DD` 的字符串(和原生的 `<input type="date">` 一样),没选是 `null`。
|
|
162
|
+
- `DatePicker` 的触发器是按钮,不能打字;只要月历不要外框,用 `Calendar`。
|
|
163
|
+
- `DateRangePicker` 点两下选一段:第一下定起始日,第二下定结束日,选完面板才关。后点的那天更早会自动排成先后;起止是同一天也行。两端必须是可选的日子,不可选的日子可以被夹在中间。
|
|
164
|
+
- 只要能选一段的月历,用 `<Calendar range>`:值从一个日期变成一对。它只显示一个月,跨月的长区间翻一次月再点第二下。
|
|
165
|
+
- `Slider` 的 `onValueChange` 拖的每一步都触发,要发请求的事放在 `onValueCommitted` 里。
|
|
166
|
+
|
|
167
|
+
自己打出来的一串短词用标签输入,选文件用文件上传:
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Field, FileItem, FileUpload, TagInput } from "@endfield-ui/react";
|
|
171
|
+
|
|
172
|
+
<Field label="站点标签" help="回车或逗号分开,最多八个。">
|
|
173
|
+
<TagInput name="tags" max={8} value={tags} onValueChange={setTags} />
|
|
174
|
+
</Field>
|
|
175
|
+
|
|
176
|
+
<Field label="交接附件" help="PDF 或图片,单个不超过 10 MB。">
|
|
177
|
+
<FileUpload
|
|
178
|
+
multiple
|
|
179
|
+
accept=".pdf,image/*"
|
|
180
|
+
maxSize={10 * 1024 * 1024}
|
|
181
|
+
name="attachments"
|
|
182
|
+
value={files}
|
|
183
|
+
onValueChange={setFiles}
|
|
184
|
+
/>
|
|
185
|
+
</Field>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `TagInput` 回车或打出分隔符(默认半角、全角逗号)就加一个,粘贴一串会拆开;没加成的(已经有了、到了 `max`、没过 `validate`)交给 `onReject`。要从现成的选项里挑,用 `Combobox` 的 `multiple`。
|
|
189
|
+
- `FileUpload` 只管选和列,不发请求。里面是一个真的 `<input type="file">`:带 `name` 时列表里的文件随表单提交。不合 `accept` / `maxSize` / `maxFiles` 的不收,并在下面说明原因。
|
|
190
|
+
- 选了就要传、要显示进度时,用 `renderFile` 自己返回带状态的那一行:`<FileItem name size status="uploading" progress={40} onRemove={remove} />`。
|
|
191
|
+
|
|
192
|
+
### 带状态的控件
|
|
193
|
+
|
|
194
|
+
步进器、分页条、胶囊导航器都有受控与非受控两种用法:传 `value` / `page` / `index` 就由外面决定,只传 `default…` 就由控件自己记。
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { Navigator, Pagination, Stepper } from "@endfield-ui/react";
|
|
198
|
+
|
|
199
|
+
<Stepper value={count} onValueChange={setCount} min={1} max={99} />
|
|
200
|
+
|
|
201
|
+
<Pagination page={page} pageCount={12} onPageChange={setPage} jump />
|
|
202
|
+
|
|
203
|
+
<Navigator
|
|
204
|
+
aria-label="勘探区"
|
|
205
|
+
items={["谷地", "第七勘探区", "荒原"]}
|
|
206
|
+
index={zone}
|
|
207
|
+
onIndexChange={setZone}
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
页码从 1 起,`index` 从 0 起。
|
|
212
|
+
|
|
213
|
+
步骤条只收一个 `current`(从 0 起),每一步是已完成、当前还是未到由位置算出来:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { Step, Steps } from "@endfield-ui/react";
|
|
217
|
+
|
|
218
|
+
<Steps aria-label="建站流程" current={step}>
|
|
219
|
+
<Step title="建站" description="选址、供电、通信" onClick={() => setStep(0)} />
|
|
220
|
+
<Step title="测绘" />
|
|
221
|
+
<Step title="复核" invalid={hasError} />
|
|
222
|
+
<Step title="归档" />
|
|
223
|
+
</Steps>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- 横排在所在的容器窄于 28rem 时自动改成竖排;`orientation="vertical"` 是一直竖排。它靠容器查询量宽度,所以要放在有确定宽度的地方(默认撑满一栏)。
|
|
227
|
+
- 传了 `href`、`render` 或 `onClick` 的那一步可以点,用来回到做过的步骤;没传就是纯文字。
|
|
228
|
+
- `Steps` 是步骤条,`Stepper` 是加减数字的步进器,别拿错。
|
|
229
|
+
|
|
230
|
+
一排作用于同一个对象的小工具收进工具栏:整条只占一个 Tab 停靠点,方向键在里面走。
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import {
|
|
234
|
+
Toolbar,
|
|
235
|
+
ToolbarButton,
|
|
236
|
+
ToolbarSeparator,
|
|
237
|
+
ToolbarToggle,
|
|
238
|
+
ToolbarToggleGroup,
|
|
239
|
+
} from "@endfield-ui/react";
|
|
240
|
+
|
|
241
|
+
<Toolbar aria-label="表格工具">
|
|
242
|
+
<ToolbarToggleGroup aria-label="图层" multiple value={layers} onValueChange={setLayers}>
|
|
243
|
+
<ToolbarToggle value="route" icon={<RouteIcon />} aria-label="路线" />
|
|
244
|
+
<ToolbarToggle value="beacon" icon={<BeaconIcon />} aria-label="信标" />
|
|
245
|
+
</ToolbarToggleGroup>
|
|
246
|
+
<ToolbarSeparator />
|
|
247
|
+
<ToolbarToggle pressed={ruled} onPressedChange={setRuled}>加重线</ToolbarToggle>
|
|
248
|
+
<ToolbarButton icon={<PrintIcon />} onClick={print}>打印</ToolbarButton>
|
|
249
|
+
</Toolbar>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- 开关组的值是数组:默认同时只按下一个(也可以一个都不按),`multiple` 可以按下几个。表单里"必须选一个"的用 `SegmentedControl`。
|
|
253
|
+
- 只有图标的钮必须有 `aria-label`。`ToolbarButton` 可以交给 `DropdownMenu` / `Popover` 的 `trigger`,也可以用 `Tooltip` 包住。
|
|
254
|
+
- 不在工具栏里放输入框:左右方向键在输入框里是移光标。
|
|
255
|
+
|
|
256
|
+
### 加载页
|
|
257
|
+
|
|
258
|
+
`Loader` 默认铺满视口,并在加载期间锁住页面滚动和焦点。内容就绪后把 `open` 置为 `false`,遮罩滑出、随后自己卸载:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { Loader } from "@endfield-ui/react";
|
|
262
|
+
|
|
263
|
+
<Loader value={progress} open={!ready} tagline="正在同步档案" />
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
不知道真实进度就不传 `value`。只想盖住页面的一块区域时加 `fullscreen={false}`,并让那块区域带 `relative` 与 `overflow-hidden`。
|
|
267
|
+
|
|
268
|
+
### 浮层
|
|
269
|
+
|
|
270
|
+
文字提示、气泡卡片、弹窗、抽屉、下拉菜单、展开条、下拉选择、轻提示。焦点的进出与锁定、键盘、定位与翻转都交给 Base UI,这里只管长相;用的时候不用自己拼部件。触发元素作为一个 React 元素传进去:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import {
|
|
274
|
+
Button,
|
|
275
|
+
Dialog,
|
|
276
|
+
DialogClose,
|
|
277
|
+
DropdownMenu,
|
|
278
|
+
DropdownMenuItem,
|
|
279
|
+
DropdownMenuSeparator,
|
|
280
|
+
IconButton,
|
|
281
|
+
Tooltip,
|
|
282
|
+
} from "@endfield-ui/react";
|
|
283
|
+
|
|
284
|
+
<Tooltip content="锁定的物品不会被销毁">
|
|
285
|
+
<IconButton aria-label="锁定">
|
|
286
|
+
<Lock />
|
|
287
|
+
</IconButton>
|
|
288
|
+
</Tooltip>
|
|
289
|
+
|
|
290
|
+
<Dialog
|
|
291
|
+
alert
|
|
292
|
+
size="sm"
|
|
293
|
+
trigger={<Button variant="danger">销毁</Button>}
|
|
294
|
+
title="销毁 3 件物资"
|
|
295
|
+
description="销毁之后无法找回。"
|
|
296
|
+
footer={
|
|
297
|
+
<>
|
|
298
|
+
<DialogClose>
|
|
299
|
+
<Button variant="light">取消</Button>
|
|
300
|
+
</DialogClose>
|
|
301
|
+
<DialogClose>
|
|
302
|
+
<Button variant="danger" onClick={destroy}>
|
|
303
|
+
销毁 3 件
|
|
304
|
+
</Button>
|
|
305
|
+
</DialogClose>
|
|
306
|
+
</>
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
|
|
310
|
+
<DropdownMenu trigger={<Button variant="light">操作</Button>}>
|
|
311
|
+
<DropdownMenuItem onClick={rename}>重命名</DropdownMenuItem>
|
|
312
|
+
<DropdownMenuItem href="/detail">查看详情</DropdownMenuItem>
|
|
313
|
+
<DropdownMenuSeparator />
|
|
314
|
+
<DropdownMenuItem tone="danger" onClick={remove}>
|
|
315
|
+
删除
|
|
316
|
+
</DropdownMenuItem>
|
|
317
|
+
</DropdownMenu>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- `Dialog` 与 `Drawer` 也可以不传 `trigger`,自己用 `open` / `onOpenChange` 控制。`alert` 是破坏性操作的确认:点遮罩不关。
|
|
321
|
+
- `Dialog` 有两处默认关着的装饰:`ornament`(标题下一排小方点)和 `cornerArt`(左下角的线稿,传你自己的原创图形)。
|
|
322
|
+
- `Drawer` 的 `side` 是 `right`(默认)、`left`、`bottom`,都可以朝来的方向划走。
|
|
323
|
+
- 菜单里除了操作项,还有单选组(`DropdownMenuRadioGroup`,当前项)、复选项(`DropdownMenuCheckboxItem`,能开能关的设置,勾了不关菜单)和一层子菜单(`DropdownMenuSub`)。
|
|
324
|
+
- `ContextMenu` 是右键菜单:`<ContextMenu menu={…}>` 包住被右键的那个元素,`menu` 里放的就是上面这些 `DropdownMenuItem`。它只是捷径——里面的操作在页面上要另有入口。
|
|
325
|
+
- `FlyoutBar` 是从一个图标按钮旁边拉开的一条横排操作(分享的几个去处),键盘上是一个横向的菜单。里面放 `FlyoutBarItem`:子元素是图标,只有图标时必须给 `aria-label`。触发按钮用 `<IconButton variant="inverse">`,它的 `aria-label` 同时是这一条的名称。
|
|
326
|
+
- `Popover` 是点击触发、里面可以操作的一小块面板(`trigger`、`title`、`description` + 子元素),不打断页面。一句说明用 `Tooltip`,必须做完才能继续的用 `Dialog`。
|
|
327
|
+
- 触发元素要是一个按钮,并且把收到的属性和 `ref` 交给原生元素。本库的 `Button`、`IconButton` 都可以直接用。
|
|
328
|
+
|
|
329
|
+
下拉选择的触发器和输入框长得一样,放进 `Field` 自动关联标签与错误说明:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
import { Field, Select } from "@endfield-ui/react";
|
|
333
|
+
|
|
334
|
+
<Field label="所属地区" error={error}>
|
|
335
|
+
<Select
|
|
336
|
+
name="region"
|
|
337
|
+
placeholder="请选择"
|
|
338
|
+
items={[
|
|
339
|
+
{ value: "valley", label: "四号谷地" },
|
|
340
|
+
{ value: "ridge", label: "北岭" },
|
|
341
|
+
]}
|
|
342
|
+
value={region}
|
|
343
|
+
onValueChange={setRegion}
|
|
344
|
+
/>
|
|
345
|
+
</Field>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
加 `multiple` 可以选多项:`value` 与 `onValueChange` 换成字符串数组,选项行首多一个小方格,选了不关面板;触发器里把已选项用顿号连起来并带一个计数,换写法用 `renderValue`。
|
|
349
|
+
|
|
350
|
+
选项多到要找的时候换成组合框——一个能打字的下拉,属性和 `Select` 差不多:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { Combobox, Field } from "@endfield-ui/react";
|
|
354
|
+
|
|
355
|
+
<Field label="常驻站点">
|
|
356
|
+
<Combobox
|
|
357
|
+
name="station"
|
|
358
|
+
placeholder="输入站名或编号"
|
|
359
|
+
items={[
|
|
360
|
+
{ value: "n7", label: "北区七号站", keywords: ["N-07"] },
|
|
361
|
+
{ value: "s3", label: "南岸三号站", keywords: ["S-03"] },
|
|
362
|
+
]}
|
|
363
|
+
value={station}
|
|
364
|
+
onValueChange={setStation}
|
|
365
|
+
/>
|
|
366
|
+
</Field>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- `label` 必须是字符串;检索匹配 `label` 和 `keywords`(不显示的别名)。要分组就传 `{ label, items }` 的数组。
|
|
370
|
+
- 值只能是选项里有的;清除之后 `onValueChange` 拿到 `null`。
|
|
371
|
+
- 加 `multiple`:值是数组,已选项在框里排成一个个小块,框会跟着长高。
|
|
372
|
+
- 选项由服务器按输入返回时:`onInputValueChange` 拿到输入的文字去取,`filter={false}` 不在本地再筛一遍,`loading` 时面板里是一行"正在查找…"。
|
|
373
|
+
|
|
374
|
+
轻提示要先在应用最外层包一个 `ToastProvider`,里面的任何地方用 `useToast()` 弹出。同时只显示一条,新的替换旧的:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { ToastProvider, useToast } from "@endfield-ui/react";
|
|
378
|
+
|
|
379
|
+
<ToastProvider>
|
|
380
|
+
<App />
|
|
381
|
+
</ToastProvider>;
|
|
382
|
+
|
|
383
|
+
function SaveButton() {
|
|
384
|
+
const toast = useToast();
|
|
385
|
+
return (
|
|
386
|
+
<Button
|
|
387
|
+
onClick={async () => {
|
|
388
|
+
await save();
|
|
389
|
+
toast({ message: "设置已保存", tone: "success" });
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
保存
|
|
393
|
+
</Button>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
带 `action` 的提示("撤销")会多停一会儿并出现关闭图标。需要用户处理的信息不要用轻提示,用 `Alert` 或 `Dialog`。
|
|
399
|
+
|
|
400
|
+
接入时要知道的三件事:
|
|
401
|
+
|
|
402
|
+
- **浮层挂在 `<body>` 下。** 它们的 `z-index` 是 `--z-overlay`(300),轻提示是 `--z-toast`(400)。应用里有更高的层时,在 `:root` 上改这两个变量。所有浮层共用一个值、靠打开的先后叠,所以弹窗里的下拉能盖住弹窗——不要单独给某个浮层加高。
|
|
403
|
+
- **局部主题会跟过去。** 暗色版块(`data-theme="dark"`)里的按钮打开的弹窗也是暗色的:浮层打开时从触发元素往上找最近的 `data-theme` / `data-choice` 抄到自己身上。没有触发元素的受控弹窗跟随 `<html>`;需要时把 `data-theme` 直接传给它。
|
|
404
|
+
- **`@base-ui/react` 是本包的依赖**,装本包时会一起装上,不用另外引入。
|
|
405
|
+
|
|
406
|
+
### 外壳
|
|
407
|
+
|
|
408
|
+
宽屏用侧轨,窄屏换成顶栏加全屏菜单——是换一套,不是把侧轨缩小。两边各管各的,什么宽度用哪个由页面的断点决定:
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import {
|
|
412
|
+
IconButton,
|
|
413
|
+
Menu,
|
|
414
|
+
NavAction,
|
|
415
|
+
NavMenu,
|
|
416
|
+
NavMenuItem,
|
|
417
|
+
SideRail,
|
|
418
|
+
SideRailItem,
|
|
419
|
+
TopBar,
|
|
420
|
+
} from "@endfield-ui/react";
|
|
421
|
+
|
|
422
|
+
<div className="flex min-h-dvh">
|
|
423
|
+
<SideRail
|
|
424
|
+
aria-label="主导航"
|
|
425
|
+
className="hidden lg:flex"
|
|
426
|
+
brand={<Logo />}
|
|
427
|
+
action={<NavAction href="/console">前往控制台</NavAction>}
|
|
428
|
+
>
|
|
429
|
+
<SideRailItem icon={<GridIcon />} href="/overview" current>
|
|
430
|
+
总览
|
|
431
|
+
</SideRailItem>
|
|
432
|
+
<SideRailItem icon={<ArchiveIcon />} href="/archive">
|
|
433
|
+
档案
|
|
434
|
+
</SideRailItem>
|
|
435
|
+
</SideRail>
|
|
436
|
+
|
|
437
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
438
|
+
<TopBar
|
|
439
|
+
className="lg:hidden"
|
|
440
|
+
brand={<Logo />}
|
|
441
|
+
action={<NavAction href="/console">控制台</NavAction>}
|
|
442
|
+
menu={
|
|
443
|
+
<NavMenu
|
|
444
|
+
title="菜单"
|
|
445
|
+
trigger={
|
|
446
|
+
<IconButton aria-label="打开菜单">
|
|
447
|
+
<Menu />
|
|
448
|
+
</IconButton>
|
|
449
|
+
}
|
|
450
|
+
>
|
|
451
|
+
<NavMenuItem icon={<GridIcon />} href="/overview" current>
|
|
452
|
+
总览
|
|
453
|
+
</NavMenuItem>
|
|
454
|
+
</NavMenu>
|
|
455
|
+
}
|
|
456
|
+
/>
|
|
457
|
+
<main>…</main>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
- `SideRail` 默认展开(图标 + 文字);`collapsed` 收起成只有图标的窄轨,悬停或键盘聚焦时栏目名浮出来。栏目多了用 `SideRailGroup label` 分组。
|
|
463
|
+
- 一个栏目下面还有几个去处:`<SideRailSub icon label>` 里放 `<SideRailSubItem>`。父项只管展开收起;当前项在里面时自己展开。侧轨收起时它变成向右弹出的菜单。
|
|
464
|
+
- `NavAction` 是"主行动块":整个产品最主要的那个去处。放进侧轨、顶栏、全屏菜单的底部时,摆法由它们决定。
|
|
465
|
+
- `NavMenu` 打开时焦点移入并被限制在内,背景不可滚动;点一个栏目默认关上。
|
|
466
|
+
- 本库不带标志和栏目图标,都由使用方给。
|
|
467
|
+
- 侧轨和顶栏默认 `sticky`,在 `--z-nav`(200)这一层,比浮层低。
|
|
468
|
+
|
|
469
|
+
### 表格
|
|
470
|
+
|
|
471
|
+
包一层原生 `<table>`,组合着用。排序的状态自己拿着,表头只告诉你"该换成哪个方向":
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
import {
|
|
475
|
+
Table,
|
|
476
|
+
TableBody,
|
|
477
|
+
TableCell,
|
|
478
|
+
TableHead,
|
|
479
|
+
TableHeaderCell,
|
|
480
|
+
TableRow,
|
|
481
|
+
} from "@endfield-ui/react";
|
|
482
|
+
|
|
483
|
+
<Table label="运输批次" stickyFirstColumn>
|
|
484
|
+
<TableHead>
|
|
485
|
+
<TableRow>
|
|
486
|
+
<TableHeaderCell>批次</TableHeaderCell>
|
|
487
|
+
<TableHeaderCell
|
|
488
|
+
numeric
|
|
489
|
+
sort={sort.key === "count" ? sort.direction : null}
|
|
490
|
+
onSort={(direction) => setSort({ key: "count", direction })}
|
|
491
|
+
>
|
|
492
|
+
件数
|
|
493
|
+
</TableHeaderCell>
|
|
494
|
+
<TableHeaderCell align="end">操作</TableHeaderCell>
|
|
495
|
+
</TableRow>
|
|
496
|
+
</TableHead>
|
|
497
|
+
<TableBody>
|
|
498
|
+
{rows.map((row) => (
|
|
499
|
+
<TableRow key={row.id} selected={picked.has(row.id)}>
|
|
500
|
+
<TableCell rowHeader>{row.id}</TableCell>
|
|
501
|
+
<TableCell numeric>{row.count}</TableCell>
|
|
502
|
+
<TableCell reveal align="end">
|
|
503
|
+
<IconButton size="sm" aria-label={`打印 ${row.id}`}>…</IconButton>
|
|
504
|
+
</TableCell>
|
|
505
|
+
</TableRow>
|
|
506
|
+
))}
|
|
507
|
+
</TableBody>
|
|
508
|
+
</Table>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- 单元格默认不折行:放不下时表格在自己的容器里横向滚动。`stickyFirstColumn` 冻结第一列。
|
|
512
|
+
- `reveal` 的单元格里放行内操作:悬停或焦点进了这一行才显示,触屏上常显。
|
|
513
|
+
- 整行不可点。要进详情把名称写成链接,要勾选在第一列放 `Checkbox`。
|
|
514
|
+
- 表头两种:`band`(反转的标题带,默认)和 `muted`;`ruled` 每隔五行加重一条线。
|
|
515
|
+
- `stickyHeader` 让表头留在容器的上沿;容器的高度上限自己给(`className="max-h-96"`)。
|
|
516
|
+
- 一行下面还有明细时,给 `TableRow` 传 `detail`,并在某一格里放一个 `TableExpander`(通常在名称前面):展开时明细出现在这一行下面,通栏。状态默认各行自己记,要"全部展开"就用 `expanded` / `onExpandedChange` 自己拿着。
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
<TableRow detail={<Manifest id={row.id} />}>
|
|
520
|
+
<TableCell rowHeader>
|
|
521
|
+
<TableExpander aria-label={`${row.id} 的明细`} /> {row.id}
|
|
522
|
+
</TableCell>
|
|
523
|
+
<TableCell numeric>{row.count}</TableCell>
|
|
524
|
+
</TableRow>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 内容与展示
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
import {
|
|
531
|
+
Accordion,
|
|
532
|
+
AccordionItem,
|
|
533
|
+
Avatar,
|
|
534
|
+
AvatarSwitcher,
|
|
535
|
+
AvatarSwitcherItem,
|
|
536
|
+
BackToTop,
|
|
537
|
+
Carousel,
|
|
538
|
+
CarouselSlide,
|
|
539
|
+
MediaCard,
|
|
540
|
+
PlayButton,
|
|
541
|
+
Schedule,
|
|
542
|
+
ScheduleItem,
|
|
543
|
+
ScheduleTrack,
|
|
544
|
+
} from "@endfield-ui/react";
|
|
545
|
+
|
|
546
|
+
<Accordion defaultValue={["route"]}>
|
|
547
|
+
<AccordionItem value="route" title="批次发出之后还能改派吗">
|
|
548
|
+
能。在调度台里选中批次,再选新的目的站。
|
|
549
|
+
</AccordionItem>
|
|
550
|
+
</Accordion>
|
|
551
|
+
|
|
552
|
+
<Avatar name="陈知远" src={portrait} />
|
|
553
|
+
|
|
554
|
+
<AvatarSwitcher aria-label="队员" value={person} onValueChange={setPerson}>
|
|
555
|
+
<AvatarSwitcherItem value="chen" label="陈知远" src={portrait} />
|
|
556
|
+
<AvatarSwitcherItem value="lin" label="林澈" />
|
|
557
|
+
</AvatarSwitcher>
|
|
558
|
+
|
|
559
|
+
<MediaCard video media={<img src={cover} alt="" />} title="秋季勘探计划 · 预告" href="/pv/1" />
|
|
560
|
+
|
|
561
|
+
<PlayButton aria-label="播放:秋季勘探计划 · 预告" onClick={play} />
|
|
562
|
+
|
|
563
|
+
<Carousel aria-label="玩法介绍">
|
|
564
|
+
<CarouselSlide title="线路测绘" description="沿着管廊布设信标。">
|
|
565
|
+
<img src={shot} alt="" />
|
|
566
|
+
</CarouselSlide>
|
|
567
|
+
</Carousel>
|
|
568
|
+
|
|
569
|
+
<Schedule label="十月排期" start="2026-10-01" end="2026-10-31" today={today}>
|
|
570
|
+
<ScheduleTrack label="测绘" icon={<RouteIcon />}>
|
|
571
|
+
<ScheduleItem start="2026-10-03" end="2026-10-12" title="管廊北段" type="限时" />
|
|
572
|
+
</ScheduleTrack>
|
|
573
|
+
</Schedule>
|
|
574
|
+
|
|
575
|
+
<BackToTop />
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
- `Accordion` 默认只开一节,`multiple` 可以同时开几节;`value` 总是数组。收起的内容不在页面里。
|
|
579
|
+
- `Avatar` 没有图、图加载失败时显示名字的首字;旁边已经写了名字时传 `alt=""`。
|
|
580
|
+
- `AvatarSwitcher` 是一列头像里选一个,语义是单选组(方向键换人);放不下时给它一个高度上限。
|
|
581
|
+
- `MediaCard` 的 `video` 在封面左下角加一个播放记号(黄色小方块,`PlayMark`)。记号不能点,整卡仍然只有一个链接;封面要"点了就在原地播放"时用 `PlayButton`,它是真的按钮。
|
|
582
|
+
- `Carousel` 不自动播放。轨道是原生的横向滚动,触屏上直接滑;不在眼前的幻灯片 `Tab` 走不进去。
|
|
583
|
+
- `Schedule` 的条目只写起止日期,位置和错行都是算出来的;放不下时在自己的容器里横向滚动。
|
|
584
|
+
- `BackToTop` 滚过 400px 才出现,默认钉在视口右下角;点了回到顶部,焦点也交还给页面的头上。要看某个滚动容器时传 `target`,并用 `className` 改位置。祖先上有 `transform` 或容器查询(`@container`)时 `fixed` 不再相对视口:把它放在那一栏的最后,加 `className="sticky bottom-4 self-end"`。
|
|
585
|
+
- 本库不带任何图片:头像、轮播和排期里的图都由使用方给。
|
|
586
|
+
|
|
587
|
+
长页面旁边放一列页内目录:
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
import { Toc, TocItem } from "@endfield-ui/react";
|
|
591
|
+
|
|
592
|
+
<Toc title="// 本页" offset={64} className="sticky top-20">
|
|
593
|
+
<TocItem href="#bulletin">最新情报</TocItem>
|
|
594
|
+
<TocItem href="#crew" level={2}>队员</TocItem>
|
|
595
|
+
</Toc>
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
- 滚到哪一节哪一项亮(`aria-current="location"`);滚到底是最后一项。点一项就是点一个锚点链接,滚不滚、平不平滑由浏览器和页面的 CSS 决定。
|
|
599
|
+
- 页面有吸顶的页头时 `offset` 传它的高度,并给各节的标题加同样大小的 `scroll-mt-*`。内容在某个滚动容器里时传 `target`。
|
|
600
|
+
- 位置自己给(`sticky`);窄屏放不下时由页面收起来。
|
|
601
|
+
|
|
602
|
+
### 游戏风格的控件
|
|
603
|
+
|
|
604
|
+
物品格的宽度跟着所在的网格走。格子里只有图标,`name` 是给读屏的名称;选中是四角的角括号,画在格子之外 4px。一组格子放进 `ItemGrid`:它自带留好空隙的网格,并且整个矩阵只占一个 Tab 停靠点,进去之后用方向键在格子之间走。
|
|
605
|
+
|
|
606
|
+
```tsx
|
|
607
|
+
import { ItemGrid, ItemSlot, Tab, TabList, Tabs } from "@endfield-ui/react";
|
|
608
|
+
|
|
609
|
+
<Tabs defaultValue="supply" variant="wedge">
|
|
610
|
+
<TabList aria-label="仓库分类">
|
|
611
|
+
<Tab value="supply">物资</Tab>
|
|
612
|
+
<Tab value="gear">装备</Tab>
|
|
613
|
+
</TabList>
|
|
614
|
+
</Tabs>
|
|
615
|
+
|
|
616
|
+
<ItemGrid aria-label="物资">
|
|
617
|
+
<ItemSlot
|
|
618
|
+
name="合金锭"
|
|
619
|
+
count={128}
|
|
620
|
+
rarity={2}
|
|
621
|
+
selected={selected === "alloy"}
|
|
622
|
+
onClick={() => setSelected("alloy")}
|
|
623
|
+
>
|
|
624
|
+
<OreIcon size={32} />
|
|
625
|
+
</ItemSlot>
|
|
626
|
+
</ItemGrid>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
复选与单选可以整体换成菱形符号。它不是组件上的属性,而是和主题一样写在 `<html>`(或局部容器)上的开关,全库选一套:
|
|
630
|
+
|
|
631
|
+
```html
|
|
632
|
+
<html data-theme="dark" data-choice="diamond">
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
菱形方案下复选和单选长得一样,"能选几个"要靠分组标题讲清楚。
|
|
636
|
+
|
|
637
|
+
数据行带是游戏里数据面板的样子:浅色画布上一条条深色的行。语义是一张表格,列由 `columns` 决定:
|
|
638
|
+
|
|
639
|
+
```tsx
|
|
640
|
+
import { DataRow, DataRowList } from "@endfield-ui/react";
|
|
641
|
+
|
|
642
|
+
<DataRowList
|
|
643
|
+
label="本周收支"
|
|
644
|
+
columns={{ name: "项目", trend: "走势", value: "当前", reference: "理论" }}
|
|
645
|
+
>
|
|
646
|
+
<DataRow
|
|
647
|
+
name="钢材"
|
|
648
|
+
categoryColor="var(--color-special)"
|
|
649
|
+
favorite={favorites.has("steel")}
|
|
650
|
+
onFavoriteChange={(next) => toggle("steel", next)}
|
|
651
|
+
series={[42, 58, 51, 77, 69, 88]}
|
|
652
|
+
value="+128"
|
|
653
|
+
tone="info"
|
|
654
|
+
reference="+140"
|
|
655
|
+
/>
|
|
656
|
+
</DataRowList>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
- `tone` 是当前值的含义:`info` 产出、`accent` 消耗、`danger` 超支。正负号连同数值一起传,颜色之外还要有符号。
|
|
660
|
+
- `series` 画成行内的小型面积图;这种图也可以单独用:`<Sparkline data variant tone />`,撑满给它的盒子。
|
|
661
|
+
- 不给 `trend` 或 `reference` 的列名就没有那一列;容器变窄时也是先收这两列。
|
|
662
|
+
- 普通的列表要这种深色行带时写 `<List variant="band">`,选中行整行反转成白底墨字。
|
|
663
|
+
|
|
664
|
+
### 母题
|
|
665
|
+
|
|
666
|
+
镂空巨字、斜纹、底纹、注册色条、刻度圆环、取景角、警示条纹、角括号各有一个组件。除了角括号(它表示选中),其余都是纯装饰:对读屏隐藏、不挡点击,高对比模式与打印时不显示。
|
|
667
|
+
|
|
668
|
+
底纹有点阵、工程网格、等高线三种(`<Texture variant="dots" | "grid" | "contour">`),铺满最近的定位祖先。一个视口选一种,文字下面垫一层实色:
|
|
669
|
+
|
|
670
|
+
```tsx
|
|
671
|
+
import { BracketTitle, Texture } from "@endfield-ui/react";
|
|
672
|
+
|
|
673
|
+
<section className="relative overflow-clip bg-surface-raised p-4">
|
|
674
|
+
<Texture />
|
|
675
|
+
<div className="relative bg-surface-raised p-5">
|
|
676
|
+
<BracketTitle>北区仓储站</BracketTitle>
|
|
677
|
+
</div>
|
|
678
|
+
</section>
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
```tsx
|
|
682
|
+
import { GhostText, TickRing, Viewfinder } from "@endfield-ui/react";
|
|
683
|
+
|
|
684
|
+
<section className="relative overflow-clip">
|
|
685
|
+
<GhostText className="absolute -right-6 -bottom-5">//Archive</GhostText>
|
|
686
|
+
<div className="relative">…</div>
|
|
687
|
+
</section>
|
|
688
|
+
|
|
689
|
+
<Viewfinder data-theme="dark" crosshair readouts={{ bottomRight: "16 : 9" }}>
|
|
690
|
+
<img src={scene} alt="谷地北侧的地形" />
|
|
691
|
+
</Viewfinder>
|
|
692
|
+
|
|
693
|
+
<TickRing size={200} spin>
|
|
694
|
+
<Model />
|
|
695
|
+
</TickRing>
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
叠在画面上的东西(取景角、刻度圆环、录制指示)颜色跟的是**画面**,不是页面:画面是深色的就在它上面加 `data-theme="dark"`。
|
|
699
|
+
|
|
700
|
+
用 Tailwind 的项目也可以直接用工具类(`cut-tr`、`wedge-r`、`corner-brackets`、`ghost-hatch`、`hatch`、`hazard`、`dot-grid`、`blueprint-grid`、`contour`),见 [styles/README.md](src/styles/README.md)。这些母题最容易用过头,什么时候该用见 [母题文档](https://github.com/icaruszezen/endfield-ui/blob/main/docs/design/elements/corner-and-wedge.md)。
|
|
701
|
+
|
|
702
|
+
### 主题
|
|
703
|
+
|
|
704
|
+
默认亮色。在 `<html>`(或任意容器)上加 `data-theme="dark"` 切到暗色;暗色区域里也可以用 `data-theme="light"` 切回来。
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
import { useTheme } from "@endfield-ui/react";
|
|
708
|
+
|
|
709
|
+
function ThemeToggle() {
|
|
710
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
711
|
+
return (
|
|
712
|
+
<button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
|
|
713
|
+
切换主题
|
|
714
|
+
</button>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
`useTheme` 读写 `<html data-theme>`,支持 `"light"`、`"dark"`、`"system"`,选择记在 `localStorage` 的 `ef-theme` 里。要避免首屏闪一下亮色,在 `<head>` 里用一小段内联脚本提前把 `data-theme` 写好。
|
|
720
|
+
|
|
721
|
+
把强调色换成自己的主题色,见 [色彩](https://github.com/icaruszezen/endfield-ui/blob/main/docs/design/foundations/color.md) 的"主题色接管"。
|
|
722
|
+
|
|
723
|
+
还有第三个取值 `data-theme="inverse"`:和所在的主题相反。面板的标题带、表格的表头带、完成横幅用的就是它,所以放进去的按钮、复选框、焦点环不用另外处理。自己做一块"和页面相反"、里面还要放控件的区域时,写 `data-theme="inverse"` 加 `bg-surface text-ink`——取到的就是 `surface-inverse` / `ink-inverse`。反转块里不要再嵌反转块。
|
|
724
|
+
|
|
725
|
+
## 目录
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
packages/ui/
|
|
729
|
+
└── src/
|
|
730
|
+
├── index.ts 入口
|
|
731
|
+
├── components/ 一组件一目录,见 components/README.md
|
|
732
|
+
├── styles/ 令牌、母题工具类、样式入口,见 styles/README.md
|
|
733
|
+
├── hooks/ 与组件无关的通用 hooks(主题、受控状态、浮层的局部主题)
|
|
734
|
+
├── lib/ 纯函数工具
|
|
735
|
+
└── icons/ 原创图标组件,不收录官方图标
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## 开发
|
|
739
|
+
|
|
740
|
+
在仓库根目录:
|
|
741
|
+
|
|
742
|
+
| 命令 | 作用 |
|
|
743
|
+
| --- | --- |
|
|
744
|
+
| `pnpm dev` | 启动 Storybook(`http://localhost:6106`),直接读本包源码,改了即热更新 |
|
|
745
|
+
| `pnpm test` | 跑单元测试 |
|
|
746
|
+
| `pnpm test:browser` | 在真的浏览器里测键盘、焦点与布局(先 `pnpm build:docs`),见 [apps/docs](https://github.com/icaruszezen/endfield-ui/blob/main/apps/docs/README.md#浏览器实测) |
|
|
747
|
+
| `pnpm typecheck` | 类型检查全部工作区包 |
|
|
748
|
+
| `pnpm build` | 生成 `dist/index.js`、`dist/index.d.ts`、`dist/styles.css` |
|
|
749
|
+
| `pnpm format` | 用 Prettier 格式化全仓库;`pnpm format:check` 只检查不改,CI 跑的是它 |
|
|
750
|
+
|
|
751
|
+
格式用 Prettier 的默认配置,提交前跑一次 `pnpm format`。Markdown 不在它的范围里(它会把表格逐列补空格对齐);Tailwind 的类名顺序也不归它管。
|
|
752
|
+
|
|
753
|
+
TypeScript 用的是 7.0。tsdown 在这个版本下生成类型声明时会提示"API 尚不稳定",目前产物正常;如果以后出问题,退路是 `tsc --emitDeclarationOnly`。
|
|
754
|
+
|
|
755
|
+
### 发布
|
|
756
|
+
|
|
757
|
+
1. 改本目录 `package.json` 里的 `version`,提交并推到 `main`,等 CI 跑完。
|
|
758
|
+
2. 打一个和版本号对应的标签推上去:`git tag v0.2.0`,`git push origin v0.2.0`。
|
|
759
|
+
3. [publish.yml](https://github.com/icaruszezen/endfield-ui/blob/main/.github/workflows/publish.yml) 核对标签与版本号,跑类型检查、测试与构建(`prepublishOnly`),然后发布到 npm。
|
|
760
|
+
|
|
761
|
+
认证用的是 npm 的 Trusted Publishing,仓库里不存 token:npm 上这个包的设置里登记了本仓库和 `publish.yml` 这个文件名,两边任何一个改名都要同步。
|
|
762
|
+
|
|
763
|
+
`LICENSE` 和 `NOTICE.md` 只在仓库根目录有一份,打包时由 `prepack` 复制到本目录(所以它们在这里是被 git 忽略的)。想看包里到底会有什么:`pnpm pack`。
|
|
764
|
+
|
|
765
|
+
## 与设计文档的关系
|
|
766
|
+
|
|
767
|
+
- 所有视觉决策以 [docs/design](https://github.com/icaruszezen/endfield-ui/blob/main/docs/design/overview.md) 为准;代码里出现文档没有的颜色或尺寸时,先改文档再改代码。
|
|
768
|
+
- 组件的状态、尺寸与形状规范在 [docs/design/components](https://github.com/icaruszezen/endfield-ui/blob/main/docs/design/components/README.md),那里同时给出了各期的清单与顺序。
|
|
769
|
+
- 令牌的唯一来源是 [src/styles/theme.css](src/styles/theme.css),文档中的令牌表与它逐项对应。
|