djm-ui 0.67.4 → 0.73.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +38 -2
  3. package/dist/djm-ui.cjs +1625 -803
  4. package/dist/djm-ui.cjs.map +4 -4
  5. package/dist/djm-ui.css +1043 -41
  6. package/dist/djm-ui.esm.js +1581 -761
  7. package/dist/djm-ui.esm.js.map +4 -4
  8. package/dist/djm-ui.esm.min.js +439 -247
  9. package/dist/djm-ui.min.cjs +439 -247
  10. package/dist/djm-ui.min.css +2 -2
  11. package/dist/djm-ui.umd.js +1104 -285
  12. package/dist/djm-ui.umd.min.js +439 -247
  13. package/dist/mcp/manifest.json +12300 -0
  14. package/dist/mcp/server.js +90 -0
  15. package/package.json +12 -4
  16. package/src/components/drawer/index.js +4 -2
  17. package/src/components/icon/index.js +13 -1
  18. package/src/components/modal/index.js +4 -2
  19. package/src/components/progress/index.js +72 -3
  20. package/src/components/rich-editor/index.js +724 -61
  21. package/src/components/spin/index.js +69 -22
  22. package/src/index.js +2 -1
  23. package/src/locale/en-US.js +33 -1
  24. package/src/locale/zh-CN.js +33 -1
  25. package/src/styles/base.css +2 -3
  26. package/src/styles/components/button.css +2 -1
  27. package/src/styles/components/console.css +5 -0
  28. package/src/styles/components/content.css +517 -0
  29. package/src/styles/components/feedback.css +46 -0
  30. package/src/styles/components/form.css +5 -0
  31. package/src/styles/components/glass.css +58 -0
  32. package/src/styles/components/layout.css +326 -0
  33. package/src/styles/components/media-picker.css +3 -3
  34. package/src/styles/components/menu.css +2 -1
  35. package/src/styles/components/org-picker.css +1 -1
  36. package/src/styles/components/responsive.css +86 -0
  37. package/src/styles/components/rich-editor.css +206 -7
  38. package/src/styles/components/skin.css +54 -0
  39. package/src/styles/components/spin.css +156 -4
  40. package/src/styles/components/template.css +5 -0
  41. package/src/styles/index.css +2 -0
  42. package/src/styles/theme-dark.css +5 -0
  43. package/src/styles/tokens.css +13 -0
  44. package/src/theme/index.js +62 -0
  45. package/src/utils/sanitize-html.js +44 -6
  46. package/types/index.d.ts +17 -0
@@ -3,6 +3,8 @@
3
3
  /* ---------------- 富文本编辑器 ---------------- */
4
4
 
5
5
  .djm-rich-editor {
6
+ display: flex;
7
+ flex-direction: column;
6
8
  border: 1px solid var(--djm-border-strong);
7
9
  border-radius: 4px;
8
10
  overflow: hidden;
@@ -14,11 +16,26 @@
14
16
  box-shadow: var(--djm-ring);
15
17
  }
16
18
 
19
+ /* 全屏:铺满视口,内容区自己滚动 */
20
+ .djm-rich-editor.djm-is-fullscreen {
21
+ position: fixed;
22
+ inset: 0;
23
+ z-index: 1200;
24
+ border: 0;
25
+ border-radius: 0;
26
+ }
27
+
28
+ .djm-rich-editor.djm-is-fullscreen .djm-rich-editor-content,
29
+ .djm-rich-editor.djm-is-fullscreen .djm-rich-editor-source {
30
+ flex: 1;
31
+ max-height: none;
32
+ }
33
+
17
34
  .djm-rich-editor-toolbar {
18
35
  display: flex;
19
36
  align-items: center;
20
37
  flex-wrap: wrap;
21
- gap: 4px;
38
+ gap: 2px;
22
39
  padding: 5px 6px;
23
40
  border-bottom: 1px solid var(--djm-border);
24
41
  background: var(--djm-surface-2);
@@ -26,9 +43,9 @@
26
43
 
27
44
  .djm-rich-editor-group {
28
45
  display: flex;
29
- gap: 2px;
30
- padding-right: 5px;
31
- margin-right: 3px;
46
+ gap: 1px;
47
+ padding-right: 4px;
48
+ margin-right: 2px;
32
49
  border-right: 1px solid var(--djm-border);
33
50
  }
34
51
 
@@ -36,10 +53,92 @@
36
53
  border-right: 0;
37
54
  }
38
55
 
56
+ /* 段落格式 / 字号下拉 */
57
+ .djm-rich-editor-select {
58
+ height: 26px;
59
+ padding: 0 2px;
60
+ border: 1px solid var(--djm-border);
61
+ border-radius: 3px;
62
+ background: var(--djm-surface);
63
+ color: var(--djm-text-2);
64
+ font: inherit;
65
+ font-size: 11.5px;
66
+ cursor: pointer;
67
+ }
68
+
69
+ .djm-rich-editor-select-size {
70
+ width: 64px;
71
+ }
72
+
73
+ .djm-rich-editor-select:focus-visible {
74
+ outline: 2px solid var(--djm-brand-500);
75
+ outline-offset: -1px;
76
+ }
77
+
78
+ .djm-rich-editor-select:disabled {
79
+ opacity: 0.5;
80
+ cursor: not-allowed;
81
+ }
82
+
83
+ /* 颜色按钮:字母底部带当前色,背景色按钮直接把颜色铺在字母上 */
84
+ .djm-rich-editor-color {
85
+ position: relative;
86
+ }
87
+
88
+ .djm-rich-editor-letter {
89
+ display: inline-block;
90
+ padding-bottom: 1px;
91
+ border-bottom: 2px solid var(--djm-text);
92
+ font-size: 12px;
93
+ font-weight: 600;
94
+ line-height: 1;
95
+ }
96
+
97
+ .djm-rich-editor-letter-bg {
98
+ padding: 0 3px;
99
+ border-bottom: 0;
100
+ border-radius: 2px;
101
+ color: var(--djm-text);
102
+ }
103
+
104
+ .djm-rich-editor-panel {
105
+ position: absolute;
106
+ top: calc(100% + 4px);
107
+ left: 0;
108
+ z-index: 5;
109
+ display: grid;
110
+ grid-template-columns: repeat(4, 20px);
111
+ gap: 4px;
112
+ padding: 6px;
113
+ border: 1px solid var(--djm-border);
114
+ border-radius: 6px;
115
+ background: var(--djm-surface);
116
+ box-shadow: var(--djm-shadow-md);
117
+ }
118
+
119
+ .djm-rich-editor-swatch {
120
+ width: 20px;
121
+ height: 20px;
122
+ padding: 0;
123
+ border: 1px solid var(--djm-border);
124
+ border-radius: 4px;
125
+ cursor: pointer;
126
+ }
127
+
128
+ .djm-rich-editor-swatch:hover {
129
+ border-color: var(--djm-brand-500);
130
+ }
131
+
132
+ /* 素材库按钮:媒体选择器默认是块级 span,塞进工具栏里要收成行内 */
133
+ .djm-rich-editor-picker {
134
+ display: inline-flex;
135
+ align-items: center;
136
+ }
137
+
39
138
  .djm-rich-editor-btn {
40
- min-width: 26px;
139
+ min-width: 24px;
41
140
  height: 26px;
42
- padding: 0 6px;
141
+ padding: 0 4px;
43
142
  display: inline-flex;
44
143
  align-items: center;
45
144
  justify-content: center;
@@ -122,11 +221,93 @@
122
221
  color: var(--djm-text-3);
123
222
  }
124
223
 
125
- .djm-rich-editor-content h2 {
224
+ .djm-rich-editor-content h1,
225
+ .djm-rich-editor-content h2,
226
+ .djm-rich-editor-content h3 {
126
227
  margin: 0 0 8px;
228
+ color: var(--djm-text);
229
+ line-height: 1.5;
230
+ }
231
+
232
+ .djm-rich-editor-content h1 {
233
+ font-size: 20px;
234
+ }
235
+
236
+ .djm-rich-editor-content h2 {
127
237
  font-size: 16px;
128
238
  }
129
239
 
240
+ .djm-rich-editor-content h3 {
241
+ font-size: 14px;
242
+ }
243
+
244
+ .djm-rich-editor-content blockquote {
245
+ margin: 0 0 8px;
246
+ padding: 4px 10px;
247
+ border-left: 3px solid var(--djm-brand-500);
248
+ background: var(--djm-surface-2);
249
+ color: var(--djm-text-2);
250
+ }
251
+
252
+ .djm-rich-editor-content pre {
253
+ margin: 0 0 8px;
254
+ padding: 10px 12px;
255
+ overflow: auto;
256
+ border-radius: 4px;
257
+ background: var(--djm-surface-3);
258
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
259
+ font-size: 12px;
260
+ line-height: 1.7;
261
+ white-space: pre-wrap;
262
+ word-break: break-word;
263
+ }
264
+
265
+ .djm-rich-editor-content code {
266
+ padding: 1px 4px;
267
+ border-radius: 3px;
268
+ background: var(--djm-surface-3);
269
+ color: var(--djm-brand-700);
270
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
271
+ font-size: 12px;
272
+ }
273
+
274
+ .djm-rich-editor-content pre code {
275
+ padding: 0;
276
+ background: none;
277
+ color: inherit;
278
+ }
279
+
280
+ .djm-rich-editor-content hr {
281
+ margin: 12px 0;
282
+ border: 0;
283
+ border-top: 1px solid var(--djm-border);
284
+ }
285
+
286
+ /* 图片:单图、多图统一限宽,避免撑破内容区 */
287
+ .djm-rich-editor-content img {
288
+ max-width: 100%;
289
+ height: auto;
290
+ border-radius: 4px;
291
+ vertical-align: bottom;
292
+ }
293
+
294
+ /* 上传插入的图片之间留一点间距,多图并排时不粘在一起 */
295
+ .djm-rich-editor-img {
296
+ margin: 2px 4px 2px 0;
297
+ }
298
+
299
+ /* 上传中的占位块,上传完成后被图片替换 */
300
+ .djm-rich-editor-uploading {
301
+ display: inline-flex;
302
+ align-items: center;
303
+ padding: 4px 10px;
304
+ border: 1px dashed var(--djm-border-strong);
305
+ border-radius: 4px;
306
+ background: var(--djm-surface-2);
307
+ color: var(--djm-text-3);
308
+ font-size: 12px;
309
+ }
310
+
130
311
  .djm-rich-editor-content p {
131
312
  margin: 0 0 8px;
132
313
  }
@@ -154,6 +335,24 @@
154
335
  line-height: 1.7;
155
336
  }
156
337
 
338
+ /* 底部状态:上传提示与字数 */
339
+ .djm-rich-editor-foot {
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: flex-end;
343
+ gap: 12px;
344
+ padding: 4px 12px;
345
+ border-top: 1px solid var(--djm-border);
346
+ background: var(--djm-surface-2);
347
+ color: var(--djm-text-3);
348
+ font-size: 11px;
349
+ }
350
+
351
+ /* 文件选择框只作为上传入口,不显示 */
352
+ .djm-rich-editor-file {
353
+ display: none;
354
+ }
355
+
157
356
  .djm-editor-bar {
158
357
  display: flex;
159
358
  align-items: center;
@@ -18,6 +18,60 @@
18
18
  color: var(--djm-text-3);
19
19
  }
20
20
 
21
+ /* 预设方案:一排小卡片,配色条直接预览效果 */
22
+ .djm-skin-presets {
23
+ display: grid;
24
+ grid-template-columns: repeat(3, minmax(0, 1fr));
25
+ gap: 6px;
26
+ }
27
+
28
+ .djm-skin-preset {
29
+ display: flex;
30
+ flex-direction: column;
31
+ gap: 5px;
32
+ padding: 5px;
33
+ border: 1px solid var(--djm-border);
34
+ border-radius: var(--djm-radius);
35
+ background: var(--djm-surface);
36
+ color: var(--djm-text-2);
37
+ font-size: 11.5px;
38
+ cursor: pointer;
39
+ }
40
+
41
+ .djm-skin-preset:hover {
42
+ border-color: var(--djm-border-strong);
43
+ color: var(--djm-text);
44
+ }
45
+
46
+ .djm-skin-preset.djm-is-active {
47
+ border-color: var(--djm-brand-500);
48
+ box-shadow: var(--djm-ring);
49
+ color: var(--djm-brand-700);
50
+ }
51
+
52
+ .djm-skin-swatch {
53
+ height: 22px;
54
+ border: 1px solid var(--djm-border);
55
+ border-radius: 3px;
56
+ }
57
+
58
+ .djm-skin-preset-label {
59
+ text-align: center;
60
+ }
61
+
62
+ .djm-skin-hint {
63
+ margin: 0;
64
+ font-size: 11.5px;
65
+ color: var(--djm-text-3);
66
+ }
67
+
68
+ .djm-skin-preset:focus-visible,
69
+ .djm-skin-seg-btn:focus-visible,
70
+ .djm-skin-color:focus-visible {
71
+ outline: 2px solid var(--djm-brand-500);
72
+ outline-offset: 1px;
73
+ }
74
+
21
75
  /* 分段按钮:用两个按钮表达互斥选择,避免额外角色语义 */
22
76
  .djm-skin-seg {
23
77
  display: flex;
@@ -1,11 +1,18 @@
1
1
  /* DJM UI · 加载中 */
2
2
 
3
+ .djm-spin {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ color: var(--djm-brand-600);
7
+ }
8
+
9
+ /* 包裹模式:内容上方盖一层遮罩 */
3
10
  .djm-spin-wrap {
4
11
  position: relative;
12
+ display: block;
5
13
  min-height: 60px;
6
14
  }
7
15
 
8
- /* 遮罩铺满被包裹内容,圆角跟随容器 */
9
16
  .djm-spin-mask {
10
17
  position: absolute;
11
18
  inset: 0;
@@ -17,13 +24,35 @@
17
24
  gap: 8px;
18
25
  border-radius: inherit;
19
26
  background: var(--djm-loading-mask);
20
- backdrop-filter: blur(1px);
27
+ -webkit-backdrop-filter: blur(2px);
28
+ backdrop-filter: blur(2px);
29
+ }
30
+
31
+ /* 内联模式:指示器与文字并排,不铺底 */
32
+ .djm-spin-inline .djm-spin-mask {
33
+ position: static;
34
+ flex-direction: row;
35
+ gap: 8px;
36
+ border-radius: 0;
37
+ background: none;
38
+ -webkit-backdrop-filter: none;
39
+ backdrop-filter: none;
21
40
  }
22
41
 
23
- .djm-spin-inline {
42
+ /* 全屏模式:遮罩铺满视口 */
43
+ .djm-spin-fullscreen {
44
+ position: fixed;
45
+ inset: 0;
46
+ z-index: var(--djm-z-toast);
47
+ display: flex;
48
+ }
49
+
50
+ .djm-spin-indicator {
24
51
  display: inline-flex;
25
52
  align-items: center;
26
- gap: 8px;
53
+ justify-content: center;
54
+ gap: 4px;
55
+ color: inherit;
27
56
  }
28
57
 
29
58
  .djm-spin-tip {
@@ -31,6 +60,8 @@
31
60
  color: var(--djm-text-2);
32
61
  }
33
62
 
63
+ /* ---------------- 尺寸 ---------------- */
64
+
34
65
  .djm-spin-sm .djm-spinner {
35
66
  width: 11px;
36
67
  height: 11px;
@@ -42,3 +73,124 @@
42
73
  height: 22px;
43
74
  border-width: 2.5px;
44
75
  }
76
+
77
+ /* ---------------- 形态 ---------------- */
78
+
79
+ /* 环形:颜色跟随 color 属性 */
80
+ .djm-spin-ring .djm-spinner {
81
+ border-top-color: currentColor;
82
+ }
83
+
84
+ /* 点状:三点依次弹起 */
85
+ .djm-spin-dots i {
86
+ width: 6px;
87
+ height: 6px;
88
+ border-radius: 50%;
89
+ background: currentColor;
90
+ animation: djm-spin-dot 1s ease-in-out infinite;
91
+ }
92
+
93
+ .djm-spin-dots i:nth-child(2) {
94
+ animation-delay: 0.15s;
95
+ }
96
+
97
+ .djm-spin-dots i:nth-child(3) {
98
+ animation-delay: 0.3s;
99
+ }
100
+
101
+ .djm-spin-sm .djm-spin-dots i {
102
+ width: 4px;
103
+ height: 4px;
104
+ }
105
+
106
+ .djm-spin-lg .djm-spin-dots i {
107
+ width: 8px;
108
+ height: 8px;
109
+ }
110
+
111
+ @keyframes djm-spin-dot {
112
+ 0%,
113
+ 80%,
114
+ 100% {
115
+ transform: scale(0.55);
116
+ opacity: 0.45;
117
+ }
118
+ 40% {
119
+ transform: scale(1);
120
+ opacity: 1;
121
+ }
122
+ }
123
+
124
+ /* 柱状:四根竖条错峰伸缩 */
125
+ .djm-spin-bars i {
126
+ width: 3px;
127
+ height: 14px;
128
+ border-radius: 2px;
129
+ background: currentColor;
130
+ animation: djm-spin-bar 1s ease-in-out infinite;
131
+ }
132
+
133
+ .djm-spin-bars i:nth-child(2) {
134
+ animation-delay: 0.12s;
135
+ }
136
+
137
+ .djm-spin-bars i:nth-child(3) {
138
+ animation-delay: 0.24s;
139
+ }
140
+
141
+ .djm-spin-bars i:nth-child(4) {
142
+ animation-delay: 0.36s;
143
+ }
144
+
145
+ .djm-spin-sm .djm-spin-bars i {
146
+ width: 2.5px;
147
+ height: 10px;
148
+ }
149
+
150
+ .djm-spin-lg .djm-spin-bars i {
151
+ width: 4px;
152
+ height: 18px;
153
+ }
154
+
155
+ @keyframes djm-spin-bar {
156
+ 0%,
157
+ 100% {
158
+ transform: scaleY(0.45);
159
+ opacity: 0.5;
160
+ }
161
+ 50% {
162
+ transform: scaleY(1);
163
+ opacity: 1;
164
+ }
165
+ }
166
+
167
+ /* 脉冲:实心圆呼吸 */
168
+ .djm-spin-pulse .djm-spin-core {
169
+ width: 16px;
170
+ height: 16px;
171
+ border-radius: 50%;
172
+ background: currentColor;
173
+ animation: djm-spin-pulse 1.2s ease-in-out infinite;
174
+ }
175
+
176
+ .djm-spin-sm .djm-spin-pulse .djm-spin-core {
177
+ width: 12px;
178
+ height: 12px;
179
+ }
180
+
181
+ .djm-spin-lg .djm-spin-pulse .djm-spin-core {
182
+ width: 22px;
183
+ height: 22px;
184
+ }
185
+
186
+ @keyframes djm-spin-pulse {
187
+ 0%,
188
+ 100% {
189
+ transform: scale(0.65);
190
+ opacity: 0.45;
191
+ }
192
+ 50% {
193
+ transform: scale(1);
194
+ opacity: 1;
195
+ }
196
+ }
@@ -194,6 +194,11 @@
194
194
  color: #fff;
195
195
  }
196
196
 
197
+ /* 暗色下品牌色阶整体提亮,白字压不住,单独换深蓝渐变 */
198
+ [data-djm-theme="dark"] .djm-login-brand {
199
+ background: linear-gradient(150deg, #0d2236, #123c5f 62%, #17527f);
200
+ }
201
+
197
202
  .djm-login-brand-inner {
198
203
  max-width: 380px;
199
204
  display: flex;
@@ -75,6 +75,7 @@
75
75
  @import './components/query-filter.css';
76
76
  @import './components/filter-tags.css';
77
77
  @import './components/media-picker.css';
78
+ @import './components/content.css';
78
79
  @import './components/kanban.css';
79
80
  @import './components/attachment-list.css';
80
81
  @import './components/notification.css';
@@ -106,4 +107,5 @@
106
107
  @import './components/grid.css';
107
108
  @import './components/frame.css';
108
109
  @import './components/charts.css';
110
+ @import './components/glass.css';
109
111
  @import './components/responsive.css';
@@ -29,6 +29,9 @@
29
29
  --djm-hover: #262e38;
30
30
  --djm-brand-soft: #1a3049;
31
31
  --djm-glass: rgba(20, 25, 31, 0.94);
32
+ --djm-glass-bg: rgba(25, 31, 38, 0.72);
33
+ --djm-glass-border: rgba(255, 255, 255, 0.12);
34
+ --djm-glass-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.6);
32
35
  --djm-loading-mask: rgba(25, 31, 38, 0.78);
33
36
  --djm-border: #2b333d;
34
37
  --djm-border-strong: #3c4652;
@@ -38,6 +41,8 @@
38
41
  --djm-text-side: #c2ccd8;
39
42
  --djm-text-2: #a6b2c0;
40
43
  --djm-text-3: #8492a1;
44
+ /* 暗色下提示框比页面背景亮一档,白字仍有足够对比 */
45
+ --djm-tooltip-bg: #3a4652;
41
46
  /* 暗色下水印改用浅色,保持可辨识 */
42
47
  --djm-watermark: rgba(232, 237, 243, 0.12);
43
48
 
@@ -15,6 +15,11 @@
15
15
  :root {
16
16
  color-scheme: light;
17
17
 
18
+ /* 字体族:宿主页面可通过覆盖这两个变量统一换字体 */
19
+ --djm-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
20
+ "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
21
+ --djm-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
22
+
18
23
  --djm-brand-50: #eef6fe;
19
24
  --djm-brand-100: #dcebfb;
20
25
  --djm-brand-200: #b9d7f6;
@@ -34,6 +39,11 @@
34
39
  --djm-hover: #eef3f9;
35
40
  --djm-brand-soft: #eaf3fd;
36
41
  --djm-glass: rgba(248, 250, 253, 0.96);
42
+ /* 毛玻璃:默认关闭,data-djm-glass="on" 或 .djm-glass 时生效 */
43
+ --djm-glass-bg: rgba(255, 255, 255, 0.72);
44
+ --djm-glass-border: rgba(255, 255, 255, 0.6);
45
+ --djm-glass-blur: 16px;
46
+ --djm-glass-shadow: 0 10px 30px -12px rgba(16, 40, 70, 0.28);
37
47
  --djm-loading-mask: rgba(255, 255, 255, 0.74);
38
48
  --djm-border: #dde6f0;
39
49
  --djm-border-strong: #cbd7e5;
@@ -66,6 +76,9 @@
66
76
  /* 彩色底上的文字(两主题均为白色) */
67
77
  --djm-text-inverse: #fff;
68
78
 
79
+ /* 文字提示的深底:暗色主题改用略亮的灰,避免和页面背景糊在一起 */
80
+ --djm-tooltip-bg: #1f2a37;
81
+
69
82
  /* 盖在图片等媒体内容上的蒙层,不随主题变化 */
70
83
  --djm-media-mask: rgba(9, 20, 33, 0.55);
71
84
 
@@ -2,8 +2,11 @@ import { ref } from 'vue'
2
2
 
3
3
  const KEY = 'djm-theme'
4
4
  const COLOR_KEY = 'djm-primary'
5
+ const GLASS_KEY = 'djm-glass'
5
6
 
6
7
  export const theme = ref('light')
8
+ // 毛玻璃开关:写入 data-djm-glass,组件样式按需切换
9
+ export const glass = ref(false)
7
10
 
8
11
  /** 预设主题色:色值取自常见后台品牌色 */
9
12
  export const PRESET_COLORS = [
@@ -139,6 +142,46 @@ export function setPrimaryColor (hex) {
139
142
  }
140
143
  }
141
144
 
145
+ /* 相对亮度与对比度:侧栏选中态按色相自动找合适的明度 */
146
+ function luminance (hex) {
147
+ const text = String(hex).replace('#', '')
148
+ const rgb = [0, 2, 4].map((i) => parseInt(text.slice(i, i + 2), 16) / 255)
149
+ const [r, g, b] = rgb.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
150
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
151
+ }
152
+
153
+ function contrastRatio (a, b) {
154
+ const l1 = luminance(a)
155
+ const l2 = luminance(b)
156
+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
157
+ }
158
+
159
+ /**
160
+ * 侧栏单独配色时用:按主色生成一套色阶
161
+ * 侧栏可能和页面主题反向(浅色页 + 深色侧栏),所以由调用方指定明暗
162
+ */
163
+ export function sidebarShades (hex, dark) {
164
+ const shades = buildShades(hex || '#1a6fc4', dark)
165
+ if (dark) {
166
+ const { h, s } = hexToHsl(hex || '#1a6fc4')
167
+ const sat = Math.min(Math.max(s, 20), 92)
168
+ const bgSat = Math.min(78, sat * 0.85)
169
+ // 选中文字固定在亮档,背景从 36% 明度往下压,直到对比度过 4.5
170
+ // 这样青碧、橙、墨绿不会再选出一团看不清的深色
171
+ const text = hslToHex(h, Math.min(90, sat), 86)
172
+ let bgL = 36
173
+ let bg = hslToHex(h, bgSat, bgL)
174
+ while (bgL > 18 && contrastRatio(bg, text) < 4.6) {
175
+ bgL -= 2
176
+ bg = hslToHex(h, bgSat, bgL)
177
+ }
178
+ shades['--djm-sider-active'] = bg
179
+ shades['--djm-sider-active-hover'] = hslToHex(h, bgSat, Math.max(14, bgL - 4))
180
+ shades['--djm-sider-active-text'] = text
181
+ }
182
+ return shades
183
+ }
184
+
142
185
  export function resetPrimaryColor () {
143
186
  setPrimaryColor('')
144
187
  }
@@ -160,6 +203,18 @@ export function toggleTheme () {
160
203
  setTheme(theme.value === 'dark' ? 'light' : 'dark')
161
204
  }
162
205
 
206
+ /** 毛玻璃开关:默认关闭,开启后弹窗、抽屉、浮层与卡片换半透明底 */
207
+ export function setGlass (enabled) {
208
+ glass.value = !!enabled
209
+ document.documentElement.dataset.djmGlass = glass.value ? 'on' : ''
210
+ try {
211
+ if (glass.value) localStorage.setItem(GLASS_KEY, '1')
212
+ else localStorage.removeItem(GLASS_KEY)
213
+ } catch (error) {
214
+ // localStorage 不可用时只影响持久化
215
+ }
216
+ }
217
+
163
218
  /** 应用启动时恢复上次选择;?theme=dark 可临时覆盖,方便截图与分享 */
164
219
  export function initTheme () {
165
220
  let saved = ''
@@ -180,7 +235,14 @@ export function initTheme () {
180
235
  } catch (error) {
181
236
  color = ''
182
237
  }
238
+ let glassSaved = false
239
+ try {
240
+ glassSaved = localStorage.getItem(GLASS_KEY) === '1'
241
+ } catch (error) {
242
+ glassSaved = false
243
+ }
183
244
  if (color) primaryColor.value = color
184
245
  const next = param === 'dark' || param === 'light' ? param : saved || 'light'
185
246
  setTheme(next)
247
+ setGlass(glassSaved)
186
248
  }