zhimo-ui 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/src/layout.js ADDED
@@ -0,0 +1,95 @@
1
+ /* ZhiMo UI 布局组件:zhimo-card / zhimo-divider */
2
+
3
+ class ZhimoCard extends HTMLElement {
4
+ constructor() {
5
+ super();
6
+ this.attachShadow({ mode: 'open' });
7
+ this.shadowRoot.innerHTML = `
8
+ <style>
9
+ :host {
10
+ display: block;
11
+ font-family: var(--zhimo-font);
12
+ background: var(--zhimo-surface-bg);
13
+ backdrop-filter: var(--zhimo-surface-blur);
14
+ -webkit-backdrop-filter: var(--zhimo-surface-blur);
15
+ border: 1px solid var(--zhimo-border);
16
+ border-radius: var(--zhimo-radius);
17
+ box-shadow: none;
18
+ overflow: hidden;
19
+ transition: box-shadow var(--zhimo-transition), border-color var(--zhimo-transition),
20
+ transform var(--zhimo-transition);
21
+ }
22
+ :host([hoverable]:hover) {
23
+ box-shadow: var(--zhimo-shadow-md);
24
+ border-color: var(--zhimo-fg);
25
+ transform: translateY(-1px);
26
+ }
27
+ header {
28
+ padding: 14px 18px;
29
+ border-bottom: 1px solid var(--zhimo-border);
30
+ font-family: var(--zhimo-font-serif);
31
+ font-size: 15px;
32
+ font-weight: 600;
33
+ color: var(--zhimo-fg);
34
+ }
35
+ .body {
36
+ padding: 18px;
37
+ font-size: 14px;
38
+ line-height: 1.6;
39
+ color: var(--zhimo-fg);
40
+ }
41
+ footer {
42
+ display: flex;
43
+ justify-content: flex-end;
44
+ gap: 8px;
45
+ padding: 12px 18px;
46
+ border-top: 1px solid var(--zhimo-border);
47
+ background: color-mix(in srgb, var(--zhimo-bg-subtle) 72%, transparent);
48
+ }
49
+ header[hidden], footer[hidden] { display: none; }
50
+ </style>
51
+ <header part="header"><slot name="header"></slot></header>
52
+ <div class="body" part="body"><slot></slot></div>
53
+ <footer part="footer"><slot name="footer"></slot></footer>
54
+ `;
55
+ // header / footer 没有内容时整块隐藏
56
+ for (const name of ['header', 'footer']) {
57
+ const slot = this.shadowRoot.querySelector(`slot[name="${name}"]`);
58
+ const wrap = slot.parentElement;
59
+ const sync = () => { wrap.hidden = slot.assignedNodes().length === 0; };
60
+ slot.addEventListener('slotchange', sync);
61
+ sync();
62
+ }
63
+ }
64
+ }
65
+
66
+ class ZhimoDivider extends HTMLElement {
67
+ constructor() {
68
+ super();
69
+ this.attachShadow({ mode: 'open' });
70
+ this.shadowRoot.innerHTML = `
71
+ <style>
72
+ :host {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 12px;
76
+ margin: 16px 0;
77
+ font-family: var(--zhimo-font-serif);
78
+ font-size: 13px;
79
+ color: var(--zhimo-fg-muted);
80
+ }
81
+ :host::before, :host::after {
82
+ content: '';
83
+ flex: 1;
84
+ height: 1px;
85
+ background: var(--zhimo-border-strong);
86
+ }
87
+ .mark { color: var(--zhimo-seal); font-size: 8px; line-height: 1; }
88
+ </style>
89
+ <slot><span class="mark">◆</span></slot>
90
+ `;
91
+ }
92
+ }
93
+
94
+ customElements.define('zhimo-card', ZhimoCard);
95
+ customElements.define('zhimo-divider', ZhimoDivider);
package/src/menu.js ADDED
@@ -0,0 +1,309 @@
1
+ /* ZhiMo UI 菜单组件:zhimo-dropdown + zhimo-menu-item / zhimo-select + zhimo-option */
2
+
3
+ const PANEL_CSS = `
4
+ .panel {
5
+ position: fixed;
6
+ z-index: 1100;
7
+ min-width: 140px;
8
+ max-height: 60vh;
9
+ overflow-y: auto;
10
+ padding: 4px;
11
+ box-sizing: border-box;
12
+ background: var(--zhimo-surface-bg);
13
+ backdrop-filter: var(--zhimo-surface-blur);
14
+ -webkit-backdrop-filter: var(--zhimo-surface-blur);
15
+ border: 1px solid var(--zhimo-border-strong);
16
+ border-radius: var(--zhimo-radius);
17
+ box-shadow: var(--zhimo-shadow-md);
18
+ font-family: var(--zhimo-font);
19
+ animation: zhimo-panel-in 120ms cubic-bezier(0.16, 1, 0.3, 1);
20
+ }
21
+ .panel[hidden] { display: none; }
22
+ @keyframes zhimo-panel-in { from { opacity: 0; transform: translateY(-4px); } }
23
+ `;
24
+
25
+ /* 浮层共用逻辑:定位、视口收敛、点外关闭、Esc 关闭 */
26
+ class ZhimoPopupBase extends HTMLElement {
27
+ connectedCallback() {
28
+ this._onDocClick = (e) => {
29
+ if (!e.composedPath().includes(this)) this.close();
30
+ };
31
+ this._onKeydown = (e) => {
32
+ if (e.key === 'Escape') { this.close(); }
33
+ else this._handleKey?.(e);
34
+ };
35
+ }
36
+
37
+ disconnectedCallback() {
38
+ this._unbind();
39
+ }
40
+
41
+ _bind() {
42
+ document.addEventListener('pointerdown', this._onDocClick, true);
43
+ document.addEventListener('keydown', this._onKeydown, true);
44
+ }
45
+
46
+ _unbind() {
47
+ document.removeEventListener('pointerdown', this._onDocClick, true);
48
+ document.removeEventListener('keydown', this._onKeydown, true);
49
+ }
50
+
51
+ get open() { return !this._panel.hidden; }
52
+
53
+ openAt(x, y) {
54
+ this._panel.hidden = false;
55
+ this._panel.style.left = '0px';
56
+ this._panel.style.top = '0px';
57
+ this._bind();
58
+ // 先渲染再量尺寸,贴边时往回收
59
+ requestAnimationFrame(() => {
60
+ const { offsetWidth: w, offsetHeight: h } = this._panel;
61
+ this._panel.style.left = `${Math.max(8, Math.min(x, innerWidth - w - 8))}px`;
62
+ this._panel.style.top = `${Math.max(8, Math.min(y, innerHeight - h - 8))}px`;
63
+ });
64
+ this.setAttribute('open', '');
65
+ }
66
+
67
+ openBelow(el) {
68
+ const r = el.getBoundingClientRect();
69
+ this._panel.style.minWidth = `${r.width}px`;
70
+ this.openAt(r.left, r.bottom + 4);
71
+ }
72
+
73
+ close() {
74
+ if (this._panel.hidden) return;
75
+ this._panel.hidden = true;
76
+ this.removeAttribute('open');
77
+ this._unbind();
78
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
79
+ }
80
+ }
81
+
82
+ class ZhimoDropdown extends ZhimoPopupBase {
83
+ constructor() {
84
+ super();
85
+ this.attachShadow({ mode: 'open' });
86
+ this.shadowRoot.innerHTML = `
87
+ <style>
88
+ :host { display: inline-block; }
89
+ ${PANEL_CSS}
90
+ </style>
91
+ <span part="trigger"><slot name="trigger"></slot></span>
92
+ <div class="panel" part="panel" role="menu" hidden><slot></slot></div>
93
+ `;
94
+ this._panel = this.shadowRoot.querySelector('.panel');
95
+
96
+ const trigger = this.shadowRoot.querySelector('[part="trigger"]');
97
+ trigger.addEventListener('click', (e) => {
98
+ e.stopPropagation();
99
+ this.open ? this.close() : this.openBelow(trigger);
100
+ });
101
+
102
+ this.addEventListener('click', (e) => {
103
+ const item = e.target.closest?.('zhimo-menu-item');
104
+ if (!item || item.hasAttribute('disabled')) return;
105
+ this.dispatchEvent(new CustomEvent('select', {
106
+ detail: { value: item.getAttribute('value') }, bubbles: true, composed: true,
107
+ }));
108
+ this.close();
109
+ });
110
+ }
111
+ }
112
+
113
+ class ZhimoMenuItem extends HTMLElement {
114
+ constructor() {
115
+ super();
116
+ this.attachShadow({ mode: 'open' });
117
+ this.shadowRoot.innerHTML = `
118
+ <style>
119
+ :host {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 8px;
123
+ padding: 7px 12px;
124
+ font-family: var(--zhimo-font);
125
+ font-size: 13.5px;
126
+ color: var(--zhimo-fg);
127
+ border-radius: var(--zhimo-radius-sm);
128
+ cursor: pointer;
129
+ user-select: none;
130
+ white-space: nowrap;
131
+ transition: background var(--zhimo-transition);
132
+ }
133
+ :host(:hover) { background: var(--zhimo-bg-hover); }
134
+ :host([danger]) { color: var(--zhimo-danger); }
135
+ :host([disabled]) { color: var(--zhimo-fg-muted); cursor: not-allowed; }
136
+ :host([disabled]:hover) { background: none; }
137
+ </style>
138
+ <slot></slot>
139
+ `;
140
+ }
141
+
142
+ connectedCallback() { this.setAttribute('role', 'menuitem'); }
143
+ }
144
+
145
+ class ZhimoSelect extends ZhimoPopupBase {
146
+ static observedAttributes = ['label', 'placeholder', 'disabled', 'value'];
147
+
148
+ constructor() {
149
+ super();
150
+ this.attachShadow({ mode: 'open' });
151
+ this.shadowRoot.innerHTML = `
152
+ <style>
153
+ :host { display: block; font-family: var(--zhimo-font); }
154
+ :host([disabled]) { opacity: 0.5; pointer-events: none; }
155
+ label {
156
+ display: block;
157
+ font-family: var(--zhimo-font-serif);
158
+ font-size: 13px;
159
+ font-weight: 600;
160
+ color: var(--zhimo-fg);
161
+ margin-bottom: 4px;
162
+ }
163
+ label:empty { display: none; }
164
+ /* 与 zhimo-input 同一张稿纸:一条底线 + 朱砂聚焦 */
165
+ .trigger {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: space-between;
169
+ gap: 8px;
170
+ width: 100%;
171
+ box-sizing: border-box;
172
+ height: 34px;
173
+ padding: 0 2px;
174
+ font-family: inherit;
175
+ font-size: 14px;
176
+ color: var(--zhimo-fg);
177
+ background: transparent;
178
+ border: none;
179
+ border-bottom: 1px solid var(--zhimo-border-strong);
180
+ border-radius: 0;
181
+ cursor: pointer;
182
+ transition: border-color var(--zhimo-transition), box-shadow var(--zhimo-transition);
183
+ }
184
+ .trigger:hover { border-bottom-color: var(--zhimo-fg); }
185
+ .trigger:focus-visible, :host([open]) .trigger {
186
+ outline: none;
187
+ border-bottom-color: var(--zhimo-seal);
188
+ box-shadow: 0 1px 0 var(--zhimo-seal);
189
+ }
190
+ .text.placeholder { color: var(--zhimo-fg-muted); }
191
+ .chevron {
192
+ flex: none;
193
+ font-size: 10px;
194
+ color: var(--zhimo-fg-muted);
195
+ transition: transform var(--zhimo-transition);
196
+ }
197
+ :host([open]) .chevron { transform: rotate(180deg); }
198
+ ${PANEL_CSS}
199
+ </style>
200
+ <label part="label"></label>
201
+ <button class="trigger" part="trigger" aria-haspopup="listbox">
202
+ <span class="text"></span><span class="chevron">▼</span>
203
+ </button>
204
+ <div class="panel" part="panel" role="listbox" hidden><slot></slot></div>
205
+ `;
206
+ this._panel = this.shadowRoot.querySelector('.panel');
207
+ this._labelEl = this.shadowRoot.querySelector('label');
208
+ this._text = this.shadowRoot.querySelector('.text');
209
+ this._trigger = this.shadowRoot.querySelector('.trigger');
210
+
211
+ this._trigger.addEventListener('click', () => {
212
+ this.open ? this.close() : this.openBelow(this._trigger);
213
+ });
214
+ this.shadowRoot.querySelector('.panel slot')
215
+ .addEventListener('slotchange', () => this._syncText());
216
+
217
+ this.addEventListener('click', (e) => {
218
+ const opt = e.target.closest?.('zhimo-option');
219
+ if (!opt || opt.hasAttribute('disabled')) return;
220
+ this._select(opt);
221
+ });
222
+ }
223
+
224
+ attributeChangedCallback(name, _old, val) {
225
+ if (name === 'label') this._labelEl.textContent = val ?? '';
226
+ else this._syncText();
227
+ }
228
+
229
+ /* 键盘:上下移动到相邻可用项并直接选中(与原生 select 行为一致) */
230
+ _handleKey(e) {
231
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
232
+ e.preventDefault();
233
+ const opts = this._options().filter((o) => !o.hasAttribute('disabled'));
234
+ if (!opts.length) return;
235
+ const cur = opts.findIndex((o) => o.getAttribute('value') === this.getAttribute('value'));
236
+ const next = opts[Math.max(0, Math.min(opts.length - 1, cur + (e.key === 'ArrowDown' ? 1 : -1)))];
237
+ this._select(next, { keepOpen: true });
238
+ }
239
+
240
+ _options() { return [...this.querySelectorAll('zhimo-option')]; }
241
+
242
+ _select(opt, { keepOpen = false } = {}) {
243
+ if (!keepOpen) this.close();
244
+ if (opt.getAttribute('value') === this.getAttribute('value')) return;
245
+ this.setAttribute('value', opt.getAttribute('value') ?? '');
246
+ this.dispatchEvent(new CustomEvent('change', {
247
+ detail: { value: this.getAttribute('value') }, bubbles: true, composed: true,
248
+ }));
249
+ }
250
+
251
+ _syncText() {
252
+ const value = this.getAttribute('value');
253
+ const current = this._options().find((o) => o.getAttribute('value') === value);
254
+ this._options().forEach((o) => o.toggleAttribute('selected', o === current));
255
+ this._text.textContent = current?.textContent ?? this.getAttribute('placeholder') ?? '请选择';
256
+ this._text.classList.toggle('placeholder', !current);
257
+ }
258
+
259
+ get value() { return this.getAttribute('value'); }
260
+ set value(v) { v == null ? this.removeAttribute('value') : this.setAttribute('value', String(v)); }
261
+ }
262
+
263
+ class ZhimoOption extends HTMLElement {
264
+ constructor() {
265
+ super();
266
+ this.attachShadow({ mode: 'open' });
267
+ this.shadowRoot.innerHTML = `
268
+ <style>
269
+ :host {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ padding: 7px 12px;
274
+ font-family: var(--zhimo-font);
275
+ font-size: 13.5px;
276
+ color: var(--zhimo-fg);
277
+ border-radius: var(--zhimo-radius-sm);
278
+ cursor: pointer;
279
+ user-select: none;
280
+ white-space: nowrap;
281
+ transition: background var(--zhimo-transition);
282
+ }
283
+ :host(:hover) { background: var(--zhimo-bg-hover); }
284
+ :host([disabled]) { color: var(--zhimo-fg-muted); cursor: not-allowed; }
285
+ :host([disabled]:hover) { background: none; }
286
+ /* 选中项盖一枚小朱砂印 */
287
+ .seal {
288
+ flex: none;
289
+ width: 6px;
290
+ height: 6px;
291
+ border-radius: 1px;
292
+ background: var(--zhimo-seal);
293
+ opacity: 0;
294
+ transition: opacity var(--zhimo-transition);
295
+ }
296
+ :host([selected]) { color: var(--zhimo-seal); }
297
+ :host([selected]) .seal { opacity: 1; }
298
+ </style>
299
+ <span class="seal" part="seal"></span><slot></slot>
300
+ `;
301
+ }
302
+
303
+ connectedCallback() { this.setAttribute('role', 'option'); }
304
+ }
305
+
306
+ customElements.define('zhimo-dropdown', ZhimoDropdown);
307
+ customElements.define('zhimo-menu-item', ZhimoMenuItem);
308
+ customElements.define('zhimo-select', ZhimoSelect);
309
+ customElements.define('zhimo-option', ZhimoOption);
package/src/nav.js ADDED
@@ -0,0 +1,173 @@
1
+ /* ZhiMo UI 导航组件:zhimo-navbar / zhimo-tabs + zhimo-tab / zhimo-breadcrumb */
2
+
3
+ class ZhimoNavbar extends HTMLElement {
4
+ constructor() {
5
+ super();
6
+ this.attachShadow({ mode: 'open' });
7
+ this.shadowRoot.innerHTML = `
8
+ <style>
9
+ :host {
10
+ display: block;
11
+ position: sticky;
12
+ top: 0;
13
+ z-index: 100;
14
+ }
15
+ .bar {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 24px;
19
+ height: 56px;
20
+ padding: 0 24px;
21
+ border-bottom: 1px solid var(--zhimo-border);
22
+ background: var(--zhimo-surface-bg);
23
+ backdrop-filter: var(--zhimo-surface-blur);
24
+ -webkit-backdrop-filter: var(--zhimo-surface-blur);
25
+ font-family: var(--zhimo-font);
26
+ }
27
+ .brand { font-family: var(--zhimo-font-serif); font-size: 16px; font-weight: 700; color: var(--zhimo-fg); }
28
+ .links { display: flex; align-items: center; gap: 4px; flex: 1; }
29
+ /* 朱砂下划线从左游走出来 */
30
+ .links ::slotted(a) {
31
+ font-size: 14px;
32
+ color: var(--zhimo-fg-muted);
33
+ text-decoration: none;
34
+ padding: 6px 10px;
35
+ background: linear-gradient(var(--zhimo-seal), var(--zhimo-seal)) no-repeat;
36
+ background-size: 0% 1px;
37
+ background-position: 10px calc(100% - 2px);
38
+ transition: color var(--zhimo-transition), background-size var(--zhimo-transition);
39
+ }
40
+ .links ::slotted(a:hover) { color: var(--zhimo-fg); background-size: calc(100% - 20px) 1px; }
41
+ .actions { display: flex; align-items: center; gap: 8px; }
42
+ </style>
43
+ <div class="bar" part="bar">
44
+ <span class="brand" part="brand"><slot name="brand"></slot></span>
45
+ <nav class="links" part="links"><slot></slot></nav>
46
+ <div class="actions" part="actions"><slot name="actions"></slot></div>
47
+ </div>
48
+ `;
49
+ }
50
+ }
51
+
52
+ class ZhimoTabs extends HTMLElement {
53
+ constructor() {
54
+ super();
55
+ this.attachShadow({ mode: 'open' });
56
+ this.shadowRoot.innerHTML = `
57
+ <style>
58
+ :host { display: block; font-family: var(--zhimo-font); }
59
+ nav {
60
+ display: flex;
61
+ gap: 4px;
62
+ border-bottom: 1px solid var(--zhimo-border);
63
+ }
64
+ button {
65
+ font-family: var(--zhimo-font);
66
+ font-size: 14px;
67
+ font-weight: 500;
68
+ background: none;
69
+ border: none;
70
+ border-bottom: 2px solid transparent;
71
+ margin-bottom: -1px;
72
+ padding: 10px 14px;
73
+ color: var(--zhimo-fg-muted);
74
+ cursor: pointer;
75
+ transition: color var(--zhimo-transition);
76
+ }
77
+ button:hover { color: var(--zhimo-fg); }
78
+ button.active { color: var(--zhimo-fg); border-bottom-color: var(--zhimo-seal); }
79
+ button:focus-visible {
80
+ outline: none;
81
+ box-shadow: var(--zhimo-focus-ring);
82
+ border-radius: var(--zhimo-radius-sm);
83
+ }
84
+ </style>
85
+ <nav part="list" role="tablist"></nav>
86
+ <div part="panels"><slot></slot></div>
87
+ `;
88
+ this._nav = this.shadowRoot.querySelector('nav');
89
+ this._index = 0;
90
+ this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => this._build());
91
+ }
92
+
93
+ _build() {
94
+ this._tabs = [...this.children].filter((el) => el.tagName === 'ZHIMO-TAB');
95
+ this._nav.innerHTML = '';
96
+ this._tabs.forEach((tab, i) => {
97
+ const btn = document.createElement('button');
98
+ btn.setAttribute('role', 'tab');
99
+ btn.textContent = tab.getAttribute('label') ?? `标签 ${i + 1}`;
100
+ btn.addEventListener('click', () => this.select(i));
101
+ this._nav.appendChild(btn);
102
+ });
103
+ this.select(Math.min(this._index, this._tabs.length - 1), { silent: true });
104
+ }
105
+
106
+ select(index, { silent = false } = {}) {
107
+ if (!this._tabs?.length || index < 0) return;
108
+ this._index = index;
109
+ this._tabs.forEach((tab, i) => { tab.hidden = i !== index; });
110
+ [...this._nav.children].forEach((btn, i) => {
111
+ btn.classList.toggle('active', i === index);
112
+ btn.setAttribute('aria-selected', String(i === index));
113
+ });
114
+ if (!silent) {
115
+ this.dispatchEvent(new CustomEvent('change', {
116
+ detail: { index, label: this._tabs[index].getAttribute('label') },
117
+ bubbles: true, composed: true,
118
+ }));
119
+ }
120
+ }
121
+ }
122
+
123
+ class ZhimoTab extends HTMLElement {
124
+ constructor() {
125
+ super();
126
+ this.attachShadow({ mode: 'open' });
127
+ this.shadowRoot.innerHTML = `
128
+ <style>
129
+ :host {
130
+ display: block;
131
+ padding: 16px 2px;
132
+ font-size: 14px;
133
+ line-height: 1.6;
134
+ color: var(--zhimo-fg);
135
+ }
136
+ :host([hidden]) { display: none; }
137
+ </style>
138
+ <slot></slot>
139
+ `;
140
+ }
141
+ }
142
+
143
+ class ZhimoBreadcrumb extends HTMLElement {
144
+ constructor() {
145
+ super();
146
+ this.attachShadow({ mode: 'open' });
147
+ this.shadowRoot.innerHTML = `
148
+ <style>
149
+ :host { display: block; font-family: var(--zhimo-font); font-size: 14px; }
150
+ nav { display: flex; align-items: center; flex-wrap: wrap; }
151
+ ::slotted(*) {
152
+ color: var(--zhimo-fg-muted);
153
+ text-decoration: none;
154
+ transition: color var(--zhimo-transition);
155
+ }
156
+ ::slotted(a:hover) { color: var(--zhimo-fg); }
157
+ ::slotted(*:last-child) { color: var(--zhimo-fg); font-weight: 500; }
158
+ ::slotted(*:not(:first-child))::before {
159
+ content: '·';
160
+ margin: 0 10px;
161
+ color: var(--zhimo-seal);
162
+ font-weight: 700;
163
+ }
164
+ </style>
165
+ <nav part="nav" aria-label="面包屑"><slot></slot></nav>
166
+ `;
167
+ }
168
+ }
169
+
170
+ customElements.define('zhimo-navbar', ZhimoNavbar);
171
+ customElements.define('zhimo-tabs', ZhimoTabs);
172
+ customElements.define('zhimo-tab', ZhimoTab);
173
+ customElements.define('zhimo-breadcrumb', ZhimoBreadcrumb);
package/src/tokens.css ADDED
@@ -0,0 +1,88 @@
1
+ /* ============================================
2
+ ZhiMo UI 设计令牌 —— 纸张与油墨(编辑部风)
3
+ 米白纸底、墨黑文字、衬线标题,
4
+ 朱砂印章红是全局唯一的强调色。
5
+ 修改这一个文件即可全局换肤。
6
+ ============================================ */
7
+
8
+ :root {
9
+ --zhimo-font: "Songti SC", "Noto Serif SC", "Noto Serif CJK SC",
10
+ "Source Han Serif SC", "SimSun", Georgia, "Times New Roman", serif;
11
+ --zhimo-font-serif: "Songti SC", "Noto Serif SC", "Noto Serif CJK SC",
12
+ "Source Han Serif SC", "SimSun", Georgia, "Times New Roman", serif;
13
+
14
+ /* 纸与墨 */
15
+ --zhimo-bg: #faf7f0;
16
+ --zhimo-bg-subtle: #f3eee1;
17
+ --zhimo-bg-hover: #eee8d8;
18
+ --zhimo-fg: #1a1714;
19
+ --zhimo-fg-muted: #7a7265;
20
+ --zhimo-border: #e0d9c8;
21
+ --zhimo-border-strong: #c6bca6;
22
+
23
+ /* 墨色:实心按钮等主操作 */
24
+ --zhimo-accent: #1a1714;
25
+ --zhimo-accent-hover: #3a342c;
26
+ --zhimo-accent-fg: #faf7f0;
27
+
28
+ /* 朱砂印章红:全局唯一强调色 */
29
+ --zhimo-seal: #b8432f;
30
+ --zhimo-seal-hover: #a03a28;
31
+
32
+ --zhimo-danger: #9e2f1c;
33
+ --zhimo-success: #4a6741;
34
+ --zhimo-warning: #b07d2e;
35
+
36
+ /* 形状与阴影:印刷品棱角分明,几乎不用阴影 */
37
+ --zhimo-radius: 4px;
38
+ --zhimo-radius-sm: 2px;
39
+ --zhimo-radius-full: 9999px;
40
+ --zhimo-shadow-sm: 0 1px 2px rgba(26, 23, 20, 0.06);
41
+ --zhimo-shadow-md: 0 2px 10px rgba(26, 23, 20, 0.1);
42
+ --zhimo-shadow-lg: 0 12px 40px rgba(26, 23, 20, 0.18);
43
+ --zhimo-focus-ring: 0 0 0 3px rgba(184, 67, 47, 0.22);
44
+
45
+ /* 遮罩:墨色薄雾 + 毛玻璃,给浮层一层高级的纸面景深 */
46
+ --zhimo-overlay-bg: rgba(26, 23, 20, 0.32);
47
+ --zhimo-overlay-blur: blur(8px) saturate(1.2);
48
+
49
+ /* 浮层磨砂纸面:所有遮挡水墨背景的面板共用,让墨色透出又不伤可读性 */
50
+ --zhimo-surface-bg: color-mix(in srgb, var(--zhimo-bg) 82%, transparent);
51
+ --zhimo-surface-blur: blur(12px) saturate(1.15);
52
+
53
+ /* 动效 */
54
+ --zhimo-transition: 180ms ease;
55
+ }
56
+
57
+ /* 夜读模式:深茶色纸,不是冷冰冰的纯黑 */
58
+ [data-theme="dark"] {
59
+ --zhimo-bg: #16140f;
60
+ --zhimo-bg-subtle: #1d1a14;
61
+ --zhimo-bg-hover: #282318;
62
+ --zhimo-fg: #e8e2d4;
63
+ --zhimo-fg-muted: #968d7c;
64
+ --zhimo-border: #353023;
65
+ --zhimo-border-strong: #4d4634;
66
+
67
+ --zhimo-accent: #e8e2d4;
68
+ --zhimo-accent-hover: #d6cfbf;
69
+ --zhimo-accent-fg: #16140f;
70
+
71
+ --zhimo-seal: #d96a52;
72
+ --zhimo-seal-hover: #e07e68;
73
+
74
+ --zhimo-danger: #d96a52;
75
+ --zhimo-success: #7d9a72;
76
+ --zhimo-warning: #cfa050;
77
+
78
+ --zhimo-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
79
+ --zhimo-shadow-md: 0 2px 10px rgba(0, 0, 0, 0.5);
80
+ --zhimo-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.6);
81
+ --zhimo-focus-ring: 0 0 0 3px rgba(217, 106, 82, 0.28);
82
+
83
+ --zhimo-overlay-bg: rgba(0, 0, 0, 0.5);
84
+ --zhimo-overlay-blur: blur(8px) saturate(1.2);
85
+
86
+ --zhimo-surface-bg: color-mix(in srgb, var(--zhimo-bg) 80%, transparent);
87
+ --zhimo-surface-blur: blur(12px) saturate(1.15);
88
+ }