@morlay/dsh-client-ui-primitives 0.0.6 → 0.0.7
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/README.md +3 -2
- package/dist/client.d.mts +26 -0
- package/dist/client.mjs +4576 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +20 -29
- package/src/client/index.ts +7 -7
- package/src/client/schema-form/Editor.tsx +20 -12
- package/src/client/schema-form/TruncatedTooltip.tsx +60 -0
- package/src/client/schema-form/index.ts +11 -0
- package/src/client/schema-form/styles.ts +26 -3
- package/src/client/schema-form/testing/resize-observer-stub.ts +63 -0
- package/src/client/schema-form/value.tsx +10 -8
- package/src/client/styling/styling.ts +6 -1
- package/src/client/theme.generated.ts +14 -1
- package/dist/client.cjs +0 -17075
- package/dist/client.d.cts +0 -11656
- package/dist/index.d.cts +0 -5
- package/dist/reference-BX54bj6_.d.cts +0 -30
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "UI Primitives",
|
|
4
|
-
"description": "css-in-js styling layer, the
|
|
4
|
+
"description": "The shared base for the conversation UI and the settings pages: the css-in-js styling layer, the reference parser and renderer (one source shared with the host), the staged settings-form primitives forked from upstream, and the schema-generated row config pages (field-slot injection). Inlined by the client rows that use it, not an assembly row."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "UI 基础层",
|
|
4
|
-
"description": "css-in-js
|
|
4
|
+
"description": "对话 UI 与设置页共用的基础面:css-in-js 样式层、host 与 client 共用一份源码的引用解析与渲染转换、上游 fork 过来的暂存式设置表单原语,以及按 schema 自动生成的行配置页(字段槽注入)。随用到它的 client 行内联,不是装配行。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-client-ui-primitives",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "css-in-js styling kit (styled / Token / Styling), the shared reference parser and renderer (findReferences / parseReferenceToken)
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "css-in-js styling kit (styled / Token / Styling), the shared reference parser and renderer (findReferences / parseReferenceToken), and the settings-form primitives forked from upstream (SettingsFormModel / field controls / form shell) plus the schema-generated row config page: consumes the official --dsw-* design tokens, so no CSS Modules precompile step is needed. Ships as an inlined client library (`dsh.client.inline`), not as an assembly row.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client",
|
|
7
7
|
"css-in-js",
|
|
@@ -24,11 +24,8 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": "./dist/index.mjs",
|
|
27
|
+
"./client": "./dist/client.mjs",
|
|
27
28
|
"./package.json": "./package.json",
|
|
28
|
-
"./client": {
|
|
29
|
-
"types": "./dist/client.d.cts",
|
|
30
|
-
"default": "./dist/client.cjs"
|
|
31
|
-
},
|
|
32
29
|
"./locale/*.json": "./locale/*.json"
|
|
33
30
|
},
|
|
34
31
|
"dependencies": {
|
|
@@ -40,15 +37,15 @@
|
|
|
40
37
|
"micromark-util-types": "^2.0.2"
|
|
41
38
|
},
|
|
42
39
|
"devDependencies": {
|
|
43
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.
|
|
44
|
-
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.
|
|
45
|
-
"@deepseek-ai/dsh-client-store": "0.2.0-rc.
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.
|
|
50
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.
|
|
51
|
-
"@deepseek-ai/dsh-util-values": "0.2.0-rc.
|
|
40
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2",
|
|
52
49
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
53
50
|
"@testing-library/react": "^16.3.0",
|
|
54
51
|
"@types/mdast": "^4.0.4",
|
|
@@ -59,23 +56,17 @@
|
|
|
59
56
|
},
|
|
60
57
|
"peerDependencies": {
|
|
61
58
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
62
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.
|
|
63
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.
|
|
59
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2"
|
|
69
66
|
},
|
|
70
67
|
"dsh": {
|
|
71
68
|
"client": {
|
|
72
|
-
"
|
|
73
|
-
"inject": [
|
|
74
|
-
"@deepseek-ai/dsh-client-locale",
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-settings",
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
78
|
-
]
|
|
69
|
+
"inline": true
|
|
79
70
|
}
|
|
80
71
|
},
|
|
81
72
|
"scripts": {
|
package/src/client/index.ts
CHANGED
|
@@ -44,15 +44,15 @@ export type { Reference, ReferenceSpan } from "../reference.ts";
|
|
|
44
44
|
export { ReferenceMarkdown, referenceMentions } from "../reference-markdown.tsx";
|
|
45
45
|
export type { ReferenceActions, ReferenceMarkdownProps } from "../reference-markdown.tsx";
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// 这里读的是 cordis 服务名(谁已 provide),Loader 拿它决定这一行何时才算 active。写错成包名,这一行就永远
|
|
51
|
-
// pending(`web boot: ... waiting for services`)。
|
|
52
|
-
//
|
|
53
|
-
// 值直接取自 schema 表单那一面,不另抄一份清单。
|
|
47
|
+
// `apply` 需要的**服务名**(cordis 服务,不是包名):样式与引用那半不要服务,schema 表单那半要槽位、字典与
|
|
48
|
+
// 配置表单。本包不是装配行,所以这份清单不再由 Loader 读;它由 `schemaForm.apply` 自己的 `ctx.inject` 用,
|
|
49
|
+
// 调用方(内联本包的 client 行)因此不必自己排服务顺序。值直接取自 schema 表单那一面,不另抄一份。
|
|
54
50
|
export const inject: readonly string[] = [...schemaForm.inject];
|
|
55
51
|
|
|
52
|
+
// 装上基础面:字典、字段槽与按行配置页。
|
|
53
|
+
//
|
|
54
|
+
// 由**内联本包的 client 行**调用(本包不是装配行):多份副本各带一份这份代码,`schemaForm.apply` 保证同一
|
|
55
|
+
// 运行时只装配一次(服务已在场就返回)。见本包 `.agents/adrs/20261001-暂时内联而不是装配行.md`。
|
|
56
56
|
export function apply(ctx: Context): void {
|
|
57
57
|
schemaForm.apply(ctx);
|
|
58
58
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// 与设置页其余部分同一套视觉与键盘行为。
|
|
6
6
|
//
|
|
7
7
|
// 字段槽仍然生效:命中时该字段**值**的位置换成注册方的组件,键名、注释、行号与行为按钮仍由这里画。
|
|
8
|
+
//
|
|
9
|
+
// 被 `ellipsis` 截断的文本(注释行、值)由官方 `Tooltip` 承载全文——**只有真的截断了才挂**(`TruncatedTooltip`)。
|
|
8
10
|
|
|
9
11
|
import { useMemo, useState, type ReactNode } from "react";
|
|
10
12
|
import {
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
} from "./lines.ts";
|
|
30
32
|
import { parseFor } from "./fields.tsx";
|
|
31
33
|
import type { ResolveText } from "./labels.ts";
|
|
34
|
+
import { TruncatedTooltip } from "./TruncatedTooltip.tsx";
|
|
32
35
|
import { isMultiline, valueText } from "./value.tsx";
|
|
33
36
|
import type { SchemaFieldOwnerProps, SchemaFormTranslate } from "./slot-contract.ts";
|
|
34
37
|
import {
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
LineRow,
|
|
49
52
|
LineToken,
|
|
50
53
|
LineValue,
|
|
54
|
+
ValueSlot,
|
|
51
55
|
ValueTrigger,
|
|
52
56
|
} from "./styles.ts";
|
|
53
57
|
|
|
@@ -202,12 +206,15 @@ function Row({
|
|
|
202
206
|
line.kind === "comment" ? (
|
|
203
207
|
<>
|
|
204
208
|
<LineFoldSpacer data-role="fold" />
|
|
205
|
-
{/* 这一行有问题时注释位装的就是那条消息:红字,别整行铺红。
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
{/* 这一行有问题时注释位装的就是那条消息:红字,别整行铺红。
|
|
210
|
+
注释被 `ellipsis` 截断时全文交给官方 `Tooltip`——**只有真的截断才挂**,放得下就原样画。 */}
|
|
211
|
+
<TruncatedTooltip label={line.text}>
|
|
212
|
+
{line.invalid ? (
|
|
213
|
+
<LineInvalid data-role="comment">{`// ${line.text}`}</LineInvalid>
|
|
214
|
+
) : (
|
|
215
|
+
<LineComment data-role="comment">{`// ${line.text}`}</LineComment>
|
|
216
|
+
)}
|
|
217
|
+
</TruncatedTooltip>
|
|
211
218
|
</>
|
|
212
219
|
) : line.kind === "open" ? (
|
|
213
220
|
<>
|
|
@@ -427,13 +434,14 @@ function FieldLine({
|
|
|
427
434
|
) : null}
|
|
428
435
|
{/* 编辑态只留输入框:原值不再在它旁边画一遍。 */}
|
|
429
436
|
{isEditing || line.variantsStandIn ? null : (
|
|
430
|
-
<
|
|
437
|
+
<ValueSlot
|
|
438
|
+
data-role="value"
|
|
431
439
|
onClick={() => {
|
|
432
440
|
if (editable) onEdit(key);
|
|
433
441
|
}}
|
|
434
442
|
>
|
|
435
443
|
{renderField(owner)}
|
|
436
|
-
</
|
|
444
|
+
</ValueSlot>
|
|
437
445
|
)}
|
|
438
446
|
<VariantSelect
|
|
439
447
|
control={line.variants}
|
|
@@ -753,11 +761,11 @@ function AddLine({
|
|
|
753
761
|
}
|
|
754
762
|
/>
|
|
755
763
|
{invalid ? <LineInvalid>{t("unknownProperty")}</LineInvalid> : null}
|
|
756
|
-
{/* 候选的说明:选中之前先看清"要加的是什么"
|
|
764
|
+
{/* 候选的说明:选中之前先看清"要加的是什么"(长了同样由 `Tooltip` 给全文,只有截断时才挂)。 */}
|
|
757
765
|
{!invalid && matched.length === 1 && matched[0]?.description !== undefined ? (
|
|
758
|
-
<
|
|
759
|
-
{resolveText(matched[0].description)}
|
|
760
|
-
</
|
|
766
|
+
<TruncatedTooltip label={resolveText(matched[0].description)}>
|
|
767
|
+
<LineComment>{resolveText(matched[0].description)}</LineComment>
|
|
768
|
+
</TruncatedTooltip>
|
|
761
769
|
) : null}
|
|
762
770
|
</AddWrap>
|
|
763
771
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 行内文本的全文浮层:**只有确实被截断**时才给官方 `Tooltip`,放得下就一个浮层也不挂。
|
|
2
|
+
//
|
|
3
|
+
// 判据是元素自己的几何——`scrollWidth > clientWidth` 就是「这一格正被 `ellipsis` 截断」,不用字符数之类的估计。
|
|
4
|
+
// 宽度变化(窗口、面板、字段槽)由 `ResizeObserver` 盯着这个元素重测;盒子没变、只是换了内容(值改了、注释换
|
|
5
|
+
// 了)不会被它通知,所以每次提交再重量一次。
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
cloneElement,
|
|
9
|
+
useCallback,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
type ReactElement,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
type Ref,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { Tooltip } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
18
|
+
|
|
19
|
+
export function TruncatedTooltip({
|
|
20
|
+
label,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
label: string;
|
|
24
|
+
children: ReactElement<{ ref?: Ref<HTMLElement> }>;
|
|
25
|
+
}): ReactNode {
|
|
26
|
+
const node = useRef<HTMLElement | null>(null);
|
|
27
|
+
const watch = useRef<ResizeObserver | null>(null);
|
|
28
|
+
const [truncated, setTruncated] = useState(false);
|
|
29
|
+
|
|
30
|
+
const measure = useCallback(() => {
|
|
31
|
+
const el = node.current;
|
|
32
|
+
if (el === null) return;
|
|
33
|
+
setTruncated(el.scrollWidth > el.clientWidth);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
// ref 一直挂在子元素上(没截断时也挂着):挂上就盯着它的宽度,卸载时退订。
|
|
37
|
+
const anchor = useCallback(
|
|
38
|
+
(el: HTMLElement | null) => {
|
|
39
|
+
watch.current?.disconnect();
|
|
40
|
+
watch.current = null;
|
|
41
|
+
node.current = el;
|
|
42
|
+
if (el === null) return;
|
|
43
|
+
const observer = new ResizeObserver(measure);
|
|
44
|
+
observer.observe(el);
|
|
45
|
+
watch.current = observer;
|
|
46
|
+
},
|
|
47
|
+
[measure],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// 每次提交都重量一次:`ResizeObserver` 只对盒子尺寸发通知,换了文本的同尺寸盒子要靠这里。
|
|
51
|
+
useLayoutEffect(measure);
|
|
52
|
+
|
|
53
|
+
// 没截断时走 `Tooltip` 自己的 `disabled`:气泡不渲染、hover 也不弹,锚元素同时不重挂
|
|
54
|
+
//(换成「有截断才包 `Tooltip`」会在截断状态翻转时把锚换掉,白丢一次 DOM)。
|
|
55
|
+
return (
|
|
56
|
+
<Tooltip label={label} side="bottom" portal disabled={!truncated}>
|
|
57
|
+
{cloneElement(children, { ref: anchor })}
|
|
58
|
+
</Tooltip>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -74,7 +74,18 @@ export type { SuggestedKeys } from "./schema-node.ts";
|
|
|
74
74
|
type RowConfigEntryProps = PropsRuntime<"plugins.row.config"> & PropsRenderFactories;
|
|
75
75
|
|
|
76
76
|
// 装上本包:字典、Factory(字段槽的声明者)与按行自动注册。
|
|
77
|
+
//
|
|
78
|
+
// 调用方是**内联了本包的 client 行**(本包不再是装配行):inline 后每个消费行的产物里各带一份这份代码、
|
|
79
|
+
// 都会调到这里,所以先等依赖服务到齐(调用方不必自己排顺序),并保证同一运行时只装一次。
|
|
77
80
|
export function apply(ctx: Context): void {
|
|
81
|
+
ctx.inject([...inject], (scope) => {
|
|
82
|
+
// 提示面已在场 = 另一份副本装过了:字典 / 槽 / 行入口都不再重复注册。
|
|
83
|
+
if (scope.get("schemaFormHints") !== undefined) return;
|
|
84
|
+
install(scope);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function install(ctx: Context): void {
|
|
78
89
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ui-schema-form: dictionaries");
|
|
79
90
|
// 提示面(dict 的候选键):同一次 apply 里提供,业务插件注册读数,本包的行投影读它。
|
|
80
91
|
const hints = new SchemaFormHints(ctx);
|
|
@@ -140,7 +140,11 @@ export const LineToken = styled("span")({
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
// 值(token):字符串 / 数字 / 布尔各有语法色。
|
|
143
|
+
//
|
|
144
|
+
// **必须是块级盒**:`overflow` / `text-overflow` 对 inline 盒不生效,inline 的 token 无论多长都不会截断,
|
|
145
|
+
// 只会把行撑宽(`min-width` / `overflow` 那两条声明在 inline 盒上是死代码)。
|
|
143
146
|
export const LineValue = styled("span")({
|
|
147
|
+
display: "block",
|
|
144
148
|
minWidth: 0,
|
|
145
149
|
overflow: "hidden",
|
|
146
150
|
textOverflow: "ellipsis",
|
|
@@ -151,6 +155,19 @@ export const LineValue = styled("span")({
|
|
|
151
155
|
"&[data-tone='empty']": { color: dsw.alias.label.caption },
|
|
152
156
|
});
|
|
153
157
|
|
|
158
|
+
// 值在行里的位置(字段槽的宿主):键名与结构符都是 `flex: none`,它是行里**唯一可收缩**的那一项。
|
|
159
|
+
//
|
|
160
|
+
// 这里不能放一个裸 `<span>`:flex item 的自动最小尺寸(`min-width: auto`)加上行上的 `white-space: nowrap`,
|
|
161
|
+
// 等于「整段值的宽度」——值长了它一步不让,直接把整行(进而整个表单壳)撑宽。给它 `min-width: 0` 之后,
|
|
162
|
+
// 值要么按自然宽度摆着,要么收缩到行尾并自己截断,长值因此不再有上限之外的影响。
|
|
163
|
+
export const ValueSlot = styled("span")({
|
|
164
|
+
display: "block",
|
|
165
|
+
// 不生长:短值保持自然宽度(不会被拉成一整行),长值才收缩。
|
|
166
|
+
flex: "0 1 auto",
|
|
167
|
+
minWidth: 0,
|
|
168
|
+
overflow: "hidden",
|
|
169
|
+
});
|
|
170
|
+
|
|
154
171
|
// 值上的下拉触发:**它自己就是值的呈现**(不再是「值 + 一个下拉」两遍),点开才出菜单。
|
|
155
172
|
export const ValueTrigger = styled("button")({
|
|
156
173
|
display: "inline-flex",
|
|
@@ -218,7 +235,8 @@ export const CompactTextField = styled("span")({
|
|
|
218
235
|
},
|
|
219
236
|
});
|
|
220
237
|
|
|
221
|
-
// 行内注释(schema
|
|
238
|
+
// 行内注释(schema 说明与业务文案):比配置值小一档、tertiary 色,占满这一行剩下的宽度,长了就截断——**真的被
|
|
239
|
+
// 截断时**才由官方 `Tooltip` 给全文(`Editor.tsx` 里的 `TruncatedTooltip` 包着它),放得下就一个浮层也不挂。
|
|
222
240
|
export const LineComment = styled("span")({
|
|
223
241
|
flex: "1 1 auto",
|
|
224
242
|
minWidth: 0,
|
|
@@ -229,9 +247,14 @@ export const LineComment = styled("span")({
|
|
|
229
247
|
fontSize: "12px",
|
|
230
248
|
});
|
|
231
249
|
|
|
232
|
-
//
|
|
250
|
+
// 行内的校验消息(与注释放同一档字号):注释位装的也可能是整段校验或解析失败的长消息,所以与注释同一套约束
|
|
251
|
+
// ——可收缩、自己截断、**截断了**才由官方 `Tooltip` 给全文。`flex: 0 1 auto`(不生长):短消息别铺满整行,添加
|
|
252
|
+
// 入口那一行里也不跟输入框抢剩下的宽度。红字先说清。
|
|
233
253
|
export const LineInvalid = styled("span")({
|
|
234
|
-
flex: "
|
|
254
|
+
flex: "0 1 auto",
|
|
255
|
+
minWidth: 0,
|
|
256
|
+
overflow: "hidden",
|
|
257
|
+
textOverflow: "ellipsis",
|
|
235
258
|
color: dsw.alias.state.error.primary,
|
|
236
259
|
fontFamily: "inherit",
|
|
237
260
|
fontSize: "12px",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// jsdom 里跑官方 `Tooltip`、以及靠 `scrollWidth` / `clientWidth` 判截断的行内文本的环境替身。
|
|
2
|
+
//
|
|
3
|
+
// 官方气泡的宽度/定位来自 `ResizeObserver` 回调(`visibility` 也只在那个回调里置为 visible),而 jsdom 没有这个
|
|
4
|
+
// API:缺了它,hover 之后的 effect 会直接抛 `ReferenceError`。替身做两件事:注册时喂回一次 `borderBoxSize`
|
|
5
|
+
// (让气泡进入可见态),并给用例一个 `notifyResize()`——真机上「盒子宽度变了」是浏览器通知的,jsdom 里没有那个
|
|
6
|
+
// 事件,用例只能自己发一次。
|
|
7
|
+
//
|
|
8
|
+
// 它**不是**几何证据:真尺寸与定位只能在浏览器里量(本包 `.agents/standards/how-to-verify.md` 记了这条与探针的
|
|
9
|
+
// 用法),所以用例的判据是「hover 出气泡、气泡里是全文」,不拿替身去断言气泡的实际尺寸。
|
|
10
|
+
|
|
11
|
+
interface StubObserver {
|
|
12
|
+
readonly targets: Set<Element>;
|
|
13
|
+
readonly notify: ResizeObserverCallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const live = new Set<StubObserver>();
|
|
17
|
+
|
|
18
|
+
export function stubResizeObserver(): void {
|
|
19
|
+
class ResizeObserverStub {
|
|
20
|
+
#notify: ResizeObserverCallback;
|
|
21
|
+
#targets = new Set<Element>();
|
|
22
|
+
#live: StubObserver;
|
|
23
|
+
|
|
24
|
+
constructor(notify: ResizeObserverCallback) {
|
|
25
|
+
this.#notify = notify;
|
|
26
|
+
this.#live = { targets: this.#targets, notify };
|
|
27
|
+
live.add(this.#live);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 注册即回调一次(真实的 ResizeObserver 也会在首帧给一次初始尺寸)。
|
|
31
|
+
observe(target: Element): void {
|
|
32
|
+
this.#targets.add(target);
|
|
33
|
+
this.#notify([entry(target)], this as unknown as ResizeObserver);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
unobserve(target: Element): void {
|
|
37
|
+
this.#targets.delete(target);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect(): void {
|
|
41
|
+
this.#targets.clear();
|
|
42
|
+
live.delete(this.#live);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
(globalThis as { ResizeObserver?: unknown }).ResizeObserver = ResizeObserverStub;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 让还活着的替身观察者再收到一次尺寸通知:用例把元素的几何改掉之后调它,等价于浏览器通知「这个盒子变了」。
|
|
49
|
+
export function notifyResize(): void {
|
|
50
|
+
for (const observer of live) {
|
|
51
|
+
const entries = [...observer.targets].map((target) => entry(target));
|
|
52
|
+
if (entries.length > 0) {
|
|
53
|
+
observer.notify(entries, undefined as unknown as ResizeObserver);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function entry(target: Element): ResizeObserverEntry {
|
|
59
|
+
return {
|
|
60
|
+
target,
|
|
61
|
+
borderBoxSize: [{ blockSize: 0, inlineSize: 0 }],
|
|
62
|
+
} as unknown as ResizeObserverEntry;
|
|
63
|
+
}
|
|
@@ -8,6 +8,7 @@ import { IconChevronDownOutlineRegular, Menu } from "@deepseek-ai/dsh-client-ui-
|
|
|
8
8
|
import type { SelectOption } from "./hints.ts";
|
|
9
9
|
import type { SchemaFieldOwnerProps } from "./slot-contract.ts";
|
|
10
10
|
import { ICON_SIZE, LineValue, ValueTrigger } from "./styles.ts";
|
|
11
|
+
import { TruncatedTooltip } from "./TruncatedTooltip.tsx";
|
|
11
12
|
|
|
12
13
|
// 编辑态文本:字符串就是原文(换行照旧),其余按 JSON 写。
|
|
13
14
|
export function valueText(value: unknown): string {
|
|
@@ -42,21 +43,22 @@ export function tokenTone(value: unknown): "string" | "number" | "boolean" | "em
|
|
|
42
43
|
export function InlineValue({ owner }: { owner: SchemaFieldOwnerProps }): ReactNode {
|
|
43
44
|
if (owner.options !== undefined) return <OptionSelect owner={owner} options={owner.options} />;
|
|
44
45
|
if (owner.node.meta.secret) {
|
|
45
|
-
// secret
|
|
46
|
+
// secret 不回显值:这里显示的是「已配置」的 `••••••`,它不会截断,`owner.hint` 也不是「被截断文本的全文」
|
|
47
|
+
// ——按行内浮层的口径它不该弹气泡,所以回到非侵入的原行为:挂原生 `title`。
|
|
46
48
|
return (
|
|
47
49
|
<LineValue data-tone="empty" title={owner.hint}>
|
|
48
50
|
{owner.secretConfigured ? "••••••" : ""}
|
|
49
51
|
</LineValue>
|
|
50
52
|
);
|
|
51
53
|
}
|
|
54
|
+
// 值长了会在行里截断(值槽可收缩):确实截断时整份内容由官方 `Tooltip` 承载,放得下就不挂。
|
|
55
|
+
const text = tokenText(owner.value);
|
|
52
56
|
return (
|
|
53
|
-
<
|
|
54
|
-
data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
>
|
|
58
|
-
{tokenText(owner.value)}
|
|
59
|
-
</LineValue>
|
|
57
|
+
<TruncatedTooltip label={text}>
|
|
58
|
+
<LineValue data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}>
|
|
59
|
+
{text}
|
|
60
|
+
</LineValue>
|
|
61
|
+
</TruncatedTooltip>
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -134,4 +134,9 @@ function incrementalHash(value: string, previous = 0): number {
|
|
|
134
134
|
return hash;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// 全局单例:本包被 inline 进各消费方的产物后,同一个页面会有多份模块实例,而样式表与注入状态必须只有一份
|
|
138
|
+
// ——否则同一份规则按各自的实例各注入一遍、同名的 global 规则互相顶替。`Styling.create()` 仍是独立实例工厂。
|
|
139
|
+
const STYLING_SINGLETON = Symbol.for("@morlay/dsh-client-ui-primitives/styling");
|
|
140
|
+
const globalScope = globalThis as unknown as { [key: symbol]: unknown };
|
|
141
|
+
|
|
142
|
+
export const styling: Styling = (globalScope[STYLING_SINGLETON] ??= Styling.create()) as Styling;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 官方主题的 `--dsw-*` token 全集(
|
|
1
|
+
// 官方主题的 `--dsw-*` token 全集(403 个),由 packages/client/ui-primitives/scripts/gen-design-tokens.mts 生成,勿手改。
|
|
2
2
|
//
|
|
3
3
|
// 叶子是该变量的**默认值**(官方 light 主题里首次出现的定义):既是类型推导的来源,也可作 fallback。
|
|
4
4
|
// `"$"` 键出现在「自身也是 token 的分支」上(命名体系里有 40 个短名同时是长名前缀的 token)。
|
|
@@ -172,6 +172,11 @@ export const designTokens = {
|
|
|
172
172
|
tag: "var(--dsw-static-neutral-bluish-75)",
|
|
173
173
|
},
|
|
174
174
|
menu: {
|
|
175
|
+
group: {
|
|
176
|
+
header: {
|
|
177
|
+
fill: "rgba(248, 249, 250, 0.94)",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
175
180
|
icon: "var(--dsw-static-neutral-bluish-800)",
|
|
176
181
|
},
|
|
177
182
|
onboarding: {
|
|
@@ -239,6 +244,14 @@ export const designTokens = {
|
|
|
239
244
|
bg: "color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%)",
|
|
240
245
|
},
|
|
241
246
|
},
|
|
247
|
+
turn: {
|
|
248
|
+
trigger: {
|
|
249
|
+
bg: {
|
|
250
|
+
$: "var(--dsw-alias-markdown-code-block)",
|
|
251
|
+
hover: "var(--dsw-alias-interactive-bg-hover)",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
242
255
|
},
|
|
243
256
|
corner: {
|
|
244
257
|
shape: "superellipse(1.5)",
|