@linxin666/dsh-client-ui-task-board 0.3.9 → 0.3.10

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,13 @@
1
1
  import type { BoardController } from '../core/controller.ts';
2
+ import type { LocaleRefreshSource } from './sidebar-entry.ts';
2
3
  /** The injected board container (kept in the DOM, hidden when inactive). */
3
4
  export declare const BOARD_VIEW_SELECTOR = "[data-dsh-taskboard-view]";
4
5
  /**
5
6
  * Mount the board React tree into the center column and bind its visibility
6
7
  * to the controller's boardOpen state.
7
8
  * @param controller - the board controller driving the view.
9
+ * @param locale - locale-change source; when given, re-renders a mounted board
10
+ * on a Language switch.
8
11
  * @returns disposer unmounting the tree and restoring the column.
9
12
  */
10
- export declare function mountBoard(controller: BoardController): () => void;
13
+ export declare function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void;
@@ -155,5 +155,7 @@ export type TaskBoardKey = keyof typeof zh;
155
155
  export type SettingsCardKey = TaskBoardKey;
156
156
  /** Active dictionary, picked by the document language at call time. */
157
157
  export declare function dictionary(): Record<TaskBoardKey, string>;
158
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
159
+ export declare function setRuntimeTranslate(t: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined): void;
158
160
  /** Translate a key with optional {name} template params. */
159
161
  export declare function t(key: TaskBoardKey, params?: Record<string, string>): string;
@@ -34,6 +34,16 @@ export interface SidebarEntryOptions {
34
34
  label(): string;
35
35
  /** Optional localized tooltip (title attribute). */
36
36
  tooltip?(): string;
37
+ /**
38
+ * Optional locale-change subscription: re-applies label / aria-label /
39
+ * tooltip whenever the active locale changes. Plain-DOM rows would
40
+ * otherwise keep the label captured at mount; pass the SDK locale runtime
41
+ * subscription (ctx.locale.subscribe) so the row follows the language
42
+ * switch without a reload.
43
+ */
44
+ refresh?: {
45
+ subscribe(listener: () => void): () => void;
46
+ };
37
47
  /** Click action (open/toggle the owning panel). */
38
48
  onToggle(): void;
39
49
  /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
@@ -14,10 +14,16 @@
14
14
  import type { BoardController } from '../core/controller.ts';
15
15
  /** Stable data attribute identifying the injected entry row. */
16
16
  export declare const ENTRY_SELECTOR = "[data-dsh-taskboard-entry]";
17
+ /** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
18
+ export interface LocaleRefreshSource {
19
+ subscribe(listener: () => void): () => void;
20
+ }
17
21
  /**
18
22
  * Mount the sidebar entry, waiting for the shell to render and self-healing
19
23
  * on later React re-renders.
20
24
  * @param controller - the board controller the entry toggles.
25
+ * @param locale - locale-change source; when given, re-applies the label on
26
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
21
27
  * @returns disposer removing the entry and its observers.
22
28
  */
23
- export declare function mountSidebarEntry(controller: BoardController): () => void;
29
+ export declare function mountSidebarEntry(controller: BoardController, locale?: LocaleRefreshSource): () => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
- "version": "0.3.9",
4
+ "version": "0.3.10",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -15,6 +15,7 @@
15
15
  import { createRoot, type Root } from 'react-dom/client'
16
16
  import type { BoardController } from '../core/controller.ts'
17
17
  import { TaskBoard } from './board/TaskBoard.tsx'
18
+ import type { LocaleRefreshSource } from './sidebar-entry.ts'
18
19
  import css from './board.module.css'
19
20
 
20
21
  /** The injected board container (kept in the DOM, hidden when inactive). */
@@ -37,11 +38,19 @@ function conversationColumn(): HTMLElement | undefined {
37
38
  * Mount the board React tree into the center column and bind its visibility
38
39
  * to the controller's boardOpen state.
39
40
  * @param controller - the board controller driving the view.
41
+ * @param locale - locale-change source; when given, re-renders a mounted board
42
+ * on a Language switch.
40
43
  * @returns disposer unmounting the tree and restoring the column.
41
44
  */
42
- export function mountBoard(controller: BoardController): () => void {
45
+ export function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void {
43
46
  let root: Root | undefined
44
47
  let container: HTMLDivElement | undefined
48
+ let unsubscribeLocale: (() => void) | undefined
49
+ try {
50
+ unsubscribeLocale = locale?.subscribe(() => {
51
+ if (root !== undefined) root.render(<TaskBoard controller={controller} />)
52
+ })
53
+ } catch { /* locale service absent: the board follows its next natural re-render */ }
45
54
 
46
55
  const ensure = (): void => {
47
56
  if (container !== undefined) {
@@ -106,6 +115,7 @@ export function mountBoard(controller: BoardController): () => void {
106
115
  document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
107
116
  waitObserver.disconnect()
108
117
  unsubscribe()
118
+ unsubscribeLocale?.()
109
119
  document.documentElement.removeAttribute(ACTIVE_ATTR)
110
120
  root?.unmount()
111
121
  root = undefined
@@ -27,7 +27,7 @@ import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
27
27
  import { mountBoard } from './board-mount.tsx'
28
28
  import { mountSidebarEntry } from './sidebar-entry.ts'
29
29
  import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
30
- import { en, zh, type TaskBoardKey } from './locales.ts'
30
+ import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
31
31
  import { HttpTaskBoardHostTransport } from './host-api.ts'
32
32
  import { reportDailyHeartbeat } from './telemetry.ts'
33
33
 
@@ -157,6 +157,12 @@ export function apply(ctx: ClientContext): void {
157
157
  }
158
158
  }, 'task-board: dictionaries')
159
159
 
160
+ // Wire the SDK translate seat into the module-level t (sidebar row and
161
+ // other plain-DOM callers): reads the active locale at call time, so they
162
+ // follow the Language setting without a reload. The register effect above
163
+ // guarantees the dictionaries exist before the first read.
164
+ try { setRuntimeTranslate(ctx.locale.bind(NS)) } catch { /* locale missing: document-language fallback stays */ }
165
+
160
166
  // Plugin configuration card: one staged form over the `task-board` settings
161
167
  // namespace, contributed to the Web UI plugin group.
162
168
  const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
@@ -246,8 +252,8 @@ export function apply(ctx: ClientContext): void {
246
252
  void pushPresetOptions()
247
253
  disposers.push(ctx.on('connection/reset', () => { void pushPresetOptions() }))
248
254
  try {
249
- disposers.push(mountSidebarEntry(controller))
250
- disposers.push(mountBoard(controller))
255
+ disposers.push(mountSidebarEntry(controller, ctx.locale))
256
+ disposers.push(mountBoard(controller, ctx.locale))
251
257
  } catch (error) {
252
258
  // DOM failures degrade the board, never the GUI.
253
259
  console.error('[dsh-task-board] mount failed:', error)
@@ -307,8 +307,23 @@ export function dictionary(): Record<TaskBoardKey, string> {
307
307
  return lang.toLowerCase().startsWith('en') ? en : zh
308
308
  }
309
309
 
310
+ /**
311
+ * SDK translate seat wired by the browser apply() once ctx.locale is bound
312
+ * (setRuntimeTranslate). When present it reads the ACTIVE locale at call
313
+ * time, so plain-DOM surfaces (sidebar row, toggles) follow a runtime
314
+ * language switch; the document-language pick above stays only as the
315
+ * unwired fallback (locale service absent, module-scope early callers).
316
+ */
317
+ let runtimeT: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined
318
+
319
+ /** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
320
+ export function setRuntimeTranslate(t: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined): void {
321
+ runtimeT = t
322
+ }
323
+
310
324
  /** Translate a key with optional {name} template params. */
311
325
  export function t(key: TaskBoardKey, params?: Record<string, string>): string {
326
+ if (runtimeT !== undefined) return runtimeT(key, params)
312
327
  let text: string = dictionary()[key]
313
328
  if (params !== undefined) {
314
329
  for (const [name, value] of Object.entries(params)) {
@@ -36,6 +36,14 @@ export interface SidebarEntryOptions {
36
36
  label(): string
37
37
  /** Optional localized tooltip (title attribute). */
38
38
  tooltip?(): string
39
+ /**
40
+ * Optional locale-change subscription: re-applies label / aria-label /
41
+ * tooltip whenever the active locale changes. Plain-DOM rows would
42
+ * otherwise keep the label captured at mount; pass the SDK locale runtime
43
+ * subscription (ctx.locale.subscribe) so the row follows the language
44
+ * switch without a reload.
45
+ */
46
+ refresh?: { subscribe(listener: () => void): () => void }
39
47
  /** Click action (open/toggle the owning panel). */
40
48
  onToggle(): void
41
49
  /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
@@ -76,7 +84,7 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
76
84
  }
77
85
 
78
86
  /** Build the entry row (a detached button; insert once the shell is up). */
79
- function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
87
+ function createEntry(options: SidebarEntryOptions): { entry: HTMLButtonElement; applyLabel: () => void } {
80
88
  const entry = document.createElement('button')
81
89
  entry.type = 'button'
82
90
  entry.setAttribute(options.rowAttribute, '')
@@ -85,12 +93,20 @@ function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
85
93
  entry.setAttribute('data-dsh-part', 'sidebar-entry')
86
94
  }
87
95
  entry.className = options.css['entry'] ?? ''
88
- entry.setAttribute('aria-label', options.label())
89
- if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
90
- entry.innerHTML = '<span class="' + (options.css['entryIcon'] ?? '') + '">' + options.icon
91
- + '</span><span class="' + (options.css['entryLabel'] ?? '') + '">' + options.label() + '</span>'
96
+ const labelSpan = document.createElement('span')
97
+ labelSpan.className = options.css['entryLabel'] ?? ''
98
+ const iconSpan = document.createElement('span')
99
+ iconSpan.className = options.css['entryIcon'] ?? ''
100
+ iconSpan.innerHTML = options.icon
101
+ entry.append(iconSpan, labelSpan)
102
+ const applyLabel = (): void => {
103
+ entry.setAttribute('aria-label', options.label())
104
+ if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
105
+ labelSpan.textContent = options.label()
106
+ }
107
+ applyLabel()
92
108
  entry.addEventListener('click', options.onToggle)
93
- return entry
109
+ return { entry, applyLabel }
94
110
  }
95
111
 
96
112
  /** Re-insert the entry after the New Session row (before the browser region). */
@@ -132,9 +148,18 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
132
148
  if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
133
149
  return () => {}
134
150
  }
135
- const entry = createEntry(options)
151
+ const { entry, applyLabel } = createEntry(options)
136
152
  let root: HTMLElement | undefined
137
153
  let placed = false
154
+ let unsubscribeRefresh: (() => void) | undefined
155
+ if (options.refresh !== undefined) {
156
+ try {
157
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel)
158
+ } catch {
159
+ // A throwing subscription must not break the mount; the label stays at
160
+ // its initial value and the next reload resolves it again.
161
+ }
162
+ }
138
163
 
139
164
  const tryPlace = (): void => {
140
165
  if (root !== undefined && !root.isConnected) {
@@ -202,6 +227,7 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
202
227
  return () => {
203
228
  waitObserver.disconnect()
204
229
  rootObserver.disconnect()
230
+ unsubscribeRefresh?.()
205
231
  unsubscribeActive?.()
206
232
  entry.remove()
207
233
  }
@@ -22,13 +22,18 @@ export const ENTRY_SELECTOR = '[data-dsh-taskboard-entry]'
22
22
  /** Inline icon normalized to the shell's 18px navigation glyph size. */
23
23
  const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
24
24
 
25
+ /** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
26
+ export interface LocaleRefreshSource { subscribe(listener: () => void): () => void }
27
+
25
28
  /**
26
29
  * Mount the sidebar entry, waiting for the shell to render and self-healing
27
30
  * on later React re-renders.
28
31
  * @param controller - the board controller the entry toggles.
32
+ * @param locale - locale-change source; when given, re-applies the label on
33
+ * a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
29
34
  * @returns disposer removing the entry and its observers.
30
35
  */
31
- export function mountSidebarEntry(controller: BoardController): () => void {
36
+ export function mountSidebarEntry(controller: BoardController, locale?: LocaleRefreshSource): () => void {
32
37
  return mountSharedSidebarEntry({
33
38
  rowAttribute: 'data-dsh-taskboard-entry',
34
39
  rowSelector: ENTRY_SELECTOR,
@@ -36,6 +41,7 @@ export function mountSidebarEntry(controller: BoardController): () => void {
36
41
  icon: ICON,
37
42
  css,
38
43
  label: () => t('entry.label'),
44
+ refresh: locale === undefined ? undefined : { subscribe: (listener) => locale.subscribe(listener) },
39
45
  onToggle: () => { controller.toggleBoard() },
40
46
  position: 'before',
41
47
  familySelectors: ['[data-dsh-taskboard-entry]', '[data-dsh-ssh-entry]'],