@linxin666/dsh-client-ui-task-board 0.1.18 → 0.1.19

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.
@@ -40,6 +40,11 @@ export declare class TaskBoardSettingsCardController {
40
40
  * @returns the card's snapshot and its form actions.
41
41
  */
42
42
  inject(): TaskBoardSettingsCardFace;
43
+ /**
44
+ * Release the card's scope subscription and bound stores; the slot
45
+ * disposer calls this on teardown.
46
+ */
47
+ dispose(): void;
43
48
  }
44
49
  /** Props the renderer binds for the task-board card. */
45
50
  export type TaskBoardSettingsCardProps = PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'task-board'> & InjectFace<TaskBoardSettingsCardFace>;
@@ -149,11 +149,19 @@ export declare class CardForm<T> {
149
149
  private readonly specs;
150
150
  private readonly staged;
151
151
  private readonly listeners;
152
+ /** The scope subscription installed in the constructor; released by dispose(). */
153
+ private readonly disposeScope;
154
+ private disposed;
152
155
  private saving;
153
156
  private failed;
154
157
  private failedReason;
155
158
  /** @param scope - the bound settings scope for this card's namespace. */
156
159
  constructor(scope: SettingsScope<T>, specs: FieldSpec[]);
160
+ /**
161
+ * Release the scope subscription and every bound store listener. The card
162
+ * must call this on teardown; later calls are no-ops.
163
+ */
164
+ dispose(): void;
157
165
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
158
166
  bind<S>(project: () => S): SnapshotStore<S>;
159
167
  /** Read the card-level state: what the Host serves, and what a save would do. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.1.18",
4
+ "version": "0.1.19",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -63,6 +63,14 @@ export class TaskBoardSettingsCardController {
63
63
  inject(): TaskBoardSettingsCardFace {
64
64
  return { hooks: { taskBoardSettingsCard: this.store }, ...this.form.actions() }
65
65
  }
66
+
67
+ /**
68
+ * Release the card's scope subscription and bound stores; the slot
69
+ * disposer calls this on teardown.
70
+ */
71
+ dispose(): void {
72
+ this.form.dispose()
73
+ }
66
74
  }
67
75
 
68
76
  /** Props the renderer binds for the task-board card. */
@@ -60,6 +60,15 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
60
60
  return (
61
61
  <div className={css.board} data-dsh-taskboard-board="">
62
62
  <header className={css.boardHeader}>
63
+ <button
64
+ type="button"
65
+ className={`${css.ghostButton} ${css.backButton}`}
66
+ aria-label={t('board.close')}
67
+ onClick={() => { controller.closeBoard() }}
68
+ >
69
+ <span aria-hidden="true">‹</span>
70
+ <span>{t('board.close')}</span>
71
+ </button>
63
72
  <h2 className={css.boardTitle}>{t('board.title')}</h2>
64
73
  <input
65
74
  className={css.search}
@@ -85,13 +94,6 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
85
94
  >
86
95
  + {t('board.new')}
87
96
  </button>
88
- <button
89
- type="button"
90
- className={css.ghostButton}
91
- onClick={() => { controller.closeBoard() }}
92
- >
93
- {t('board.close')}
94
- </button>
95
97
  </header>
96
98
 
97
99
  <div className={css.columns}>
@@ -18,6 +18,8 @@
18
18
  inset: 0;
19
19
  display: none;
20
20
  z-index: 60;
21
+ container-name: task-board-view;
22
+ container-type: inline-size;
21
23
  /* Opaque backdrop: the conversation subtree stays mounted under the view.
22
24
  Even if the hide rule below were defeated by a stronger host inline
23
25
  style, this background keeps sticky code-block banners (host z-index 6)
@@ -103,6 +105,7 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
103
105
  .board {
104
106
  display: flex;
105
107
  flex-direction: column;
108
+ box-sizing: border-box;
106
109
  height: 100%;
107
110
  min-width: 0;
108
111
  min-height: 0;
@@ -128,6 +131,12 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
128
131
  white-space: nowrap;
129
132
  }
130
133
 
134
+ .backButton {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ gap: 4px;
138
+ }
139
+
131
140
  .search {
132
141
  flex: 0 1 260px;
133
142
  min-width: 120px;
@@ -148,10 +157,38 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
148
157
 
149
158
  .columns {
150
159
  display: grid;
151
- grid-template-columns: repeat(5, minmax(0, 1fr));
160
+ grid-auto-flow: column;
161
+ grid-auto-columns: minmax(220px, 1fr);
152
162
  gap: 12px;
153
163
  flex: 1;
154
164
  min-height: 0;
165
+ overflow-x: auto;
166
+ overflow-y: hidden;
167
+ overscroll-behavior-inline: contain;
168
+ padding-bottom: 6px;
169
+ scrollbar-color: var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);
170
+ scrollbar-width: thin;
171
+ }
172
+
173
+ .columns::-webkit-scrollbar {
174
+ height: 10px;
175
+ }
176
+
177
+ .columns::-webkit-scrollbar-track {
178
+ background: var(--dsw-alias-interactive-bg-hover);
179
+ border-radius: 999px;
180
+ }
181
+
182
+ .columns::-webkit-scrollbar-thumb {
183
+ background: var(--dsw-alias-border-l3);
184
+ background-clip: content-box;
185
+ border: 2px solid transparent;
186
+ border-radius: 999px;
187
+ }
188
+
189
+ .columns::-webkit-scrollbar-thumb:hover {
190
+ background: var(--dsw-alias-border-l4);
191
+ background-clip: content-box;
155
192
  }
156
193
 
157
194
  .column {
@@ -770,6 +807,36 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
770
807
  color: var(--dsw-alias-label-tertiary);
771
808
  }
772
809
 
810
+ /* --- constrained board containers ------------------------------------------ */
811
+
812
+ @container task-board-view (max-width: 720px) {
813
+ .board {
814
+ gap: 10px;
815
+ padding: 10px;
816
+ }
817
+
818
+ .columns {
819
+ gap: 10px;
820
+ }
821
+ }
822
+
823
+ @container task-board-view (max-width: 600px) {
824
+ .boardHeader {
825
+ flex-wrap: wrap;
826
+ gap: 8px;
827
+ }
828
+
829
+ .search {
830
+ flex: 1 1 calc(100% - 72px);
831
+ min-width: 0;
832
+ }
833
+
834
+ .boardHeader > button {
835
+ flex: 1 1 0;
836
+ min-width: max-content;
837
+ }
838
+ }
839
+
773
840
  /* -----------------------------------------------------------------------------
774
841
  * Polish pass: complete interaction states (hover / active / focus-visible /
775
842
  * disabled), unified 120ms motion, and reduced-motion fallback.
@@ -92,13 +92,19 @@ export function apply(ctx: ClientContext): void {
92
92
  const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
93
93
  const settingsScope = binder.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
94
94
  const settingsCard = new TaskBoardSettingsCardController(settingsScope)
95
- ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
96
- name: 'web-ui.plugin.item',
97
- id: 'task-board',
98
- order: 110,
99
- locale: NS,
100
- inject: () => settingsCard.inject(),
101
- }, TaskBoardSettingsCard))
95
+ ctx.slots.inject('web-ui.plugin.item', () => {
96
+ const unregister = ctx.slots.register({
97
+ name: 'web-ui.plugin.item',
98
+ id: 'task-board',
99
+ order: 110,
100
+ locale: NS,
101
+ inject: () => settingsCard.inject(),
102
+ }, TaskBoardSettingsCard)
103
+ return () => {
104
+ settingsCard.dispose()
105
+ unregister()
106
+ }
107
+ })
102
108
 
103
109
  // The sidebar entry and board view mount once the settings scope settles;
104
110
  // while the scope is still loading, the composition default is unknown, so
@@ -9,7 +9,7 @@
9
9
  export const zh = {
10
10
  'entry.label': '任务看板',
11
11
  'board.title': '任务看板',
12
- 'board.close': '返回对话',
12
+ 'board.close': '返回会话',
13
13
  'board.new': '新建任务',
14
14
  'board.search': '筛选任务…',
15
15
  'board.empty': '这个状态还没有任务',
@@ -234,6 +234,9 @@ export class CardForm<T> {
234
234
  private readonly specs: Map<string, FieldSpec>
235
235
  private readonly staged = new Map<string, StagedEdit>()
236
236
  private readonly listeners = new Set<() => void>()
237
+ /** The scope subscription installed in the constructor; released by dispose(). */
238
+ private readonly disposeScope: () => void
239
+ private disposed = false
237
240
  private saving = false
238
241
  private failed = false
239
242
  private failedReason: string | undefined
@@ -244,7 +247,18 @@ export class CardForm<T> {
244
247
  specs: FieldSpec[],
245
248
  ) {
246
249
  this.specs = new Map(specs.map(spec => [spec.field, spec]))
247
- scope.subscribe(() => { this.publish() })
250
+ this.disposeScope = scope.subscribe(() => { this.publish() })
251
+ }
252
+
253
+ /**
254
+ * Release the scope subscription and every bound store listener. The card
255
+ * must call this on teardown; later calls are no-ops.
256
+ */
257
+ dispose(): void {
258
+ if (this.disposed) return
259
+ this.disposed = true
260
+ this.disposeScope()
261
+ this.listeners.clear()
248
262
  }
249
263
 
250
264
  /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
@@ -89,6 +89,10 @@ export function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undef
89
89
  }
90
90
 
91
91
  function randomUuid(): string {
92
+ const randomUUID = globalThis.crypto?.randomUUID
93
+ if (randomUUID !== undefined) {
94
+ return randomUUID.call(globalThis.crypto!)
95
+ }
92
96
  const bytes = globalThis.crypto?.getRandomValues(new Uint8Array(16))
93
97
  if (bytes === undefined) {
94
98
  // Non-secure fallback (tests, odd environments).