@morlay/dsh-client-ui-primitives 0.0.5 → 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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as parseReferenceToken, n as ReferenceSpan, r as findReferences, t as Reference } from "./reference-BX54bj6_.mjs";
2
2
  //#region src/index.d.ts
3
- declare function apply(): void;
3
+ export declare function apply(): void;
4
4
  //#endregion
5
- export { type Reference, type ReferenceSpan, apply, findReferences, parseReferenceToken };
5
+ export { type Reference, type ReferenceSpan, findReferences, parseReferenceToken };
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 shared reference parser and renderer for the forked conversation UI, and the settings-form primitives forked from upstream. It also carries the schema-driven row config pages (field-slot injection)."
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 样式层、被 fork 的对话 UI 共用的引用解析与渲染转换,以及从上游 fork 过来的设置表单原语。另含按行 schema 自动生成的行配置页(字段槽注入)。"
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.5",
4
- "description": "css-in-js styling kit (styled / Token / Styling), the shared reference parser and renderer (findReferences / parseReferenceToken) for the forked conversation UI, and the settings-form primitives forked from upstream (SettingsFormModel / field controls / form shell): consumes the official --dsw-* design tokens, so no CSS Modules precompile step is needed.",
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.1",
44
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
45
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
46
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.1",
47
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
48
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
49
- "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
50
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
51
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.1",
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.1",
63
- "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
64
- "@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
65
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.1",
66
- "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
67
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
68
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1"
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
- "platform": "web",
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": {
@@ -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
- // 需要的**服务名**:样式与引用那半不要服务,schema 表单那半要槽位、字典与配置表单。
48
- //
49
- // 与包清单里的 `dsh.client.inject` 不是一回事:那份是**装配行 id**(谁先到),交给模块系统排 arrival 顺序;
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
- {line.invalid ? (
207
- <LineInvalid title={line.text}>{`// ${line.text}`}</LineInvalid>
208
- ) : (
209
- <LineComment title={line.text}>{`// ${line.text}`}</LineComment>
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
- <span
437
+ <ValueSlot
438
+ data-role="value"
431
439
  onClick={() => {
432
440
  if (editable) onEdit(key);
433
441
  }}
434
442
  >
435
443
  {renderField(owner)}
436
- </span>
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
- <LineComment title={resolveText(matched[0].description)}>
759
- {resolveText(matched[0].description)}
760
- </LineComment>
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 说明与业务文案):比配置值小一档,占满这一行剩下的宽度,长了就截断(hover 看全文)。
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: "none",
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
- <LineValue
54
- data-tone={owner.node.readOnly === null ? tokenTone(owner.value) : "empty"}
55
- // 值长了会截断:整份内容挂在 title 上,hover 就能看全。
56
- title={tokenText(owner.value)}
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
- export const styling = Styling.create();
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 全集(400 个),由 packages/client/ui-primitives/scripts/gen-design-tokens.mts 生成,勿手改。
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)",