@keyfunc/keybricks-ds 2.5.1 → 2.6.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/DEPLOY.md +3 -3
- package/INTEGRATION.md +9 -1
- package/changelog.js +5 -0
- package/components/buttons/BUTTON_MIGRATION.md +111 -0
- package/components/buttons/Button.vue +59 -12
- package/components/buttons/Button.vue.d.ts +44 -0
- package/components/feedback/MESSAGE_SERVICES.md +71 -0
- package/components/feedback/Toast.vue +5 -5
- package/components/feedback/_internal/MessageBoxView.vue +237 -0
- package/components/feedback/_internal/MessageView.vue +73 -0
- package/components/feedback/_internal/service-utils.js +46 -0
- package/components/feedback/message-box.d.ts +69 -0
- package/components/feedback/message-box.js +92 -0
- package/components/feedback/message.d.ts +37 -0
- package/components/feedback/message.js +68 -0
- package/package.json +9 -1
package/DEPLOY.md
CHANGED
|
@@ -55,11 +55,11 @@ deploys the documentation site. Watch it under *Build → Pipelines*.
|
|
|
55
55
|
**5. Cut the first release.**
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
git tag v2.
|
|
58
|
+
git tag v2.6.0 && git push --tags
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
That tag runs `generated:check` → `version:check` → `npm publish`. Confirm under *Deploy →
|
|
62
|
-
Package Registry*: `@keyfunc/keybricks-ds` 2.
|
|
62
|
+
Package Registry*: `@keyfunc/keybricks-ds` 2.6.0.
|
|
63
63
|
|
|
64
64
|
**6. Hand developers two things:** the Pages URL, and the `.npmrc` snippet in §4 below.
|
|
65
65
|
|
|
@@ -187,7 +187,7 @@ GitLab injects `CI_JOB_TOKEN` — use that instead and no secret is stored anywh
|
|
|
187
187
|
**Install — straight from git** (no registry, no publish; works today)
|
|
188
188
|
|
|
189
189
|
```json
|
|
190
|
-
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.
|
|
190
|
+
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.6.0"
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
Because the package has no build step, a git dependency needs no `prepare` script — npm
|
package/INTEGRATION.md
CHANGED
|
@@ -31,7 +31,7 @@ Not using a registry yet? A git dependency works with no publishing at all — t
|
|
|
31
31
|
has no build step, so npm can use the checkout as-is:
|
|
32
32
|
|
|
33
33
|
```json
|
|
34
|
-
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.
|
|
34
|
+
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.6.0"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
**Source-only package.** `.jsx` and `.vue` files are published as authored, so your
|
|
@@ -522,6 +522,14 @@ Pin a minor range and update deliberately:
|
|
|
522
522
|
- **Tokens outside CSS.** `tokens.json` / `tokens.js` / `tokens.d.ts` gained the tier tokens
|
|
523
523
|
(`--control-*`, `--bar-h`, `--group-gap`, …) — 352 light tokens in all (2.0.2 adds `--danger-soft-hover`).
|
|
524
524
|
|
|
525
|
+
### 2.6.0(未发布)
|
|
526
|
+
|
|
527
|
+
- Vue Button 支持原生点击事件、组件 / VNode 图标、图标及加载插槽、独立尾部图标、后备文本和原生按钮类型;旧 actnplanBtn 的适配方式见 [按钮适配说明](components/buttons/BUTTON_MIGRATION.md)。Vue 专用声明位于 `Button.vue.d.ts`,React 接口保持原样。
|
|
528
|
+
- Vue 新增 `@keyfunc/keybricks-ds/vue/message` 和 `@keyfunc/keybricks-ds/vue/message-box` 命令式服务入口;提供消息堆叠与分组,以及 alert / confirm / prompt、输入校验、关闭拦截、应用上下文和可配置默认文案。
|
|
529
|
+
- 复用现有 DS 控件,补充 Toast 默认内容、图标插槽及可配置关闭按钮标签。
|
|
530
|
+
- 与 Element Plus 的 API 对照、接入示例和兼容边界见 [消息服务迁移说明](components/feedback/MESSAGE_SERVICES.md)。不承诺所有 Element Plus 功能及样式完全兼容。
|
|
531
|
+
- 本次仅修改源码及发布元数据;未执行测试、类型检查、构建、生成脚本或浏览器验证,发布前需同步生成产物中的版本元数据。
|
|
532
|
+
|
|
525
533
|
### 2.5.1
|
|
526
534
|
|
|
527
535
|
Fixes only — no API removed.
|
package/changelog.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Each item: tag (new | improved | fixed | breaking | removed) and the text as [en, zh, ja].
|
|
4
4
|
INTEGRATION.md keeps the full migration notes; this list is the short, reader-facing summary. */
|
|
5
5
|
window.KB_CHANGELOG = [
|
|
6
|
+
{ v: "2.6.0", date: "2026-11-02", status: "unreleased", items: [
|
|
7
|
+
{ tag: "improved", t: ["Vue Button now forwards native click events and supports component / VNode icons, icon / loading / trailing slots, trailing icons, native button types and aria-busy.", "Vue Button 补充原生点击事件、组件 / VNode 图标、icon / loading / trailing 插槽、独立尾部图标、原生按钮类型及 aria-busy。", "Vue Button がネイティブ click イベント、コンポーネント / VNode アイコン、icon / loading / trailing スロット、末尾アイコン、ボタン種別と aria-busy に対応。"] },
|
|
8
|
+
{ tag: "new", t: ["Vue Message service: timed and grouped Toast notifications, stacking, close handles, VNode content and app context.", "Vue 新增 Message 服务:支持自动关闭、消息分组与堆叠、关闭句柄、VNode 内容及应用上下文。", "Vue Message サービスを追加:自動終了、グループ化、スタック、終了ハンドル、VNode とアプリコンテキストに対応。"] },
|
|
9
|
+
{ tag: "new", t: ["Vue MessageBox service: alert / confirm / prompt, queued dialogs, validation, beforeClose, focus management and configurable labels. See the migration guide for compatibility limits.", "Vue 新增 MessageBox 服务:支持 alert / confirm / prompt、弹窗队列、输入校验、beforeClose、焦点管理及可配置文案;兼容边界见迁移说明。", "Vue MessageBox サービスを追加:alert / confirm / prompt、キュー、入力検証、beforeClose、フォーカス管理とラベル設定。互換性の制限は移行ガイドを参照。"] },
|
|
10
|
+
] },
|
|
6
11
|
{ v: "2.5.1", date: "2026-10-30", items: [
|
|
7
12
|
{ tag: "fixed", t: ["Keyboard support: Checkbox, DatePicker, MultiSelect, Accordion, Dropzone, pipeline nodes and palette, selectable bar lists, and the Sunburst / World map charts can all be used without a mouse.", "键盘支持:复选框、日期选择、多选、折叠面板、上传区、管道节点与节点面板、可点选的条形列表,以及旭日图 / 世界地图,都可以不用鼠标操作。", "キーボード対応:チェックボックス、日付選択、マルチセレクト、アコーディオン、ドロップゾーン、パイプラインのノードとパレット、選択可能なバーリスト、サンバースト / 世界地図をマウスなしで操作可能に。"] },
|
|
8
13
|
{ tag: "fixed", t: ["Long text no longer spills: field values, menu items, segments, tree rows and navigation names truncate; drawer titles wrap.", "长文字不再溢出:输入框的值、菜单项、分段选择、树形列表和导航名称会截断;侧滑面板标题会换行。", "長いテキストがはみ出さないように:入力値、メニュー項目、セグメント、ツリー行、ナビゲーション名は省略表示、ドロワーのタイトルは折り返し。"] },
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Vue Button 与旧按钮适配
|
|
2
|
+
|
|
3
|
+
2.6.0(未发布)为 Vue Button 补充适配旧按钮所需的通用能力。
|
|
4
|
+
原有图标名、variant、默认插槽用法继续支持;本次没有修改 React Button。
|
|
5
|
+
本次只修改源码与文档,没有执行测试、类型检查、构建或浏览器验证,不能据此宣称已完美替换业务组件。
|
|
6
|
+
|
|
7
|
+
## 新增接口
|
|
8
|
+
|
|
9
|
+
| 接口 | 行为 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `@click` | 传递原生 MouseEvent;disabled 或 loading 时不发出事件 |
|
|
12
|
+
| `nativeType` | `button / submit / reset`,默认仍为 button |
|
|
13
|
+
| `text` | 默认插槽的后备文本,已有插槽优先 |
|
|
14
|
+
| `icon` | 原 DS 图标名之外,额外接受 Vue 组件或 VNode |
|
|
15
|
+
| `trailingIcon` | 独立右侧图标;loading 时继续显示,适合 popup 下拉箭头 |
|
|
16
|
+
| `#icon` | 覆盖主图标,可放旧 svgicon、字体图标或自定义 SVG;随 loading 隐藏 |
|
|
17
|
+
| `#loading` | 覆盖加载图标;默认继续使用 DS spinner |
|
|
18
|
+
| `#trailing` | 覆盖独立尾部内容,不随 loading 隐藏 |
|
|
19
|
+
| `aria-busy` | 与 loading 同步;原生 disabled 仍由 disabled 或 loading 控制 |
|
|
20
|
+
|
|
21
|
+
icon / loading 插槽接收 `{ size, style }`,trailing 额外接收 loading。
|
|
22
|
+
默认继承当前尺寸区;显式 iconSize 优先于尺寸区。自定义插槽中的图标由调用方应用传入的尺寸。
|
|
23
|
+
class、id、title、style、原生表单属性以及其他未声明属性继续透传到同一个原生 button 根节点。
|
|
24
|
+
Vue 专用类型文件为 `Button.vue.d.ts`,`Button.d.ts` 仍是 React 类型。
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<template>
|
|
28
|
+
<KbButton
|
|
29
|
+
:loading="saving"
|
|
30
|
+
:icon="SaveIcon"
|
|
31
|
+
trailing-icon="chevron-down"
|
|
32
|
+
@click="save($event)"
|
|
33
|
+
>
|
|
34
|
+
保存
|
|
35
|
+
</KbButton>
|
|
36
|
+
</template>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## actnplanBtn 的适配边界
|
|
40
|
+
|
|
41
|
+
建议保留 `actnplanBtn.vue` 的路径、props、事件和兼容 class,内部改为 KbButton。
|
|
42
|
+
DS 接收通用接口;`regular / negative / warning / inbody / popup` 以及旧图标名由应用转换。
|
|
43
|
+
|
|
44
|
+
| 原配置 | 应用层转换 |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| regular | variant=primary |
|
|
47
|
+
| regular + inbody | variant=outline |
|
|
48
|
+
| negative | variant=outline;它通常表示取消,不是危险操作 |
|
|
49
|
+
| warning | variant=danger |
|
|
50
|
+
| disabled / loading | 同名属性,loading 时主图标替换为 spinner,文字保留 |
|
|
51
|
+
| popup | trailingIcon=chevron-down,loading 时仍显示箭头 |
|
|
52
|
+
| text | 保留 `<span class="text">` 默认插槽,兼容已有选择器 |
|
|
53
|
+
| click | 继续传出原生事件,不吞掉 `$event`、冒泡或表单默认行为 |
|
|
54
|
+
| class / id / style | 透传;保留 btn、actnplanButton、原 type class 和 inbody class |
|
|
55
|
+
|
|
56
|
+
旧 actnplanBtn 没有指定原生 button type,在表单里通常默认提交;严格兼容时适配层应设置 nativeType=submit,并核对各表单的实际使用。
|
|
57
|
+
不能把旧业务 type 直接绑定到原生 type。外部已经通过原生属性控制 form、name、value 的场景也应保留。
|
|
58
|
+
|
|
59
|
+
## 图标映射
|
|
60
|
+
|
|
61
|
+
映射应独立放在应用的 `actnplanButtonIcons.ts`,不要塞进 DS 的通用图标字典。
|
|
62
|
+
应用先从空格分隔的 class 中提取 glyphicon-*,因此 `glyphicon glyphicon-refresh` 与 `glyphicon-refresh` 进入相同映射。
|
|
63
|
+
|
|
64
|
+
| 旧图标 | 建议的 DS 图标 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| 1em-ok / glyphicon-ok | check |
|
|
67
|
+
| 1em-cancel / glyphicon-remove | x |
|
|
68
|
+
| 1em-add / glyphicon-plus | plus |
|
|
69
|
+
| glyphicon-floppy-disk | save |
|
|
70
|
+
| 1em-delete / glyphicon-trash | trash-2 |
|
|
71
|
+
| 1em-refresh / glyphicon-refresh | refresh-cw |
|
|
72
|
+
| 1em-clone | copy |
|
|
73
|
+
| glyphicon-search | search |
|
|
74
|
+
| glyphicon-menu-down | chevron-down |
|
|
75
|
+
|
|
76
|
+
已经确认对应关系的图标转换为 DS 名称。业务专用 SVG 或无可靠对应的 Glyphicon 通过 icon 插槽保留;不要把未知旧名称直接传入 DS Icon,否则会显示空图标。
|
|
77
|
+
动态 icon 使用 computed 解析;旧 SVG 使用 watch 加载并忽略过期请求,避免切换按钮状态后显示旧图标。
|
|
78
|
+
旧 fill 样式仅作用于旧 SVG,DS 图标保留 fill=none、stroke=currentColor。
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<template>
|
|
82
|
+
<KbButton
|
|
83
|
+
class="btn actnplanButton"
|
|
84
|
+
:class="[type, { inbody }]"
|
|
85
|
+
:variant="variant"
|
|
86
|
+
:disabled="disabled"
|
|
87
|
+
:loading="loading"
|
|
88
|
+
:trailing-icon="popup ? 'chevron-down' : undefined"
|
|
89
|
+
native-type="submit"
|
|
90
|
+
@click="emit('click', $event)"
|
|
91
|
+
>
|
|
92
|
+
<template v-if="icon" #icon="{ size, style }">
|
|
93
|
+
<AppIcon v-if="mappedIcon" :name="mappedIcon" :size="size" :style="style" />
|
|
94
|
+
<span v-else-if="isGlyph" class="glyphicon" :class="glyphClass" aria-hidden="true" />
|
|
95
|
+
<svgicon v-else-if="svgData" :data="svgData" :fill="false" :style="style" />
|
|
96
|
+
</template>
|
|
97
|
+
<span class="text">{{ text }}</span>
|
|
98
|
+
</KbButton>
|
|
99
|
+
</template>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
以上为应用适配层的模板示意,其中 variant、mappedIcon 等由应用解析。
|
|
103
|
+
图标放在按钮内部即可,不需要 svgContainer,避免把一个按钮拆成多个点击区域。
|
|
104
|
+
|
|
105
|
+
## 接入顺序
|
|
106
|
+
|
|
107
|
+
先发布包含本次改动的 DS,再升级应用安装版本,最后修改 actnplanBtn 适配层并移除旧的按钮视觉样式。
|
|
108
|
+
应用所有版本都需要加载 DS 样式,不能只依赖 V2 的适配器注册入口。
|
|
109
|
+
同时清理 runtime/modal.vue 中临时添加的默认按钮视觉覆盖,让按钮外观由 DS 统一管理;默认按钮位置可继续由 modal 决定。
|
|
110
|
+
客户主题 mixin 和外部 `.actnplanButton` 规则需要逐项处理,不能继续用旧的 disabled / fill / hover 规则覆盖新按钮。
|
|
111
|
+
源码修改不会自动影响应用已安装的旧版本;本轮未切换 actnplanBtn,也未修改应用依赖、lock 文件或生成产物。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { px } from '../px.js'
|
|
3
|
-
import { computed, ref } from 'vue'
|
|
3
|
+
import { cloneVNode, computed, h, isVNode, ref } from 'vue'
|
|
4
4
|
import Icon from '../icons/Icon.vue'
|
|
5
5
|
|
|
6
6
|
// Every dimension comes from the size tier the button sits in (tokens/layout.css): height, text, icon and padding
|
|
@@ -25,11 +25,13 @@ const colorHoverVariants = { text: 'var(--text-heading)' }
|
|
|
25
25
|
// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm 28 | md 32 | lg 40 — omit it to inherit the region's tier (the default).
|
|
26
26
|
const props = defineProps({
|
|
27
27
|
variant: { type: String, default: 'primary' }, size: String,
|
|
28
|
-
icon: String,
|
|
28
|
+
icon: [String, Object, Function], trailingIcon: [String, Object, Function],
|
|
29
|
+
iconSize: Number, iconPosition: { type: String, default: 'left' },
|
|
29
30
|
medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,
|
|
30
|
-
label: String, pressed: Boolean, focused: Boolean, style: Object,
|
|
31
|
+
label: String, text: String, pressed: Boolean, focused: Boolean, style: Object,
|
|
32
|
+
nativeType: { type: String, default: 'button', validator: value => ['button', 'submit', 'reset'].includes(value) },
|
|
31
33
|
})
|
|
32
|
-
defineEmits(['click'])
|
|
34
|
+
const emit = defineEmits(['click'])
|
|
33
35
|
const hover = ref(false), press = ref(false), focus = ref(false)
|
|
34
36
|
const busy = computed(() => props.disabled || props.loading)
|
|
35
37
|
const isPressed = computed(() => press.value || props.pressed)
|
|
@@ -52,15 +54,60 @@ const s = computed(() => {
|
|
|
52
54
|
})
|
|
53
55
|
const iSize = computed(() => props.iconSize || 16)
|
|
54
56
|
const iconStyle = computed(() => props.iconSize ? null : tierIcon)
|
|
57
|
+
|
|
58
|
+
// 旧 SVG、组件和 VNode 都可以由适配层传入,业务图标名称不进入 DS。
|
|
59
|
+
function glyph(value) {
|
|
60
|
+
if (!value) return null
|
|
61
|
+
const attrs = {
|
|
62
|
+
size: iSize.value, width: iSize.value, height: iSize.value, 'aria-hidden': true, focusable: false,
|
|
63
|
+
style: { display: 'block', flexShrink: 0, width: px(iSize.value), height: px(iSize.value), ...iconStyle.value },
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'string') return h(Icon, { ...attrs, name: value, size: iSize.value })
|
|
66
|
+
return isVNode(value) ? cloneVNode(value, attrs) : h(value, attrs)
|
|
67
|
+
}
|
|
68
|
+
const LeadingIcon = () => glyph(props.icon)
|
|
69
|
+
const TrailingIcon = () => glyph(props.trailingIcon)
|
|
70
|
+
|
|
71
|
+
function onClick(event) {
|
|
72
|
+
if (busy.value) return
|
|
73
|
+
emit('click', event)
|
|
74
|
+
}
|
|
55
75
|
</script>
|
|
56
76
|
<template>
|
|
57
|
-
<button
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
<button
|
|
78
|
+
:type="nativeType"
|
|
79
|
+
data-press="own"
|
|
80
|
+
:data-size="size"
|
|
81
|
+
:style="s"
|
|
82
|
+
:disabled="busy"
|
|
83
|
+
:aria-busy="loading"
|
|
84
|
+
:aria-label="iconOnly ? label : undefined"
|
|
85
|
+
@click="onClick"
|
|
86
|
+
@mouseenter="hover = true"
|
|
87
|
+
@mouseleave="hover = false; press = false"
|
|
88
|
+
@mousedown="press = true"
|
|
89
|
+
@mouseup="press = false"
|
|
90
|
+
@keydown.enter="press = true"
|
|
91
|
+
@keydown.space="press = true"
|
|
92
|
+
@keyup="press = false"
|
|
93
|
+
@focus="focus = $event.target.matches(':focus-visible')"
|
|
94
|
+
@blur="focus = false"
|
|
95
|
+
>
|
|
96
|
+
<slot v-if="loading" name="loading" :size="iSize" :style="iconStyle">
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
:style="{ display: 'inline-block', width: px(iSize), height: px(iSize), ...iconStyle, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }"
|
|
100
|
+
/>
|
|
101
|
+
</slot>
|
|
102
|
+
<slot v-else-if="iconPosition === 'left'" name="icon" :size="iSize" :style="iconStyle">
|
|
103
|
+
<LeadingIcon v-if="icon" />
|
|
104
|
+
</slot>
|
|
105
|
+
<slot v-if="!iconOnly">{{ text }}</slot>
|
|
106
|
+
<slot v-if="!loading && iconPosition === 'right'" name="icon" :size="iSize" :style="iconStyle">
|
|
107
|
+
<LeadingIcon v-if="icon" />
|
|
108
|
+
</slot>
|
|
109
|
+
<slot name="trailing" :size="iSize" :style="iconStyle" :loading="loading">
|
|
110
|
+
<TrailingIcon v-if="trailingIcon" />
|
|
111
|
+
</slot>
|
|
65
112
|
</button>
|
|
66
113
|
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Component, ComponentOptionsMixin, CSSProperties, DefineComponent, PublicProps, SlotsType, VNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
/** Vue 专用声明;同目录 Button.d.ts 继续描述 React 接口。 */
|
|
4
|
+
export interface VueButtonProps {
|
|
5
|
+
variant?: 'primary' | 'dark' | 'outline' | 'ghost' | 'primary-soft' | 'danger' | 'danger-soft' | 'text';
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
icon?: string | Component | VNode;
|
|
8
|
+
/** 不随 loading 隐藏,适用于下拉箭头等独立的尾部标记。 */
|
|
9
|
+
trailingIcon?: string | Component | VNode;
|
|
10
|
+
iconSize?: number;
|
|
11
|
+
iconPosition?: 'left' | 'right';
|
|
12
|
+
medium?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
fullWidth?: boolean;
|
|
16
|
+
iconOnly?: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
/** 默认插槽的后备文本。 */
|
|
19
|
+
text?: string;
|
|
20
|
+
pressed?: boolean;
|
|
21
|
+
focused?: boolean;
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
/** 原生按钮类型,默认 button;旧表单适配时可明确传入 submit。 */
|
|
24
|
+
nativeType?: 'button' | 'submit' | 'reset';
|
|
25
|
+
onClick?: (event: MouseEvent) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface VueButtonIconSlotProps {
|
|
29
|
+
size: number;
|
|
30
|
+
style: CSSProperties | null;
|
|
31
|
+
}
|
|
32
|
+
export interface VueButtonSlots {
|
|
33
|
+
default?: () => VNode[];
|
|
34
|
+
icon?: (props: VueButtonIconSlotProps) => VNode[];
|
|
35
|
+
loading?: (props: VueButtonIconSlotProps) => VNode[];
|
|
36
|
+
trailing?: (props: VueButtonIconSlotProps & { loading: boolean }) => VNode[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare const Button: DefineComponent<
|
|
40
|
+
VueButtonProps, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin,
|
|
41
|
+
{ click: (event: MouseEvent) => void }, string, PublicProps,
|
|
42
|
+
Readonly<VueButtonProps>, {}, SlotsType<VueButtonSlots>
|
|
43
|
+
>;
|
|
44
|
+
export default Button;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Vue 消息服务与 Element Plus 迁移
|
|
2
|
+
|
|
3
|
+
2.6.0 新增源码入口,适用于 Vue 3,复用 Keybricks Toast、Button、IconButton 和 Icon。
|
|
4
|
+
本次仅补齐源码,未执行测试、类型检查、构建或浏览器验证,也尚未发布。
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import Message from '@keyfunc/keybricks-ds/vue/message'
|
|
8
|
+
import MessageBox from '@keyfunc/keybricks-ds/vue/message-box'
|
|
9
|
+
import '@keyfunc/keybricks-ds/styles.css'
|
|
10
|
+
|
|
11
|
+
const notice = Message({ message: '保存成功', type: 'success', showClose: true })
|
|
12
|
+
notice.close()
|
|
13
|
+
Message.warning({ message: '请检查输入', duration: 6000 })
|
|
14
|
+
Message.closeAll()
|
|
15
|
+
|
|
16
|
+
await MessageBox.confirm('确认删除?', '删除', {
|
|
17
|
+
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning',
|
|
18
|
+
})
|
|
19
|
+
const { value } = await MessageBox.prompt('请输入名称', '新建', {
|
|
20
|
+
inputValidator: value => value.trim() ? true : '名称不能为空',
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 已提供的能力
|
|
25
|
+
|
|
26
|
+
| 原接口 | 新服务 |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `ElMessage(options / string / VNode)` | `Message(...)`,返回 `{ close() }` |
|
|
29
|
+
| `success / info / warning / error` | 同名快捷方法 |
|
|
30
|
+
| `duration / showClose / offset / zIndex / customClass / center` | 支持;默认 3000 毫秒,0 不自动关闭 |
|
|
31
|
+
| 消息堆叠与 `grouping` | 同一挂载目标按真实高度向下堆叠;分组显示重复次数并重置计时 |
|
|
32
|
+
| 悬停与焦点暂停计时 | 支持,离开后重新计时 |
|
|
33
|
+
| `onClose / closeAll(type?)` | 支持,手动及自动关闭均清理实例 |
|
|
34
|
+
| `MessageBox(options)` | 返回 Promise,弹窗按调用顺序排队 |
|
|
35
|
+
| `alert / confirm / prompt` | 支持 `(message, title?, options?, appContext?)` 和 `(message, options?, appContext?)` |
|
|
36
|
+
| 确认、取消、关闭 | 确认 resolve;取消 reject `'cancel'`;关闭默认 reject `'cancel'`,开启 `distinguishCancelAndClose` 后 reject `'close'` |
|
|
37
|
+
| 输入结果 | `showInput` 开启时确认返回 `{ value, action: 'confirm' }`,包括 confirm 配合 `showInput` 的场景 |
|
|
38
|
+
| `inputValue / inputType / inputPlaceholder` | 支持,`textarea` 支持换行 |
|
|
39
|
+
| `inputPattern / inputValidator / inputErrorMessage` | 支持;额外支持异步校验,输入变更或关闭后忽略过期结果 |
|
|
40
|
+
| `beforeClose(action, instance, done)` | 调用 done 才关闭;instance 提供可写的输入值、按钮文案、loading、disabled 等状态 |
|
|
41
|
+
| `callback` | 有 callback 时按原服务方式回调,不结算返回的 Promise;开启输入时回调第一个参数为 `{ value, action }` |
|
|
42
|
+
| `MessageBox.close()` | 清空队列,不调用操作回调,不结算已有 Promise |
|
|
43
|
+
| 按钮显示、文案、class、loading、disabled | 支持;`el-button--danger` 显式映射为 DS danger 按钮 |
|
|
44
|
+
| `closeOnClickModal / closeOnPressEscape / showClose` | 分别控制遮罩、Esc、关闭按钮;alert 默认禁用遮罩和 Esc 关闭 |
|
|
45
|
+
| 键盘及滚动 | 焦点圈定、初始聚焦、关闭还原焦点、引用计数锁定 body 滚动 |
|
|
46
|
+
| 内容及图标 | 消息支持字符串、VNode、返回 VNode 的函数;icon 支持 DS 图标名、Vue 组件或 VNode |
|
|
47
|
+
| `appendTo / appContext` | 支持挂载目标和应用上下文,后者作为第二参数或快捷方法最后一个参数传入 |
|
|
48
|
+
|
|
49
|
+
## 兼容边界
|
|
50
|
+
|
|
51
|
+
这不是 Element Plus 的完整同构实现,不能宣称任意项目可完美替换。
|
|
52
|
+
|
|
53
|
+
- 默认采用 DS 外观和 class;原来的 `.el-message`、`.el-message-box` 等选择器及依赖其 DOM 的逻辑需要调整。`customClass` 会保留,应用需检查类名对应的具体规则。
|
|
54
|
+
- 不读取 Element Plus 的 ConfigProvider、locale、全局 zIndex 或应用插件配置。可通过 `Message.defaults` 和 `MessageBox.defaults` 设置文案、层级、挂载目标;切换语言时更新 defaults。默认按钮文案为英文。
|
|
55
|
+
- 默认层级采用 `--z-toast / --z-modal`。与 Element Plus 弹层混用时,应用应根据现有弹层层级设置 defaults.zIndex。
|
|
56
|
+
- 不提供 `draggable / overflow / buttonSize / closeIcon / loadingIcon / showHeader`、消息 `placement` 等完整扩展接口;`plain` 当前仅移除消息阴影。实例不是 Element Plus 的组件内部实例,仅提供文档约定的状态。
|
|
57
|
+
- `appendTo` 只改变挂载节点,定位仍为 fixed;自定义目标的 transform、主题、语言继承以及与其他弹层的焦点管理需要应用确认。
|
|
58
|
+
- 默认字符串按文本渲染。`dangerouslyUseHTMLString` 显式开启后按 HTML 渲染;DS 不做 HTML 净化,应用必须保留自己的安全处理。
|
|
59
|
+
- 本服务仅支持 Vue;React 原有 Toast、Modal 接口不受影响。服务使用独立子路径导出,不依赖生成的 Vue barrel。
|
|
60
|
+
|
|
61
|
+
## emp2 的接入边界
|
|
62
|
+
|
|
63
|
+
`vue3/ts/message.ts` 中的业务行为应保留:HTML 净化、错误脱敏、SAP 字符处理、国际化、错误详情开关、默认持续时间、基于 id 的去重和 `closeMessage`。
|
|
64
|
+
DS 发布并更新应用安装版本后,再把文件里的 `ElMessage / ElMessageBox` 导入改为上述两个服务;业务封装继续调用相同方法。
|
|
65
|
+
接入时还需配置当前语言的默认按钮文案、与现有弹层兼容的 zIndex,并检查 `limited-height-message` 和其他依赖 Element DOM 的自定义样式。
|
|
66
|
+
本轮未修改业务封装,也未修改应用依赖或 lock 文件。
|
|
67
|
+
|
|
68
|
+
## 发布说明
|
|
69
|
+
|
|
70
|
+
源码版本已更新为 2.6.0,changelog 标记为 unreleased。发布前需要由维护者执行仓库正常的生成及检查流程,使包含包版本的 token 元数据同步到新版本。
|
|
71
|
+
按本次任务约束,未运行这些命令,未查看或修改生成的 barrel、token 产物、文档 bundle 或 dist,也未执行 Git 操作。
|
|
@@ -3,17 +3,17 @@ import { computed } from 'vue'
|
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
4
|
import IconButton from '../buttons/IconButton.vue'
|
|
5
5
|
const toastTones = { success: { icon: 'circle-check', color: 'var(--success-strong)' }, danger: { icon: 'triangle-alert', color: 'var(--danger)' }, warning: { icon: 'triangle-alert', color: 'var(--warning)' }, info: { icon: 'info', color: 'var(--action-primary)' } }
|
|
6
|
-
const props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })
|
|
6
|
+
const props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, closeLabel: { type: String, default: 'Dismiss' }, style: Object })
|
|
7
7
|
const emit = defineEmits(['close'])
|
|
8
|
-
const t = computed(() => toastTones[props.tone])
|
|
8
|
+
const t = computed(() => toastTones[props.tone] || toastTones.info)
|
|
9
9
|
</script>
|
|
10
10
|
<template>
|
|
11
11
|
<div :style="{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px', background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }">
|
|
12
|
-
<span :style="{ color: t.color, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }"><Icon :name="t.icon" :size="16" /></span>
|
|
12
|
+
<span :style="{ color: t.color, display: 'inline-flex', alignItems: 'center', flexShrink: 0, height: 'var(--leading-base)' }"><slot name="icon"><Icon :name="t.icon" :size="16" /></slot></span>
|
|
13
13
|
<div style="display:flex;flex-direction:column;flex:1;min-width:0">
|
|
14
|
-
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
|
|
14
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading);overflow-wrap:anywhere"><slot>{{ title }}</slot></span>
|
|
15
15
|
<span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ meta }}</span>
|
|
16
16
|
</div>
|
|
17
|
-
<IconButton v-if="dismissible" icon="x" size="sm" muted label="
|
|
17
|
+
<IconButton v-if="dismissible" icon="x" size="sm" muted :label="closeLabel" :style="{ margin: '-2px 0' }" @click="emit('close')" />
|
|
18
18
|
</div>
|
|
19
19
|
</template>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, h, isVNode, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import Button from '../../buttons/Button.vue'
|
|
4
|
+
import IconButton from '../../buttons/IconButton.vue'
|
|
5
|
+
import Icon from '../../icons/Icon.vue'
|
|
6
|
+
import { contentNode, lockBody } from './service-utils.js'
|
|
7
|
+
|
|
8
|
+
const props = defineProps({ options: { type: Object, required: true } })
|
|
9
|
+
const emit = defineEmits(['action'])
|
|
10
|
+
const panel = ref(null)
|
|
11
|
+
const input = ref(null)
|
|
12
|
+
const error = ref('')
|
|
13
|
+
const validating = ref(false)
|
|
14
|
+
const Content = () => contentNode(props.options.message, props.options.dangerouslyUseHTMLString)
|
|
15
|
+
const Title = () => contentNode(props.options.title)
|
|
16
|
+
const icons = { success: 'circle-check', warning: 'triangle-alert', error: 'triangle-alert', info: 'info' }
|
|
17
|
+
const colors = { success: 'var(--success-strong)', warning: 'var(--warning)', error: 'var(--danger)', info: 'var(--action-primary)' }
|
|
18
|
+
const icon = computed(() => props.options.icon || icons[props.options.type])
|
|
19
|
+
const CustomIcon = () => isVNode(icon.value) ? icon.value : h(icon.value)
|
|
20
|
+
const confirmVariant = computed(() => props.options.confirmButtonClass?.includes('el-button--danger') ? 'danger' : 'primary')
|
|
21
|
+
let unlock
|
|
22
|
+
let previousFocus
|
|
23
|
+
let disposed = false
|
|
24
|
+
let validationRevision = 0
|
|
25
|
+
|
|
26
|
+
async function confirm() {
|
|
27
|
+
const options = props.options
|
|
28
|
+
if (validating.value || options.confirmButtonDisabled || options.confirmButtonLoading) return
|
|
29
|
+
if (options.showInput) {
|
|
30
|
+
const revision = ++validationRevision
|
|
31
|
+
const value = options.inputValue ?? ''
|
|
32
|
+
validating.value = true
|
|
33
|
+
let message = ''
|
|
34
|
+
try {
|
|
35
|
+
if (options.inputPattern) {
|
|
36
|
+
// 避免全局正则的 lastIndex 导致同一输入校验结果交替。
|
|
37
|
+
const pattern = new RegExp(options.inputPattern.source, options.inputPattern.flags)
|
|
38
|
+
if (!pattern.test(value)) message = options.inputErrorMessage || 'Invalid input'
|
|
39
|
+
}
|
|
40
|
+
if (!message && options.inputValidator) {
|
|
41
|
+
const result = await options.inputValidator(value)
|
|
42
|
+
if (result === false || typeof result === 'string') message = result || options.inputErrorMessage || 'Invalid input'
|
|
43
|
+
}
|
|
44
|
+
} catch (reason) {
|
|
45
|
+
message = options.inputErrorMessage || reason?.message || 'Invalid input'
|
|
46
|
+
} finally {
|
|
47
|
+
validating.value = false
|
|
48
|
+
}
|
|
49
|
+
if (disposed || revision !== validationRevision) return
|
|
50
|
+
error.value = message
|
|
51
|
+
if (message) { input.value?.focus(); return }
|
|
52
|
+
}
|
|
53
|
+
emit('action', 'confirm')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function close() { emit('action', 'close') }
|
|
57
|
+
function onKeydown(event) {
|
|
58
|
+
if (event.key === 'Escape' && props.options.closeOnPressEscape) {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
event.stopImmediatePropagation()
|
|
61
|
+
close()
|
|
62
|
+
}
|
|
63
|
+
if (event.key !== 'Tab') return
|
|
64
|
+
const focusable = [...panel.value.querySelectorAll('button:not(:disabled), input:not(:disabled), textarea:not(:disabled), select:not(:disabled), a[href], [tabindex]:not([tabindex="-1"])')]
|
|
65
|
+
.filter(node => node.getClientRects().length)
|
|
66
|
+
const first = focusable[0], last = focusable[focusable.length - 1]
|
|
67
|
+
if (!first) { event.preventDefault(); panel.value.focus(); return }
|
|
68
|
+
if (event.shiftKey && (document.activeElement === first || !panel.value.contains(document.activeElement))) {
|
|
69
|
+
event.preventDefault(); last.focus()
|
|
70
|
+
} else if (!event.shiftKey && (document.activeElement === last || !panel.value.contains(document.activeElement))) {
|
|
71
|
+
event.preventDefault(); first.focus()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function keepFocus(event) {
|
|
75
|
+
if (!panel.value.contains(event.target)) panel.value.focus()
|
|
76
|
+
}
|
|
77
|
+
function changeInput(event) {
|
|
78
|
+
validationRevision++
|
|
79
|
+
props.options.inputValue = event.target.value
|
|
80
|
+
error.value = ''
|
|
81
|
+
}
|
|
82
|
+
onMounted(async () => {
|
|
83
|
+
previousFocus = document.activeElement
|
|
84
|
+
if (props.options.lockScroll) unlock = lockBody()
|
|
85
|
+
document.addEventListener('keydown', onKeydown, true)
|
|
86
|
+
document.addEventListener('focusin', keepFocus)
|
|
87
|
+
await nextTick()
|
|
88
|
+
if (disposed || !props.options.autofocus) return
|
|
89
|
+
const target = input.value || panel.value.querySelector('[data-confirm] button') || panel.value
|
|
90
|
+
target.focus()
|
|
91
|
+
if (input.value) input.value.select()
|
|
92
|
+
})
|
|
93
|
+
onBeforeUnmount(() => {
|
|
94
|
+
disposed = true
|
|
95
|
+
unlock?.()
|
|
96
|
+
document.removeEventListener('keydown', onKeydown, true)
|
|
97
|
+
document.removeEventListener('focusin', keepFocus)
|
|
98
|
+
if (previousFocus?.isConnected) previousFocus.focus()
|
|
99
|
+
})
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<div
|
|
104
|
+
:class="['kb-message-box-overlay', options.modalClass]"
|
|
105
|
+
:style="{ zIndex: options.zIndex ?? 'var(--z-modal)', background: options.modal === false ? 'transparent' : undefined }"
|
|
106
|
+
@click.self="options.closeOnClickModal && close()"
|
|
107
|
+
>
|
|
108
|
+
<section
|
|
109
|
+
ref="panel"
|
|
110
|
+
:class="['kb-message-box', options.customClass, { 'kb-message-box--center': options.center, 'kb-message-box--round': options.roundButton }]"
|
|
111
|
+
:style="{ width: typeof options.width === 'number' ? `${options.width}px` : options.width }"
|
|
112
|
+
:data-size="options.size || 'lg'"
|
|
113
|
+
:aria-labelledby="`${options.id}-title`"
|
|
114
|
+
:aria-describedby="`${options.id}-content`"
|
|
115
|
+
role="alertdialog"
|
|
116
|
+
aria-modal="true"
|
|
117
|
+
tabindex="-1"
|
|
118
|
+
>
|
|
119
|
+
<header class="kb-message-box__header">
|
|
120
|
+
<span :id="`${options.id}-title`"><Title /></span>
|
|
121
|
+
<IconButton v-if="options.showClose" icon="x" :label="options.closeLabel || 'Close'" @click="close" />
|
|
122
|
+
</header>
|
|
123
|
+
<div :id="`${options.id}-content`" class="kb-message-box__body">
|
|
124
|
+
<div class="kb-message-box__message">
|
|
125
|
+
<span v-if="icon" :style="{ color: colors[options.type], display: 'inline-flex' }">
|
|
126
|
+
<Icon v-if="typeof icon === 'string'" :name="icon" :size="20" />
|
|
127
|
+
<CustomIcon v-else />
|
|
128
|
+
</span>
|
|
129
|
+
<div><Content /></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div v-if="options.showInput" class="kb-message-box__input-wrap">
|
|
132
|
+
<component
|
|
133
|
+
:is="options.inputType === 'textarea' ? 'textarea' : 'input'"
|
|
134
|
+
ref="input"
|
|
135
|
+
class="kb-message-box__input"
|
|
136
|
+
:type="options.inputType === 'textarea' ? undefined : options.inputType"
|
|
137
|
+
:value="options.inputValue"
|
|
138
|
+
:placeholder="options.inputPlaceholder"
|
|
139
|
+
:aria-label="options.inputPlaceholder || options.title || 'Input'"
|
|
140
|
+
:aria-invalid="!!error"
|
|
141
|
+
:aria-describedby="error ? `${options.id}-error` : undefined"
|
|
142
|
+
@input="changeInput"
|
|
143
|
+
@keydown.enter="!$event.isComposing && options.inputType !== 'textarea' && confirm()"
|
|
144
|
+
/>
|
|
145
|
+
<div v-if="error" :id="`${options.id}-error`" class="kb-message-box__error" role="alert">{{ error }}</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<footer class="kb-message-box__footer">
|
|
149
|
+
<Button
|
|
150
|
+
v-if="options.showCancelButton"
|
|
151
|
+
variant="outline"
|
|
152
|
+
:class="options.cancelButtonClass"
|
|
153
|
+
:disabled="options.cancelButtonDisabled"
|
|
154
|
+
:loading="options.cancelButtonLoading"
|
|
155
|
+
@click="emit('action', 'cancel')"
|
|
156
|
+
>
|
|
157
|
+
{{ options.cancelButtonText ?? 'Cancel' }}
|
|
158
|
+
</Button>
|
|
159
|
+
<span v-if="options.showConfirmButton" data-confirm>
|
|
160
|
+
<Button
|
|
161
|
+
:variant="confirmVariant"
|
|
162
|
+
:class="options.confirmButtonClass"
|
|
163
|
+
:disabled="options.confirmButtonDisabled"
|
|
164
|
+
:loading="options.confirmButtonLoading || validating"
|
|
165
|
+
@click="confirm"
|
|
166
|
+
>
|
|
167
|
+
{{ options.confirmButtonText ?? 'OK' }}
|
|
168
|
+
</Button>
|
|
169
|
+
</span>
|
|
170
|
+
</footer>
|
|
171
|
+
</section>
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
|
|
175
|
+
<style scoped>
|
|
176
|
+
.kb-message-box-overlay {
|
|
177
|
+
position: fixed;
|
|
178
|
+
inset: 0;
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
background: var(--bg-scrim);
|
|
183
|
+
animation: kb-fade-in var(--dur-fast) var(--ease-out);
|
|
184
|
+
}
|
|
185
|
+
.kb-message-box {
|
|
186
|
+
width: 440px;
|
|
187
|
+
max-width: 92vw;
|
|
188
|
+
max-height: 92vh;
|
|
189
|
+
overflow: auto;
|
|
190
|
+
box-sizing: border-box;
|
|
191
|
+
background: var(--bg-raised);
|
|
192
|
+
border: 1px solid var(--border-card);
|
|
193
|
+
border-radius: var(--radius-lg);
|
|
194
|
+
box-shadow: var(--shadow-modal);
|
|
195
|
+
font-family: var(--font-sans);
|
|
196
|
+
font-size: var(--text-base);
|
|
197
|
+
line-height: var(--leading-base);
|
|
198
|
+
letter-spacing: var(--tracking-base);
|
|
199
|
+
color: var(--text-body);
|
|
200
|
+
animation: kb-rise var(--dur) var(--ease-out);
|
|
201
|
+
}
|
|
202
|
+
.kb-message-box__header {
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: space-between;
|
|
206
|
+
gap: var(--group-gap);
|
|
207
|
+
padding: var(--section-gap) var(--section-gap) 0;
|
|
208
|
+
font-weight: var(--weight-semibold);
|
|
209
|
+
font-size: var(--text-md);
|
|
210
|
+
line-height: var(--leading-md);
|
|
211
|
+
letter-spacing: var(--tracking-md);
|
|
212
|
+
color: var(--text-heading);
|
|
213
|
+
}
|
|
214
|
+
.kb-message-box__header > span { min-width: 0; overflow-wrap: anywhere; }
|
|
215
|
+
.kb-message-box__body { padding: 20px var(--section-gap) 0; }
|
|
216
|
+
.kb-message-box__message { display: flex; align-items: flex-start; gap: var(--control-px); }
|
|
217
|
+
.kb-message-box__message > div { min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
218
|
+
.kb-message-box__input-wrap { margin-top: var(--group-gap); }
|
|
219
|
+
.kb-message-box__input {
|
|
220
|
+
box-sizing: border-box;
|
|
221
|
+
width: 100%;
|
|
222
|
+
min-height: var(--control-h);
|
|
223
|
+
padding: 0 var(--control-px);
|
|
224
|
+
font: inherit;
|
|
225
|
+
color: var(--text-heading);
|
|
226
|
+
background: var(--bg-surface);
|
|
227
|
+
border: 1px solid var(--border-control);
|
|
228
|
+
border-radius: var(--radius-md);
|
|
229
|
+
}
|
|
230
|
+
.kb-message-box__input:focus { outline: none; border-color: var(--focus-border); box-shadow: var(--ring-focus); }
|
|
231
|
+
.kb-message-box__input[aria-invalid="true"] { border-color: var(--danger); }
|
|
232
|
+
.kb-message-box__error { margin-top: var(--control-gap); color: var(--danger-text); }
|
|
233
|
+
.kb-message-box__footer { display: flex; justify-content: flex-end; gap: var(--control-gap); padding: var(--section-gap); }
|
|
234
|
+
.kb-message-box--center .kb-message-box__message { justify-content: center; text-align: center; }
|
|
235
|
+
.kb-message-box--center .kb-message-box__footer { justify-content: center; }
|
|
236
|
+
.kb-message-box--round :deep(.kb-message-box__footer button) { border-radius: var(--radius-full) !important; }
|
|
237
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, h, isVNode, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import Toast from '../Toast.vue'
|
|
4
|
+
import Icon from '../../icons/Icon.vue'
|
|
5
|
+
import { contentNode } from './service-utils.js'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({ options: { type: Object, required: true } })
|
|
8
|
+
const emit = defineEmits(['close', 'height'])
|
|
9
|
+
const root = ref(null)
|
|
10
|
+
const tone = computed(() => props.options.type === 'error' ? 'danger' : props.options.type)
|
|
11
|
+
const Content = () => contentNode(props.options.message, props.options.dangerouslyUseHTMLString)
|
|
12
|
+
const CustomIcon = () => typeof props.options.icon === 'string'
|
|
13
|
+
? h(Icon, { name: props.options.icon, size: 16 })
|
|
14
|
+
: isVNode(props.options.icon) ? props.options.icon : h(props.options.icon)
|
|
15
|
+
let timer
|
|
16
|
+
let observer
|
|
17
|
+
let paused = false
|
|
18
|
+
function stop() { clearTimeout(timer) }
|
|
19
|
+
function start() {
|
|
20
|
+
stop()
|
|
21
|
+
if (!paused && props.options.duration > 0) timer = setTimeout(() => emit('close'), props.options.duration)
|
|
22
|
+
}
|
|
23
|
+
function onEscape(event) {
|
|
24
|
+
if (event.key === 'Escape') emit('close')
|
|
25
|
+
}
|
|
26
|
+
watch(() => [props.options.restart, props.options.duration], start)
|
|
27
|
+
onMounted(() => {
|
|
28
|
+
start()
|
|
29
|
+
const measure = () => emit('height', root.value.getBoundingClientRect().height)
|
|
30
|
+
measure()
|
|
31
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
32
|
+
observer = new ResizeObserver(measure)
|
|
33
|
+
observer.observe(root.value)
|
|
34
|
+
}
|
|
35
|
+
document.addEventListener('keydown', onEscape)
|
|
36
|
+
})
|
|
37
|
+
onBeforeUnmount(() => {
|
|
38
|
+
stop()
|
|
39
|
+
observer?.disconnect()
|
|
40
|
+
document.removeEventListener('keydown', onEscape)
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div
|
|
46
|
+
:id="options.id"
|
|
47
|
+
ref="root"
|
|
48
|
+
:class="['kb-message', options.customClass, { 'kb-message--center': options.center, 'kb-message--plain': options.plain }]"
|
|
49
|
+
:role="options.type === 'error' || options.type === 'warning' ? 'alert' : 'status'"
|
|
50
|
+
:style="{ position: 'fixed', top: `${options.top}px`, left: '50%', transform: 'translateX(-50%)', zIndex: options.zIndex ?? 'var(--z-toast)', maxWidth: '92vw' }"
|
|
51
|
+
@mouseenter="paused = true; stop()"
|
|
52
|
+
@mouseleave="paused = false; start()"
|
|
53
|
+
@focusin="paused = true; stop()"
|
|
54
|
+
@focusout="paused = false; start()"
|
|
55
|
+
>
|
|
56
|
+
<Toast
|
|
57
|
+
:tone="tone"
|
|
58
|
+
:dismissible="options.showClose"
|
|
59
|
+
:close-label="options.closeLabel"
|
|
60
|
+
:style="{ minWidth: 'min(280px, 92vw)', maxWidth: '100%', boxSizing: 'border-box', ...(options.plain ? { boxShadow: 'none' } : {}) }"
|
|
61
|
+
@close="emit('close')"
|
|
62
|
+
>
|
|
63
|
+
<template v-if="options.icon" #icon><CustomIcon /></template>
|
|
64
|
+
<Content />
|
|
65
|
+
<span v-if="options.repeatNum > 1" class="kb-message__count"> × {{ options.repeatNum }}</span>
|
|
66
|
+
</Toast>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.kb-message--center { text-align: center; }
|
|
72
|
+
.kb-message__count { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { h, isVNode, render } from 'vue'
|
|
2
|
+
|
|
3
|
+
// 默认按文本渲染,只有调用方显式开启时才渲染可信 HTML。
|
|
4
|
+
export function contentNode(message, html = false) {
|
|
5
|
+
const value = typeof message === 'function' ? message() : message
|
|
6
|
+
if (isVNode(value) || Array.isArray(value)) return value
|
|
7
|
+
return html ? h('span', { innerHTML: String(value ?? '') }) : String(value ?? '')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveTarget(options = {}) {
|
|
11
|
+
const target = typeof options.appendTo === 'string'
|
|
12
|
+
? document.querySelector(options.appendTo) : options.appendTo || document.body
|
|
13
|
+
if (!target) throw new Error('Keybricks: appendTo 未找到挂载目标')
|
|
14
|
+
return target
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function mountService(component, props, options = {}, appContext) {
|
|
18
|
+
const target = resolveTarget(options)
|
|
19
|
+
const container = document.createElement('div')
|
|
20
|
+
const vnode = h(component, props)
|
|
21
|
+
vnode.appContext = appContext || null
|
|
22
|
+
target.appendChild(container)
|
|
23
|
+
try {
|
|
24
|
+
render(vnode, container)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
render(null, container)
|
|
27
|
+
container.remove()
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
return () => {
|
|
31
|
+
render(null, container)
|
|
32
|
+
container.remove()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let locks = 0
|
|
37
|
+
let previousOverflow
|
|
38
|
+
export function lockBody() {
|
|
39
|
+
if (locks++ === 0) {
|
|
40
|
+
previousOverflow = document.body.style.overflow
|
|
41
|
+
document.body.style.overflow = 'hidden'
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
if (--locks === 0) document.body.style.overflow = previousOverflow
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AppContext, Component, VNode } from 'vue';
|
|
2
|
+
import type { MessageContent, MessageType } from './message.js';
|
|
3
|
+
|
|
4
|
+
export type MessageBoxAction = 'confirm' | 'cancel' | 'close';
|
|
5
|
+
export interface MessageBoxInputResult { value: string; action: MessageBoxAction; }
|
|
6
|
+
export type MessageBoxResult = MessageBoxAction | MessageBoxInputResult;
|
|
7
|
+
export interface MessageBoxOptions {
|
|
8
|
+
title?: MessageContent;
|
|
9
|
+
message?: MessageContent;
|
|
10
|
+
type?: MessageType | '';
|
|
11
|
+
icon?: string | Component | VNode;
|
|
12
|
+
width?: string | number;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
customClass?: string;
|
|
15
|
+
modalClass?: string;
|
|
16
|
+
zIndex?: number;
|
|
17
|
+
appendTo?: string | HTMLElement;
|
|
18
|
+
modal?: boolean;
|
|
19
|
+
lockScroll?: boolean;
|
|
20
|
+
autofocus?: boolean;
|
|
21
|
+
center?: boolean;
|
|
22
|
+
roundButton?: boolean;
|
|
23
|
+
showClose?: boolean;
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
closeOnClickModal?: boolean;
|
|
26
|
+
closeOnPressEscape?: boolean;
|
|
27
|
+
distinguishCancelAndClose?: boolean;
|
|
28
|
+
dangerouslyUseHTMLString?: boolean;
|
|
29
|
+
showConfirmButton?: boolean;
|
|
30
|
+
showCancelButton?: boolean;
|
|
31
|
+
confirmButtonText?: string;
|
|
32
|
+
cancelButtonText?: string;
|
|
33
|
+
confirmButtonClass?: string;
|
|
34
|
+
cancelButtonClass?: string;
|
|
35
|
+
confirmButtonLoading?: boolean;
|
|
36
|
+
cancelButtonLoading?: boolean;
|
|
37
|
+
confirmButtonDisabled?: boolean;
|
|
38
|
+
cancelButtonDisabled?: boolean;
|
|
39
|
+
showInput?: boolean;
|
|
40
|
+
inputValue?: string;
|
|
41
|
+
inputType?: string;
|
|
42
|
+
inputPlaceholder?: string;
|
|
43
|
+
inputPattern?: RegExp;
|
|
44
|
+
inputErrorMessage?: string;
|
|
45
|
+
inputValidator?: (value: string) => boolean | string | Promise<boolean | string>;
|
|
46
|
+
beforeClose?: (action: MessageBoxAction, instance: MessageBoxState, done: () => void) => void;
|
|
47
|
+
callback?: (result: MessageBoxResult, instance: MessageBoxState) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface MessageBoxState extends MessageBoxOptions {
|
|
50
|
+
inputValue: string;
|
|
51
|
+
confirmButtonLoading: boolean;
|
|
52
|
+
cancelButtonLoading: boolean;
|
|
53
|
+
action?: MessageBoxAction;
|
|
54
|
+
}
|
|
55
|
+
export interface MessageBoxShortcut<Result = MessageBoxResult> {
|
|
56
|
+
(message: MessageContent, title?: MessageContent, options?: MessageBoxOptions, appContext?: AppContext | null): Promise<Result>;
|
|
57
|
+
(message: MessageContent, options?: MessageBoxOptions, appContext?: AppContext | null): Promise<Result>;
|
|
58
|
+
}
|
|
59
|
+
export interface MessageBoxService {
|
|
60
|
+
(options?: MessageBoxOptions | MessageContent, appContext?: AppContext | null): Promise<MessageBoxResult>;
|
|
61
|
+
alert: MessageBoxShortcut;
|
|
62
|
+
confirm: MessageBoxShortcut;
|
|
63
|
+
prompt: MessageBoxShortcut<MessageBoxInputResult>;
|
|
64
|
+
/** 清空队列,不结算已有 Promise,与 ElMessageBox.close 相同。 */
|
|
65
|
+
close(): void;
|
|
66
|
+
defaults: Partial<MessageBoxOptions>;
|
|
67
|
+
}
|
|
68
|
+
export declare const MessageBox: MessageBoxService;
|
|
69
|
+
export default MessageBox;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getCurrentInstance, isVNode, reactive } from 'vue'
|
|
2
|
+
import MessageBoxView from './_internal/MessageBoxView.vue'
|
|
3
|
+
import { mountService } from './_internal/service-utils.js'
|
|
4
|
+
|
|
5
|
+
const queue = []
|
|
6
|
+
let active
|
|
7
|
+
let seed = 0
|
|
8
|
+
|
|
9
|
+
function next() {
|
|
10
|
+
if (active || !queue.length) return
|
|
11
|
+
const entry = active = queue.shift()
|
|
12
|
+
try {
|
|
13
|
+
entry.dispose = mountService(MessageBoxView, {
|
|
14
|
+
options: entry.state,
|
|
15
|
+
onAction: action => requestAction(entry, action),
|
|
16
|
+
}, entry.state, entry.context)
|
|
17
|
+
} catch (error) {
|
|
18
|
+
active = undefined
|
|
19
|
+
entry.reject(error)
|
|
20
|
+
next()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function finish(entry, action) {
|
|
25
|
+
if (active !== entry || entry.closed) return
|
|
26
|
+
entry.closed = true
|
|
27
|
+
entry.dispose?.()
|
|
28
|
+
active = undefined
|
|
29
|
+
const result = entry.state.showInput ? { value: entry.state.inputValue, action } : action
|
|
30
|
+
try {
|
|
31
|
+
if (entry.state.callback) entry.state.callback(result, entry.state)
|
|
32
|
+
else if (action === 'confirm') entry.resolve(result)
|
|
33
|
+
else entry.reject(action === 'close' && !entry.state.distinguishCancelAndClose ? 'cancel' : action)
|
|
34
|
+
} finally {
|
|
35
|
+
next()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requestAction(entry, action) {
|
|
40
|
+
if (entry.closed || active !== entry) return
|
|
41
|
+
entry.state.action = action
|
|
42
|
+
const done = () => finish(entry, action)
|
|
43
|
+
if (entry.state.beforeClose) entry.state.beforeClose(action, entry.state, done)
|
|
44
|
+
else done()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Vue 弹窗服务;队列内同一时刻只显示一个确认框。 */
|
|
48
|
+
export function MessageBox(value = {}, appContext) {
|
|
49
|
+
if (typeof document === 'undefined') return Promise.reject(new Error('Keybricks MessageBox 需要浏览器环境'))
|
|
50
|
+
const options = typeof value === 'string' || typeof value === 'function' || isVNode(value) ? { message: value } : value
|
|
51
|
+
const state = reactive({
|
|
52
|
+
id: `kb-message-box-${++seed}`, title: '', message: '', type: '',
|
|
53
|
+
showClose: true, showConfirmButton: true, showCancelButton: false,
|
|
54
|
+
closeOnClickModal: true, closeOnPressEscape: true, lockScroll: true,
|
|
55
|
+
autofocus: true, inputValue: '', inputType: 'text', inputErrorMessage: '',
|
|
56
|
+
confirmButtonLoading: false, cancelButtonLoading: false,
|
|
57
|
+
confirmButtonDisabled: false, cancelButtonDisabled: false,
|
|
58
|
+
...MessageBox.defaults, ...options,
|
|
59
|
+
})
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
queue.push({ state, resolve, reject, context: appContext || getCurrentInstance()?.appContext })
|
|
62
|
+
next()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const kind of ['alert', 'confirm', 'prompt']) {
|
|
67
|
+
MessageBox[kind] = (message, title, options, appContext) => {
|
|
68
|
+
if (title && typeof title === 'object' && !isVNode(title)) {
|
|
69
|
+
appContext = options
|
|
70
|
+
options = title
|
|
71
|
+
title = undefined
|
|
72
|
+
}
|
|
73
|
+
return MessageBox({
|
|
74
|
+
boxType: kind, showCancelButton: kind !== 'alert', showInput: kind === 'prompt',
|
|
75
|
+
closeOnClickModal: kind !== 'alert', closeOnPressEscape: kind !== 'alert',
|
|
76
|
+
...options, message, ...(title === undefined ? {} : { title }),
|
|
77
|
+
}, appContext)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 与 ElMessageBox.close 一致:清空所有弹窗,不触发用户操作回调或结算 Promise。
|
|
82
|
+
MessageBox.close = () => {
|
|
83
|
+
queue.length = 0
|
|
84
|
+
if (active) {
|
|
85
|
+
const entry = active
|
|
86
|
+
active = undefined
|
|
87
|
+
entry.closed = true
|
|
88
|
+
entry.dispose?.()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export default MessageBox
|
|
92
|
+
MessageBox.defaults = {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AppContext, Component, VNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type MessageType = 'success' | 'info' | 'warning' | 'error';
|
|
4
|
+
export type MessageContent = string | VNode | (() => VNode);
|
|
5
|
+
export interface MessageHandle { close(): void; }
|
|
6
|
+
export interface MessageOptions {
|
|
7
|
+
message?: MessageContent;
|
|
8
|
+
type?: MessageType;
|
|
9
|
+
icon?: string | Component | VNode;
|
|
10
|
+
/** 毫秒;0 表示不自动关闭,默认 3000。 */
|
|
11
|
+
duration?: number;
|
|
12
|
+
showClose?: boolean;
|
|
13
|
+
closeLabel?: string;
|
|
14
|
+
offset?: number;
|
|
15
|
+
zIndex?: number;
|
|
16
|
+
customClass?: string;
|
|
17
|
+
center?: boolean;
|
|
18
|
+
plain?: boolean;
|
|
19
|
+
grouping?: boolean;
|
|
20
|
+
/** 仅允许可信且已由应用净化的 HTML。 */
|
|
21
|
+
dangerouslyUseHTMLString?: boolean;
|
|
22
|
+
appendTo?: string | HTMLElement;
|
|
23
|
+
onClose?: (handle: MessageHandle) => void;
|
|
24
|
+
}
|
|
25
|
+
export type MessageParams = MessageContent | MessageOptions;
|
|
26
|
+
export interface MessageService {
|
|
27
|
+
(options?: MessageParams, appContext?: AppContext | null): MessageHandle;
|
|
28
|
+
success(options: MessageParams, appContext?: AppContext | null): MessageHandle;
|
|
29
|
+
info(options: MessageParams, appContext?: AppContext | null): MessageHandle;
|
|
30
|
+
warning(options: MessageParams, appContext?: AppContext | null): MessageHandle;
|
|
31
|
+
error(options: MessageParams, appContext?: AppContext | null): MessageHandle;
|
|
32
|
+
closeAll(type?: MessageType): void;
|
|
33
|
+
/** 在调用时读取;可随应用语言切换更新。 */
|
|
34
|
+
defaults: Partial<MessageOptions>;
|
|
35
|
+
}
|
|
36
|
+
export declare const Message: MessageService;
|
|
37
|
+
export default Message;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getCurrentInstance, isVNode, reactive } from 'vue'
|
|
2
|
+
import MessageView from './_internal/MessageView.vue'
|
|
3
|
+
import { mountService, resolveTarget } from './_internal/service-utils.js'
|
|
4
|
+
|
|
5
|
+
const instances = []
|
|
6
|
+
let seed = 0
|
|
7
|
+
const normalize = value => typeof value === 'string' || typeof value === 'function' || isVNode(value)
|
|
8
|
+
? { message: value } : { ...value }
|
|
9
|
+
|
|
10
|
+
/** Vue 消息服务;样式由 Toast 提供,业务错误脱敏仍由应用处理。 */
|
|
11
|
+
export function Message(value = {}, appContext) {
|
|
12
|
+
if (typeof document === 'undefined') return { close() {} }
|
|
13
|
+
const options = { type: 'info', duration: 3000, offset: 16, ...Message.defaults, ...normalize(value) }
|
|
14
|
+
const target = resolveTarget(options)
|
|
15
|
+
if (options.grouping) {
|
|
16
|
+
const existing = instances.find(item => item.target === target && item.options.message === options.message)
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.options.repeatNum++
|
|
19
|
+
existing.options.type = options.type
|
|
20
|
+
existing.options.restart++
|
|
21
|
+
return existing.handle
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const state = reactive({ ...options, id: `kb-message-${++seed}`, repeatNum: 1, restart: 0, height: 0, top: options.offset })
|
|
25
|
+
let dispose
|
|
26
|
+
let closed = false
|
|
27
|
+
const handle = { close() {
|
|
28
|
+
if (closed) return
|
|
29
|
+
closed = true
|
|
30
|
+
instances.splice(instances.indexOf(entry), 1)
|
|
31
|
+
dispose?.()
|
|
32
|
+
reflow()
|
|
33
|
+
options.onClose?.(handle)
|
|
34
|
+
} }
|
|
35
|
+
const entry = { options: state, handle, target }
|
|
36
|
+
function reflow() {
|
|
37
|
+
const tops = new Map()
|
|
38
|
+
for (const item of instances) {
|
|
39
|
+
const target = item.target
|
|
40
|
+
item.options.top = Math.max(Number(item.options.offset) || 0, tops.get(target) || 0)
|
|
41
|
+
tops.set(target, item.options.top + item.options.height + 16)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
instances.push(entry)
|
|
45
|
+
try {
|
|
46
|
+
dispose = mountService(MessageView, {
|
|
47
|
+
options: state, onClose: handle.close,
|
|
48
|
+
onHeight: height => { state.height = height; reflow() },
|
|
49
|
+
}, options, appContext || getCurrentInstance()?.appContext)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
instances.splice(instances.indexOf(entry), 1)
|
|
52
|
+
reflow()
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
reflow()
|
|
56
|
+
return handle
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const type of ['success', 'info', 'warning', 'error']) {
|
|
60
|
+
Message[type] = (value, appContext) => Message({ ...normalize(value), type }, appContext)
|
|
61
|
+
}
|
|
62
|
+
Message.closeAll = type => {
|
|
63
|
+
for (const entry of [...instances]) {
|
|
64
|
+
if (!type || entry.options.type === type) entry.handle.close()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export default Message
|
|
68
|
+
Message.defaults = {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyfunc/keybricks-ds",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Keybricks design system — design tokens, three size tiers and 121 UI components in React and Vue 3, source-only.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
"default": "./components/index.js"
|
|
14
14
|
},
|
|
15
15
|
"./vue": "./components/_vue/index.mjs",
|
|
16
|
+
"./vue/message": {
|
|
17
|
+
"types": "./components/feedback/message.d.ts",
|
|
18
|
+
"default": "./components/feedback/message.js"
|
|
19
|
+
},
|
|
20
|
+
"./vue/message-box": {
|
|
21
|
+
"types": "./components/feedback/message-box.d.ts",
|
|
22
|
+
"default": "./components/feedback/message-box.js"
|
|
23
|
+
},
|
|
16
24
|
"./styles.css": "./keybricks.css",
|
|
17
25
|
"./keybricks.css": "./keybricks.css",
|
|
18
26
|
"./tokens": {
|