Skip to content

Commit ef2598e

Browse files
chaxusclaude
andcommitted
style(history): give the page the site's own visual language
Design polish, but specific rather than "make it nicer" -- four devices the homepage already uses, applied to the three surfaces that talk about saved work, so they read as one feature instead of three screens. Hairlines move into the shadow layer: box-shadow 0 0 0 1px instead of a border, the way Geist draws surfaces. Outside the box model, so rounded corners stop clipping a row's hover fill and the line animates with the surface. The document list, the recovery card and the homepage's autosave line all changed together. Mono is for stamping, not for reading: the file-kind badge and the storage count are machine facts and take Geist Mono, the same as the homepage's chip and trust row. Prose stays in the sans -- the failure mode here is making every label monospace. Status is a dot, not a grey pill. "Not exported" marks the rows holding the only copy of that work, which is the entire reason the list exists; a 5px dot in the warning colour with a soft ring says that, where a grey pill said "another piece of metadata". Same device as the homepage chip's live dot and the recovery card's title. The page now opens chip -> title -> intro, the homepage hero's own sequence, so arriving from it does not feel like arriving at an admin screen. And the empty state stops being an ellipsis where the product thinking should be: it says why the list is empty, what will fill it, and offers the one thing to do next -- with a real icon from ranui's set (lazily bundled as its own chunk) rather than a typographic character standing in for one. docs/design-system.md now carries these four devices, since the point of writing them down is that the next page inherits them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e81c1f0 commit ef2598e

7 files changed

Lines changed: 202 additions & 28 deletions

File tree

‎docs/design-system.md‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,33 @@ Notion 正文 708)。**不存在"一个正确的宽度",只存在按内容
5757
依据是"这个按钮作用于什么",不是"它在哪"。一行一个的表格操作若与 CTA 同尺寸,
5858
一屏就变成一叠行动号召。组件库的 `size` 分级是同一个道理。
5959

60-
## 五、颜色一律走 `--ran-*`
60+
## 五、四个视觉手法(站点自己的语言,新页面照抄)
61+
62+
这几条不是装饰,是让不同页面看起来出自同一处的东西。首页早就在用,`/history`、
63+
恢复条、首页的自动保存条现在也用同一套:
64+
65+
1. **hairline 画在 shadow 层,不用 border**:`box-shadow: 0 0 0 1px var(--ran-color-border)`。
66+
零偏移零模糊的 1px 扩散,不占 box model,所以圆角处不会裁掉行的 hover 底色,
67+
线本身也能跟着表面一起过渡。Geist 的做法,比真 border 更轻。
68+
2. **mono 用来"盖章",不用来读**:文件类型、时间戳、用量这些机器事实用
69+
`--ran-font-mono`(首页 chip / trust 行、`/history` 的格式徽章与计数)。
70+
反过来说——**不要把每个标签都做成 mono**,正文永远是 Geist Sans。
71+
3. **状态用点,不用灰色 pill**:一个 6px 的圆点加一个词,点用语义色
72+
(成功绿、警示橙)并带一圈 22% 透明的光晕,读起来是"亮着的"而不是"画上去的"。
73+
首页 chip、`/history` 的"未导出"、恢复条标题都是这个形状。
74+
4. **chip → 标题 → 说明** 是页面开场的固定顺序(首页 hero 定的),
75+
`/history` 沿用,所以从首页走过去不会觉得进了另一个后台。
76+
77+
**空状态是要做产品判断的地方,不是省略号**:说明为什么是空的、接下来做什么,
78+
并给一个真图标(ranui 内置 52 个,`<r-icon name="book">` 零注册懒加载,
79+
打包成独立 chunk),不是拿排版字符凑。
80+
81+
## 六、颜色一律走 `--ran-*`
6182

6283
不写死任何颜色值。暗色模式因此不需要任何额外工作——token 层自己翻转,
6384
`/history` 的暗色版本是零成本得到的。
6485

65-
## 六、已知的合理例外
86+
## 七、已知的合理例外
6687

6788
- **首页 h1 是 64px**,比其它页大——hero 就该更重,这是有意的层级差。
6889
- **`/embed-demo.html` 是全宽两栏**(340px 控制面板 + 编辑器),侧栏文字行宽 41 字符。

‎lib/history-page.ts‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
import 'ranui/button';
1515
import 'ranui/input';
16+
import 'ranui/icon';
1617
import { Div, View } from 'ranui/builder';
1718
import '../styles/history.css';
1819
import { applyDocumentLanguage, t } from '@ranuts/shared/i18n';
@@ -90,7 +91,14 @@ function buildRow(doc: HistoryDoc, refresh: () => void): HTMLElement {
9091
.attr('href', `/editor?doc=${encodeURIComponent(doc.id)}`)
9192
.text(doc.title)
9293
.build(),
93-
...(hasUnsavedWork(doc) ? [Div().class('history-badge').text(t('historyUnsaved')).build()] : []),
94+
...(hasUnsavedWork(doc)
95+
? [
96+
Div()
97+
.class('history-badge')
98+
.children(View('i').class('history-dot').build(), View('span').text(t('historyUnsaved')).build())
99+
.build(),
100+
]
101+
: []),
94102
)
95103
.build();
96104

@@ -236,6 +244,13 @@ async function render(): Promise<void> {
236244
const header = Div()
237245
.class('history-header')
238246
.children(
247+
// chip -> title -> intro: the homepage hero's own opening, so the page a
248+
// visitor reaches from it is recognisably the same product rather than
249+
// an admin screen that happens to share a header.
250+
Div()
251+
.class('history-chip')
252+
.children(View('i').class('history-chip-dot').build(), View('span').text(t('historyChip')).build())
253+
.build(),
239254
Div()
240255
.class('history-title-row')
241256
.children(
@@ -267,7 +282,38 @@ async function render(): Promise<void> {
267282
: Div()
268283
.class('history-empty')
269284
.id('history-empty')
270-
.text(query ? t('historyEmptySearch') : t('historyEmpty'))
285+
.children(
286+
Div()
287+
.class('history-empty-mark')
288+
.text(query ? '⌕' : '⌂')
289+
.attr('aria-hidden', 'true')
290+
.build(),
291+
Div()
292+
.class('history-empty-title')
293+
.text(query ? t('historyEmptySearchTitle') : t('historyEmptyTitle'))
294+
.build(),
295+
Div()
296+
.class('history-empty-body')
297+
.text(query ? t('historyEmptySearch') : t('historyEmpty'))
298+
.build(),
299+
query
300+
? button(
301+
t('historyClearSearch'),
302+
() => {
303+
query = '';
304+
page = 1;
305+
refresh();
306+
},
307+
{ type: 'text', class: 'history-empty-action' },
308+
)
309+
: button(
310+
t('historyBack'),
311+
() => {
312+
window.location.href = '/';
313+
},
314+
{ type: 'primary', class: 'history-empty-action' },
315+
),
316+
)
271317
.build();
272318

273319
// The two standing facts about this data live under the list, where they read

‎lib/history/recovery.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ function buildBar(doc: HistoryDoc): HTMLElement {
108108
Div()
109109
.class('recovery-bar-text')
110110
.children(
111-
Div().class('recovery-bar-title').text(t('recoveryTitle')).build(),
111+
Div()
112+
.class('recovery-bar-title')
113+
.children(View('i').class('recovery-dot').build(), View('span').text(t('recoveryTitle')).build())
114+
.build(),
112115
Div()
113116
.class('recovery-bar-body')
114117
.text(t('recoveryBody', { title: doc.title, when: formatRelativeTime(doc.updatedAt) }))

‎packages/shared/src/i18n.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export interface I18nMessages {
223223
*/
224224
recoveryDismiss: string;
225225
recoveryViewAll: string;
226+
historyChip: string;
226227
historyTitle: string;
227228
historyIntro: string;
228229
historyColDocument: string;
@@ -231,8 +232,11 @@ export interface I18nMessages {
231232
historyColExpires: string;
232233
historyNotBackup: string;
233234
historySearchPlaceholder: string;
235+
historyEmptyTitle: string;
234236
historyEmpty: string;
237+
historyEmptySearchTitle: string;
235238
historyEmptySearch: string;
239+
historyClearSearch: string;
236240
historyOpen: string;
237241
historyDelete: string;
238242
historyCancel: string;
@@ -336,6 +340,7 @@ const completeMessages: Record<LanguageCode.ZH | LanguageCode.EN, I18nMessages>
336340
recoveryRestore: '继续编辑',
337341
recoveryDismiss: '以后再说',
338342
recoveryViewAll: '全部文档',
343+
historyChip: '本机存储 · 从不上传',
339344
historyTitle: '本机保存的文档',
340345
historyIntro:
341346
'你编辑过的文档,副本保存在这台设备的浏览器里——刷新、误关标签页、浏览器崩溃之后都能找回。这些内容从未上传。',
@@ -345,8 +350,11 @@ const completeMessages: Record<LanguageCode.ZH | LanguageCode.EN, I18nMessages>
345350
historyColExpires: '自动删除',
346351
historyNotBackup: '这些副本是为了让你接着做没做完的事,不是备份——想长期保留,请导出保存到电脑。',
347352
historySearchPlaceholder: '按文件名搜索',
348-
historyEmpty: '这里还是空的。你在本站编辑过的文档会自动出现在这个列表里。',
349-
historyEmptySearch: '没有文件名匹配的文档。',
353+
historyEmptyTitle: '这里还是空的',
354+
historyEmpty: '你在本站编辑过的文档会自动出现在这里,不需要手动保存。',
355+
historyEmptySearchTitle: '没有匹配的文档',
356+
historyEmptySearch: '没有文件名包含这段文字的文档。',
357+
historyClearSearch: '清除搜索',
350358
historyOpen: '打开',
351359
historyDelete: '删除',
352360
historyCancel: '取消',
@@ -439,6 +447,7 @@ const completeMessages: Record<LanguageCode.ZH | LanguageCode.EN, I18nMessages>
439447
recoveryRestore: 'Pick up where I left off',
440448
recoveryDismiss: 'Not now',
441449
recoveryViewAll: 'Saved documents',
450+
historyChip: 'On this device · never uploaded',
442451
historyTitle: 'Saved documents',
443452
historyIntro:
444453
'Copies of the documents you edited, kept in this browser on this device, so a refresh, a closed tab or a crash cannot cost you the work. None of this was uploaded.',
@@ -449,8 +458,11 @@ const completeMessages: Record<LanguageCode.ZH | LanguageCode.EN, I18nMessages>
449458
historyNotBackup:
450459
'These copies exist so you can pick up work you were in the middle of. They are not a backup -- export anything you want to keep.',
451460
historySearchPlaceholder: 'Search by file name',
452-
historyEmpty: 'Nothing saved yet. Documents you edit here will show up in this list on their own.',
453-
historyEmptySearch: 'No file name matches that search.',
461+
historyEmptyTitle: 'Nothing saved yet',
462+
historyEmpty: 'Documents you edit here show up in this list on their own -- there is nothing to save by hand.',
463+
historyEmptySearchTitle: 'No matches',
464+
historyEmptySearch: 'No file name contains that text.',
465+
historyClearSearch: 'Clear search',
454466
historyOpen: 'Open',
455467
historyDelete: 'Delete',
456468
historyCancel: 'Cancel',

‎public/home.css‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,9 +944,11 @@
944944
display: block;
945945
max-width: 62ch;
946946
padding: var(--ran-space-3) var(--ran-space-4);
947-
border: 1px solid var(--ran-color-border);
948947
border-radius: var(--ran-radius-md);
949948
background: var(--ran-color-bg-subtle);
949+
/* Hairline in the shadow layer, matching the document list and the recovery
950+
card: the three surfaces that talk about saved work look like one feature. */
951+
box-shadow: 0 0 0 1px var(--ran-color-border);
950952
font-size: 14px;
951953
line-height: 1.6;
952954
color: var(--ran-color-text-secondary);

‎styles/base.css‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,13 @@ r-dropdown[role='listbox'] {
387387
flex-direction: column;
388388
gap: var(--ran-space-3);
389389
padding: var(--ran-space-4) var(--ran-space-5, 20px);
390-
border: 1px solid var(--ran-color-border);
391390
border-radius: var(--ran-radius-lg);
392391
background: var(--ran-color-bg-elevated);
393-
box-shadow: var(--ran-shadow-modal);
392+
/* The hairline lives in the shadow layer with the elevation, the way the rest
393+
of the site draws its surfaces -- one line, no box-model side effects. */
394+
box-shadow:
395+
0 0 0 1px var(--ran-color-border),
396+
var(--ran-shadow-modal);
394397
color: var(--ran-color-text);
395398
animation: recovery-bar-in 200ms ease-out;
396399
}
@@ -405,10 +408,23 @@ r-dropdown[role='listbox'] {
405408
flex-direction: column;
406409
gap: var(--ran-space-1);
407410
}
411+
/* Dot and title, the site's way of stating a state (homepage chip, the "only
412+
copy here" marker in the document list). */
408413
.recovery-bar-title {
414+
display: flex;
415+
align-items: center;
416+
gap: var(--ran-space-2);
409417
font-weight: 600;
410418
font-size: 15px;
411419
}
420+
.recovery-dot {
421+
width: 6px;
422+
height: 6px;
423+
flex-shrink: 0;
424+
border-radius: var(--ran-radius-full);
425+
background: var(--ran-color-warning);
426+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ran-color-warning) 22%, transparent);
427+
}
412428
.recovery-bar-body {
413429
color: var(--ran-color-text-secondary);
414430
font-size: 13px;

0 commit comments

Comments
 (0)