djm-ui 0.67.4 → 0.72.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 (39) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +38 -2
  3. package/dist/djm-ui.cjs +1032 -347
  4. package/dist/djm-ui.cjs.map +4 -4
  5. package/dist/djm-ui.css +752 -17
  6. package/dist/djm-ui.esm.js +1105 -420
  7. package/dist/djm-ui.esm.js.map +4 -4
  8. package/dist/djm-ui.esm.min.js +373 -227
  9. package/dist/djm-ui.min.cjs +373 -227
  10. package/dist/djm-ui.min.css +2 -2
  11. package/dist/djm-ui.umd.js +944 -259
  12. package/dist/djm-ui.umd.min.js +373 -227
  13. package/dist/mcp/manifest.json +12195 -0
  14. package/dist/mcp/server.js +90 -0
  15. package/package.json +12 -4
  16. package/src/components/icon/index.js +13 -1
  17. package/src/components/rich-editor/index.js +724 -61
  18. package/src/index.js +1 -1
  19. package/src/locale/en-US.js +33 -1
  20. package/src/locale/zh-CN.js +33 -1
  21. package/src/styles/base.css +2 -3
  22. package/src/styles/components/button.css +2 -1
  23. package/src/styles/components/console.css +5 -0
  24. package/src/styles/components/content.css +517 -0
  25. package/src/styles/components/form.css +5 -0
  26. package/src/styles/components/layout.css +326 -0
  27. package/src/styles/components/media-picker.css +3 -3
  28. package/src/styles/components/menu.css +2 -1
  29. package/src/styles/components/org-picker.css +1 -1
  30. package/src/styles/components/responsive.css +86 -0
  31. package/src/styles/components/rich-editor.css +206 -7
  32. package/src/styles/components/skin.css +54 -0
  33. package/src/styles/components/template.css +5 -0
  34. package/src/styles/index.css +1 -0
  35. package/src/styles/theme-dark.css +2 -0
  36. package/src/styles/tokens.css +8 -0
  37. package/src/theme/index.js +40 -0
  38. package/src/utils/sanitize-html.js +44 -6
  39. package/types/index.d.ts +8 -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;
@@ -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';
@@ -38,6 +38,8 @@
38
38
  --djm-text-side: #c2ccd8;
39
39
  --djm-text-2: #a6b2c0;
40
40
  --djm-text-3: #8492a1;
41
+ /* 暗色下提示框比页面背景亮一档,白字仍有足够对比 */
42
+ --djm-tooltip-bg: #3a4652;
41
43
  /* 暗色下水印改用浅色,保持可辨识 */
42
44
  --djm-watermark: rgba(232, 237, 243, 0.12);
43
45
 
@@ -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;
@@ -66,6 +71,9 @@
66
71
  /* 彩色底上的文字(两主题均为白色) */
67
72
  --djm-text-inverse: #fff;
68
73
 
74
+ /* 文字提示的深底:暗色主题改用略亮的灰,避免和页面背景糊在一起 */
75
+ --djm-tooltip-bg: #1f2a37;
76
+
69
77
  /* 盖在图片等媒体内容上的蒙层,不随主题变化 */
70
78
  --djm-media-mask: rgba(9, 20, 33, 0.55);
71
79
 
@@ -139,6 +139,46 @@ export function setPrimaryColor (hex) {
139
139
  }
140
140
  }
141
141
 
142
+ /* 相对亮度与对比度:侧栏选中态按色相自动找合适的明度 */
143
+ function luminance (hex) {
144
+ const text = String(hex).replace('#', '')
145
+ const rgb = [0, 2, 4].map((i) => parseInt(text.slice(i, i + 2), 16) / 255)
146
+ const [r, g, b] = rgb.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
147
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
148
+ }
149
+
150
+ function contrastRatio (a, b) {
151
+ const l1 = luminance(a)
152
+ const l2 = luminance(b)
153
+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
154
+ }
155
+
156
+ /**
157
+ * 侧栏单独配色时用:按主色生成一套色阶
158
+ * 侧栏可能和页面主题反向(浅色页 + 深色侧栏),所以由调用方指定明暗
159
+ */
160
+ export function sidebarShades (hex, dark) {
161
+ const shades = buildShades(hex || '#1a6fc4', dark)
162
+ if (dark) {
163
+ const { h, s } = hexToHsl(hex || '#1a6fc4')
164
+ const sat = Math.min(Math.max(s, 20), 92)
165
+ const bgSat = Math.min(78, sat * 0.85)
166
+ // 选中文字固定在亮档,背景从 36% 明度往下压,直到对比度过 4.5
167
+ // 这样青碧、橙、墨绿不会再选出一团看不清的深色
168
+ const text = hslToHex(h, Math.min(90, sat), 86)
169
+ let bgL = 36
170
+ let bg = hslToHex(h, bgSat, bgL)
171
+ while (bgL > 18 && contrastRatio(bg, text) < 4.6) {
172
+ bgL -= 2
173
+ bg = hslToHex(h, bgSat, bgL)
174
+ }
175
+ shades['--djm-sider-active'] = bg
176
+ shades['--djm-sider-active-hover'] = hslToHex(h, bgSat, Math.max(14, bgL - 4))
177
+ shades['--djm-sider-active-text'] = text
178
+ }
179
+ return shades
180
+ }
181
+
142
182
  export function resetPrimaryColor () {
143
183
  setPrimaryColor('')
144
184
  }
@@ -17,7 +17,13 @@ const TAGS = new Set([
17
17
  ])
18
18
 
19
19
  // 通用属性
20
- const GLOBAL_ATTRS = new Set(['class', 'title'])
20
+ const GLOBAL_ATTRS = new Set(['class', 'title', 'style'])
21
+
22
+ // 富文本用到的内联样式白名单:只放行排版类属性,避免靠样式做定位或引外部资源
23
+ const STYLE_PROPS = new Set([
24
+ 'color', 'background-color', 'font-size', 'font-weight', 'font-style',
25
+ 'text-decoration', 'text-align', 'text-indent', 'line-height'
26
+ ])
21
27
 
22
28
  // 各标签额外允许的属性
23
29
  const TAG_ATTRS = {
@@ -28,13 +34,39 @@ const TAG_ATTRS = {
28
34
  td: new Set(['colspan', 'rowspan'])
29
35
  }
30
36
 
37
+ // 逐条过滤样式声明,丢掉白名单之外的属性与可疑取值
38
+ function cleanStyle (value) {
39
+ return String(value || '')
40
+ .split(';')
41
+ .map((item) => {
42
+ const index = item.indexOf(':')
43
+ if (index === -1) return ''
44
+ const name = item.slice(0, index).trim().toLowerCase()
45
+ const val = item.slice(index + 1).trim()
46
+ if (!STYLE_PROPS.has(name)) return ''
47
+ if (/url\s*\(|expression\s*\(|javascript:/i.test(val)) return ''
48
+ return name + ': ' + val
49
+ })
50
+ .filter(Boolean)
51
+ .join('; ')
52
+ }
53
+
54
+ // 允许的协议:相对地址没有协议,直接放行
55
+ const SAFE_SCHEMES = new Set(['http', 'https', 'mailto', 'tel'])
56
+
31
57
  function safeUrl (value, tag) {
32
58
  // 浏览器解析 URL 时会忽略换行、制表符等控制字符,先去掉再判断协议
33
- const url = String(value || '').replace(/[\u0000-\u0020]+/g, '').toLowerCase()
34
- if (url.startsWith('javascript:') || url.startsWith('vbscript:')) return false
35
- // data: 只允许图片,且只能用于 img,避免链接跳到可执行的 SVG
36
- if (url.startsWith('data:')) return tag === 'img' && url.startsWith('data:image/')
37
- return true
59
+ const url = String(value || '').replace(/[\u0000-\u0020]+/g, '')
60
+ const scheme = url.match(/^([a-z][a-z0-9+.-]*):/i)
61
+ if (!scheme) return true
62
+ const name = scheme[1].toLowerCase()
63
+ if (SAFE_SCHEMES.has(name)) return true
64
+ // 图片额外允许 blob 与 data:image;data:text/html 这类一律拒绝
65
+ if (tag === 'img') {
66
+ if (name === 'blob') return true
67
+ if (name === 'data' && /^data:image\//i.test(url)) return true
68
+ }
69
+ return false
38
70
  }
39
71
 
40
72
  function cleanNode (node) {
@@ -63,6 +95,12 @@ function cleanNode (node) {
63
95
  child.removeAttribute(attr.name)
64
96
  return
65
97
  }
98
+ if (name === 'style') {
99
+ const style = cleanStyle(attr.value)
100
+ if (style) child.setAttribute('style', style)
101
+ else child.removeAttribute('style')
102
+ return
103
+ }
66
104
  if ((name === 'href' || name === 'src') && !safeUrl(attr.value, tag)) {
67
105
  child.removeAttribute(attr.name)
68
106
  }
package/types/index.d.ts CHANGED
@@ -1117,6 +1117,7 @@ export const DjmTreeSelect: DefineComponent<{
1117
1117
 
1118
1118
  export type DjmRichEditorEmits = {
1119
1119
  'update:modelValue': (html: string) => void
1120
+ 'upload-error': (payload: { file: File, message: string }) => void
1120
1121
  }
1121
1122
 
1122
1123
  export const DjmRichEditor: DefineComponent<{
@@ -1125,6 +1126,13 @@ export const DjmRichEditor: DefineComponent<{
1125
1126
  minHeight?: number
1126
1127
  sanitize?: boolean
1127
1128
  disabled?: boolean
1129
+ toolbar?: 'simple' | 'basic' | 'full' | Array<'history' | 'format' | 'inline' | 'color' | 'align' | 'list' | 'insert' | 'view'>
1130
+ uploadImage?: ((file: File) => Promise<string | { url: string }>) | null
1131
+ media?: Array<string | { id?: string | number, url: string, name?: string }>
1132
+ accept?: string
1133
+ maxImageSize?: number
1134
+ multiple?: boolean
1135
+ showCount?: boolean
1128
1136
  }, {}, {}, {}, {}, {}, {}, DjmRichEditorEmits>
1129
1137
 
1130
1138
  /* ---------------- 业务组件 ---------------- */