@rs-dsh/dsh-core 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - **Chinese wording** reviewed: messages follow DSH's own terms (指令 for commands) and end with 。.
6
+ - **Triple-click selections** now get the Comment button. A triple-click on a message's last paragraph runs on to the start of the next message, so the selection belonged to no message; it is now trimmed to the text it covers.
7
+ - **Cards focus their text field** when they open, so you can type a comment or a quote note right away.
8
+ - **The selected passage stays highlighted** while its comment or quote card is open, though focusing the field clears the page selection.
9
+
3
10
  ## 0.1.0
4
11
 
5
12
  The first release. dsh-threads 0.2.0 and dsh-quote 0.2.0 are built on it.
package/README.md CHANGED
@@ -9,7 +9,7 @@ The shared base of the rs-dsh plugins for DeepSeek Harness. It gives them one se
9
9
  Install the plugins you want. Each one brings this package and adds it to the profile:
10
10
 
11
11
  ```sh
12
- dsh plugin --profile <name> add @rs-dsh/dsh-threads@0.2.0 @rs-dsh/dsh-quote@0.2.0
12
+ dsh plugin --profile <name> add @rs-dsh/dsh-threads@0.3.0 @rs-dsh/dsh-quote@0.2.1
13
13
  ```
14
14
 
15
15
  Then reload the DSH web page. However many rs-dsh plugins you install, the core runs once.
package/client.js CHANGED
@@ -143,15 +143,17 @@ window.__ModuleLoader__.load({
143
143
  }
144
144
  const zh = {
145
145
  actions: '选中文本操作',
146
- busy: '当前无法编辑输入框,稍后再试',
147
- command: '命令草稿中不能插入',
148
- noSession: '找不到该会话的输入框',
146
+ busy: '输入框暂时无法编辑,请稍后再试。',
147
+ command: '输入指令时无法插入内容。',
148
+ noSession: '找不到该会话的输入框。',
149
149
  }
150
150
 
151
151
  // The only stylesheet the core owns: positioning of its two floating hosts.
152
152
  // Everything visible inside them is DSH's own components and tokens. The
153
153
  // toolbar keeps the menu's translucent material; text-heavy cards take the
154
154
  // solid layer of DSH's preview cards, so the page never shows through.
155
+ // The passage a toolbar action was taken on, painted while its card is open.
156
+ const HELD = 'dsh-core-held'
155
157
  const STYLE = `
156
158
  [${ROOT}="dsh-core"][data-part]{position:fixed;z-index:1200;max-width:calc(100vw - 16px)}
157
159
  [${ROOT}="dsh-core"][data-part][hidden]{display:none}
@@ -162,6 +164,7 @@ window.__ModuleLoader__.load({
162
164
  [${ROOT}="dsh-core"][data-part="popover"] [data-part-card]{padding:10px;display:flex;flex-direction:column;max-height:min(70vh,560px);
163
165
  background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-panel)}
164
166
  [${ROOT}="dsh-core"][data-part="popover"] [data-part-card]>[aria-hidden="true"]:first-child{display:none}
167
+ ::highlight(${HELD}){background-color:Highlight;color:HighlightText}
165
168
  `
166
169
  // ------------------------------------------------------------ plugin body
167
170
 
@@ -248,10 +251,36 @@ window.__ModuleLoader__.load({
248
251
  * in, the chat row, the range and its rectangle, and the cleaned text.
249
252
  * Selections in the composer, in inputs or in rs-dsh's own UI are none.
250
253
  */
254
+ /**
255
+ * The range shrunk to the text it actually covers. A triple-click on
256
+ * the last paragraph of a message ends the range at offset 0 of the
257
+ * next message, so its common ancestor would be the whole chat and the
258
+ * selection would belong to no row.
259
+ */
260
+ function trimRange(range) {
261
+ const root = range.commonAncestorContainer
262
+ if (root.nodeType === 3 || typeof document.createTreeWalker !== 'function') return range
263
+ const walker = document.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */)
264
+ let first = null, last = null
265
+ for (let n = walker.nextNode(); n !== null; n = walker.nextNode()) {
266
+ if (!range.intersectsNode(n)) continue
267
+ const start = n === range.startContainer ? range.startOffset : 0
268
+ const end = n === range.endContainer ? range.endOffset : n.data.length
269
+ if (start >= end || n.data.slice(start, end).trim() === '') continue
270
+ if (first === null) first = { n, start }
271
+ last = { n, end }
272
+ }
273
+ if (first === null) return range
274
+ const trimmed = range.cloneRange()
275
+ trimmed.setStart(first.n, first.start)
276
+ trimmed.setEnd(last.n, last.end)
277
+ return trimmed
278
+ }
279
+
251
280
  function selectionContext() {
252
281
  const sel = window.getSelection()
253
282
  if (sel === null || sel.isCollapsed || sel.rangeCount === 0) return null
254
- const range = sel.getRangeAt(0)
283
+ const range = trimRange(sel.getRangeAt(0))
255
284
  const node = range.commonAncestorContainer
256
285
  const el = node instanceof Element ? node : node.parentElement
257
286
  if (el === null) return null
@@ -283,6 +312,7 @@ window.__ModuleLoader__.load({
283
312
  const open = current
284
313
  if (open === null) return
285
314
  current = null
315
+ if (open.held) releaseSelection()
286
316
  // Close is often called from inside the card's own React event (Enter
287
317
  // in an editor, a button). Detach now; unmount once React has finished
288
318
  // that event, never during it.
@@ -321,6 +351,18 @@ window.__ModuleLoader__.load({
321
351
  host.style.left = at.left + 'px'
322
352
  host.style.top = at.top + 'px'
323
353
  host.style.visibility = ''
354
+ // The card mounts hidden until placed, so a field's autoFocus fires
355
+ // while it cannot take focus. Focus its first field once it shows.
356
+ if (!entry.focused) {
357
+ entry.focused = true
358
+ if (!host.contains(document.activeElement)) {
359
+ const field = host.querySelector('textarea,input:not([type="hidden"]),[contenteditable="true"]')
360
+ if (field !== null) {
361
+ field.focus({ preventScroll: true })
362
+ if (typeof field.setSelectionRange === 'function') field.setSelectionRange(field.value.length, field.value.length)
363
+ }
364
+ }
365
+ }
324
366
  }
325
367
  const draw = () => {
326
368
  root.render(jsx(ui.MenuSurface, { 'data-part-card': '', children: jsx(PopoverBody, { render, close, place }) }))
@@ -335,6 +377,24 @@ window.__ModuleLoader__.load({
335
377
  return render(close)
336
378
  }
337
379
 
380
+ // -------------------------------------------------- held selection
381
+ //
382
+ // Focusing the card's field takes the page selection away. So when a
383
+ // toolbar action opens a card, the passage stays painted with a CSS
384
+ // highlight, in the selection's colours, until the card closes.
385
+
386
+ const canHold = typeof Highlight === 'function' && window.CSS !== undefined && window.CSS.highlights !== undefined
387
+
388
+ function holdSelection(range) {
389
+ if (!canHold || !range) return false
390
+ window.CSS.highlights.set(HELD, new Highlight(range))
391
+ return true
392
+ }
393
+
394
+ function releaseSelection() {
395
+ if (canHold) window.CSS.highlights.delete(HELD)
396
+ }
397
+
338
398
  // -------------------------------------------------- selection toolbar
339
399
 
340
400
  const buttons = [] // { id, plugin, label, icon, order, seq, when, onSelect }
@@ -391,7 +451,10 @@ window.__ModuleLoader__.load({
391
451
  onMouseDown: (e) => e.preventDefault(),
392
452
  onClick: () => {
393
453
  hideToolbar()
454
+ const before = current
394
455
  try { b.onSelect(target) } catch (error) { console.error(error) }
456
+ // The action opened a card: keep its passage painted while it is open.
457
+ if (current !== null && current !== before) current.held = holdSelection(target.range)
395
458
  },
396
459
  children: typeof b.label === 'function' ? b.label() : b.label,
397
460
  }, b.plugin + '/' + b.id)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rs-dsh/dsh-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared UI layer for the rs-dsh DSH plugins: one selection toolbar, anchored cards and DSH's own components, so the plugins look like DSH and stay small.",
5
5
  "license": "MIT",
6
6
  "type": "module",