@keyfunc/keybricks-ds 2.5.1 → 2.7.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/DESIGN.md +13 -4
- package/INTEGRATION.md +25 -2
- package/_adherence.oxlintrc.json +57 -11
- package/_ds_bundle.js +2137 -186
- package/_scripts/build-index.mjs +1 -1
- package/_src/icon-index.js +1 -1
- package/assets/icons/icon-index.json +1 -1
- package/changelog.js +15 -0
- package/components/_cards/accordion.card.html +1 -1
- package/components/_cards/action-chip.card.html +1 -1
- package/components/_cards/activity.card.html +1 -1
- package/components/_cards/alert.card.html +1 -1
- package/components/_cards/app-chrome.card.html +1 -1
- package/components/_cards/banner.card.html +1 -1
- package/components/_cards/breadcrumb.card.html +1 -1
- package/components/_cards/button-1-base.card.html +5 -2
- package/components/_cards/button-2-states.card.html +5 -5
- package/components/_cards/button-3-sizes-icons.card.html +2 -2
- package/components/_cards/button-4-group.card.html +2 -2
- package/components/_cards/button-5-ai.card.html +34 -0
- package/components/_cards/canvas-zoom-controls.card.html +1 -1
- package/components/_cards/cards-indicators.card.html +1 -1
- package/components/_cards/charts-1-sparkline.card.html +1 -1
- package/components/_cards/charts-10-gauge.card.html +1 -1
- package/components/_cards/charts-11-funnel.card.html +1 -1
- package/components/_cards/charts-12-treemap.card.html +1 -1
- package/components/_cards/charts-14-radar.card.html +1 -1
- package/components/_cards/charts-15-distribution.card.html +1 -1
- package/components/_cards/charts-16-pareto.card.html +1 -1
- package/components/_cards/charts-17-candlestick.card.html +1 -1
- package/components/_cards/charts-18-sankey.card.html +1 -1
- package/components/_cards/charts-19-chord.card.html +1 -1
- package/components/_cards/charts-2-line.card.html +1 -1
- package/components/_cards/charts-20-sunburst.card.html +1 -1
- package/components/_cards/charts-21-worldmap.card.html +1 -1
- package/components/_cards/charts-22-gantt.card.html +1 -1
- package/components/_cards/charts-23-bar3d.card.html +1 -1
- package/components/_cards/charts-24-pie3d.card.html +1 -1
- package/components/_cards/charts-25-surface3d.card.html +1 -1
- package/components/_cards/charts-26-scatter3d.card.html +1 -1
- package/components/_cards/charts-27-ribbon3d.card.html +1 -1
- package/components/_cards/charts-3-bar.card.html +1 -1
- package/components/_cards/charts-4-barlist.card.html +1 -1
- package/components/_cards/charts-5-donut.card.html +1 -1
- package/components/_cards/charts-6-scatter.card.html +1 -1
- package/components/_cards/charts-7-heatmap.card.html +1 -1
- package/components/_cards/charts-8-waterfall.card.html +1 -1
- package/components/_cards/charts-9-bullet.card.html +1 -1
- package/components/_cards/charts-99-fullpage.card.html +1 -1
- package/components/_cards/checkbox.card.html +1 -1
- package/components/_cards/codeblock.card.html +1 -1
- package/components/_cards/combobox.card.html +1 -1
- package/components/_cards/context-menu.card.html +1 -1
- package/components/_cards/date-picker.card.html +1 -1
- package/components/_cards/diffvalue.card.html +1 -1
- package/components/_cards/divider.card.html +1 -1
- package/components/_cards/drawer.card.html +1 -1
- package/components/_cards/dropdown-menu.card.html +1 -1
- package/components/_cards/dropzone.card.html +1 -1
- package/components/_cards/empty-state.card.html +1 -1
- package/components/_cards/filter-bar.card.html +1 -1
- package/components/_cards/icon-button.card.html +1 -1
- package/components/_cards/icons-0-usage.card.html +2 -2
- package/components/_cards/icons-1-core.card.html +1 -1
- package/components/_cards/icons-2-feather.card.html +1 -1
- package/components/_cards/icons-2b-lucide.card.html +1 -1
- package/components/_cards/icons-3-unicons.card.html +1 -1
- package/components/_cards/icons-4-datasource.card.html +1 -1
- package/components/_cards/input.card.html +1 -1
- package/components/_cards/kbd.card.html +5 -7
- package/components/_cards/keyvalue.card.html +1 -1
- package/components/_cards/listrow.card.html +1 -1
- package/components/_cards/loading.card.html +1 -1
- package/components/_cards/log-viewer.card.html +1 -1
- package/components/_cards/multi-select.card.html +1 -1
- package/components/_cards/number-stepper.card.html +1 -1
- package/components/_cards/pagination.card.html +1 -1
- package/components/_cards/people.card.html +1 -1
- package/components/_cards/pipeline-node.card.html +1 -1
- package/components/_cards/pipeline-palette.card.html +1 -1
- package/components/_cards/radio.card.html +1 -1
- package/components/_cards/relative-time-picker.card.html +1 -1
- package/components/_cards/search-global.card.html +72 -0
- package/components/_cards/search.card.html +1 -1
- package/components/_cards/segmented-control.card.html +1 -1
- package/components/_cards/select.card.html +1 -1
- package/components/_cards/slider.card.html +1 -1
- package/components/_cards/smart-table.card.html +1 -1
- package/components/_cards/spreadsheet.card.html +1 -1
- package/components/_cards/stepper.card.html +1 -1
- package/components/_cards/table.card.html +1 -1
- package/components/_cards/tabs.card.html +1 -1
- package/components/_cards/tag.card.html +1 -1
- package/components/_cards/textarea.card.html +1 -1
- package/components/_cards/timeline.card.html +1 -1
- package/components/_cards/toast.card.html +1 -1
- package/components/_cards/toggles.card.html +1 -1
- package/components/_cards/tooltip.card.html +1 -1
- package/components/_cards/tree.card.html +1 -1
- package/components/_vue/index.mjs +2 -0
- package/components/buttons/BUTTON_MIGRATION.md +111 -0
- package/components/buttons/Button.d.ts +14 -5
- package/components/buttons/Button.jsx +45 -8
- package/components/buttons/Button.vue +61 -12
- package/components/buttons/Button.vue.d.ts +47 -0
- package/components/buttons/ai-button.js +54 -0
- package/components/buttons/buttons.prompt.md +7 -2
- package/components/display/Kbd.d.ts +3 -1
- package/components/display/Kbd.jsx +8 -3
- package/components/display/Kbd.vue +7 -3
- package/components/display/display.prompt.md +3 -1
- package/components/feedback/MESSAGE_SERVICES.md +71 -0
- package/components/feedback/Modal.d.ts +7 -0
- package/components/feedback/Modal.jsx +9 -9
- package/components/feedback/Toast.vue +8 -8
- 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/feedback.prompt.md +2 -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/components/forms/GlobalSearch.d.ts +83 -0
- package/components/forms/GlobalSearch.jsx +172 -0
- package/components/forms/GlobalSearch.vue +161 -0
- package/components/forms/forms.prompt.md +7 -0
- package/components/forms/global-search.js +161 -0
- package/components/icons/Icon.d.ts +2 -0
- package/components/icons/Icon.jsx +14 -6
- package/components/icons/Icon.vue +11 -4
- package/components/icons/icon-paths.js +4 -2
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/navigation/TopBar.d.ts +9 -3
- package/components/navigation/TopBar.jsx +5 -7
- package/components/navigation/TopBar.vue +5 -6
- package/covers.js +4 -2
- package/gallery.html +1 -1
- package/guidelines/brand-logo.html +1 -1
- package/guidelines/colors-schemes.html +1 -1
- package/guidelines/colors-transparency.html +1 -1
- package/guidelines/foundations-borders.html +1 -1
- package/guidelines/foundations-chrome.html +1 -1
- package/guidelines/foundations-motion.html +1 -1
- package/guidelines/foundations-size-tiers.html +1 -1
- package/guidelines/foundations-spacing.html +1 -1
- package/guidelines/interaction-states.html +1 -1
- package/guidelines/type-extended.html +1 -1
- package/guidelines/type-mono.html +1 -1
- package/guidelines/type-scale.html +1 -1
- package/guidelines/type-weights.html +1 -1
- package/index.html +8 -8
- package/package.json +10 -2
- package/readme.md +4 -4
- package/readme.zh-CN.md +4 -4
- package/site-nav.js +1 -1
- package/styles.css +1 -0
- package/tokens/ai.css +17 -0
- package/tokens/colors.css +1 -1
- package/tokens/tokens.d.ts +28 -0
- package/tokens/tokens.js +47 -5
- package/tokens/tokens.json +123 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Component, ComponentOptionsMixin, CSSProperties, DefineComponent, PublicProps, SlotsType, VNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
/** Vue 专用声明;同目录 Button.d.ts 继续描述 React 接口。 */
|
|
4
|
+
export interface VueButtonProps {
|
|
5
|
+
/** ai:仅用于 Prophet AI(方知)入口;它的 icon 只接受 DS 图标名,默认 prophet-sparkle。 */
|
|
6
|
+
variant?: 'primary' | 'dark' | 'outline' | 'ghost' | 'primary-soft' | 'danger' | 'danger-soft' | 'text' | 'ai';
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
icon?: string | Component | VNode;
|
|
9
|
+
/** 不随 loading 隐藏,适用于下拉箭头等独立的尾部标记。 */
|
|
10
|
+
trailingIcon?: string | Component | VNode;
|
|
11
|
+
iconSize?: number;
|
|
12
|
+
iconPosition?: 'left' | 'right';
|
|
13
|
+
medium?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
fullWidth?: boolean;
|
|
17
|
+
iconOnly?: boolean;
|
|
18
|
+
label?: string;
|
|
19
|
+
/** 默认插槽的后备文本。 */
|
|
20
|
+
text?: string;
|
|
21
|
+
/** 强制悬停样式,用于规范展示。 */
|
|
22
|
+
hovered?: boolean;
|
|
23
|
+
pressed?: boolean;
|
|
24
|
+
focused?: boolean;
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
/** 原生按钮类型,默认 button;旧表单适配时可明确传入 submit。 */
|
|
27
|
+
nativeType?: 'button' | 'submit' | 'reset';
|
|
28
|
+
onClick?: (event: MouseEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface VueButtonIconSlotProps {
|
|
32
|
+
size: number;
|
|
33
|
+
style: CSSProperties | null;
|
|
34
|
+
}
|
|
35
|
+
export interface VueButtonSlots {
|
|
36
|
+
default?: () => VNode[];
|
|
37
|
+
icon?: (props: VueButtonIconSlotProps) => VNode[];
|
|
38
|
+
loading?: (props: VueButtonIconSlotProps) => VNode[];
|
|
39
|
+
trailing?: (props: VueButtonIconSlotProps & { loading: boolean }) => VNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare const Button: DefineComponent<
|
|
43
|
+
VueButtonProps, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin,
|
|
44
|
+
{ click: (event: MouseEvent) => void }, string, PublicProps,
|
|
45
|
+
Readonly<VueButtonProps>, {}, SlotsType<VueButtonSlots>
|
|
46
|
+
>;
|
|
47
|
+
export default Button;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Button variant="ai" — the Prophet AI (方知) entry treatment, shared by Button.jsx and Button.vue.
|
|
2
|
+
Same outer box as every Button at the tier (height, radius, padding); the 1px border is replaced by a 1px ring:
|
|
3
|
+
at rest a gradient comet travels round a neutral track; on hover the full ring, an 8% gradient tint, a soft glow and a
|
|
4
|
+
gradient icon fade in and the ring speeds up. Tokens: tokens/ai.css. One CSS animation (kb-ai-spin) drives the angle;
|
|
5
|
+
hover speed is ramped with Animation.playbackRate, so the angle never jumps. */
|
|
6
|
+
/* The animated angle needs a registered <angle> property and a keyframe on it — injected once, so the token files stay plain. */
|
|
7
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-kb-ai]")) {
|
|
8
|
+
const el = document.createElement("style");
|
|
9
|
+
el.setAttribute("data-kb-ai", "");
|
|
10
|
+
el.textContent = "@property --kb-ai-angle{syntax:'<angle>';inherits:true;initial-value:0deg}@keyframes kb-ai-spin{to{--kb-ai-angle:360deg}}";
|
|
11
|
+
document.head.appendChild(el);
|
|
12
|
+
}
|
|
13
|
+
const FADE = "opacity 300ms var(--ease-out)";
|
|
14
|
+
const FULL = "conic-gradient(from var(--kb-ai-angle), var(--ai-gradient-stops))";
|
|
15
|
+
const COMET = "conic-gradient(from var(--kb-ai-angle), var(--ai-ring-rest-track) 0 50%, var(--ai-gradient-start) 78%, var(--ai-gradient-end) 100%)";
|
|
16
|
+
const layer = { position: "absolute", inset: "0", borderRadius: "inherit", pointerEvents: "none", transition: FADE };
|
|
17
|
+
|
|
18
|
+
/** A random phase per instance, so neighbouring rings never move in lockstep. */
|
|
19
|
+
export const aiSpinDelay = () => "-" + (Math.random() * 5).toFixed(2) + "s";
|
|
20
|
+
|
|
21
|
+
/** on = hovered (and not busy). Lengths are strings so the same objects work in React and Vue. */
|
|
22
|
+
export function aiStyles({ on, press, busy, focused, medium, fullWidth, iconOnly, delay, style }) {
|
|
23
|
+
const tint = press ? "var(--ai-tint-press)" : "var(--ai-tint)";
|
|
24
|
+
return {
|
|
25
|
+
outer: { position: "relative", isolation: "isolate", display: fullWidth ? "flex" : "inline-flex", alignItems: "stretch", width: fullWidth ? "100%" : iconOnly ? "var(--control-h)" : undefined, height: "var(--control-h)", padding: "var(--ai-ring-width)", border: "none", borderRadius: "var(--radius-md)", boxSizing: "border-box", background: COMET, animation: "kb-ai-spin var(--ai-spin-rest) linear " + delay + " infinite", cursor: busy ? "default" : "pointer", opacity: busy ? .55 : undefined, boxShadow: focused && !busy ? "var(--ring-focus)" : undefined, fontFamily: "var(--font-sans)", fontWeight: medium ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-body)", whiteSpace: "nowrap", verticalAlign: "middle", ...style },
|
|
26
|
+
glow: { ...layer, zIndex: "-1", background: FULL, filter: "blur(var(--ai-glow-blur))", opacity: on ? "var(--ai-glow-opacity)" : "0" },
|
|
27
|
+
ring: { ...layer, background: FULL, opacity: on ? "1" : "0" },
|
|
28
|
+
surface: { position: "relative", flex: "1 1 auto", display: "flex", alignItems: "center", justifyContent: "center", gap: iconOnly ? "0" : "6px", padding: iconOnly ? "0" : "0 calc(var(--control-px) - var(--ai-ring-width))", borderRadius: "calc(var(--radius-md) - var(--ai-ring-width))", background: "var(--bg-surface)", overflow: "hidden" },
|
|
29
|
+
tint: { ...layer, background: "linear-gradient(90deg, color-mix(in srgb, var(--ai-gradient-start) " + tint + ", transparent), color-mix(in srgb, var(--ai-gradient-end) " + tint + ", transparent))", opacity: on || press ? "1" : "0" },
|
|
30
|
+
iconBox: { position: "relative", display: "inline-flex", flexShrink: "0" },
|
|
31
|
+
iconRest: { transition: FADE, opacity: on ? "0" : "1" },
|
|
32
|
+
iconGrad: { position: "absolute", inset: "0", transition: FADE, opacity: on ? "1" : "0" },
|
|
33
|
+
label: { position: "relative" },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const secs = (v, d) => { v = (v || "").trim(); const n = parseFloat(v); return isNaN(n) ? d : /ms$/.test(v) ? n / 1000 : n; };
|
|
38
|
+
/** Ease the ring's speed toward rest (fast=false) or hover (fast=true). Returns a cancel function. */
|
|
39
|
+
export function rampAiSpin(el, fast) {
|
|
40
|
+
if (!el || !el.getAnimations || (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches)) return () => {};
|
|
41
|
+
const anim = el.getAnimations().find((a) => a.animationName === "kb-ai-spin");
|
|
42
|
+
if (!anim) return () => {};
|
|
43
|
+
const cs = getComputedStyle(el);
|
|
44
|
+
const target = fast ? secs(cs.getPropertyValue("--ai-spin-rest"), 5) / secs(cs.getPropertyValue("--ai-spin-hover"), 1.2) : 1;
|
|
45
|
+
let raf = 0, last = performance.now();
|
|
46
|
+
const step = (t) => {
|
|
47
|
+
const dt = Math.min(0.05, (t - last) / 1000); last = t;
|
|
48
|
+
const r = anim.playbackRate + (target - anim.playbackRate) * Math.min(1, dt * 5);
|
|
49
|
+
if (Math.abs(target - r) < 0.01) { anim.playbackRate = target; return; }
|
|
50
|
+
anim.playbackRate = r; raf = requestAnimationFrame(step);
|
|
51
|
+
};
|
|
52
|
+
raf = requestAnimationFrame(step);
|
|
53
|
+
return () => cancelAnimationFrame(raf);
|
|
54
|
+
}
|
|
@@ -2,7 +2,8 @@ Buttons: `Button` (primary/dark/outline/ghost/primary-soft), `IconButton` (squar
|
|
|
2
2
|
|
|
3
3
|
```jsx
|
|
4
4
|
<Button icon="send">Submit</Button>
|
|
5
|
-
<Button variant="dark" icon="
|
|
5
|
+
<Button variant="dark" icon="upload-cloud">Publish</Button>
|
|
6
|
+
<Button variant="ai">Ask Prophet AI</Button> // 中文:问方知 — icon defaults to prophet-sparkle
|
|
6
7
|
<Button variant="ghost" icon="play">Run Test</Button>
|
|
7
8
|
<Button variant="text">Cancel</Button>
|
|
8
9
|
<Button variant="primary-soft" icon="eye">Data Preview</Button>
|
|
@@ -17,4 +18,8 @@ Buttons: `Button` (primary/dark/outline/ghost/primary-soft), `IconButton` (squar
|
|
|
17
18
|
<ActionChip tone="success">Mark as New</ActionChip>
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
Primary = the single blue CTA per view. Dark = brand-level actions (
|
|
21
|
+
Primary = the single blue CTA per view. Dark = brand-level actions that should outweigh the page but not compete with the blue CTA (Publish, Show). Never for AI — Prophet AI entries use `ai`. Ghost = no box at rest, grey wash on hover — the default for actions in card and section headers, table toolbars and panel chrome ("Export", "Filter", "Run Test", "Back"); same height as outline so it lines up. `text` = bare label, underline on hover — links that happen to be buttons ("View all", "Clear filters"), never beside a boxed button and never a toolbar action. Emphasis is always the variant, never the size: a quieter action gets `ghost` or `text`, not `size="sm"`. Danger/danger-soft = destructive actions. lg (36px) comes from an lg region — dialogs and wizard footers; `Modal` sets it for you. A `size` prop on a single button is only for a control that stands alone, never to adjust one button inside a bar. `loading` swaps the icon for a spinner and disables the button without changing its size; `iconOnly` needs a `label` for accessibility; `ButtonGroup` fuses adjacent buttons into one segmented control or a split button (main action + attached icon-only).
|
|
22
|
+
|
|
23
|
+
`ai` = Prophet AI (方知) entry points only — never a generic primary action, at most one per region. Same box, tiers and generic states (focus, disabled, loading) as every Button; only the surface differs: a 1px travelling gradient ring at rest, and on hover the full ring, an 8% gradient tint, a soft glow and a gradient icon, with the ring speeding up. It only triggers (opens the Prophet AI panel); it never shows AI content in place. Tokens live in `tokens/ai.css` (`--ai-*`); `<Icon gradient/>` renders the gradient glyph.
|
|
24
|
+
|
|
25
|
+
Legacy adapters (both frameworks): `trailingIcon` (e.g. "chevron-down" on a menu button, stays visible while loading), `nativeType` ("submit" for old forms), `text` (fallback label), element / component icons, `aria-busy`. Vue adds `#icon` / `#loading` / `#trailing` slots and has its own types in `Button.vue.d.ts`. Migrating the old actnplanBtn: `BUTTON_MIGRATION.md`.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
/** Keyboard shortcut keycap row (20px).
|
|
2
|
+
/** Keyboard shortcut keycap row (20px). Semantic keys adapt to the OS: "mod" → ⌘ on macOS / Ctrl elsewhere; also "alt"/"opt", "shift", "enter", "backspace", "ctrl", "esc". Any other string renders as-is. */
|
|
3
3
|
export interface KbdProps {
|
|
4
4
|
keys?: string[];
|
|
5
|
+
/** "auto" detects from the browser (default); force "mac" or "win" for docs/screenshots. */
|
|
6
|
+
platform?: "auto" | "mac" | "win";
|
|
5
7
|
style?: React.CSSProperties;
|
|
6
8
|
}
|
|
7
9
|
export declare function Kbd(props: KbdProps): JSX.Element;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const KBD_MAP = { mod: ["⌘", "Ctrl"], cmd: ["⌘", "Ctrl"], ctrl: ["⌃", "Ctrl"], alt: ["⌥", "Alt"], opt: ["⌥", "Alt"], shift: ["⇧", "Shift"], enter: ["⏎", "Enter"], backspace: ["⌫", "Backspace"], esc: ["Esc", "Esc"] };
|
|
4
|
+
const isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test((navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || navigator.userAgent || "");
|
|
5
|
+
const resolveKbd = (k, mac) => { const m = KBD_MAP[String(k).toLowerCase()]; return m ? m[mac ? 0 : 1] : k; };
|
|
6
|
+
|
|
7
|
+
/** Keyboard shortcut hint. Use semantic keys so it adapts per OS: <Kbd keys={["mod","K"]}/> → ⌘ K on macOS, Ctrl K on Windows/Linux. Literal labels ("⌘", "F5") pass through unchanged. platform: "auto" (default) | "mac" | "win". */
|
|
8
|
+
export function Kbd({ keys = [], platform = "auto", style }) {
|
|
9
|
+
const mac = platform === "auto" ? isMacPlatform() : platform === "mac";
|
|
5
10
|
return (
|
|
6
11
|
<span style={{ display: "inline-flex", gap: 3, ...style }}>
|
|
7
12
|
{keys.map((k, i) => (
|
|
8
|
-
<span key={i} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 20, height: 20, padding: "0 5px", borderRadius: "var(--radius-sm)", background: "var(--bg-inset)", border: "1px solid var(--border-control)", boxSizing: "border-box", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{k}</span>
|
|
13
|
+
<span key={i} style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 20, height: 20, padding: "0 5px", borderRadius: "var(--radius-sm)", background: "var(--bg-inset)", border: "1px solid var(--border-control)", boxSizing: "border-box", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{resolveKbd(k, mac)}</span>
|
|
9
14
|
))}
|
|
10
15
|
</span>
|
|
11
16
|
);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
// Keyboard shortcut hint
|
|
3
|
-
|
|
2
|
+
// Keyboard shortcut hint. Semantic keys adapt per OS: <Kbd :keys="['mod','K']"/> → ⌘ K on macOS, Ctrl K on Windows/Linux. Literal labels pass through. platform: 'auto' | 'mac' | 'win'.
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
const props = defineProps({ keys: { type: Array, default: () => [] }, platform: { type: String, default: 'auto' }, style: Object })
|
|
5
|
+
const KBD_MAP = { mod: ['⌘', 'Ctrl'], cmd: ['⌘', 'Ctrl'], ctrl: ['⌃', 'Ctrl'], alt: ['⌥', 'Alt'], opt: ['⌥', 'Alt'], shift: ['⇧', 'Shift'], enter: ['⏎', 'Enter'], backspace: ['⌫', 'Backspace'], esc: ['Esc', 'Esc'] }
|
|
6
|
+
const isMac = () => typeof navigator !== 'undefined' && /mac|iphone|ipad|ipod/i.test((navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || navigator.userAgent || '')
|
|
7
|
+
const labels = computed(() => { const mac = props.platform === 'auto' ? isMac() : props.platform === 'mac'; return props.keys.map(k => { const m = KBD_MAP[String(k).toLowerCase()]; return m ? m[mac ? 0 : 1] : k }) })
|
|
4
8
|
</script>
|
|
5
9
|
<template>
|
|
6
10
|
<span :style="{ display: 'inline-flex', gap: '3px', ...style }">
|
|
7
|
-
<span v-for="(k, i) in
|
|
11
|
+
<span v-for="(k, i) in labels" :key="i" style="display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border-radius:var(--radius-sm);background:var(--bg-inset);border:1px solid var(--border-control);box-sizing:border-box;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-secondary)">{{ k }}</span>
|
|
8
12
|
</span>
|
|
9
13
|
</template>
|
|
@@ -26,5 +26,7 @@ Cards are flat (no shadow). IconTile hues: source green, join blue, transform or
|
|
|
26
26
|
```jsx
|
|
27
27
|
<Tag onRemove={remove}>Financial Settings</Tag>
|
|
28
28
|
<Divider label="OR"/> <Divider vertical/>
|
|
29
|
-
<Kbd keys={["
|
|
29
|
+
<Kbd keys={["mod","K"]}/> // ⌘ K on macOS, Ctrl K on Windows/Linux
|
|
30
30
|
```
|
|
31
|
+
|
|
32
|
+
Kbd: write modifiers semantically — `mod` (⌘/Ctrl), `alt`/`opt` (⌥/Alt), `shift` (⇧/Shift), `enter` (⏎/Enter), `backspace`, `ctrl`, `esc`. Don't hard-code "⌘": Windows users have no Command key. `platform="mac"|"win"` forces one side for docs.
|
|
@@ -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 操作。
|
|
@@ -27,6 +27,13 @@ export interface ToastProps {
|
|
|
27
27
|
tone?: "success" | "danger" | "warning" | "info";
|
|
28
28
|
title?: string;
|
|
29
29
|
meta?: string;
|
|
30
|
+
/** Replaces the tone glyph (Vue: #icon slot) */
|
|
31
|
+
icon?: React.ReactNode;
|
|
32
|
+
/** Content in place of title (Vue: default slot) */
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
/** aria-label of the dismiss button (default "Dismiss") */
|
|
35
|
+
closeLabel?: string;
|
|
36
|
+
/** Shows the dismiss button */
|
|
30
37
|
onClose?: () => void;
|
|
31
38
|
style?: React.CSSProperties;
|
|
32
39
|
}
|
|
@@ -43,19 +43,19 @@ export function Modal({ open, title, subtitle, onClose, footer, width = 440, siz
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const toastTones = { success: { icon: "circle-check",
|
|
46
|
+
const toastTones = { success: { icon: "circle-check", bg: "var(--success-bg)", border: "var(--success)", fg: "var(--success-text)" }, danger: { icon: "triangle-alert", bg: "var(--danger-bg)", border: "var(--danger)", fg: "var(--danger-text)" }, warning: { icon: "triangle-alert", bg: "var(--warning-bg)", border: "var(--warning)", fg: "var(--warning-text)" }, info: { icon: "info", bg: "var(--info-bg)", border: "var(--focus-border)", fg: "var(--info-fg)" } };
|
|
47
47
|
|
|
48
|
-
/** Single toast row. */
|
|
49
|
-
export function Toast({ tone = "success", title, meta, onClose, style }) {
|
|
50
|
-
const t = toastTones[tone];
|
|
48
|
+
/** Single toast row — tinted: tone wash over --bg-raised, solid tone border, tone-coloured icon, title and dismiss. */
|
|
49
|
+
export function Toast({ tone = "success", title, meta, icon, closeLabel = "Dismiss", onClose, children, style }) {
|
|
50
|
+
const t = toastTones[tone] || toastTones.info;
|
|
51
51
|
return (
|
|
52
|
-
<div style={{ display: "flex", alignItems: "flex-start", gap: 10, minWidth: 280, maxWidth: 380,
|
|
53
|
-
<span style={{ color: t.
|
|
52
|
+
<div style={{ display: "flex", alignItems: "flex-start", gap: 10, minWidth: 280, maxWidth: 380, backgroundColor: "var(--bg-raised)", backgroundImage: `linear-gradient(${t.bg}, ${t.bg})`, border: `1px solid ${t.border}`, borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: "12px 12px 12px 14px", animation: "kb-toast-in var(--dur) var(--ease-out)", ...style }}>
|
|
53
|
+
<span style={{ color: t.fg, display: "inline-flex", alignItems: "center", flexShrink: 0, height: "var(--leading-base)" }}>{icon || <Icon name={t.icon} size={16} />}</span>
|
|
54
54
|
<div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
|
|
55
|
-
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "
|
|
56
|
-
{meta ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-
|
|
55
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: t.fg, overflowWrap: "anywhere" }}>{children ?? title}</span>
|
|
56
|
+
{meta ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-body)" }}>{meta}</span> : null}
|
|
57
57
|
</div>
|
|
58
|
-
{onClose ? <IconButton icon="x" size="sm" muted onClick={onClose} label=
|
|
58
|
+
{onClose ? <IconButton icon="x" size="sm" muted onClick={onClose} label={closeLabel} style={{ margin: "-2px 0", color: t.fg }} /> : null}
|
|
59
59
|
</div>
|
|
60
60
|
);
|
|
61
61
|
}
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
4
|
import IconButton from '../buttons/IconButton.vue'
|
|
5
|
-
const toastTones = { success: { icon: 'circle-check',
|
|
6
|
-
const props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })
|
|
5
|
+
const toastTones = { success: { icon: 'circle-check', bg: 'var(--success-bg)', border: 'var(--success)', fg: 'var(--success-text)' }, danger: { icon: 'triangle-alert', bg: 'var(--danger-bg)', border: 'var(--danger)', fg: 'var(--danger-text)' }, warning: { icon: 'triangle-alert', bg: 'var(--warning-bg)', border: 'var(--warning)', fg: 'var(--warning-text)' }, info: { icon: 'info', bg: 'var(--info-bg)', border: 'var(--focus-border)', fg: 'var(--info-fg)' } }
|
|
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
|
-
<div :style="{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px',
|
|
12
|
-
<span :style="{ color: t.
|
|
11
|
+
<div :style="{ display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: '280px', maxWidth: '380px', backgroundColor: 'var(--bg-raised)', backgroundImage: `linear-gradient(${t.bg}, ${t.bg})`, border: `1px solid ${t.border}`, 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.fg, 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="
|
|
15
|
-
<span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-
|
|
14
|
+
<span :style="{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t.fg, overflowWrap: 'anywhere' }"><slot>{{ title }}</slot></span>
|
|
15
|
+
<span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-body)">{{ 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', color: t.fg }" @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>
|