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/README.md +103 -0
- package/package.json +28 -0
- package/src/base.js +585 -0
- package/src/feedback.js +269 -0
- package/src/index.js +23 -0
- package/src/ink.js +951 -0
- package/src/layout.js +95 -0
- package/src/menu.js +309 -0
- package/src/nav.js +173 -0
- package/src/tokens.css +88 -0
package/src/feedback.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/* ZhiMo UI 反馈组件:zhimo-modal / zhimo-toast / zhimo-spinner / zhimo-progress */
|
|
2
|
+
|
|
3
|
+
class ZhimoModal extends HTMLElement {
|
|
4
|
+
static observedAttributes = ['open', 'heading'];
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.attachShadow({ mode: 'open' });
|
|
9
|
+
this.shadowRoot.innerHTML = `
|
|
10
|
+
<style>
|
|
11
|
+
:host { display: none; }
|
|
12
|
+
:host([open]) { display: block; }
|
|
13
|
+
.backdrop {
|
|
14
|
+
position: fixed;
|
|
15
|
+
inset: 0;
|
|
16
|
+
z-index: 1000;
|
|
17
|
+
background: var(--zhimo-overlay-bg);
|
|
18
|
+
backdrop-filter: var(--zhimo-overlay-blur);
|
|
19
|
+
-webkit-backdrop-filter: var(--zhimo-overlay-blur);
|
|
20
|
+
animation: fade-in 150ms ease;
|
|
21
|
+
}
|
|
22
|
+
.dialog {
|
|
23
|
+
position: fixed;
|
|
24
|
+
z-index: 1001;
|
|
25
|
+
top: 50%;
|
|
26
|
+
left: 50%;
|
|
27
|
+
transform: translate(-50%, -50%);
|
|
28
|
+
width: min(var(--zhimo-modal-width, 480px), calc(100vw - 32px));
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
background: var(--zhimo-surface-bg);
|
|
31
|
+
backdrop-filter: var(--zhimo-surface-blur);
|
|
32
|
+
-webkit-backdrop-filter: var(--zhimo-surface-blur);
|
|
33
|
+
border: 1px solid var(--zhimo-border-strong);
|
|
34
|
+
border-top: 3px solid var(--zhimo-seal);
|
|
35
|
+
border-radius: var(--zhimo-radius);
|
|
36
|
+
box-shadow: var(--zhimo-shadow-lg);
|
|
37
|
+
font-family: var(--zhimo-font);
|
|
38
|
+
animation: pop-in 180ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
39
|
+
}
|
|
40
|
+
header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
padding: 16px 20px 0;
|
|
45
|
+
font-family: var(--zhimo-font-serif);
|
|
46
|
+
font-size: 17px;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
color: var(--zhimo-fg);
|
|
49
|
+
}
|
|
50
|
+
.body {
|
|
51
|
+
padding: 12px 20px 20px;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
line-height: 1.6;
|
|
54
|
+
color: var(--zhimo-fg-muted);
|
|
55
|
+
}
|
|
56
|
+
footer {
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: flex-end;
|
|
59
|
+
gap: 8px;
|
|
60
|
+
padding: 0 20px 20px;
|
|
61
|
+
}
|
|
62
|
+
.close {
|
|
63
|
+
border: none;
|
|
64
|
+
background: none;
|
|
65
|
+
padding: 4px 8px;
|
|
66
|
+
border-radius: var(--zhimo-radius-sm);
|
|
67
|
+
font-size: 16px;
|
|
68
|
+
line-height: 1;
|
|
69
|
+
color: var(--zhimo-fg-muted);
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: background var(--zhimo-transition), color var(--zhimo-transition);
|
|
72
|
+
}
|
|
73
|
+
.close:hover { background: var(--zhimo-bg-hover); color: var(--zhimo-fg); }
|
|
74
|
+
@keyframes fade-in { from { opacity: 0; } }
|
|
75
|
+
@keyframes pop-in {
|
|
76
|
+
from { opacity: 0; transform: translate(-50%, -50%) scale(0.96); }
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
79
|
+
<div class="backdrop" part="backdrop"></div>
|
|
80
|
+
<div class="dialog" role="dialog" aria-modal="true" part="dialog">
|
|
81
|
+
<header><span class="heading"></span><button class="close" aria-label="关闭">✕</button></header>
|
|
82
|
+
<div class="body"><slot></slot></div>
|
|
83
|
+
<footer><slot name="footer"></slot></footer>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
this.shadowRoot.querySelector('.backdrop').addEventListener('click', () => this.close());
|
|
87
|
+
this.shadowRoot.querySelector('.close').addEventListener('click', () => this.close());
|
|
88
|
+
this._onKeydown = (e) => { if (e.key === 'Escape') this.close(); };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
attributeChangedCallback(name, _old, val) {
|
|
92
|
+
if (name === 'heading') {
|
|
93
|
+
this.shadowRoot.querySelector('.heading').textContent = val ?? '';
|
|
94
|
+
}
|
|
95
|
+
if (name === 'open') {
|
|
96
|
+
if (val !== null) {
|
|
97
|
+
document.addEventListener('keydown', this._onKeydown);
|
|
98
|
+
document.body.style.overflow = 'hidden';
|
|
99
|
+
} else {
|
|
100
|
+
document.removeEventListener('keydown', this._onKeydown);
|
|
101
|
+
document.body.style.overflow = '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
show() { this.setAttribute('open', ''); }
|
|
107
|
+
|
|
108
|
+
close() {
|
|
109
|
+
if (!this.hasAttribute('open')) return;
|
|
110
|
+
this.removeAttribute('open');
|
|
111
|
+
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class ZhimoToast extends HTMLElement {
|
|
116
|
+
constructor() {
|
|
117
|
+
super();
|
|
118
|
+
this.attachShadow({ mode: 'open' });
|
|
119
|
+
this.shadowRoot.innerHTML = `
|
|
120
|
+
<style>
|
|
121
|
+
:host {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 10px;
|
|
125
|
+
min-width: 220px;
|
|
126
|
+
max-width: 360px;
|
|
127
|
+
padding: 12px 16px;
|
|
128
|
+
box-sizing: border-box;
|
|
129
|
+
background: var(--zhimo-surface-bg);
|
|
130
|
+
backdrop-filter: var(--zhimo-surface-blur);
|
|
131
|
+
-webkit-backdrop-filter: var(--zhimo-surface-blur);
|
|
132
|
+
border: 1px solid var(--zhimo-border);
|
|
133
|
+
border-radius: var(--zhimo-radius);
|
|
134
|
+
box-shadow: var(--zhimo-shadow-md);
|
|
135
|
+
font-family: var(--zhimo-font);
|
|
136
|
+
font-size: 14px;
|
|
137
|
+
color: var(--zhimo-fg);
|
|
138
|
+
animation: toast-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
139
|
+
}
|
|
140
|
+
:host([leaving]) { animation: toast-out 200ms ease forwards; }
|
|
141
|
+
/* 印章式小方点 */
|
|
142
|
+
.dot {
|
|
143
|
+
flex: none;
|
|
144
|
+
width: 8px;
|
|
145
|
+
height: 8px;
|
|
146
|
+
border-radius: 1px;
|
|
147
|
+
background: var(--zhimo-fg-muted);
|
|
148
|
+
}
|
|
149
|
+
:host([type="success"]) .dot { background: var(--zhimo-success); }
|
|
150
|
+
:host([type="error"]) .dot { background: var(--zhimo-danger); }
|
|
151
|
+
:host([type="warning"]) .dot { background: var(--zhimo-warning); }
|
|
152
|
+
@keyframes toast-in {
|
|
153
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
154
|
+
}
|
|
155
|
+
@keyframes toast-out {
|
|
156
|
+
to { opacity: 0; transform: translateY(8px); }
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
159
|
+
<span class="dot" part="dot"></span><slot></slot>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let _toastContainer = null;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 弹出一条 Toast 提示。
|
|
168
|
+
* toast('已保存', { type: 'success', duration: 3000 })
|
|
169
|
+
* type: 'default' | 'success' | 'error' | 'warning'
|
|
170
|
+
*/
|
|
171
|
+
export function toast(message, { type = 'default', duration = 3000 } = {}) {
|
|
172
|
+
if (!_toastContainer) {
|
|
173
|
+
_toastContainer = document.createElement('div');
|
|
174
|
+
_toastContainer.style.cssText =
|
|
175
|
+
'position:fixed;bottom:24px;right:24px;z-index:2000;display:flex;flex-direction:column;gap:8px;';
|
|
176
|
+
document.body.appendChild(_toastContainer);
|
|
177
|
+
}
|
|
178
|
+
const el = document.createElement('zhimo-toast');
|
|
179
|
+
el.setAttribute('type', type);
|
|
180
|
+
el.textContent = message;
|
|
181
|
+
_toastContainer.appendChild(el);
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
el.setAttribute('leaving', '');
|
|
184
|
+
el.addEventListener('animationend', () => el.remove(), { once: true });
|
|
185
|
+
}, duration);
|
|
186
|
+
return el;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
class ZhimoSpinner extends HTMLElement {
|
|
190
|
+
constructor() {
|
|
191
|
+
super();
|
|
192
|
+
this.attachShadow({ mode: 'open' });
|
|
193
|
+
this.shadowRoot.innerHTML = `
|
|
194
|
+
<style>
|
|
195
|
+
:host { display: inline-block; width: 20px; height: 20px; }
|
|
196
|
+
:host([size="sm"]) { width: 14px; height: 14px; }
|
|
197
|
+
:host([size="lg"]) { width: 28px; height: 28px; }
|
|
198
|
+
.ring {
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 100%;
|
|
201
|
+
box-sizing: border-box;
|
|
202
|
+
border: 2px solid var(--zhimo-border);
|
|
203
|
+
border-top-color: var(--zhimo-seal);
|
|
204
|
+
border-radius: 50%;
|
|
205
|
+
animation: zhimo-spin 0.7s linear infinite;
|
|
206
|
+
}
|
|
207
|
+
@keyframes zhimo-spin { to { transform: rotate(360deg); } }
|
|
208
|
+
</style>
|
|
209
|
+
<div class="ring" part="ring" role="status" aria-label="加载中"></div>
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class ZhimoProgress extends HTMLElement {
|
|
215
|
+
static observedAttributes = ['value'];
|
|
216
|
+
|
|
217
|
+
constructor() {
|
|
218
|
+
super();
|
|
219
|
+
this.attachShadow({ mode: 'open' });
|
|
220
|
+
this.shadowRoot.innerHTML = `
|
|
221
|
+
<style>
|
|
222
|
+
:host { display: block; }
|
|
223
|
+
.track {
|
|
224
|
+
height: 4px;
|
|
225
|
+
border-radius: 0;
|
|
226
|
+
background: var(--zhimo-border-strong);
|
|
227
|
+
border: none;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
}
|
|
230
|
+
.bar {
|
|
231
|
+
height: 100%;
|
|
232
|
+
width: 0%;
|
|
233
|
+
border-radius: 0;
|
|
234
|
+
background: var(--zhimo-seal);
|
|
235
|
+
transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
236
|
+
}
|
|
237
|
+
:host([indeterminate]) .bar {
|
|
238
|
+
width: 40%;
|
|
239
|
+
animation: zhimo-indeterminate 1.2s ease-in-out infinite;
|
|
240
|
+
}
|
|
241
|
+
@keyframes zhimo-indeterminate {
|
|
242
|
+
0% { transform: translateX(-110%); }
|
|
243
|
+
100% { transform: translateX(280%); }
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
246
|
+
<div class="track" part="track" role="progressbar" aria-valuemin="0" aria-valuemax="100">
|
|
247
|
+
<div class="bar" part="bar"></div>
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
this._bar = this.shadowRoot.querySelector('.bar');
|
|
251
|
+
this._track = this.shadowRoot.querySelector('.track');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
attributeChangedCallback(name, _old, val) {
|
|
255
|
+
if (name === 'value' && !this.hasAttribute('indeterminate')) {
|
|
256
|
+
const v = Math.max(0, Math.min(100, Number(val) || 0));
|
|
257
|
+
this._bar.style.width = `${v}%`;
|
|
258
|
+
this._track.setAttribute('aria-valuenow', String(v));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get value() { return Number(this.getAttribute('value')) || 0; }
|
|
263
|
+
set value(v) { this.setAttribute('value', String(v)); }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
customElements.define('zhimo-modal', ZhimoModal);
|
|
267
|
+
customElements.define('zhimo-toast', ZhimoToast);
|
|
268
|
+
customElements.define('zhimo-spinner', ZhimoSpinner);
|
|
269
|
+
customElements.define('zhimo-progress', ZhimoProgress);
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* ZhiMo UI 统一入口:引入即注册全部组件并注入设计令牌 */
|
|
2
|
+
import './base.js';
|
|
3
|
+
import './layout.js';
|
|
4
|
+
import './nav.js';
|
|
5
|
+
import './menu.js';
|
|
6
|
+
import './ink.js';
|
|
7
|
+
import { toast } from './feedback.js';
|
|
8
|
+
|
|
9
|
+
// tokens.css 定义全局 :root 变量,组件(含 Shadow DOM)靠 CSS 自定义属性继承取色。
|
|
10
|
+
// new URL(..., import.meta.url) 在原生浏览器与打包器(Vite/webpack/Rollup)下都成立,
|
|
11
|
+
// 故零依赖、无构建步骤即可让消费者 `import 'zhimo-ui'` 后样式直接生效,无需手动引样式。
|
|
12
|
+
if (typeof document !== 'undefined' && !document.querySelector('[data-zhimo-tokens]')) {
|
|
13
|
+
const link = document.createElement('link');
|
|
14
|
+
link.rel = 'stylesheet';
|
|
15
|
+
link.href = new URL('./tokens.css', import.meta.url).href;
|
|
16
|
+
link.dataset.zhimoTokens = '';
|
|
17
|
+
document.head.prepend(link);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { toast };
|
|
21
|
+
|
|
22
|
+
// 方便在控制台或非模块脚本里调用:ZhiMo.toast('你好')
|
|
23
|
+
globalThis.ZhiMo = { toast };
|