dsh-paste-code-block 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -4,72 +4,96 @@
4
4
 
5
5
  **Cherry Studio‑style text/code block pasting for DeepSeek Harness Web**
6
6
 
7
- Paste text/code into the DSH Web composer → it collapses into a **bordered, collapsible, language-tagged card** instead of a long flat string; on send it restores the original content as a fenced code block.
7
+ Paste long code or text into the DSH Web composer → it lands as a **bordered, collapsible, language‑tagged card** instead of a wall of text. Names follow the DSH interface language (中文 / English). On send, the original content is restored as a fenced code block.
8
8
 
9
- `type: module` · `runtime: host` · `client: web` · MIT
9
+ [![npm version](https://img.shields.io/npm/v/dsh-paste-code-block?logo=npm)](https://www.npmjs.com/package/dsh-paste-code-block)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
11
+ [![platform: web](https://img.shields.io/badge/platform-DSH%20Web-blueviolet)](https://github.com/deepseek-ai/deepseek-harness)
10
12
 
11
- **[中文](./README.zh-CN.md) | English**
13
+ **English | [简体中文](./README.zh-CN.md)**
12
14
 
13
15
  </div>
14
16
 
15
- ---
17
+ ![Preview — pasted blocks become tidy cards](./docs/images/hero.png)
18
+
19
+ ## Why
20
+
21
+ Copying a 200‑line stack trace, an API response, or a long log into a chat input turns the composer into an unreadable blob. This plugin folds any qualifying paste into a **chip** (built on DSH's own hidden‑reference mechanism, the same one file uploads use) plus an **editing detail card** — the draft stays clean, and the content stays fully editable and restorable.
16
22
 
17
23
  ## Features
18
24
 
19
- - 📋 **Smart detection** — pasted content with ``` fences, newlines, or long indented code becomes a card; ordinary short prose pastes as-is.
20
- - 🏷️ **Language tags** — auto-detects Python / JS / JSON / YAML / SQL / bash / HTML·XML / C++ / Go / Ruby (incl. shebang). Plain text blocks are tagged `text`.
21
- - 📂 **Collapsible card** — collapsed by default showing a one-line preview; click to expand full code (scrollable, up to 260px).
22
- - ✏️ **In‑card editing** — edit the block content right in the detail card; the send path uses your edited text.
23
- - 📤 **Restore on send** — each card is restored to a ```` ```lang … ``` ```` fenced block; a failed send auto-fills it back into the input.
24
- - 🔢 **Smart numbering** — code and text are counted apart; deleting a block frees its number for reuse (smallest-free allocation).
25
- - 📱 **All surfaces** — pure client plugin; works on PC & mobile Web, adapts to light/dark themes.
25
+ - 🧩 **Auto block detection** — pasted text with ``` fences, newlines, heavy indentation, or length ≥ 96 becomes a card; ordinary short prose pastes through untouched.
26
+ - 🏷️ **Language tags** — auto-detects Python / JS / JSON / YAML / SQL / bash / HTML·XML / C++ / Go / Ruby (incl. shebangs); plain-text blocks are tagged `text`.
27
+ - ✏️ **In-card editing** — edit the content right in the detail card; the edited text is exactly what gets sent. Plus copy, collapse/expand, and hide.
28
+ - 🈯 **Localized names** — chips are named in the current DSH language: `代码块 1` / `Code #1`, `文本块 N` / `Text #N`. A leading glyph and tint tells the two apart at a glance — `</>` blue for code, `Aa` gray for text. Switching **Settings → Language** live-retitles every chip.
29
+ - × **One-click remove** — every chip carries its own `×` delete button (the detail card has one too); a removed number is recycled immediately.
30
+ - 📤 **Fenced restore on send** — each card expands back into a ```` ```lang … ``` ```` block when you send; a failed send automatically refills the input.
31
+ - 🔢 **Smart numbering** — code and text count apart; the smallest free number is always reused.
32
+ - 📱 **Every surface** — pure client plugin; works on desktop & mobile web, follows light/dark themes.
26
33
 
27
- ## Install
34
+ ## How it works
35
+
36
+ ![Four steps: copy · paste · tidy · send](./docs/images/workflow.png)
37
+
38
+ ## Localization
39
+
40
+ Chip labels, tooltips, line counts, and the send-time error notice all render through the official DSH locale service: the plugin registers a `paste-code-block` dictionary namespace with complete **zh / en** key sets, and its composer dock declares that namespace, so everything re-renders on every language switch.
28
41
 
29
- Requires DSH `0.1.x` (current developer preview — APIs may change).
42
+ ![Block names follow the UI language](./docs/images/languages.png)
30
43
 
31
- Published on **npm** — install directly by name:
44
+ - **New pastes** are named in whatever language is active at paste time.
45
+ - **Existing chips** are re-titled live when you flip 设置 → 语言 / Settings → Language — no reload needed.
46
+ - Clicks and deletes match chips **across languages**: a `代码块 2` chip still resolves to its block after switching to English (where it now reads `Code #2`), and vice versa.
47
+
48
+ ## Install
49
+
50
+ Requires DSH `0.1.x` (developer preview — APIs may change). Published on **npm** — install by name:
32
51
 
33
52
  ```sh
34
53
  dsh plugin --profile web add dsh-paste-code-block
35
54
  ```
36
55
 
37
- Alternatively, install from this Git repository or a local folder:
56
+ Or from this repository / a local checkout:
38
57
 
39
58
  ```sh
40
- # From GitHub
41
- dsh plugin --profile web add github:cjm-m/dsh-paste-code-block
42
-
43
- # From a local checkout
44
- dsh plugin --profile web add file:/path/to/dsh-paste-code-block
59
+ dsh plugin --profile web add github:cjm-m/dsh-paste-code-block # from GitHub
60
+ dsh plugin --profile web add file:/path/to/dsh-paste-code-block # from a local folder
45
61
  ```
46
62
 
47
63
  **Restart `dsh web`** after installing.
48
64
 
49
65
  ## Usage
50
66
 
51
- 1. Copy a block of code or a multi-line block of text anywhere.
52
- 2. Paste it into the DSH input → it becomes a code-block card.
53
- 3. Expand / copy / edit / remove as you like; press Enter or send and it's restored as a fenced code block.
67
+ 1. Copy any code or multi-line text from anywhere.
68
+ 2. Paste into the DSH input → it collapses into a chip such as `代码块 1` / `Code #1`.
69
+ 3. Click the chip to open the card: read, edit inline, copy, collapse. Click the chip's (or the card's) `×` to delete that block.
70
+ 4. Send — the full original text goes out as a proper fenced code block.
71
+
72
+ Detection threshold: **contains a newline / ≥ 2 lines / length ≥ 96 / indented & brace-dense / fenced** — any one turns the paste into a block.
54
73
 
55
74
  ## Notes & boundaries
56
75
 
57
- - Blocks are **appended at the end of the draft** (reusing DSH's hidden-reference semantics), keeping cursor/undo stable; ordinary short text is not intercepted.
58
- - Plain-text blocks are wrapped in ```` ```text ``` ```` on send so they don't smear into one line.
59
- - Detection threshold: **contains newline / ≥2 lines / length ≥96 / indented or fenced** — any one triggers the card.
76
+ - Blocks are **appended at the end of the draft** (reusing DSH's hidden-reference semantics), which keeps cursor/undo stable.
77
+ - Plain-text blocks are sent wrapped in ```` ```text ``` ```` so they never smear into one line.
78
+ - Removing a chip any other way (e.g. `Backspace`) also frees its number — plugin state is reconciled against the editor continuously.
79
+ - Deleting one block never touches the others, and removal is an ordinary editor edit — `Ctrl+Z` brings it back.
80
+ - Creating a block leaves **no stray space**: DSH appends a separating space after a freshly inserted chip, and the plugin removes exactly that one character, so text typed after a block does not start with a space and pasting several blocks never piles spaces into the message. Spaces you typed yourself are never touched by pasting or deleting.
60
81
 
61
82
  ## Project layout
62
83
 
63
84
  ```
64
85
  dsh-paste-code-block/
65
86
  ├── src/
66
- │ ├── client.js # Web (browser) half — paste capture, chip styling, detail card
87
+ │ ├── client.js # Web (browser) half — paste capture, chips, detail card, × delete
67
88
  │ ├── index.js # Host (node) half — intentionally empty (pure UI plugin)
68
- │ └── parse.js # Pure block-parsing logic (canonical, unit-tested)
89
+ │ ├── parse.js # Pure block-detection logic (canonical, unit-tested)
90
+ │ └── i18n.js # Locale dictionaries + label parsing (canonical, unit-tested)
69
91
  ├── tests/
70
- │ └── parse.test.js # node:test unit tests for src/parse.js
92
+ │ ├── parse.test.js # node:test unit tests for src/parse.js
93
+ │ └── i18n.test.js # node:test unit tests for src/i18n.js
71
94
  ├── docs/
72
- │ └── design.md # Design rationale (numbering, anchor stability, codec)
95
+ │ ├── design.md # Design rationale (numbering, anchors, codec, i18n, chip ×)
96
+ │ └── images/ # README mockups (PNG + editable SVG sources)
73
97
  ├── cordis.patch.yml # DSH bundle patch declaring the host plugin
74
98
  ├── package.json # Package + DSH plugin manifest
75
99
  ├── README.md # English
@@ -79,8 +103,10 @@ dsh-paste-code-block/
79
103
  ## Development
80
104
 
81
105
  ```sh
82
- npm run check # syntax-check both JS halves
83
- npm test # run the unit tests (node:test)
106
+ npm run check # syntax-check the JS halves
107
+ npm test # run unit tests (node:test, 38 tests)
84
108
  ```
85
109
 
86
- MIT — see [LICENSE](./LICENSE).
110
+ ## License
111
+
112
+ MIT — see [LICENSE](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -4,74 +4,96 @@
4
4
 
5
5
  **Cherry Studio 式的文本块/代码块粘贴效果(DeepSeek Harness Web)**
6
6
 
7
- 把复制的**文本块 / 代码块**粘到 DSH Web 输入框时,不再是一长串普通文字,而是收成一张**带边框、可折叠、带语言标签的代码块卡片**;发送时自动还原成真正的 ```` ```lang … ``` ```` 围栏代码块发送。
7
+ 把复制的**长代码 / 长文本**粘到 DSH Web 输入框时,不再糊成一长串普通文字,而是收成一张**带边框、可折叠、带语言标签的卡片**;块名称自动跟随 DSH 界面语言(中文 / English);发送时自动还原成真正的 ```` ```lang … ``` ```` 围栏代码块。
8
8
 
9
- 纯客户端插件(浏览器侧),PC 与手机 Web 通用,实时适配深浅主题。
10
-
11
- `type: module` · `runtime: host` · `client: web` · MIT
9
+ [![npm version](https://img.shields.io/npm/v/dsh-paste-code-block?logo=npm)](https://www.npmjs.com/package/dsh-paste-code-block)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
11
+ [![platform: web](https://img.shields.io/badge/platform-DSH%20Web-blueviolet)](https://github.com/deepseek-ai/deepseek-harness)
12
12
 
13
13
  **[English](./README.md) | 中文**
14
14
 
15
15
  </div>
16
16
 
17
- ---
17
+ ![预览:粘贴的块自动收成整洁卡片](./docs/images/hero.png)
18
+
19
+ ## 为什么需要它
20
+
21
+ 往聊天输入框里粘贴 200 行报错栈、API 返回、长日志,输入框立刻变成一坨无法阅读的文字墙。本插件把符合特征的粘贴折叠成 **芯片(chip)**(复用 DSH 自带的隐藏引用机制,与文件上传同源)外加一张**可编辑详情卡**——草稿保持整洁,内容随时可读、可改、可还原。
18
22
 
19
23
  ## 功能
20
24
 
21
- - 📋 **智能识别**:粘贴内容已带 ``` 围栏、含换行、或是较长的缩进代码 → 收成卡片;普通短文本照常粘贴。
22
- - 🏷️ **语言标签**:自动识别 Python / JS / JSON / YAML / SQL / bash / HTML·XML / C++ / Go / Ruby 等常见语言(可识别 shebang)。纯文本块标为 `text`。
23
- - 📂 **可折叠卡片**:默认收起显示首行预览,点击展开完整代码(最多 260px 高可滚动)。
24
- - ✏️ **卡片内编辑**:在详情卡片里直接编辑代码块内容,发送时使用编辑后的文本。
25
- - 📤 **发送还原**:发出时把每个卡片还原成 ```` ```lang ``` ```` 围栏代码块;发送失败自动回填回输入框。
26
- - 🔢 **智能编号**:代码与文本分开计数,从最小可用号分配;删除某块即释放其编号供复用。
27
- - 📱 **全端适配**:纯客户端插件,PC 与手机 Web 通用,实时适配深浅主题。
25
+ - 🧩 **智能识别**:粘贴内容带 ``` 围栏、含换行、明显缩进、或长度 ≥ 96 → 收成卡片;普通短文本照常粘贴,绝不打扰。
26
+ - 🏷️ **语言标签**:自动识别 Python / JS / JSON / YAML / SQL / bash / HTML·XML / C++ / Go / Ruby 等(可识别 shebang);纯文本块标为 `text`。
27
+ - ✏️ **卡片内编辑**:详情卡里直接修改内容,发送的正是编辑后的文本;另有复制、折叠/展开、收起。
28
+ - 🈯 **名称本地化**:芯片按当前 DSH 界面语言命名——`代码块 1` / `Code #1`、`文本块 N` / `Text #N`;前缀图标与配色一眼区分两类块(代码 `</>` 蓝色、文本 `Aa` 灰色);在「设置 → 语言」切换后所有芯片即时改名。
29
+ - × **一键删除**:每个芯片自带 `×` 删除按钮(详情卡上也有);删掉的块编号立刻回收复用。
30
+ - 📤 **发送还原**:发送时每个卡片展开还原为 ```` ```lang … ``` ```` 围栏代码块;发送失败自动回填输入框。
31
+ - 🔢 **智能编号**:代码与文本分开计数,永远分配最小可用号。
32
+ - 📱 **全端适配**:纯客户端插件,PC 与手机 Web 通用,实时跟随深浅主题。
28
33
 
29
- ## 安装
34
+ ## 工作流
35
+
36
+ ![四步:复制 · 粘贴 · 整理 · 发送](./docs/images/workflow.png)
37
+
38
+ ## 名称本地化
30
39
 
31
- 需要 DSH `0.1.x`(当前为开发者预览版,接口可能变化)。
40
+ 芯片名称、按钮提示、行数、发送失效提示全部走 DSH 官方 locale 服务:插件注册自己的 `paste-code-block` 词典命名空间(zh / en 键集完全一致),输入坞声明该命名空间,因此切换语言时所有文案自动重渲染。
32
41
 
33
- 已发布到 **npm**,可直接按包名安装:
42
+ ![块名称跟随界面语言](./docs/images/languages.png)
43
+
44
+ - **新粘贴**的块按粘贴时的界面语言命名。
45
+ - **已插入的芯片**在 设置 → 语言 切换后原地改名,无需刷新页面。
46
+ - 点击与删除**跨语言匹配**:切到英文后,原来叫 `代码块 2` 的芯片(此时显示 `Code #2`)依然指向同一个块,反之亦然。
47
+
48
+ ## 安装
49
+
50
+ 需要 DSH `0.1.x`(开发者预览版,接口可能变化)。已发布到 **npm**,直接按包名安装:
34
51
 
35
52
  ```sh
36
53
  dsh plugin --profile web add dsh-paste-code-block
37
54
  ```
38
55
 
39
- 也可以从本 Git 仓库或本地目录安装:
56
+ 也可以从本仓库或本地目录安装:
40
57
 
41
58
  ```sh
42
- # 从 GitHub
43
- dsh plugin --profile web add github:cjm-m/dsh-paste-code-block
44
-
45
- # 从本地目录
46
- dsh plugin --profile web add file:/path/to/dsh-paste-code-block
59
+ dsh plugin --profile web add github:cjm-m/dsh-paste-code-block # 从 GitHub
60
+ dsh plugin --profile web add file:/path/to/dsh-paste-code-block # 从本地目录
47
61
  ```
48
62
 
49
63
  安装后**重启 `dsh web`** 生效。
50
64
 
51
65
  ## 使用
52
66
 
53
- 1. 在任意地方复制一段代码 / 一段带换行的文本。
54
- 2. 在 DSH 输入框粘贴 → 变成一张代码块卡片。
55
- 3. 可展开 / 复制 / 编辑 / 移除;回车或发送即还原为围栏代码块发出。
67
+ 1. 在任意地方复制一段代码 / 一段多行文本。
68
+ 2. 在 DSH 输入框粘贴 → 折叠成 `代码块 1` 这样的芯片。
69
+ 3. 点芯片打开详情卡:阅读、就地编辑、复制、折叠;点芯片(或卡片)上的 `×` 删除该块。
70
+ 4. 发送——完整原文以围栏代码块形式发出。
71
+
72
+ 识别阈值:**含换行 / 行数 ≥ 2 / 长度 ≥ 96 / 缩进且符号密集 / 带围栏**,任一命中即成块。
56
73
 
57
74
  ## 说明与边界
58
75
 
59
- - 默认**追加在草稿末尾**(复用 DSH 隐藏引用按文件/上传的既有语义),保证光标/撤销稳定;短普通文本不拦截。
60
- - 纯文本块发送时用 ```` ```text ``` ```` 围栏包裹,避免在对话里糊成一行。
61
- - 识别阈值:**含换行 / 行数 ≥2 / 长度 ≥96 / 有缩进或围栏**其一即视为块。
76
+ - 块一律**追加在草稿末尾**(复用 DSH 隐藏引用语义),光标与撤销保持稳定。
77
+ - 纯文本块发送时用 ```` ```text ``` ```` 围栏包裹,不会在对话里糊成一行。
78
+ - 用其他方式删除芯片(如 `Backspace`)同样会释放编号——插件状态与编辑器持续对账。
79
+ - 删除单个块绝不影响其它块;删除是一次普通的编辑器编辑,可用 `Ctrl+Z` 撤销找回。
80
+ - 创建块**不会残留空格**:DSH 会在新插入的芯片后自动补一个分隔空格,插件只把这一个字符删掉——因此块后面接着输入的文字不会顶着一个前导空格,连续粘贴多个块也不会在正文里堆出多余空格;你自己敲的空格在粘贴/删除时都不会被动到。
62
81
 
63
82
  ## 目录结构
64
83
 
65
84
  ```
66
85
  dsh-paste-code-block/
67
86
  ├── src/
68
- │ ├── client.js # Web(浏览器侧)半端 —— 粘贴拦截、徽章样式、详情卡片
69
- │ ├── index.js # Host(Node 侧)半端 —— 有意为空(纯 UI 插件)
70
- │ └── parse.js # 纯文本块解析逻辑(规范版,含单元测试)
87
+ │ ├── client.js # Web(浏览器侧)—— 粘贴拦截、芯片、详情卡片、× 删除
88
+ │ ├── index.js # Host(Node 侧)—— 有意为空(纯 UI 插件)
89
+ │ ├── parse.js # 纯块识别逻辑(规范版,含单元测试)
90
+ │ └── i18n.js # 词典与块名称解析(规范版,含单元测试)
71
91
  ├── tests/
72
- │ └── parse.test.js # node:test 单元测试(针对 src/parse.js)
92
+ │ ├── parse.test.js # node:test 单元测试(src/parse.js)
93
+ │ └── i18n.test.js # node:test 单元测试(src/i18n.js)
73
94
  ├── docs/
74
- │ └── design.md # 设计说明(编号复用、锚点稳定、codec)
95
+ │ ├── design.md # 设计说明(编号复用、锚点稳定、codec、i18n、芯片 ×)
96
+ │ └── images/ # README 示意图(PNG + 可编辑 SVG 源)
75
97
  ├── cordis.patch.yml # DSH bundle patch,声明宿主插件
76
98
  ├── package.json # 包与 DSH 插件清单
77
99
  ├── README.md # English
@@ -81,10 +103,10 @@ dsh-paste-code-block/
81
103
  ## 开发
82
104
 
83
105
  ```sh
84
- npm run check # 语法检查两个 JS 半端
85
- npm test # 运行单元测试(node:test)
106
+ npm run check # 语法检查 JS 半端
107
+ npm test # 运行单元测试(node:test,38 项)
86
108
  ```
87
109
 
88
110
  ## License
89
111
 
90
- MIT — 见 [LICENSE](./LICENSE)。
112
+ MIT — 见 [LICENSE](./LICENSE)。
package/docs/design.md CHANGED
@@ -16,10 +16,14 @@ A DSH plugin can ship a **host** (node) half and a **client** (browser) half. Th
16
16
  reference chips into boxed badges, and renders an editing detail card above the composer.
17
17
 
18
18
  Because the client is loaded by DSH's module loader — which resolves *package names*, not
19
- relative ESM files — `client.js` **cannot** `import` a sibling module. Its parsing logic is
19
+ relative ESM files — `client.js` **cannot** `import` a sibling module. Its runtime logic is
20
20
  therefore **embedded** in the bundle. To keep that logic unit-testable, the same functions are
21
- extracted into `src/parse.js` (a plain, dependency-free ESM module) which the test suite
22
- exercises. **Keep `src/parse.js` and the embedded copy in `src/client.js` in sync.**
21
+ extracted into plain, dependency-free ESM modules which the test suite exercises:
22
+
23
+ - `src/parse.js` — block detection / language detection / serialization.
24
+ - `src/i18n.js` — locale dictionaries, label building, and label parsing.
25
+
26
+ **Keep `src/parse.js` / `src/i18n.js` and their embedded copies in `src/client.js` in sync.**
23
27
 
24
28
  ## 2. How a paste becomes a card (the pipeline)
25
29
 
@@ -44,8 +48,9 @@ mechanism that renders file/upload references inline. The plugin registers a sou
44
48
  - `codec.clipboardText` → `''` (the chip leaves no visible text on copy)
45
49
  - `codec.serialize(ref)` → the block's fenced ```` ```lang … ``` ```` text
46
50
 
47
- The chip label is the human-readable badge (`复制代码块N` / `复制文本块N`); clicking it opens the
48
- detail card.
51
+ The chip label is the localized human-readable badge (`代码块 N` / `Code #N` — see §9);
52
+ clicking it opens the detail card, and each chip carries a `×` button that deletes the block
53
+ (see §10).
49
54
 
50
55
  ## 4. Why `clipboardText` is U+200B (zero-width space)
51
56
 
@@ -65,7 +70,7 @@ Numbers are tracked **per session, split by type** (`code` vs `text`):
65
70
  - `nextLabel` always allocates the **smallest free integer** (`while (used.has(n)) n++`).
66
71
  - Deleting a chip frees its number; the next paste reuses it.
67
72
 
68
- So `复制代码块1` … `复制代码块N` and `复制文本块1` … `复制文本块M` each count independently and
73
+ So `代码块 1` … `代码块 N` and `文本块 1` … `文本块 M` each count independently and
69
74
  stay dense after removals.
70
75
 
71
76
  ## 6. Markdown vs. code detection
@@ -84,10 +89,117 @@ send fails — so a failed send auto-refills the input.
84
89
 
85
90
  ## 8. Testing strategy
86
91
 
87
- `tests/parse.test.js` exercises the pure parsing module (`src/parse.js`) with the built-in
88
- `node:test` runner (no test framework dependency). The DOM/controller glue is not unit-tested
89
- because it depends on the live DSH client runtime; it is verified manually in the browser.
92
+ `tests/parse.test.js` and `tests/i18n.test.js` exercise the two pure modules (`src/parse.js`,
93
+ `src/i18n.js`) with the built-in `node:test` runner (no test framework dependency). The
94
+ DOM/controller glue is not unit-tested because it depends on the live DSH client runtime; it is
95
+ verified manually in the browser.
90
96
 
91
97
  ```sh
92
98
  npm test
93
- ```
99
+ ```
100
+
101
+ ## 9. Localization (i18n)
102
+
103
+ Block names are produced through **DSH's own locale service**, the same one first-party plugins
104
+ (schedule, session-log-export, …) use:
105
+
106
+ - `apply()` registers the `paste-code-block` namespace via `ctx.locale.register(NS, {zh, en})`
107
+ with *identical key sets* in both dictionaries (the built-in locale runtime assumes that).
108
+ - All user-visible strings — chip labels (`block.code` / `block.text` with `{n}`), button
109
+ tooltips, the `{count}` line counter (with a `lines.one` / `lines.other` plural split),
110
+ aria labels, slot label, and the stale-block send error — go through the bound translator
111
+ `t(key, params)`.
112
+ - The dock slot entry declares `locale: NS`, so the renderer hands it a fresh `t` seat **and
113
+ re-renders it on every locale revision**.
114
+
115
+ **Naming and visual identity (0.1.3).** A chip is a *reference badge*, not an action button, so
116
+ the pre-0.1.3 names carried a verb nobody invokes on it — `复制代码块N` / `Code block #N` read
117
+ like a command and burned width inside the composer. Current names are noun + number
118
+ (`代码块 1` / `Code #1`). The code/text distinction moved from wording to visuals: `scanChips`
119
+ tags every chip host with `data-pcb-type`, and CSS renders a leading glyph — `</>` for code
120
+ (mono, business-primary) and `Aa` for text (neutral gray) — plus a soft blue tint on code
121
+ chips. Pre-0.1.3 labels stay parseable through the `legacyLabels` table, so chips inserted by
122
+ an older bundle keep their click/✕/retitle identity until the next locale-synced retitle.
123
+
124
+ **Chip retitling across switches.** The chip label is *insert-time* content: Lexical caches the
125
+ label inside the decorator node, and React never re-renders those node bodies while the node
126
+ state is unchanged. So on a locale switch the plugin: (1) recomputes `block.label` for every
127
+ tracked block and rebuilds `labelIndex` (`retitleAll`), then (2) patches the visible DOM —
128
+ `title` attribute and label span `.textContent` — inside `scanChips`. React's virtual DOM
129
+ record still says the old label, so it never fights the patch; a full chip remount (the only
130
+ revert vector) is caught by the same MutationObserver that manages the ✕ button.
131
+
132
+ **Cross-language identity.** `parseBlockLabel` recognizes a rendered label in **any** shipped
133
+ locale and recovers `{ type, n }`; lookups (chip click, ✕ delete, retitle matching) resolve the
134
+ block by that identity within the chip's own session. A chip inserted under one language keeps
135
+ working after switching to the other.
136
+
137
+ ## 10. The per-chip ✕ delete button
138
+
139
+ DSH renders chips from Lexical decorator nodes; third parties cannot add React children to
140
+ them. Instead the plugin **appends a `<span class="dsh-pcb-chip-x">×</span>` into each chip's
141
+ inner element** and keeps them present with a `MutationObserver` on `document.body`
142
+ (rAF-coalesced; the scan exits immediately when no block is tracked). A capture-phase
143
+ `pointerdown` handler checks `.dsh-pcb-chip-x` **before** the toggle logic and calls
144
+ `controller.remove`, which removes **just that chip's Lexical node** and frees its number. If
145
+ the chip is not backed by a tracked block (e.g. a restored draft after redraw), no ✕ is added —
146
+ no dead affordances. Touch targets grow on coarse pointers (`@media (pointer:coarse)`).
147
+
148
+ **Why node-level removal matters — the 0.1.1 incident.** The first `remove()` implementation
149
+ spliced the chip's placeholder char out of the draft string and wrote the result through
150
+ `shell.setDraft()`. But `setDraft` **clears the editor root and rebuilds plain-text
151
+ paragraphs** — chips are decorator nodes that cannot round-trip through text, so deleting one
152
+ block destroyed *every* chip in the draft. `removeChipNodes()` instead scans the editor's node
153
+ map for `ReferenceChipNode`s whose public `getSource()`/`getReference()` match this plugin and
154
+ the block's ref, and calls the node's own `remove()` inside one discrete update — a surgical,
155
+ undoable (`Ctrl+Z`) deletion that leaves sibling chips untouched.
156
+
157
+ **The 0.1.2 silent no-op.** The node map (`editor.getEditorState()._nodeMap`) is keyed by
158
+ NodeKey and **its values are the nodes themselves**; the older `NodeState` wrapper with a
159
+ `.node` field no longer exists. Reading `value.node` therefore matched *nothing* for every
160
+ entry, `removeChipNodes()` removed zero chips, yet the call looked successful — the block's
161
+ state was pruned behind a chip that was still in the draft, so clicking ✕ appeared to do
162
+ nothing at all. `src/chip-nodes.js` now normalizes both shapes (`chipNodeOf`) and is unit
163
+ tested for exactly this rule; `tests/lexical-chip-probe.mjs` reproduces the original defect
164
+ against real `lexical@0.49.0`:
165
+
166
+ ```
167
+ _nodeMap value shape: valueIsNode=true, hasNodeProp=false
168
+ OLD code (value.node): removed = 0 | chips after = r1,r2,r3 <- silent no-op
169
+ NEW code (value is node): removed = 1 | chips after = r1,r3 <- only the target
170
+ ```
171
+
172
+ **Failure is never silent and never destructive.** `remove()` gates every state change on
173
+ `chipPresent()` — the editor's node map first, the published occurrence projection as a
174
+ cross-check — so a block is pruned only once its chip is verifiably gone. If the chip survives
175
+ the attempt the block stays tracked and the composer surfaces `error.remove`; the old
176
+ `setDraft` fallback is gone for good. State that diverges from the editor still self-heals via
177
+ `reconcile`.
178
+
179
+ ## 11. Insertion leaves no stray space
180
+
181
+ `SessionInputShell.insertReference` inserts `[chip, ' ']` — a separating space — whenever the
182
+ character at the pick-time span is not itself a space. Because a block is always appended at
183
+ the **end** of the draft, that condition is always true, so every paste used to leave a space
184
+ behind: text typed next started with a space (the visible "prefix space"), and pasting several
185
+ blocks piled stray spaces into the message body.
186
+
187
+ Two rules keep the draft clean without ever touching user whitespace:
188
+
189
+ 1. **Insert, then drop exactly the appended character.** `dropSeparatorSpace()` asks
190
+ `separatorSpaceSpan()` (src/projection.js) for the detect span of the character directly
191
+ after *our* chip and deletes it with the same public `slash/input-insert-text` verb DSH uses
192
+ for its own span edits. Since the chip is inserted at the end, that character can only have
193
+ come from the insertion itself — a trailing space the user had already typed still sits
194
+ before the chip, untouched. The span is only used when the projection shows a space right
195
+ after this block's chip, and a failure to apply just logs (the draft keeps DSH's behaviour).
196
+ 2. **Delete removes the chip node only.** `remove()` drops the chip's Lexical node
197
+ (§10) and never rewrites text, so spaces around a deleted block survive exactly as they were.
198
+
199
+ `src/projection.js` owns the coordinate conversion this needs: the published `draft` is the
200
+ clipboard projection (a chip occupies `occurrence.length` characters, i.e. one zero-width space
201
+ for us) while the scoped edit verbs address detect coordinates (a chip collapses to exactly one
202
+ placeholder character). `detectOffsetOf()` converts with
203
+ `detect = clipboard - Σ(len_i - 1)` over preceding occurrences, so blocks stay correctly
204
+ addressed even when other plugins' chips (file references and the like) sit in the same draft.
205
+ Both helpers are pure and unit-tested (8 cases in tests/projection.test.js).
Binary file
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700">
2
+ <rect width="1200" height="700" fill="#0d1220"/>
3
+ <text x="40" y="56" font-size="26" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">粘贴长代码 / 长文本 → 输入框里只留一张整洁卡片</text><text x="40" y="86" font-size="14.5" fill="#93a3c0" font-family="Noto Sans SC">Paste long code or text into the DSH composer — it lands as a tidy, collapsible card, never a wall of text</text><rect x="40" y="112" width="720" height="516" rx="16" fill="#151c2f"/><rect x="40" y="112" width="720" height="516" rx="16" fill="none" stroke="#2a3450" stroke-width="1"/><text x="64" y="140" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">DSH Web · 输入框 / composer</text><rect x="64" y="152" width="672" height="96" rx="10" fill="#10162a"/><rect x="64" y="152" width="672" height="96" rx="10" fill="none" stroke="#232c45" stroke-width="1"/><rect x="80" y="168" width="126" height="27" rx="7" fill="#151e3c"/><rect x="80" y="168" width="126" height="27" rx="7" fill="none" stroke="#263777" stroke-width="1"/><text x="89" y="185.5" font-size="10.5" fill="#7aa2ff" font-family="JetBrains Mono" font-weight="700">&lt;/&gt;</text><text x="112" y="186" font-size="12.5" fill="#8fb0ff" font-family="Noto Sans SC">代码块 1</text><text x="196" y="187" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="214" y="168" width="126" height="27" rx="7" fill="#1d2742"/><rect x="214" y="168" width="126" height="27" rx="7" fill="none" stroke="#3a4a75" stroke-width="1"/><text x="223" y="185.5" font-size="10.5" fill="#93a3c0" font-family="Noto Sans SC" font-weight="700">Aa</text><text x="243" y="186" font-size="12.5" fill="#cdd9f2" font-family="Noto Sans SC">文本块 1</text><text x="330" y="187" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="348" y="168" width="126" height="27" rx="7" fill="#151e3c"/><rect x="348" y="168" width="126" height="27" rx="7" fill="none" stroke="#263777" stroke-width="1"/><text x="357" y="185.5" font-size="10.5" fill="#7aa2ff" font-family="JetBrains Mono" font-weight="700">&lt;/&gt;</text><text x="380" y="186" font-size="12.5" fill="#8fb0ff" font-family="Noto Sans SC">代码块 2</text><text x="464" y="187" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><circle cx="464" cy="181.5" r="11" fill="none" stroke="#ff7b7b" stroke-width="1.6" stroke-dasharray="4 3"/><text x="80" y="234" font-size="13" fill="#5b6a8a" font-family="Noto Sans SC">帮我分析这段代码,重点是并发问题…</text><rect x="64" y="268" width="672" height="312" rx="12" fill="#131a2c"/><rect x="64" y="268" width="672" height="312" rx="12" fill="none" stroke="#232c45" stroke-width="1"/><rect x="80" y="282" width="60" height="22" rx="8" fill="#1c2b4d"/><text x="90" y="297" font-size="11.5" fill="#7aa2ff" font-family="JetBrains Mono">python</text><text x="154" y="297" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">42 行</text><path d="M 610 291 L 617 284.5 L 624 291" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="642" y="284" width="11" height="13" rx="2" fill="none"/><rect x="642" y="284" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><rect x="647" y="288" width="11" height="13" rx="2" fill="#131a2c"/><rect x="647" y="288" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><path d="M 680 284.5 L 673.5 290.5 L 680 297" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><text x="708" y="296.5" font-size="16" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><circle cx="708" cy="291" r="12" fill="none" stroke="#ff7b7b" stroke-width="1.6" stroke-dasharray="4 3"/><rect x="80" y="318" width="640" height="240" rx="8" fill="#0b1120"/><text x="98" y="348" font-size="13" fill="#c792ea" font-family="JetBrains Mono">def </text><text x="132" y="348" font-size="13" fill="#7aa2ff" font-family="JetBrains Mono">analyze</text><text x="180" y="348" font-size="13" fill="#d6deeb" font-family="JetBrains Mono">(chunks):</text><text x="98" y="376" font-size="13" fill="#d6deeb" font-family="JetBrains Mono"> total = 0</text><text x="98" y="404" font-size="13" fill="#d6deeb" font-family="JetBrains Mono"> results = []</text><text x="98" y="432" font-size="13" fill="#c792ea" font-family="JetBrains Mono"> for </text><text x="132" y="432" font-size="13" fill="#d6deeb" font-family="JetBrains Mono">c</text><text x="143" y="432" font-size="13" fill="#c792ea" font-family="JetBrains Mono"> in </text><text x="164" y="432" font-size="13" fill="#d6deeb" font-family="JetBrains Mono">chunks</text><text x="213" y="432" font-size="13" fill="#d6deeb" font-family="JetBrains Mono">:</text><text x="224" y="432" font-size="13" fill="#5b6a8a" font-family="JetBrains Mono"># 42 rows total — full, editable</text><text x="98" y="460" font-size="13" fill="#d6deeb" font-family="JetBrains Mono"> total += len(c.text)</text><text x="98" y="488" font-size="13" fill="#c792ea" font-family="JetBrains Mono"> return </text><text x="148" y="488" font-size="13" fill="#7aa2ff" font-family="JetBrains Mono">sorted</text><text x="198" y="488" font-size="13" fill="#d6deeb" font-family="JetBrains Mono">(results)[-5:]</text><rect x="98" y="504" width="520" height="1.6" fill="#232c45"/><text x="98" y="528" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">卡片内直接编辑 · 编辑后的文本即发送内容</text><rect x="664" y="596" width="72" height="28" rx="8" fill="#3b5bdb"/><text x="700" y="615" font-size="13.5" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">发送</text><path d="M 806 168 C 675.5 168, 675.5 182, 545 182" stroke="#5b6a8a" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="820" cy="168" r="13" fill="#3b5bdb"/><text x="820" y="172.5" font-size="13.5" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">1</text><text x="842" y="169" font-size="15" fill="#e6ecf8" font-family="Noto Sans SC">点击芯片 → 展开 / 收起详情卡</text><text x="842" y="189" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">click a chip to open its card</text><path d="M 806 262 C 753 262, 753 291, 700 291" stroke="#5b6a8a" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="820" cy="268" r="13" fill="#3b5bdb"/><text x="820" y="272.5" font-size="13.5" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">2</text><text x="842" y="269" font-size="15" fill="#e6ecf8" font-family="Noto Sans SC">× 一键删除对应块,编号自动回收复用</text><text x="842" y="289" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">one-click remove · the number is recycled</text><path d="M 806 372 C 764 372, 764 430, 722 430" stroke="#5b6a8a" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/><circle cx="820" cy="372" r="13" fill="#3b5bdb"/><text x="820" y="376.5" font-size="13.5" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">3</text><text x="842" y="373" font-size="15" fill="#e6ecf8" font-family="Noto Sans SC">详情卡:编辑 / 复制 / 折叠</text><text x="842" y="393" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">edit, copy, collapse — all inline</text><line x1="756" y1="612" x2="806" y2="612" stroke="#5b6a8a" stroke-width="1.4"/><path d="M 806 612 l -8 -4 v 8 z" stroke="#5b6a8a" stroke-width="1" fill="#5b6a8a" stroke-linecap="round" stroke-linejoin="round"/><rect x="812" y="470" width="348" height="150" rx="12" fill="#10162a"/><rect x="812" y="470" width="348" height="150" rx="12" fill="none" stroke="#232c45" stroke-width="1"/><text x="832" y="496" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">发送后 · on send</text><text x="832" y="524" font-size="12.5" fill="#93a3c0" font-family="JetBrains Mono">```python</text><text x="832" y="548" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono">def analyze(chunks):</text><text x="832" y="572" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono"> ...完整原始文本...</text><text x="832" y="596" font-size="12.5" fill="#93a3c0" font-family="JetBrains Mono">```</text><circle cx="820" cy="662" r="13" fill="#3b5bdb"/><text x="820" y="666.5" font-size="13.5" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">4</text><text x="842" y="663" font-size="15" fill="#e6ecf8" font-family="Noto Sans SC">发送时自动还原为围栏代码块</text><text x="842" y="683" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">restored as a fenced code block on send</text><text x="40" y="662" font-size="12.5" fill="#5b6a8a" font-family="Noto Sans SC">PC 与移动端 Web 通用 · 深浅主题自适应 · 标签随 DSH 界面语言自动切换(中文 / English)</text>
4
+ </svg>
Binary file
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="464" viewBox="0 0 1200 464">
2
+ <rect width="1200" height="464" fill="#0d1220"/>
3
+ <text x="40" y="50" font-size="22" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">块名称跟随 DSH 界面语言 · Block names follow the UI language</text><rect x="36" y="84" width="548" height="296" rx="14" fill="#151c2f"/><rect x="36" y="84" width="548" height="296" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><rect x="60" y="108" width="226" height="28" rx="14" fill="#0b1120"/><rect x="60" y="108" width="226" height="28" rx="14" fill="none" stroke="#232c45" stroke-width="1"/><text x="76" y="126" font-size="12.5" fill="#93a3c0" font-family="Noto Sans SC">设置 → 语言:中文</text><rect x="60" y="158" width="126" height="27" rx="7" fill="#1a2341"/><rect x="60" y="158" width="126" height="27" rx="7" fill="none" stroke="#2a3b7a" stroke-width="1"/><text x="69" y="175.5" font-size="10.5" fill="#7aa2ff" font-family="JetBrains Mono" font-weight="700">&lt;/&gt;</text><text x="92" y="176" font-size="12.5" fill="#8fb0ff" font-family="Noto Sans SC">代码块 1</text><text x="176" y="177" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="194" y="158" width="126" height="27" rx="7" fill="#1d2742"/><rect x="194" y="158" width="126" height="27" rx="7" fill="none" stroke="#3a4a75" stroke-width="1"/><text x="203" y="175.5" font-size="10.5" fill="#93a3c0" font-family="Noto Sans SC" font-weight="700">Aa</text><text x="223" y="176" font-size="12.5" fill="#cdd9f2" font-family="Noto Sans SC">文本块 1</text><text x="310" y="177" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="60" y="206" width="500" height="148" rx="10" fill="#131a2c"/><rect x="60" y="206" width="500" height="148" rx="10" fill="none" stroke="#232c45" stroke-width="1"/><rect x="76" y="220" width="62" height="20" rx="7" fill="#1c2b4d"/><text x="86" y="234" font-size="11" fill="#7aa2ff" font-family="JetBrains Mono">python</text><text x="152" y="234" font-size="11" fill="#5b6a8a" font-family="Noto Sans SC">18 行</text><path d="M 436 229 L 443 222.5 L 450 229" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="468" y="222" width="11" height="13" rx="2" fill="none"/><rect x="468" y="222" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><rect x="473" y="226" width="11" height="13" rx="2" fill="#131a2c"/><rect x="473" y="226" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><path d="M 506 222.5 L 499.5 228.5 L 506 235" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><text x="534" y="234.5" font-size="16" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><text x="76" y="266" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono">def analyze(chunks):</text><text x="76" y="290" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono"> total = 0</text><text x="76" y="314" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono"> return sorted(results)</text><rect x="616" y="84" width="548" height="296" rx="14" fill="#151c2f"/><rect x="616" y="84" width="548" height="296" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><rect x="640" y="108" width="226" height="28" rx="14" fill="#0b1120"/><rect x="640" y="108" width="226" height="28" rx="14" fill="none" stroke="#232c45" stroke-width="1"/><text x="656" y="126" font-size="12.5" fill="#93a3c0" font-family="Noto Sans SC">Settings → Language: English</text><rect x="640" y="158" width="122" height="27" rx="7" fill="#1a2341"/><rect x="640" y="158" width="122" height="27" rx="7" fill="none" stroke="#2a3b7a" stroke-width="1"/><text x="649" y="175.5" font-size="10.5" fill="#7aa2ff" font-family="JetBrains Mono" font-weight="700">&lt;/&gt;</text><text x="672" y="176" font-size="12.5" fill="#8fb0ff" font-family="Noto Sans SC">Code #1</text><text x="751" y="177" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="770" y="158" width="122" height="27" rx="7" fill="#1d2742"/><rect x="770" y="158" width="122" height="27" rx="7" fill="none" stroke="#3a4a75" stroke-width="1"/><text x="779" y="175.5" font-size="10.5" fill="#93a3c0" font-family="Noto Sans SC" font-weight="700">Aa</text><text x="799" y="176" font-size="12.5" fill="#cdd9f2" font-family="Noto Sans SC">Text #1</text><text x="881" y="177" font-size="15" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><rect x="640" y="206" width="500" height="148" rx="10" fill="#131a2c"/><rect x="640" y="206" width="500" height="148" rx="10" fill="none" stroke="#232c45" stroke-width="1"/><rect x="656" y="220" width="62" height="20" rx="7" fill="#1c2b4d"/><text x="666" y="234" font-size="11" fill="#7aa2ff" font-family="JetBrains Mono">python</text><text x="732" y="234" font-size="11" fill="#5b6a8a" font-family="Noto Sans SC">18 lines</text><path d="M 1016 229 L 1023 222.5 L 1030 229" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="1048" y="222" width="11" height="13" rx="2" fill="none"/><rect x="1048" y="222" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><rect x="1053" y="226" width="11" height="13" rx="2" fill="#131a2c"/><rect x="1053" y="226" width="11" height="13" rx="2" fill="none" stroke="#93a3c0" stroke-width="1"/><path d="M 1086 222.5 L 1079.5 228.5 L 1086 235" stroke="#93a3c0" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><text x="1114" y="234.5" font-size="16" fill="#ff7b7b" font-family="Noto Sans SC" text-anchor="middle">×</text><text x="656" y="266" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono">def analyze(chunks):</text><text x="656" y="290" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono"> total = 0</text><text x="656" y="314" font-size="12.5" fill="#d6deeb" font-family="JetBrains Mono"> return sorted(results)</text><path d="M 590 226 h 20 m -7 -6 l 7 6 l -7 6 M 610 246 h -20 m 7 6 l -7 -6 l 7 -6" stroke="#7aa2ff" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><text x="600" y="284" font-size="11" fill="#5b6a8a" font-family="Noto Sans SC" text-anchor="middle">切换</text><text x="600" y="300" font-size="10.5" fill="#5b6a8a" font-family="Noto Sans SC" text-anchor="middle">live</text><text x="40" y="416" font-size="13.5" fill="#93a3c0" font-family="Noto Sans SC">语言切换即时生效:新粘贴的块用新语言命名,已插入的芯片也会同步改名</text><text x="40" y="438" font-size="12" fill="#5b6a8a" font-family="Noto Sans SC">Switch anytime — new pastes land in the new language, existing chips retitle live</text>
4
+ </svg>
@@ -0,0 +1,20 @@
1
+ // Regenerate the README mockup PNGs from the SVG sources in this folder.
2
+ // Dev-only helper (not loaded by the plugin). Requires: npm i @resvg/resvg-js
3
+ // and the fonts referenced below (apt: fonts-noto-cjk fonts-jetbrains-mono).
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const { Resvg } = require('@resvg/resvg-js')
7
+
8
+ const FONTS = [
9
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
10
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',
11
+ '/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Regular.ttf',
12
+ '/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Bold.ttf',
13
+ ]
14
+ for (const name of ['hero', 'languages', 'workflow']) {
15
+ let src = fs.readFileSync(path.join(__dirname, name + '.svg'), 'utf8')
16
+ src = src.split('font-family="Noto Sans SC"').join('font-family="Noto Sans CJK SC"')
17
+ const r = new Resvg(src, { fitTo: { mode: 'zoom', value: 2 }, font: { fontFiles: FONTS, loadSystemFonts: false, defaultFontFamily: 'Noto Sans CJK SC', sansSerifFamily: 'Noto Sans CJK SC', monospaceFamily: 'JetBrains Mono' } })
18
+ fs.writeFileSync(path.join(__dirname, name + '.png'), r.render().asPng())
19
+ console.log('rendered', name + '.png')
20
+ }
Binary file
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="360" viewBox="0 0 1200 360">
2
+ <rect width="1200" height="360" fill="#0d1220"/>
3
+ <text x="40" y="52" font-size="22" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">使用只要四步 · Four steps, zero noise</text><rect x="40" y="92" width="247" height="190" rx="14" fill="#151c2f"/><rect x="40" y="92" width="247" height="190" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><circle cx="74" cy="132" r="17" fill="#3b5bdb"/><text x="74" y="138" font-size="17" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">1</text><text x="102" y="139" font-size="18" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">复制</text><rect x="196" y="118" width="26" height="30" rx="4" fill="none"/><rect x="196" y="118" width="26" height="30" rx="4" fill="none" stroke="#7aa2ff" stroke-width="1"/><rect x="202" y="113" width="14" height="10" rx="3" fill="#151c2f"/><rect x="202" y="113" width="14" height="10" rx="3" fill="none" stroke="#7aa2ff" stroke-width="1"/><line x1="202" y1="127" x2="216" y2="127" stroke="#93a3c0" stroke-width="1.5"/><line x1="202" y1="134" x2="216" y2="134" stroke="#93a3c0" stroke-width="1.5"/><line x1="202" y1="141" x2="212" y2="141" stroke="#93a3c0" stroke-width="1.5"/><text x="62" y="186" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">在任意应用里复制一段代码或长文本</text><text x="62" y="226" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">Copy code or long text anywhere</text><path d="M 295 187 h 60 m -10 -7 l 10 7 l -10 7" stroke="#5b6a8a" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="331" y="92" width="247" height="190" rx="14" fill="#151c2f"/><rect x="331" y="92" width="247" height="190" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><circle cx="365" cy="132" r="17" fill="#3b5bdb"/><text x="365" y="138" font-size="17" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">2</text><text x="393" y="139" font-size="18" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">粘贴</text><rect x="481" y="122" width="44" height="24" rx="6" fill="#0b1120"/><rect x="481" y="122" width="44" height="24" rx="6" fill="none" stroke="#232c45" stroke-width="1"/><line x1="489" y1="129" x2="489" y2="139" stroke="#7aa2ff" stroke-width="2"/><text x="353" y="186" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">粘到 DSH 输入框(Ctrl+V</text><text x="353" y="206" font-size="13" fill="#93a3c0" font-family="Noto Sans SC"> / 长按)</text><text x="353" y="246" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">Paste into the DSH composer</text><path d="M 586 187 h 60 m -10 -7 l 10 7 l -10 7" stroke="#5b6a8a" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="622" y="92" width="247" height="190" rx="14" fill="#151c2f"/><rect x="622" y="92" width="247" height="190" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><circle cx="656" cy="132" r="17" fill="#3b5bdb"/><text x="656" y="138" font-size="17" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">3</text><text x="684" y="139" font-size="18" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">整理</text><rect x="772" y="122" width="44" height="24" rx="12" fill="#1d2742"/><rect x="772" y="122" width="44" height="24" rx="12" fill="none" stroke="#3a4a75" stroke-width="1"/><text x="780" y="139" font-size="11" fill="#d6deeb" font-family="Noto Sans SC">Aa</text><text x="806" y="140" font-size="13" fill="#ff7b7b" font-family="Noto Sans SC">×</text><text x="644" y="186" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">自动收成卡片:点芯片看详情,× 删</text><text x="644" y="206" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">除</text><text x="644" y="246" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">Auto card: open · edit · remove ×</text><path d="M 877 187 h 60 m -10 -7 l 10 7 l -10 7" stroke="#5b6a8a" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="913" y="92" width="247" height="190" rx="14" fill="#151c2f"/><rect x="913" y="92" width="247" height="190" rx="14" fill="none" stroke="#2a3450" stroke-width="1"/><circle cx="947" cy="132" r="17" fill="#3b5bdb"/><text x="947" y="138" font-size="17" fill="#fff" font-family="Noto Sans SC" text-anchor="middle">4</text><text x="975" y="139" font-size="18" fill="#e6ecf8" font-family="Noto Sans SC" font-weight="700">发送</text><path d="M 1063 134 L 1103 120 L 1089 142 L 1083 134 Z" stroke="#7aa2ff" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/><text x="935" y="186" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">还原为 ```lang 围栏代码块</text><text x="935" y="206" font-size="13" fill="#93a3c0" font-family="Noto Sans SC">发出</text><text x="935" y="246" font-size="11.5" fill="#5b6a8a" font-family="Noto Sans SC">Sent back as fenced code</text><text x="40" y="326" font-size="12.5" fill="#5b6a8a" font-family="Noto Sans SC">代码 / Markdown / 普通长文本自动识别;短普通文本照常粘贴,不打扰</text>
4
+ </svg>