dsh-manage-session 0.2.3 → 0.4.8

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,234 +1,337 @@
1
- /**
2
- * 会话管家客户端半身:样式、字典、侧边栏入口(dsh-manage-session)与设置卡。
3
- *
4
- * 侧边栏入口点击后打开「养老院 / 体检」双页签对话框;两个页签的可见性由配置开关决定,
5
- * 关掉的子域不渲染页签,也不留空壳。开关读取走 settingsScope 的订阅快照,
6
- * 写通道只有设置卡(显式控件)。
7
- */
8
- import type { Context } from 'cordis'
9
- import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
10
- import {
11
- DEFAULT_CONFIG,
12
- STEWARD_ENTRY_ID,
13
- STEWARD_SETTINGS_NAMESPACE,
14
- type StewardConfig,
15
- } from '../config.ts'
16
- import { StewardSettingsCard, type StewardCardScope } from './card.tsx'
17
- import { en, translate, zh, type LocaleKey } from './locales.ts'
18
- import { StewardFooter, StewardPanel, TAB_HEALTH, TAB_HISTORY } from './panel.tsx'
19
-
20
- export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH, TAB_HISTORY }
21
- export { translate } from './locales.ts'
22
-
23
- /** 字典命名空间(locale.register 用)。 */
24
- export const NS = 'dsh-manage-session'
25
-
26
- /** 槽位服务面(结构化镜像)。 */
27
- interface StewardSlotsService {
28
- inject(name: string, callback: () => unknown, label?: string): void
29
- register(config: Record<string, unknown>, component: unknown): unknown
30
- }
31
-
32
- /** locale 服务面。 */
33
- interface StewardLocaleService {
34
- register(ns: string, dictionaries: { zh: unknown; en: unknown }): () => void
35
- }
36
-
37
- /** settingsScope 服务面。 */
38
- interface StewardSettingsScope {
39
- bind<U>(input: { namespace: string }): (StewardCardScope & { getSnapshot(): { value: U | undefined } }) | undefined
40
- }
41
-
42
- /** 客户端插件声明的注入面。 */
43
- export const inject = ['slots']
44
-
45
- /** 侧边栏脚部入口的 props(结构子集)。 */
46
- export interface StewardFooterProps {
47
- /** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
48
- wide?: boolean
49
- onClick?: () => void
50
- title?: string
51
- }
52
-
53
- /** 注入样式(幂等)。 */
54
- function injectStyles(): () => void {
55
- const id = 'dsh-manage-session-styles'
56
- if (typeof document === 'undefined') return () => {}
57
- if (document.getElementById(id) !== null) return () => {}
58
- const style = document.createElement('style')
59
- style.id = id
60
- style.textContent = `
61
- .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
62
- .dss_entryWrapWide{margin-left:6px}
63
- .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:42px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
64
- .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
65
- .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
66
- .dss_footerIcon{flex:none;font-size:15px;line-height:1}
67
- .dss_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
68
- .dss_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
69
- .dss_panel{position:fixed;z-index:1001;left:50%;top:8vh;transform:translateX(-50%);width:min(760px,92vw);max-height:78vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
70
- .dss_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
71
- .dss_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
72
- .dss_tabRow{display:inline-flex;gap:4px}
73
- .dss_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
74
- .dss_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
75
- .dss_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
76
- .dss_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
77
- .dss_actBtn:disabled{opacity:.5;cursor:default}
78
- .dss_dangerBtn{border-color:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-state-error-primary,#dc2626)}
79
- .dss_editActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12))}
80
- .dss_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
81
- .dss_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
82
- .dss_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
83
- .dss_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
84
- .dss_rowTitle{display:flex;gap:6px;align-items:center;flex:1 1 auto;min-width:0}
85
- .dss_titleText{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
86
- .dss_tag{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
87
- .dss_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
88
- .dss_metaLine{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
89
- .dss_uuid{font-family:var(--ds-font-family-code,monospace)}
90
- /* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
91
- .dss_size{flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}
92
- .dss_check{display:inline-flex;align-items:center}
93
- .dss_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
94
- .dss_warnText{color:var(--dsw-alias-state-warning-primary,#d97706)}
95
- .dss_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
96
- .dss_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
97
- .dss_gateList{display:flex;flex-direction:column;gap:6px}
98
- .dss_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
99
- .dss_gateName{font-weight:600;font-size:12px}
100
- .dss_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
101
- .dss_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
102
- .dss_level_ok{background:rgba(22,163,74,.12);color:#15803d}
103
- .dss_level_warn{background:rgba(217,119,6,.14);color:#b45309}
104
- .dss_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
105
- .dss_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
106
- .dss_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
107
- .dss_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
108
- .dss_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
109
- .dss_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
110
- .dss_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
111
- .dss_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
112
- .dss_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
113
- @keyframes dssSpin{to{transform:rotate(360deg)}}
114
- @keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
115
- @media (prefers-reduced-motion:reduce){.dss_spinner{animation:none}.dss_progressIndeterminate{animation:none;width:100%;opacity:.5}.dss_setChev{transition:none}.dss_setCard{transition:none}}
116
- .dss_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
117
- .dss_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
118
- .dss_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
119
- .dss_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
120
- .dss_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
121
- .dss_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
122
- .dss_phaseRow{display:flex;gap:8px;align-items:center}
123
- .dss_phase{font-weight:600;font-size:12px}
124
- .dss_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
125
- .dss_cmdLine{display:flex;gap:6px;align-items:center}
126
- .dss_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
127
- .dss_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
128
- .dss_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
129
- .dss_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
130
- .dss_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
131
- .dss_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
132
- .dss_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
133
- .dss_setCardOpen .dss_setChev{transform:rotate(180deg)}
134
- .dss_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
135
- .dss_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
136
- .dss_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
137
- .dss_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
138
- .dss_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
139
- .dss_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
140
- .dss_toggle input{position:absolute;opacity:0;width:0;height:0}
141
- .dss_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
142
- .dss_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
143
- .dss_toggleOn .dss_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
144
- .dss_toggleOn .dss_toggleKnob{left:18px}
145
- `
146
- document.head.appendChild(style)
147
- return () => { style.remove() }
148
- }
149
-
150
- /**
151
- * 客户端插件主体。
152
- * @param ctx - 客户端上下文(slots、可选 locale / settingsScope)。
153
- */
154
- export function apply(ctx: Context): void {
155
- ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
156
-
157
- const locale = ctx.get('locale') as StewardLocaleService | undefined
158
- if (locale !== undefined && typeof locale.register === 'function') {
159
- ctx.effect(() => locale.register(NS, { zh, en }), 'dsh-manage-session: dictionaries')
160
- }
161
-
162
- const settingsScope = ctx.get('settingsScope') as StewardSettingsScope | undefined
163
- const bound = settingsScope?.bind<StewardConfig>({ namespace: STEWARD_SETTINGS_NAMESPACE })
164
-
165
- const slots = ctx.get('slots') as StewardSlotsService | undefined
166
- if (slots === undefined) return
167
-
168
- // 侧边栏入口:一个按钮,打开「养老院 / 体检」双页签面板。
169
- slots.inject('sidebar.footer.action', () => slots.register(
170
- { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 12 },
171
- (props: StewardFooterProps) => createElement(StewardEntry, { ...props, scope: bound }),
172
- ), 'dsh-manage-session: sidebar footer entry')
173
-
174
- slots.inject('settings.plugin.item', () => slots.register({
175
- name: 'settings.plugin.item',
176
- id: STEWARD_SETTINGS_NAMESPACE,
177
- key: STEWARD_SETTINGS_NAMESPACE,
178
- locale: locale !== undefined ? NS : undefined,
179
- inject: () => ({
180
- scope: bound ?? {
181
- getSnapshot: () => ({
182
- status: 'ready' as const,
183
- value: DEFAULT_CONFIG,
184
- revision: undefined,
185
- writable: false,
186
- }),
187
- subscribe: () => () => {},
188
- set: async () => {},
189
- },
190
- }),
191
- }, StewardSettingsCard), 'dsh-manage-session: plugin settings card')
192
- }
193
-
194
- /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
195
- export function StewardEntry(
196
- props: StewardFooterProps & {
197
- scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
198
- },
199
- ): ReactElement {
200
- const [open, setOpen] = useState(false)
201
- const snapshot = useSyncExternalStore(
202
- (listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
203
- () => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
204
- )
205
- const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
206
- const historyFiles = config.historyFiles !== false
207
- const healthCheck = config.healthCheck !== false
208
- const visible = config.enabled !== false && (historyFiles || healthCheck)
209
- const wide = props.wide === true
210
- // 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
211
- const label = translate(undefined, 'panel.title')
212
-
213
- if (!visible) return createElement('span', { className: 'dss_entryWrap' })
214
- return createElement('span', { className: wide ? 'dss_entryWrap dss_entryWrapWide' : 'dss_entryWrap' }, [
215
- createElement(StewardFooter, {
216
- key: 'btn',
217
- title: label,
218
- label,
219
- wide,
220
- onClick: () => { setOpen(!open) },
221
- }),
222
- open
223
- ? createElement(StewardPanel, {
224
- key: 'panel',
225
- historyFiles,
226
- healthCheck,
227
- onClose: () => { setOpen(false) },
228
- })
229
- : null,
230
- ])
231
- }
232
-
233
- /** 导出字典查找 type 供消费者复用。 */
234
- export type { LocaleKey }
1
+ /**
2
+ * 会话管家客户端半身:样式、字典、侧边栏入口(dsh-manage-session)与设置卡。
3
+ *
4
+ * 侧边栏入口点击后打开「养老院 / 体检」双页签对话框;两个页签的可见性由配置开关决定,
5
+ * 关掉的子域不渲染页签,也不留空壳。开关读取走 configForms(0.1.7,entry id 键)/
6
+ * 旧宿主设置服务的订阅快照;配置写入由 volatile 字段自动生成的设置表单承担。
7
+ */
8
+ import type { Context } from 'cordis'
9
+ import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
10
+ import {
11
+ DEFAULT_CONFIG,
12
+ STEWARD_ENTRY_ID,
13
+ STEWARD_SETTINGS_NAMESPACE,
14
+ type StewardConfig,
15
+ } from '../config.ts'
16
+ import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
17
+ import { en, translate, zh, type LocaleKey } from './locales.ts'
18
+ import { StewardFooter, StewardPanel, TAB_HEALTH, TAB_HISTORY } from './panel.tsx'
19
+
20
+ export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH, TAB_HISTORY }
21
+ export { translate } from './locales.ts'
22
+
23
+ /** 字典命名空间(locale.register 用)。 */
24
+ export const NS = 'dsh-manage-session'
25
+
26
+ /** 槽位服务面(结构化镜像)。 */
27
+ interface StewardSlotsService {
28
+ inject(name: string, callback: () => unknown, label?: string): void
29
+ register(config: Record<string, unknown>, component: unknown): unknown
30
+ entries(name: string): Array<{ options: { id?: string; order?: number; label?: unknown } }>
31
+ getVersion(name: string): number
32
+ subscribe(name: string, listener: () => void): () => void
33
+ }
34
+
35
+ /** locale 服务面。 */
36
+ interface StewardLocaleService {
37
+ register(ns: string, dictionaries: { zh: unknown; en: unknown }): () => void
38
+ }
39
+
40
+ /** 旧宿主设置服务面(0.1.7 前的回退路径,结构化镜像)。 */
41
+ interface StewardSettingsScope {
42
+ bind<U>(input: { namespace: string }): (StewardCardScope & { getSnapshot(): { value: U | undefined } }) | undefined
43
+ }
44
+
45
+ /** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
46
+ interface StewardConfigForms {
47
+ get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
48
+ }
49
+
50
+ /** 客户端插件声明的注入面。 */
51
+ export const inject = ['slots']
52
+
53
+ /** 侧边栏脚部入口的 props(结构子集)。 */
54
+ export interface StewardFooterProps {
55
+ /** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
56
+ wide?: boolean
57
+ onClick?: () => void
58
+ title?: string
59
+ }
60
+
61
+ /** 注入样式(幂等)。 */
62
+ function injectStyles(): () => void {
63
+ const id = 'dsh-manage-session-styles'
64
+ if (typeof document === 'undefined') return () => {}
65
+ if (document.getElementById(id) !== null) return () => {}
66
+ const style = document.createElement('style')
67
+ style.id = id
68
+ style.textContent = `
69
+ /* 侧栏 footer 槽位公约(2026-09-26):一行多入口(第三方 dsh-context 等)会互相
70
+ 挤占 —— 宿主 .footerActions 是 nowrap flex 行。这里允许容器换行,并把本插件
71
+ 入口钉成独占一整行(flex-basis:100%);其余入口(含第三方)自然落到后续行,
72
+ 各行内部自行布局,谁也不挤谁。类名用 [class*=] 中段匹配:宿主是 CSS Module
73
+ 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
74
+ 中段跨版本稳定。 */
75
+ [class*="footerActions"]{flex-wrap:wrap;row-gap:2px;height:auto;min-height:0}
76
+ /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
77
+ .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
78
+ .dss_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
79
+ .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:42px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
80
+ .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
81
+ .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
82
+ .dss_footerIcon{flex:none;font-size:15px;line-height:1}
83
+ .dss_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
84
+ .dss_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
85
+ .dss_panel{position:fixed;z-index:1001;left:50%;top:8vh;transform:translateX(-50%);width:min(760px,92vw);max-height:78vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
86
+ .dss_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
87
+ .dss_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
88
+ .dss_tabRow{display:inline-flex;gap:4px}
89
+ .dss_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
90
+ .dss_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
91
+ .dss_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
92
+ .dss_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
93
+ .dss_actBtn:disabled{opacity:.5;cursor:default}
94
+ .dss_dangerBtn{border-color:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-state-error-primary,#dc2626)}
95
+ .dss_editActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12))}
96
+ .dss_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
97
+ .dss_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
98
+ .dss_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
99
+ .dss_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
100
+ .dss_rowTitle{display:flex;gap:6px;align-items:center;flex:1 1 auto;min-width:0}
101
+ .dss_titleText{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
102
+ .dss_tag{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
103
+ .dss_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
104
+ .dss_metaLine{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
105
+ .dss_uuid{font-family:var(--ds-font-family-code,monospace)}
106
+ /* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
107
+ .dss_size{flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}
108
+ .dss_check{display:inline-flex;align-items:center}
109
+ .dss_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
110
+ .dss_warnText{color:var(--dsw-alias-state-warning-primary,#d97706)}
111
+ .dss_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
112
+ .dss_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
113
+ .dss_gateList{display:flex;flex-direction:column;gap:6px}
114
+ .dss_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
115
+ .dss_gateName{font-weight:600;font-size:12px}
116
+ .dss_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
117
+ .dss_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
118
+ .dss_level_ok{background:rgba(22,163,74,.12);color:#15803d}
119
+ .dss_level_warn{background:rgba(217,119,6,.14);color:#b45309}
120
+ .dss_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
121
+ .dss_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
122
+ .dss_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
123
+ .dss_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
124
+ .dss_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
125
+ .dss_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
126
+ .dss_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
127
+ .dss_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
128
+ .dss_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
129
+ @keyframes dssSpin{to{transform:rotate(360deg)}}
130
+ @keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
131
+ @media (prefers-reduced-motion:reduce){.dss_spinner{animation:none}.dss_progressIndeterminate{animation:none;width:100%;opacity:.5}.dss_setChev{transition:none}.dss_setCard{transition:none}}
132
+ .dss_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
133
+ .dss_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
134
+ .dss_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
135
+ .dss_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
136
+ .dss_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
137
+ .dss_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
138
+ .dss_phaseRow{display:flex;gap:8px;align-items:center}
139
+ .dss_phase{font-weight:600;font-size:12px}
140
+ .dss_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
141
+ .dss_cmdLine{display:flex;gap:6px;align-items:center}
142
+ .dss_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
143
+ .dss_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
144
+ .dss_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
145
+ .dss_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
146
+ .dss_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
147
+ .dss_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
148
+ .dss_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
149
+ .dss_setCardOpen .dss_setChev{transform:rotate(180deg)}
150
+ .dss_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
151
+ .dss_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
152
+ .dss_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
153
+ .dss_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
154
+ .dss_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
155
+ .dss_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
156
+ .dss_toggle input{position:absolute;opacity:0;width:0;height:0}
157
+ .dss_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
158
+ .dss_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
159
+ .dss_toggleOn .dss_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
160
+ .dss_toggleOn .dss_toggleKnob{left:18px}
161
+ `
162
+ document.head.appendChild(style)
163
+ return () => { style.remove() }
164
+ }
165
+
166
+ /**
167
+ * 客户端插件主体。
168
+ * @param ctx - 客户端上下文(slots、可选 locale / configForms)。
169
+ */
170
+ export function apply(ctx: Context): void {
171
+ ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
172
+
173
+ const locale = ctx.get('locale') as StewardLocaleService | undefined
174
+ if (locale !== undefined && typeof locale.register === 'function') {
175
+ ctx.effect(() => locale.register(NS, { zh, en }), 'dsh-manage-session: dictionaries')
176
+ }
177
+
178
+ // 0.1.7:configForms 以 entry id 取句柄;旧宿主回退到按命名空间绑定。
179
+ const configForms = ctx.get('configForms') as StewardConfigForms | undefined
180
+ const legacySettings = ctx.get('settingsScope') as StewardSettingsScope | undefined
181
+ const bound = configForms?.get<StewardConfig>(STEWARD_ENTRY_ID)
182
+ ?? legacySettings?.bind<StewardConfig>({ namespace: STEWARD_SETTINGS_NAMESPACE })
183
+
184
+ const slots = ctx.get('slots') as StewardSlotsService | undefined
185
+ if (slots === undefined) return
186
+
187
+ // 侧边栏入口:一个按钮,打开「养老院 / 体检」双页签面板。
188
+ slots.inject('sidebar.footer.action', () => slots.register(
189
+ { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
190
+ (props: StewardFooterProps) => createElement(StewardEntry, { ...props, scope: bound }),
191
+ ), 'dsh-manage-session: sidebar footer entry')
192
+
193
+ // 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子
194
+ // 席位)。thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
195
+ slots.inject('dsh-family.tab', () => slots.register(
196
+ {
197
+ name: 'dsh-family.tab',
198
+ id: STEWARD_ENTRY_ID,
199
+ order: 40,
200
+ // 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
201
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
202
+ locale: NS,
203
+ },
204
+ (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
205
+ scope: bound as StewardCardScope,
206
+ t: props.t,
207
+ }),
208
+ ), 'dsh-manage-session: family settings tab')
209
+
210
+ // 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
211
+ // 设置」节。下载量排名 session-steward(1090) 低于 session-guard(1522) → 宽限期
212
+ // 更长,guard 先尝试接管;guard 也缺席时本插件按同一 id `dsh-family` 接管,
213
+ // 声明同一个 dsh-family.tab 子席位并通用渲染全部贡献卡。与 guard 的并发竞争
214
+ // 由 ui-slots 同 id 重复注册抛错仲裁,先到者胜、后到者放弃。
215
+ const familyTabsHooks = makeFamilyTabsHooks(slots)
216
+ ctx.effect(() => {
217
+ let claimed: unknown
218
+ const timer = setTimeout(() => {
219
+ if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
220
+ try {
221
+ claimed = slots.register({
222
+ name: 'settings.section',
223
+ id: FAMILY_SECTION_ID,
224
+ order: 40,
225
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
226
+ locale: NS,
227
+ inject: () => ({ hooks: { tabs: familyTabsHooks } }),
228
+ children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
229
+ }, StewardFamilySection)
230
+ } catch {
231
+ // 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
232
+ }
233
+ }, FAMILY_HOST_GRACE_MS)
234
+ return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
235
+ }, 'dsh-manage-session: family fallback host')
236
+ }
237
+
238
+ /** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
239
+ const FAMILY_SECTION_ID = 'dsh-family'
240
+ /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
241
+ const FAMILY_CHILD_KEY = 'dsh-family.tab'
242
+ /** 接管宽限期:guard 2000ms 先试,本插件 2600ms 兜底。 */
243
+ const FAMILY_HOST_GRACE_MS = 2600
244
+
245
+ /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形;locale 变更也触发重渲染)。 */
246
+ function makeFamilyTabsHooks(slots: StewardSlotsService): {
247
+ getSnapshot: () => readonly FamilyTabEntry[]
248
+ subscribe: (listener: () => void) => () => void
249
+ } {
250
+ let version = -1
251
+ let tabs: readonly FamilyTabEntry[] = []
252
+ return {
253
+ getSnapshot: () => {
254
+ const next = slots.getVersion(FAMILY_CHILD_KEY)
255
+ if (next !== version) {
256
+ version = next
257
+ tabs = slots.entries(FAMILY_CHILD_KEY)
258
+ .map(entry => ({
259
+ id: entry.options.id ?? '',
260
+ order: entry.options.order ?? 0,
261
+ label: typeof entry.options.label === 'function'
262
+ ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
263
+ : String(entry.options.label ?? entry.options.id ?? ''),
264
+ }))
265
+ .sort((a, b) => a.order - b.order)
266
+ }
267
+ return tabs
268
+ },
269
+ subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
270
+ }
271
+ }
272
+
273
+ /** 账本条目(id/order/label,label 已解析为字符串)。 */
274
+ interface FamilyTabEntry {
275
+ id: string
276
+ order: number
277
+ label: string
278
+ }
279
+
280
+ /**
281
+ * 家族节接管组件(fallback host section):纯通用渲染 —— 把 `dsh-family.tab`
282
+ * 账本里的每张贡献卡(含本插件自己的,接管时本插件也声明了该子席位)按
283
+ * order 依次 renderSlot;各卡本身是默认展开的抽屉,整节即一页抽屉。
284
+ */
285
+ function StewardFamilySection(props: {
286
+ renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
287
+ hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
288
+ }): ReactElement {
289
+ const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
290
+ return createElement('div', { style: { display: 'grid', gap: '12px' } },
291
+ ...tabs.map(row => createElement('div', { key: row.id },
292
+ props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
293
+ )),
294
+ )
295
+ }
296
+
297
+ /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
298
+ export function StewardEntry(
299
+ props: StewardFooterProps & {
300
+ scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
301
+ },
302
+ ): ReactElement {
303
+ const [open, setOpen] = useState(false)
304
+ const snapshot = useSyncExternalStore(
305
+ (listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
306
+ () => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
307
+ )
308
+ const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
309
+ const historyFiles = config.historyFiles !== false
310
+ const healthCheck = config.healthCheck !== false
311
+ const visible = config.enabled !== false && (historyFiles || healthCheck)
312
+ const wide = props.wide === true
313
+ // 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
314
+ const label = translate(undefined, 'panel.title')
315
+
316
+ if (!visible) return createElement('span', { className: 'dss_entryWrap' })
317
+ return createElement('span', { className: wide ? 'dss_entryWrap dss_entryWrapWide' : 'dss_entryWrap' }, [
318
+ createElement(StewardFooter, {
319
+ key: 'btn',
320
+ title: label,
321
+ label,
322
+ wide,
323
+ onClick: () => { setOpen(!open) },
324
+ }),
325
+ open
326
+ ? createElement(StewardPanel, {
327
+ key: 'panel',
328
+ historyFiles,
329
+ healthCheck,
330
+ onClose: () => { setOpen(false) },
331
+ })
332
+ : null,
333
+ ])
334
+ }
335
+
336
+ /** 导出字典查找 type 供消费者复用。 */
337
+ export type { LocaleKey }