one-design-next 0.0.88 → 0.0.90

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.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * 内联引用 chip 的品类 icon。
3
3
  *
4
- * chip 视觉锚点按「类型」而非条目:skill 一律跟 Composer 技能按钮同源,
5
- * 不使用 SkillItem.icon(那只服务技能菜单辨识)。
4
+ * chip 视觉锚点按「类型」而非条目:skill 一律跟 Composer 与 SkillTab 的
5
+ * 技能入口同源,不使用 SkillItem.icon(那只服务技能菜单辨识)。
6
6
  */
7
- export declare const SKILL_CATEGORY_ICON = "wand-sparkles";
7
+ export declare const SKILL_CATEGORY_ICON = "astroid";
8
8
  /** skill / 未标明的 invocation 通道 → 技能品类 icon;其它 kind 回退 data.icon */
9
9
  export declare function resolveInvocationCategoryIcon(kind?: string, fallback?: string): string | undefined;
10
10
  /** mention 按 kind 给品类 icon;未知 kind 回退 data.icon */
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * 内联引用 chip 的品类 icon。
3
3
  *
4
- * chip 视觉锚点按「类型」而非条目:skill 一律跟 Composer 技能按钮同源,
5
- * 不使用 SkillItem.icon(那只服务技能菜单辨识)。
4
+ * chip 视觉锚点按「类型」而非条目:skill 一律跟 Composer 与 SkillTab 的
5
+ * 技能入口同源,不使用 SkillItem.icon(那只服务技能菜单辨识)。
6
6
  */
7
- export var SKILL_CATEGORY_ICON = 'wand-sparkles';
7
+ export var SKILL_CATEGORY_ICON = 'astroid';
8
8
 
9
9
  /** skill / 未标明的 invocation 通道 → 技能品类 icon;其它 kind 回退 data.icon */
10
10
  export function resolveInvocationCategoryIcon(kind, fallback) {
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ export { default as Mention, type MentionProps } from './mention';
70
70
  export { default as MessageImage, type MessageImageItem, type MessageImageProps, } from './message-image';
71
71
  export { default as PreviewPanel, type PreviewPanelActionTooltips, type PreviewPanelArtifact, type PreviewPanelProps, } from './preview-panel';
72
72
  export { default as Provenance, type ProvenanceProps, type ProvenanceType, } from './provenance';
73
+ export { default as SkillCard, SkillCardIcon, SkillCardMedia, skillCardSwitchStyle, type SkillCardButton, type SkillCardControl, type SkillCardIconProps, type SkillCardIconSize, type SkillCardIconTone, type SkillCardMediaProps, type SkillCardOperation, type SkillCardOperationType, type SkillCardProps, type SkillCardStatusLabel, type SkillCardStatusType, type SkillCardTag, type SkillCardTagType, } from './skill-card';
73
74
  export { default as SkillSlot, type SkillSlotHandle, type SkillSlotProps, } from './skill-slot';
74
75
  export { default as Sources, type SourcesProps } from './sources';
75
76
  export { default as StreamPulse, type StreamPulseProps } from './stream-pulse';
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ export { default as Mention } from "./mention";
75
75
  export { default as MessageImage } from "./message-image";
76
76
  export { default as PreviewPanel } from "./preview-panel";
77
77
  export { default as Provenance } from "./provenance";
78
+ export { default as SkillCard, SkillCardIcon, SkillCardMedia, skillCardSwitchStyle } from "./skill-card";
78
79
  export { default as SkillSlot } from "./skill-slot";
79
80
  export { default as Sources } from "./sources";
80
81
  export { default as StreamPulse } from "./stream-pulse";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * iframe 导航守卫:
3
+ *
4
+ * PreviewPanel 渲染 text/html 时使用 sandbox="allow-scripts" 的 iframe,
5
+ * 内部链接默认被沙箱拦截(既不响应点击,也不能逃逸到顶层)。
6
+ * 通过 `allowedNavigateDomains` 白名单放行特定域名:命中白名单的链接
7
+ * 在点击时以新窗口打开,其余链接继续拦截。
8
+ *
9
+ * 注意:开启白名单后 sandbox 必须追加 allow-popups / allow-popups-to-escape-sandbox
10
+ * (否则 window.open 被沙箱阻止),因此守卫脚本同时监听 click 与 auxclick
11
+ * (中键点击不触发 click,需单独拦截,否则可绕过白名单直接弹窗)。
12
+ */
13
+ /**
14
+ * 判断 hostname 是否命中白名单。
15
+ * 支持精确匹配与子域名匹配(如白名单 `example.com` 可放行 `www.example.com`),
16
+ * 忽略大小写;返回 true 表示“允许跳转”。
17
+ */
18
+ export declare function isDomainAllowed(hostname: string | null | undefined, allowedDomains: readonly string[] | undefined): boolean;
19
+ /**
20
+ * 将导航守卫脚本注入 HTML,返回新的 srcDoc 字符串。
21
+ *
22
+ * 脚本逻辑(依赖 sandbox 已放行 allow-scripts / allow-popups /
23
+ * allow-popups-to-escape-sandbox):
24
+ * - 锚点(`#xxx`)或非链接元素:不拦截,保留默认行为;
25
+ * - href 无法解析,或协议非 http/https(如 javascript:、data:):拦截;
26
+ * - hostname 命中白名单:以新窗口打开(_blank + noopener);
27
+ * - 其余链接:拦截(防止 iframe 被导航走 / 逃逸到顶层);
28
+ * - click 与 auxclick(中键)都拦截,避免中键绕过白名单直接弹窗。
29
+ *
30
+ * 白名单通过 `window.__ODN_NAV_GUARD_ALLOWED__` 注入,便于校验脚本与数据解耦;
31
+ * 注入前将 `<` 转义为 `\u003c`,防止白名单含 `</script>` 时提前闭合脚本标签。
32
+ */
33
+ export declare function injectNavGuardScript(html: string, allowedDomains: readonly string[]): string;
34
+ /** 将 html 增强为“可白名单放行导航”的 srcDoc;未传白名单时原样返回。 */
35
+ export declare function withNavGuard(html: string, allowedDomains: readonly string[] | undefined): string;
@@ -0,0 +1,67 @@
1
+ /* cspell:words noopener */
2
+ /**
3
+ * iframe 导航守卫:
4
+ *
5
+ * PreviewPanel 渲染 text/html 时使用 sandbox="allow-scripts" 的 iframe,
6
+ * 内部链接默认被沙箱拦截(既不响应点击,也不能逃逸到顶层)。
7
+ * 通过 `allowedNavigateDomains` 白名单放行特定域名:命中白名单的链接
8
+ * 在点击时以新窗口打开,其余链接继续拦截。
9
+ *
10
+ * 注意:开启白名单后 sandbox 必须追加 allow-popups / allow-popups-to-escape-sandbox
11
+ * (否则 window.open 被沙箱阻止),因此守卫脚本同时监听 click 与 auxclick
12
+ * (中键点击不触发 click,需单独拦截,否则可绕过白名单直接弹窗)。
13
+ */
14
+
15
+ /**
16
+ * 判断 hostname 是否命中白名单。
17
+ * 支持精确匹配与子域名匹配(如白名单 `example.com` 可放行 `www.example.com`),
18
+ * 忽略大小写;返回 true 表示“允许跳转”。
19
+ */
20
+ export function isDomainAllowed(hostname, allowedDomains) {
21
+ if (!hostname || !allowedDomains || allowedDomains.length === 0) {
22
+ return false;
23
+ }
24
+ var normalized = hostname.toLowerCase();
25
+ return allowedDomains.some(function (domain) {
26
+ if (!domain || typeof domain !== 'string') return false;
27
+ var allowed = domain.trim().toLowerCase();
28
+ if (!allowed) return false;
29
+ return normalized === allowed || normalized.endsWith(".".concat(allowed));
30
+ });
31
+ }
32
+
33
+ /**
34
+ * 将导航守卫脚本注入 HTML,返回新的 srcDoc 字符串。
35
+ *
36
+ * 脚本逻辑(依赖 sandbox 已放行 allow-scripts / allow-popups /
37
+ * allow-popups-to-escape-sandbox):
38
+ * - 锚点(`#xxx`)或非链接元素:不拦截,保留默认行为;
39
+ * - href 无法解析,或协议非 http/https(如 javascript:、data:):拦截;
40
+ * - hostname 命中白名单:以新窗口打开(_blank + noopener);
41
+ * - 其余链接:拦截(防止 iframe 被导航走 / 逃逸到顶层);
42
+ * - click 与 auxclick(中键)都拦截,避免中键绕过白名单直接弹窗。
43
+ *
44
+ * 白名单通过 `window.__ODN_NAV_GUARD_ALLOWED__` 注入,便于校验脚本与数据解耦;
45
+ * 注入前将 `<` 转义为 `\u003c`,防止白名单含 `</script>` 时提前闭合脚本标签。
46
+ */
47
+ export function injectNavGuardScript(html, allowedDomains) {
48
+ // JSON.stringify 不转义 <,这里显式转义,避免白名单内出现 </script> 闭合脚本
49
+ var serializedDomains = JSON.stringify(allowedDomains).replace(/</g, "\\u003c");
50
+ var navGuardScript = "\n<script>\nwindow.__ODN_NAV_GUARD_ALLOWED__ = ".concat(serializedDomains, ";\n(function () {\n var allowed = window.__ODN_NAV_GUARD_ALLOWED__ || [];\n function hostnameAllowed(hostname) {\n if (!hostname || allowed.length === 0) return false;\n hostname = hostname.toLowerCase();\n return allowed.some(function (domain) {\n if (!domain) return false;\n domain = domain.toLowerCase();\n return hostname === domain || hostname.slice(-(domain.length + 1)) === '.' + domain;\n });\n }\n function isSafeHttp(url) {\n return url.protocol === 'http:' || url.protocol === 'https:';\n }\n function intercept(event) {\n var target = event.target;\n if (!target || target.nodeType !== 1) return;\n var anchor = target.closest && target.closest('a[href]');\n if (!anchor) return;\n var href = anchor.getAttribute('href') || '';\n if (href.charAt(0) === '#') return;\n if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n var url;\n try {\n url = new URL(href, window.location.href);\n } catch (e) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (!isSafeHttp(url)) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (hostnameAllowed(url.hostname)) {\n event.preventDefault();\n event.stopPropagation();\n window.open(url.href, '_blank', 'noopener');\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n if (document.addEventListener) {\n document.addEventListener('click', intercept, true);\n document.addEventListener('auxclick', intercept, true);\n }\n})();\n</script>");
51
+ var lower = html.toLowerCase();
52
+ if (lower.includes('</body>')) {
53
+ return html.replace(/<\/body>/i, "".concat(navGuardScript, "</body>"));
54
+ }
55
+ if (lower.includes('</head>')) {
56
+ return html.replace(/<\/head>/i, "".concat(navGuardScript, "</head>"));
57
+ }
58
+ return "".concat(html).concat(navGuardScript);
59
+ }
60
+
61
+ /** 将 html 增强为“可白名单放行导航”的 srcDoc;未传白名单时原样返回。 */
62
+ export function withNavGuard(html, allowedDomains) {
63
+ if (!allowedDomains || allowedDomains.length === 0) {
64
+ return html;
65
+ }
66
+ return injectNavGuardScript(html, allowedDomains);
67
+ }
@@ -78,8 +78,18 @@ export interface PreviewPanelProps {
78
78
  * 未传则不渲染 Tooltip(向后兼容)。
79
79
  */
80
80
  actionTooltips?: PreviewPanelActionTooltips;
81
+ /**
82
+ * HTML 预览(`text/html`)时允许跳转的域名白名单。
83
+ *
84
+ * iframe 默认 `sandbox="allow-scripts"`,内部链接一律被沙箱拦截;
85
+ * 传入本配置后,命中白名单的链接在点击时会以新窗口打开,其余链接保持拦截。
86
+ * 支持精确匹配与子域名匹配(如 `example.com` 可放行 `www.example.com`),
87
+ * 忽略大小写;协议仅放行 `http:` / `https:`,`javascript:` 等始终拦截。
88
+ * 未传时保持原行为不变。
89
+ */
90
+ allowedNavigateDomains?: string[];
81
91
  }
82
- export declare function PreviewPanel({ tabs, activeTabId, onTabChange, onTabClose, onCloseAll, artifacts, onArtifactOpen, onDownload, onOpenInBrowser, onShare, fullscreen, onFullscreenChange, open, emptyTitle, emptyDescription, widthMode, actionTooltips, }: PreviewPanelProps): import("react").JSX.Element;
92
+ export declare function PreviewPanel({ tabs, activeTabId, onTabChange, onTabClose, onCloseAll, artifacts, onArtifactOpen, onDownload, onOpenInBrowser, onShare, fullscreen, onFullscreenChange, open, emptyTitle, emptyDescription, widthMode, actionTooltips, allowedNavigateDomains, }: PreviewPanelProps): import("react").JSX.Element;
83
93
  export declare namespace PreviewPanel {
84
94
  var displayName: string;
85
95
  }
@@ -12,6 +12,7 @@ import Popover from "../popover";
12
12
  import StreamText from "../stream-text";
13
13
  import Tooltip from "../tooltip";
14
14
  import { CsvPreviewTable } from "./CsvPreviewTable";
15
+ import { withNavGuard } from "./iframeNavGuard";
15
16
  import { isCsvTab, parseCsv } from "./parseCsv";
16
17
  import "./style";
17
18
 
@@ -91,7 +92,8 @@ export function PreviewPanel(_ref2) {
91
92
  emptyDescription = _ref2$emptyDescriptio === void 0 ? '对话生成的附件会显示在这里' : _ref2$emptyDescriptio,
92
93
  _ref2$widthMode = _ref2.widthMode,
93
94
  widthMode = _ref2$widthMode === void 0 ? 'half' : _ref2$widthMode,
94
- actionTooltips = _ref2.actionTooltips;
95
+ actionTooltips = _ref2.actionTooltips,
96
+ allowedNavigateDomains = _ref2.allowedNavigateDomains;
95
97
  var activeTab = (_ref3 = (_tabs$find = tabs.find(function (t) {
96
98
  return t.id === activeTabId;
97
99
  })) !== null && _tabs$find !== void 0 ? _tabs$find : tabs[0]) !== null && _ref3 !== void 0 ? _ref3 : null;
@@ -187,6 +189,10 @@ export function PreviewPanel(_ref2) {
187
189
  if (!activeTab || !isCsvTab(activeTab.type, activeTab.name)) return null;
188
190
  return parseCsv(activeTab.content);
189
191
  }, [activeTab]);
192
+ var hasNavGuard = !!allowedNavigateDomains && allowedNavigateDomains.length > 0;
193
+ var htmlSrcDoc = useMemo(function () {
194
+ return (activeTab === null || activeTab === void 0 ? void 0 : activeTab.type) === 'text/html' ? withNavGuard(activeTab.content, allowedNavigateDomains) : undefined;
195
+ }, [activeTab, allowedNavigateDomains]);
190
196
  var handleOpenInBrowser = function handleOpenInBrowser() {
191
197
  if (!activeTab) return;
192
198
  if (onOpenInBrowser) {
@@ -447,9 +453,10 @@ export function PreviewPanel(_ref2) {
447
453
  "data-odn-preview-panel-scroll": true,
448
454
  onScroll: handlePreviewScroll
449
455
  }, activeTab.type === 'text/html' ? /*#__PURE__*/React.createElement("iframe", {
450
- srcDoc: activeTab.content,
451
- sandbox: "allow-scripts",
456
+ srcDoc: htmlSrcDoc,
457
+ sandbox: hasNavGuard ? 'allow-scripts allow-popups allow-popups-to-escape-sandbox' : 'allow-scripts',
452
458
  "data-odn-preview-panel-iframe": true,
459
+ "data-odn-preview-panel-iframe-nav-guard": hasNavGuard || undefined,
453
460
  title: activeTab.name
454
461
  }) : isMarkdownTab(activeTab) ? /*#__PURE__*/React.createElement("div", {
455
462
  "data-odn-preview-panel-markdown": true
@@ -0,0 +1,105 @@
1
+ import * as React from 'react';
2
+ import { type IconName } from '../icon';
3
+ import './style';
4
+ export type SkillCardTagType = 'normal' | 'brand' | 'hot';
5
+ export type SkillCardStatusType = 'info' | 'error';
6
+ export type SkillCardOperationType = 'normal' | 'error';
7
+ export type SkillCardIconSize = 'card' | 'detail';
8
+ export type SkillCardIconTone = 'tinted' | 'plain';
9
+ export interface SkillCardTag {
10
+ type: SkillCardTagType;
11
+ label: string;
12
+ }
13
+ export interface SkillCardStatusLabel {
14
+ type: SkillCardStatusType;
15
+ label: string;
16
+ }
17
+ export interface SkillCardOperation {
18
+ type: SkillCardOperationType;
19
+ label: string;
20
+ icon?: IconName;
21
+ disabled?: boolean;
22
+ onClick?: () => void;
23
+ }
24
+ export interface SkillCardButton {
25
+ icon?: IconName;
26
+ label?: string;
27
+ disabled?: boolean;
28
+ onClick?: () => void;
29
+ }
30
+ /** 官方市场的添加态与已拥有技能的启停态不能混用。 */
31
+ export type SkillCardControl = {
32
+ type: 'add';
33
+ added: boolean;
34
+ /** 添加/移除请求中的受控加载态。 */
35
+ loading?: boolean;
36
+ onChange?: (added: boolean) => void;
37
+ } | {
38
+ type: 'toggle';
39
+ checked: boolean;
40
+ onChange?: (checked: boolean) => void;
41
+ };
42
+ export interface SkillCardIconProps {
43
+ /** 图标内容由业务选择;推荐传入 Lucide 图标。 */
44
+ children: React.ReactNode;
45
+ /** `card` = 48px,`detail` = 80px。 */
46
+ size?: SkillCardIconSize;
47
+ /** `tinted` 用于卡片,`plain` 用于详情页等白底场景。 */
48
+ tone?: SkillCardIconTone;
49
+ className?: string;
50
+ style?: React.CSSProperties;
51
+ }
52
+ export interface SkillCardMediaProps {
53
+ /** 头像 URL;为空或加载失败时回退到 `icon`。 */
54
+ avatarUrl?: string;
55
+ /** 业务侧选择的 Lucide 图标,同时作为头像加载失败的回退。 */
56
+ icon: React.ReactNode;
57
+ alt?: string;
58
+ size?: SkillCardIconSize;
59
+ tone?: SkillCardIconTone;
60
+ className?: string;
61
+ style?: React.CSSProperties;
62
+ }
63
+ /** Skill 图标的统一视觉容器。 */
64
+ export declare function SkillCardIcon({ children, size, tone, className, style, }: SkillCardIconProps): React.JSX.Element;
65
+ /** 展示头像或 Lucide 图标,并在头像失效时回退到图标。 */
66
+ export declare function SkillCardMedia({ avatarUrl, icon, alt, size, tone, className, style, }: SkillCardMediaProps): React.JSX.Element;
67
+ /** SkillCard 内的 Switch 视觉 token,详情页可复用同一规格。 */
68
+ export declare const skillCardSwitchStyle: React.CSSProperties;
69
+ export interface SkillCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'onClick'> {
70
+ /** 技能中文展示名。 */
71
+ name: string;
72
+ /** 业务侧的英文技能标识;空间不足时优先省略。 */
73
+ nameEn?: string;
74
+ /** 一句话技能描述。 */
75
+ description: string;
76
+ /** 业务侧选择的 Lucide 图标。 */
77
+ icon: React.ReactNode;
78
+ /** 由业务决定是否传入;为空或失败时展示 icon。 */
79
+ avatarUrl?: string;
80
+ /** 已按展示顺序映射好的来源、热度、场景、版本等标签。 */
81
+ tags?: SkillCardTag[];
82
+ /** 审核等状态优先于所有操作;传入后隐藏控制、菜单与按钮。 */
83
+ statusLabel?: SkillCardStatusLabel;
84
+ /** 标准化的更多操作菜单;业务侧决定权限、顺序与回调。 */
85
+ operations?: SkillCardOperation[];
86
+ /** Hover 后的主操作,例如「去试试」。 */
87
+ button?: SkillCardButton;
88
+ /** 官方添加态或我的技能启停态,保持受控。 */
89
+ control?: SkillCardControl;
90
+ /** 点击卡片内容区。组件不负责打开详情。 */
91
+ onOpen?: () => void;
92
+ className?: string;
93
+ style?: React.CSSProperties;
94
+ }
95
+ /**
96
+ * 技能市场的展示卡片。
97
+ *
98
+ * 组件处理布局、状态优先级、Hover 动效和标准化 Action;数据筛选、
99
+ * 权限、详情路由及实际的添加、启停、试用行为均交由业务侧回调实现。
100
+ */
101
+ export declare function SkillCard({ name, nameEn, description, icon, avatarUrl, tags, statusLabel, operations, button, control, onOpen, className, style, ...rest }: SkillCardProps): React.JSX.Element;
102
+ export declare namespace SkillCard {
103
+ var displayName: string;
104
+ }
105
+ export default SkillCard;
@@ -0,0 +1,320 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
3
+ import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
4
+ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
5
+ var _excluded = ["name", "nameEn", "description", "icon", "avatarUrl", "tags", "statusLabel", "operations", "button", "control", "onOpen", "className", "style"];
6
+ import clsx from 'clsx';
7
+ import * as React from 'react';
8
+ import Button from "../button";
9
+ import Card from "../card";
10
+ import Dropdown from "../dropdown";
11
+ import Icon from "../icon";
12
+ import Switch from "../switch";
13
+ import Tooltip from "../tooltip";
14
+ import "./style";
15
+
16
+ /** 官方市场的添加态与已拥有技能的启停态不能混用。 */
17
+
18
+ /** Skill 图标的统一视觉容器。 */
19
+ export function SkillCardIcon(_ref) {
20
+ var children = _ref.children,
21
+ _ref$size = _ref.size,
22
+ size = _ref$size === void 0 ? 'card' : _ref$size,
23
+ _ref$tone = _ref.tone,
24
+ tone = _ref$tone === void 0 ? 'tinted' : _ref$tone,
25
+ className = _ref.className,
26
+ style = _ref.style;
27
+ return /*#__PURE__*/React.createElement("span", {
28
+ "data-odn-skill-card-media": "icon",
29
+ "data-odn-skill-card-icon-box": true,
30
+ "data-odn-skill-card-icon-size": size,
31
+ "data-odn-skill-card-icon-tone": tone,
32
+ className: clsx(className),
33
+ style: style
34
+ }, children);
35
+ }
36
+
37
+ /** 展示头像或 Lucide 图标,并在头像失效时回退到图标。 */
38
+ export function SkillCardMedia(_ref2) {
39
+ var avatarUrl = _ref2.avatarUrl,
40
+ icon = _ref2.icon,
41
+ _ref2$alt = _ref2.alt,
42
+ alt = _ref2$alt === void 0 ? '' : _ref2$alt,
43
+ _ref2$size = _ref2.size,
44
+ size = _ref2$size === void 0 ? 'card' : _ref2$size,
45
+ _ref2$tone = _ref2.tone,
46
+ tone = _ref2$tone === void 0 ? 'tinted' : _ref2$tone,
47
+ className = _ref2.className,
48
+ style = _ref2.style;
49
+ var _React$useState = React.useState(false),
50
+ _React$useState2 = _slicedToArray(_React$useState, 2),
51
+ avatarFailed = _React$useState2[0],
52
+ setAvatarFailed = _React$useState2[1];
53
+ var shouldShowAvatar = !!avatarUrl && !avatarFailed;
54
+ React.useEffect(function () {
55
+ return setAvatarFailed(false);
56
+ }, [avatarUrl]);
57
+ if (!shouldShowAvatar) {
58
+ return /*#__PURE__*/React.createElement(SkillCardIcon, {
59
+ size: size,
60
+ tone: tone,
61
+ className: className,
62
+ style: style
63
+ }, icon);
64
+ }
65
+ return /*#__PURE__*/React.createElement("img", {
66
+ "data-odn-skill-card-media": "avatar",
67
+ "data-odn-skill-card-icon-box": true,
68
+ "data-odn-skill-card-icon-size": size,
69
+ "data-odn-skill-card-icon-tone": tone,
70
+ className: clsx(className),
71
+ style: style,
72
+ src: avatarUrl,
73
+ alt: alt,
74
+ onError: function onError() {
75
+ return setAvatarFailed(true);
76
+ }
77
+ });
78
+ }
79
+
80
+ /** SkillCard 内的 Switch 视觉 token,详情页可复用同一规格。 */
81
+ export var skillCardSwitchStyle = {
82
+ '--odn-switch-track-bg': 'rgba(74, 97, 143, 0.1)',
83
+ '--odn-switch-track-bg-hover': 'rgba(74, 97, 143, 0.14)',
84
+ '--odn-switch-track-bg-active': 'rgba(74, 97, 143, 0.18)',
85
+ '--odn-switch-track-bg-checked': '#2694FD',
86
+ '--odn-switch-track-bg-checked-hover': '#2694FD',
87
+ '--odn-switch-track-bg-checked-active': '#2694FD'
88
+ };
89
+ var TAG_GAP = 6;
90
+ var MIN_TRUNCATABLE_TAG_WIDTH = 36;
91
+
92
+ /** 来源优先;末项不足两字时收纳为 +N,避免难以辨识的单字截断。 */
93
+ function SkillCardTagList(_ref3) {
94
+ var tags = _ref3.tags;
95
+ var groupRef = React.useRef(null);
96
+ var measureRefs = React.useRef([]);
97
+ var _React$useState3 = React.useState(0),
98
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
99
+ collapsedCount = _React$useState4[0],
100
+ setCollapsedCount = _React$useState4[1];
101
+ var recalculate = React.useCallback(function () {
102
+ var _measureRefs$current$;
103
+ var group = groupRef.current;
104
+ if (!group || group.clientWidth <= 0 || tags.length < 2) return;
105
+ var sourceWidth = (_measureRefs$current$ = measureRefs.current[0]) === null || _measureRefs$current$ === void 0 ? void 0 : _measureRefs$current$.getBoundingClientRect().width;
106
+ if (!sourceWidth) return;
107
+ var remainingWidth = group.clientWidth - sourceWidth;
108
+ var nextCollapsedCount = 0;
109
+ for (var index = 1; index < tags.length; index += 1) {
110
+ var _measureRefs$current$2;
111
+ var tagWidth = (_measureRefs$current$2 = measureRefs.current[index]) === null || _measureRefs$current$2 === void 0 ? void 0 : _measureRefs$current$2.getBoundingClientRect().width;
112
+ if (!tagWidth) return;
113
+ remainingWidth -= TAG_GAP;
114
+ var isLastTag = index === tags.length - 1;
115
+ if (isLastTag) {
116
+ if (remainingWidth < MIN_TRUNCATABLE_TAG_WIDTH) {
117
+ nextCollapsedCount = tags.length - index;
118
+ }
119
+ break;
120
+ }
121
+ if (remainingWidth < tagWidth) {
122
+ nextCollapsedCount = tags.length - index;
123
+ break;
124
+ }
125
+ remainingWidth -= tagWidth;
126
+ }
127
+ setCollapsedCount(function (current) {
128
+ return current === nextCollapsedCount ? current : nextCollapsedCount;
129
+ });
130
+ }, [tags]);
131
+ React.useLayoutEffect(function () {
132
+ recalculate();
133
+ var group = groupRef.current;
134
+ if (!group || typeof ResizeObserver === 'undefined') return;
135
+ var observer = new ResizeObserver(recalculate);
136
+ observer.observe(group);
137
+ return function () {
138
+ return observer.disconnect();
139
+ };
140
+ }, [recalculate]);
141
+ var visibleCount = tags.length - collapsedCount;
142
+ return /*#__PURE__*/React.createElement("span", {
143
+ ref: groupRef,
144
+ "data-odn-skill-card-label-group": true
145
+ }, tags.slice(0, visibleCount).map(function (tag, index) {
146
+ return /*#__PURE__*/React.createElement("span", {
147
+ key: "".concat(tag.type, "-").concat(tag.label, "-").concat(index),
148
+ "data-odn-skill-card-tag": tag.type,
149
+ "data-odn-skill-card-tag-last": collapsedCount === 0 && index === tags.length - 1 || undefined
150
+ }, tag.type === 'hot' ? /*#__PURE__*/React.createElement(Icon, {
151
+ name: "hot-filled",
152
+ size: 14
153
+ }) : null, /*#__PURE__*/React.createElement("span", {
154
+ "data-odn-skill-card-tag-label": true
155
+ }, tag.label));
156
+ }), collapsedCount > 0 ? /*#__PURE__*/React.createElement("span", {
157
+ "data-odn-skill-card-tag": "normal",
158
+ "data-odn-skill-card-tag-summary": true
159
+ }, /*#__PURE__*/React.createElement("span", {
160
+ "data-odn-skill-card-tag-label": true
161
+ }, "+", collapsedCount)) : null, /*#__PURE__*/React.createElement("span", {
162
+ "aria-hidden": true,
163
+ "data-odn-skill-card-tag-measurer": true
164
+ }, tags.map(function (tag, index) {
165
+ return /*#__PURE__*/React.createElement("span", {
166
+ key: "".concat(tag.type, "-").concat(tag.label, "-").concat(index),
167
+ ref: function ref(element) {
168
+ measureRefs.current[index] = element;
169
+ },
170
+ "data-odn-skill-card-tag": tag.type
171
+ }, tag.type === 'hot' ? /*#__PURE__*/React.createElement(Icon, {
172
+ name: "hot-filled",
173
+ size: 14
174
+ }) : null, /*#__PURE__*/React.createElement("span", {
175
+ "data-odn-skill-card-tag-label": true
176
+ }, tag.label));
177
+ })));
178
+ }
179
+
180
+ /**
181
+ * 技能市场的展示卡片。
182
+ *
183
+ * 组件处理布局、状态优先级、Hover 动效和标准化 Action;数据筛选、
184
+ * 权限、详情路由及实际的添加、启停、试用行为均交由业务侧回调实现。
185
+ */
186
+ export function SkillCard(_ref4) {
187
+ var _control$type, _statusLabel$type;
188
+ var name = _ref4.name,
189
+ nameEn = _ref4.nameEn,
190
+ description = _ref4.description,
191
+ icon = _ref4.icon,
192
+ avatarUrl = _ref4.avatarUrl,
193
+ _ref4$tags = _ref4.tags,
194
+ tags = _ref4$tags === void 0 ? [] : _ref4$tags,
195
+ statusLabel = _ref4.statusLabel,
196
+ _ref4$operations = _ref4.operations,
197
+ operations = _ref4$operations === void 0 ? [] : _ref4$operations,
198
+ button = _ref4.button,
199
+ control = _ref4.control,
200
+ onOpen = _ref4.onOpen,
201
+ className = _ref4.className,
202
+ style = _ref4.style,
203
+ rest = _objectWithoutProperties(_ref4, _excluded);
204
+ var isAddControl = (control === null || control === void 0 ? void 0 : control.type) === 'add';
205
+ var isToggleControl = (control === null || control === void 0 ? void 0 : control.type) === 'toggle';
206
+ var hasButton = !!button && !!(button.label || button.icon);
207
+ var canShowHoverActions = !statusLabel && (operations.length > 0 || hasButton) && (!isAddControl || control.added);
208
+ var hasActionGroup = !statusLabel && (!!control || canShowHoverActions);
209
+ /** 审核态也需要 Hover 动效;仅状态标签保持占位,不展示 ActionGroup。 */
210
+ var isHoverable = !!statusLabel || !!onOpen || hasActionGroup;
211
+ var operationMenu = operations.flatMap(function (operation, index) {
212
+ var _operations;
213
+ return [].concat(_toConsumableArray(operation.type === 'error' && index > 0 && ((_operations = operations[index - 1]) === null || _operations === void 0 ? void 0 : _operations.type) !== 'error' ? [{
214
+ type: 'separator'
215
+ }] : []), [{
216
+ label: /*#__PURE__*/React.createElement("span", {
217
+ className: clsx('flex items-center gap-2', operation.type === 'error' && 'text-red-6')
218
+ }, operation.icon ? /*#__PURE__*/React.createElement(Icon, {
219
+ name: operation.icon,
220
+ size: 16
221
+ }) : null, operation.label),
222
+ value: String(index),
223
+ disabled: operation.disabled
224
+ }]);
225
+ });
226
+ return /*#__PURE__*/React.createElement(Card, _extends({
227
+ elevation: 0,
228
+ "data-odn-skill-card": true,
229
+ "data-odn-skill-card-control": (_control$type = control === null || control === void 0 ? void 0 : control.type) !== null && _control$type !== void 0 ? _control$type : 'none',
230
+ "data-odn-skill-card-added": isAddControl ? String(control.added) : undefined,
231
+ "data-odn-skill-card-status": (_statusLabel$type = statusLabel === null || statusLabel === void 0 ? void 0 : statusLabel.type) !== null && _statusLabel$type !== void 0 ? _statusLabel$type : 'default',
232
+ "data-odn-skill-card-hoverable": isHoverable || undefined,
233
+ className: clsx(className),
234
+ style: style
235
+ }, rest), onOpen ? /*#__PURE__*/React.createElement("button", {
236
+ type: "button",
237
+ "data-odn-skill-card-open": true,
238
+ "aria-label": "\u6253\u5F00".concat(name, "\u8BE6\u60C5"),
239
+ onClick: onOpen
240
+ }) : null, /*#__PURE__*/React.createElement("span", {
241
+ "data-odn-skill-card-glow": true,
242
+ "aria-hidden": true
243
+ }), /*#__PURE__*/React.createElement("span", {
244
+ "data-odn-skill-card-icon": true,
245
+ "aria-hidden": true
246
+ }, /*#__PURE__*/React.createElement(SkillCardMedia, {
247
+ avatarUrl: avatarUrl,
248
+ icon: icon,
249
+ alt: "".concat(name, "\u5934\u50CF")
250
+ })), /*#__PURE__*/React.createElement("span", {
251
+ "data-odn-skill-card-copy": true
252
+ }, /*#__PURE__*/React.createElement("span", {
253
+ "data-odn-skill-card-name-group": true
254
+ }, /*#__PURE__*/React.createElement("span", {
255
+ "data-odn-skill-card-name": true
256
+ }, name), nameEn ? /*#__PURE__*/React.createElement("span", {
257
+ "data-odn-skill-card-alias": true
258
+ }, nameEn) : null), tags.length > 0 ? /*#__PURE__*/React.createElement(SkillCardTagList, {
259
+ tags: tags
260
+ }) : null), /*#__PURE__*/React.createElement("p", {
261
+ "data-odn-skill-card-description": true
262
+ }, description), statusLabel ? /*#__PURE__*/React.createElement("span", {
263
+ "data-odn-skill-card-status-badge": statusLabel.type
264
+ }, statusLabel.label) : hasActionGroup ? /*#__PURE__*/React.createElement("span", {
265
+ "data-odn-skill-card-action-group": true
266
+ }, isAddControl ? /*#__PURE__*/React.createElement("span", {
267
+ "data-odn-skill-card-default-action": true
268
+ }, /*#__PURE__*/React.createElement(Tooltip, {
269
+ popup: control.added ? '移除已添加技能' : '添加技能',
270
+ placement: "top"
271
+ }, /*#__PURE__*/React.createElement(Button, {
272
+ type: "button",
273
+ size: "small",
274
+ intent: "normal",
275
+ icon: control.added ? 'check' : 'plus',
276
+ loading: control.loading,
277
+ "data-odn-skill-card-added-action": control.added || undefined,
278
+ "aria-label": control.added ? '移除已添加技能' : '添加技能',
279
+ disabled: !control.onChange || control.loading,
280
+ onClick: function onClick() {
281
+ var _control$onChange;
282
+ return (_control$onChange = control.onChange) === null || _control$onChange === void 0 ? void 0 : _control$onChange.call(control, !control.added);
283
+ }
284
+ }))) : null, canShowHoverActions ? /*#__PURE__*/React.createElement("span", {
285
+ "data-odn-skill-card-hover-actions": true
286
+ }, operations.length > 0 ? /*#__PURE__*/React.createElement(Dropdown, {
287
+ popupClassName: "odn-skill-card-menu",
288
+ menu: operationMenu,
289
+ onSelect: function onSelect(value) {
290
+ var _operations$Number, _operations$Number$on;
291
+ return (_operations$Number = operations[Number(value)]) === null || _operations$Number === void 0 || (_operations$Number$on = _operations$Number.onClick) === null || _operations$Number$on === void 0 ? void 0 : _operations$Number$on.call(_operations$Number);
292
+ }
293
+ }, /*#__PURE__*/React.createElement("span", {
294
+ "data-odn-skill-card-operation-trigger": true
295
+ }, /*#__PURE__*/React.createElement(Button, {
296
+ light: true,
297
+ size: "small",
298
+ icon: "ellipsis",
299
+ "aria-label": "\u66F4\u591A\u64CD\u4F5C"
300
+ }))) : null, hasButton && button ? /*#__PURE__*/React.createElement(Button, {
301
+ type: "button",
302
+ size: "small",
303
+ intent: "normal",
304
+ icon: button.icon,
305
+ disabled: button.disabled || !button.onClick,
306
+ "data-odn-skill-card-try": true,
307
+ onClick: button.onClick
308
+ }, button.label) : null) : null, isToggleControl ? /*#__PURE__*/React.createElement("span", {
309
+ "data-odn-skill-card-switch": true
310
+ }, /*#__PURE__*/React.createElement(Switch, {
311
+ size: "medium",
312
+ style: skillCardSwitchStyle,
313
+ checked: control.checked,
314
+ disabled: !control.onChange,
315
+ "aria-label": control.checked ? "\u505C\u7528".concat(name) : "\u542F\u7528".concat(name),
316
+ onChange: control.onChange
317
+ })) : null) : null);
318
+ }
319
+ SkillCard.displayName = 'SkillCard';
320
+ export default SkillCard;
@@ -0,0 +1,430 @@
1
+ @charset "UTF-8";
2
+ html {
3
+ --odn-skill-card-height: 144px;
4
+ --odn-skill-card-padding: 20px;
5
+ --odn-skill-card-action-gap: 8px;
6
+ --odn-skill-card-hover-duration: 0.3s;
7
+ --odn-skill-card-hover-ease: cubic-bezier(0.4, 0, 0.2, 1);
8
+ --odn-skill-card-text: rgba(0, 0, 0, 0.95);
9
+ --odn-skill-card-secondary: rgba(51, 55, 61, 0.58);
10
+ --odn-skill-card-source: #296bef;
11
+ --odn-skill-card-source-bg: rgba(41, 107, 239, 0.1);
12
+ --odn-skill-card-soft-bg: rgba(74, 97, 143, 0.08);
13
+ --odn-skill-card-border: rgba(74, 97, 143, 0.08);
14
+ }
15
+
16
+ [data-odn-skill-card] {
17
+ --odn-card-radius: 16px;
18
+ box-sizing: border-box;
19
+ position: relative;
20
+ isolation: isolate;
21
+ width: 100%;
22
+ min-width: 0;
23
+ height: var(--odn-skill-card-height);
24
+ overflow: hidden;
25
+ border: 1px solid var(--odn-skill-card-border);
26
+ border-radius: 16px;
27
+ background: var(--odn-color-bg-elevated);
28
+ color: var(--odn-skill-card-text);
29
+ text-align: left;
30
+ transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
31
+ }
32
+
33
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover {
34
+ border-color: var(--odn-color-black-6);
35
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
36
+ }
37
+
38
+ [data-odn-skill-card-open] {
39
+ position: absolute;
40
+ inset: 0;
41
+ z-index: 1;
42
+ padding: 0;
43
+ border: 0;
44
+ background: transparent;
45
+ cursor: pointer;
46
+ }
47
+
48
+ [data-odn-skill-card-open]:focus {
49
+ outline: none;
50
+ }
51
+
52
+ [data-odn-skill-card-open]:focus-visible {
53
+ outline: 2px solid color-mix(in srgb, var(--odn-color-primary) 55%, transparent);
54
+ outline-offset: -2px;
55
+ }
56
+
57
+ [data-odn-skill-card-glow] {
58
+ position: absolute;
59
+ inset: 0;
60
+ z-index: 0;
61
+ overflow: hidden;
62
+ border-radius: inherit;
63
+ pointer-events: none;
64
+ opacity: 0;
65
+ transition: opacity var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease);
66
+ }
67
+
68
+ [data-odn-skill-card-glow]::after {
69
+ content: "";
70
+ position: absolute;
71
+ left: -65px;
72
+ top: -30px;
73
+ width: 230px;
74
+ height: 130px;
75
+ transform: rotate(-8.4deg);
76
+ background-image: radial-gradient(ellipse at center, rgb(38, 148, 253), transparent 60%);
77
+ }
78
+
79
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover [data-odn-skill-card-glow] {
80
+ opacity: 0.12;
81
+ }
82
+
83
+ [data-odn-skill-card-icon] {
84
+ position: absolute;
85
+ left: var(--odn-skill-card-padding);
86
+ top: var(--odn-skill-card-padding);
87
+ z-index: 10;
88
+ display: inline-flex;
89
+ pointer-events: none;
90
+ transform: scale(1);
91
+ transform-origin: center;
92
+ opacity: 1;
93
+ will-change: transform;
94
+ transition: transform var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), opacity var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease);
95
+ }
96
+
97
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover [data-odn-skill-card-icon] {
98
+ transform: scale(0.73);
99
+ opacity: 0;
100
+ }
101
+
102
+ [data-odn-skill-card-icon-box] {
103
+ display: inline-flex;
104
+ flex-shrink: 0;
105
+ align-items: center;
106
+ justify-content: center;
107
+ overflow: hidden;
108
+ border: 1px solid var(--odn-color-black-1);
109
+ border-radius: 8px;
110
+ color: rgb(38, 148, 253);
111
+ object-fit: cover;
112
+ }
113
+
114
+ [data-odn-skill-card-icon-box][data-odn-skill-card-icon-size=card] {
115
+ width: 48px;
116
+ height: 48px;
117
+ }
118
+
119
+ [data-odn-skill-card-icon-box][data-odn-skill-card-icon-size=detail] {
120
+ width: 80px;
121
+ height: 80px;
122
+ }
123
+
124
+ [data-odn-skill-card-icon-box][data-odn-skill-card-icon-tone=tinted] {
125
+ background: var(--odn-color-blue-1, #f2f7ff);
126
+ }
127
+
128
+ [data-odn-skill-card-icon-box][data-odn-skill-card-icon-tone=plain] {
129
+ background: var(--odn-color-bg-elevated);
130
+ }
131
+
132
+ [data-odn-skill-card-copy] {
133
+ position: absolute;
134
+ top: var(--odn-skill-card-padding);
135
+ right: 70px;
136
+ left: 80px;
137
+ z-index: 20;
138
+ display: flex;
139
+ min-width: 0;
140
+ flex-direction: column;
141
+ pointer-events: none;
142
+ transform: translateX(0);
143
+ will-change: left, right, transform;
144
+ transition: left var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), right var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), transform var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease);
145
+ }
146
+
147
+ [data-odn-skill-card][data-odn-skill-card-control=add] [data-odn-skill-card-copy] {
148
+ right: 62px;
149
+ }
150
+
151
+ [data-odn-skill-card][data-odn-skill-card-status=info] [data-odn-skill-card-copy],
152
+ [data-odn-skill-card][data-odn-skill-card-status=error] [data-odn-skill-card-copy] {
153
+ right: 92px;
154
+ }
155
+
156
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover [data-odn-skill-card-copy] {
157
+ transform: translateX(-60px);
158
+ }
159
+
160
+ /* 审核态右上不切换操作,但左侧图标让位后 nameEn 需要获得真实可用宽度。 */
161
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true][data-odn-skill-card-status=info]:hover [data-odn-skill-card-copy],
162
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true][data-odn-skill-card-status=error]:hover [data-odn-skill-card-copy] {
163
+ left: var(--odn-skill-card-padding);
164
+ transform: translateX(0);
165
+ }
166
+
167
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true][data-odn-skill-card-status=default][data-odn-skill-card-control=toggle]:hover [data-odn-skill-card-copy] {
168
+ right: 116px;
169
+ }
170
+
171
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true][data-odn-skill-card-status=default][data-odn-skill-card-control=add][data-odn-skill-card-added=true]:hover [data-odn-skill-card-copy] {
172
+ right: 70px;
173
+ }
174
+
175
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true][data-odn-skill-card-status=default][data-odn-skill-card-control=add][data-odn-skill-card-added=false]:hover [data-odn-skill-card-copy] {
176
+ right: 2px;
177
+ }
178
+
179
+ [data-odn-skill-card-name-group] {
180
+ display: flex;
181
+ min-width: 0;
182
+ align-items: baseline;
183
+ gap: 8px;
184
+ }
185
+
186
+ [data-odn-skill-card-name] {
187
+ min-width: 0;
188
+ max-width: calc(100% - 8px);
189
+ flex: 0 1 auto;
190
+ overflow: hidden;
191
+ color: var(--odn-skill-card-text);
192
+ font-size: 14px;
193
+ font-weight: 500;
194
+ line-height: 22px;
195
+ text-overflow: ellipsis;
196
+ white-space: nowrap;
197
+ }
198
+
199
+ [data-odn-skill-card-alias] {
200
+ min-width: 0;
201
+ flex: 1 1 0;
202
+ overflow: hidden;
203
+ color: var(--odn-skill-card-secondary);
204
+ font-size: 14px;
205
+ line-height: 22px;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ }
209
+
210
+ [data-odn-skill-card-label-group] {
211
+ display: flex;
212
+ min-width: 0;
213
+ align-items: center;
214
+ gap: 6px;
215
+ margin-top: 4px;
216
+ overflow: hidden;
217
+ white-space: nowrap;
218
+ }
219
+
220
+ [data-odn-skill-card-tag] {
221
+ display: inline-flex;
222
+ box-sizing: border-box;
223
+ height: 22px;
224
+ flex-shrink: 0;
225
+ align-items: center;
226
+ border-radius: 4px;
227
+ padding: 0 6px;
228
+ font-size: 12px;
229
+ line-height: 22px;
230
+ }
231
+
232
+ [data-odn-skill-card-tag-label] {
233
+ min-width: 0;
234
+ white-space: nowrap;
235
+ }
236
+
237
+ [data-odn-skill-card-tag-last=true] {
238
+ min-width: 24px;
239
+ flex: 0 1 auto;
240
+ overflow: hidden;
241
+ }
242
+
243
+ [data-odn-skill-card-tag-last=true] [data-odn-skill-card-tag-label] {
244
+ display: block;
245
+ overflow: hidden;
246
+ text-overflow: ellipsis;
247
+ }
248
+
249
+ [data-odn-skill-card-tag-measurer] {
250
+ position: absolute;
251
+ top: 0;
252
+ left: -9999px;
253
+ display: flex;
254
+ visibility: hidden;
255
+ white-space: nowrap;
256
+ }
257
+
258
+ [data-odn-skill-card-tag-measurer] [data-odn-skill-card-tag] {
259
+ flex: none;
260
+ }
261
+
262
+ [data-odn-skill-card-tag=brand] {
263
+ max-width: 84px;
264
+ overflow: hidden;
265
+ color: var(--odn-skill-card-source);
266
+ background: var(--odn-skill-card-source-bg);
267
+ font-weight: 500;
268
+ }
269
+
270
+ [data-odn-skill-card-tag=brand] [data-odn-skill-card-tag-label] {
271
+ display: block;
272
+ overflow: hidden;
273
+ text-overflow: ellipsis;
274
+ }
275
+
276
+ [data-odn-skill-card-tag=hot] {
277
+ gap: 4px;
278
+ color: var(--odn-color-orange-6);
279
+ background: var(--odn-color-orange-1);
280
+ font-weight: 500;
281
+ }
282
+
283
+ [data-odn-skill-card-tag=normal] {
284
+ color: var(--odn-skill-card-secondary);
285
+ background: var(--odn-skill-card-soft-bg);
286
+ }
287
+
288
+ [data-odn-skill-card-description] {
289
+ position: absolute;
290
+ top: 80px;
291
+ right: var(--odn-skill-card-padding);
292
+ left: var(--odn-skill-card-padding);
293
+ z-index: 20;
294
+ display: -webkit-box;
295
+ margin: 0;
296
+ overflow: hidden;
297
+ color: var(--odn-skill-card-secondary);
298
+ font-size: 14px;
299
+ line-height: 22px;
300
+ pointer-events: none;
301
+ -webkit-box-orient: vertical;
302
+ -webkit-line-clamp: 2;
303
+ }
304
+
305
+ [data-odn-skill-card-status-badge] {
306
+ position: absolute;
307
+ top: var(--odn-skill-card-padding);
308
+ right: var(--odn-skill-card-padding);
309
+ z-index: 30;
310
+ display: inline-flex;
311
+ box-sizing: border-box;
312
+ height: 22px;
313
+ align-items: center;
314
+ border-radius: 4px;
315
+ padding: 0 8px;
316
+ font-size: 12px;
317
+ font-weight: 500;
318
+ line-height: 22px;
319
+ }
320
+
321
+ [data-odn-skill-card-status-badge=info] {
322
+ color: var(--odn-color-blue-6);
323
+ background: var(--odn-color-blue-1);
324
+ }
325
+
326
+ [data-odn-skill-card-status-badge=error] {
327
+ color: var(--odn-color-red-6);
328
+ background: var(--odn-color-red-1);
329
+ }
330
+
331
+ [data-odn-skill-card-action-group] {
332
+ position: absolute;
333
+ top: var(--odn-skill-card-padding);
334
+ right: var(--odn-skill-card-padding);
335
+ z-index: 30;
336
+ width: 144px;
337
+ height: 30px;
338
+ }
339
+
340
+ [data-odn-skill-card][data-odn-skill-card-control=add] [data-odn-skill-card-action-group],
341
+ [data-odn-skill-card][data-odn-skill-card-control=none] [data-odn-skill-card-action-group] {
342
+ width: 98px;
343
+ }
344
+
345
+ [data-odn-skill-card-default-action] {
346
+ position: absolute;
347
+ inset: 0;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: flex-end;
351
+ transition: opacity var(--odn-skill-card-hover-duration) ease-out, transform var(--odn-skill-card-hover-duration) ease-out;
352
+ }
353
+
354
+ [data-odn-skill-card-default-action] [data-odn-button] {
355
+ width: 30px;
356
+ min-width: 30px;
357
+ height: 30px;
358
+ padding: 0;
359
+ }
360
+
361
+ /* 已添加仅保留绿色对勾;未添加仍使用带描边的“+”按钮。 */
362
+ [data-odn-skill-card-default-action] [data-odn-button][data-odn-skill-card-added-action=true],
363
+ [data-odn-skill-card-default-action] [data-odn-button][data-odn-skill-card-added-action=true]:hover {
364
+ color: var(--odn-color-green-6);
365
+ background: var(--odn-color-green-1);
366
+ border-color: transparent;
367
+ }
368
+
369
+ [data-odn-skill-card-hover-actions] {
370
+ position: absolute;
371
+ top: 0;
372
+ right: 0;
373
+ display: flex;
374
+ height: 30px;
375
+ align-items: center;
376
+ gap: var(--odn-skill-card-action-gap);
377
+ pointer-events: none;
378
+ opacity: 0;
379
+ transform: translateX(4px);
380
+ transition: opacity var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease), transform var(--odn-skill-card-hover-duration) var(--odn-skill-card-hover-ease);
381
+ }
382
+
383
+ [data-odn-skill-card][data-odn-skill-card-control=toggle] [data-odn-skill-card-hover-actions] {
384
+ right: 46px;
385
+ }
386
+
387
+ /* 已添加 check 保持同一元素:默认居右,Hover 时位移到「去试试」左侧。 */
388
+ [data-odn-skill-card][data-odn-skill-card-control=add][data-odn-skill-card-added=true] [data-odn-skill-card-default-action] {
389
+ right: 0;
390
+ left: auto;
391
+ width: 30px;
392
+ justify-content: center;
393
+ }
394
+
395
+ [data-odn-skill-card][data-odn-skill-card-control=add][data-odn-skill-card-added=true]:hover [data-odn-skill-card-default-action] {
396
+ transform: translateX(-68px);
397
+ }
398
+
399
+ [data-odn-skill-card][data-odn-skill-card-hoverable=true]:hover [data-odn-skill-card-hover-actions] {
400
+ pointer-events: auto;
401
+ opacity: 1;
402
+ transform: translateX(0);
403
+ }
404
+
405
+ [data-odn-skill-card][data-odn-skill-card-control=add][data-odn-skill-card-added=true]:hover [data-odn-skill-card-hover-actions] {
406
+ right: 0;
407
+ }
408
+
409
+ [data-odn-skill-card-try] {
410
+ width: 60px;
411
+ min-width: 60px;
412
+ }
413
+
414
+ [data-odn-skill-card-hover-actions] [data-odn-button][aria-label=更多操作] {
415
+ width: 30px;
416
+ min-width: 30px;
417
+ height: 30px;
418
+ padding: 0;
419
+ }
420
+
421
+ [data-odn-dropdown-content].odn-skill-card-menu {
422
+ min-width: 120px;
423
+ padding: 4px 0;
424
+ }
425
+
426
+ [data-odn-skill-card-switch] {
427
+ position: absolute;
428
+ top: 5px;
429
+ right: 0;
430
+ }
@@ -0,0 +1,2 @@
1
+ import '../../style';
2
+ import './index.scss';
@@ -0,0 +1,2 @@
1
+ import "../../style";
2
+ import "./index.css";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",