dsh-plugin-infj-pet 1.0.4 → 1.0.9

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
@@ -3,6 +3,91 @@
3
3
  All notable changes to this plugin are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 1.0.9
7
+
8
+ **Fix: the character was drawn cropped**
9
+
10
+ The element frames the artwork, but the background offset subtracted only the
11
+ frame origin and not where the artwork sits inside its cell. Every frame was
12
+ therefore drawn `art.x` too far right — 37 source pixels, 74 at 2× — and the
13
+ excess fell outside the element. At the default size roughly half the character
14
+ was cut off.
15
+
16
+ The bug had been there since the sheet was adopted. The original 12 px margin on
17
+ the element absorbed exactly that much of it, so it read as a slightly off-centre
18
+ character rather than a broken one; removing the margin in 1.0.5 to fix the
19
+ alignment exposed it.
20
+
21
+ A test now checks every frame of every animation against the element that draws
22
+ it, so a frame that would be cropped fails the suite rather than reaching a
23
+ release.
24
+
25
+ ## 1.0.8
26
+
27
+ - The default resting place is right-anchored again, 96px from the right and 40px
28
+ up from the bottom. 1.0.7's top-left corner put the sage under the shell's left
29
+ sidebar, which is about 264px wide, so only its right-hand pixels showed.
30
+ - The reason is worth recording: the companion is positioned against the
31
+ **viewport**, so it cannot see the shell's layout. A left inset lands under the
32
+ sidebar and a top inset under the window's title strip; the right edge is the
33
+ one side that is always clear.
34
+ - The layout version moved to 3, so the corrected default reaches anyone who had
35
+ already dragged the sage out of the 1.0.7 corner.
36
+
37
+ ## 1.0.7
38
+
39
+ - The sage rests in the **top-left** corner. It floats above the shell, so it
40
+ overlaps the sidebar rather than being hidden behind it.
41
+ - A changed default position now reaches people who have dragged the sage before.
42
+ A remembered position beats the default, which is right while someone is
43
+ arranging their own desktop — but it also meant a corrected default was never
44
+ seen. The saved record now carries the layout it belongs to, and a position
45
+ saved under an older layout is ignored once.
46
+ - Two drag tests were anchored to the old bottom-right start and are rewritten to
47
+ derive from the viewport, so a future default change will not need them edited.
48
+
49
+ ## 1.0.6
50
+
51
+ - The resting position sits further from the right edge. Removing the box's
52
+ internal slack in 1.0.5 left the sage flush against the window edge, which read
53
+ as too far right; the corner inset is now wider than the bottom's.
54
+ - The speech bubble is a quarter shorter — 1.875× the character's width instead
55
+ of 2.5×, so about 219 px small, 439 px medium and 658 px large. The 1.0.5
56
+ widening overshot; this keeps the comfortable wrap without the long measure.
57
+ - The README is the project owner's revision.
58
+
59
+ ## 1.0.5
60
+
61
+ **The character and its speech bubble line up**
62
+
63
+ - The box that carries the character was not the size of the character: it
64
+ reserved a ground gap as height while the sprite pushed itself up by the same
65
+ amount as a bottom margin. The visible pixels therefore sat above the box's
66
+ bottom edge, so the sage looked off-centre while the bubble, anchored to the
67
+ box, sat opposite it. The box is now exactly the artwork, and the ground
68
+ clearance is padding on the container — the drawing and the box agree, so the
69
+ speech is centred on the character at every size.
70
+ - The five pixels of horizontal margin that widened the box have gone for the
71
+ same reason.
72
+
73
+ **The bubble is sized to the character**
74
+
75
+ - The bubble was a fixed 250 px at every scale, which crowded a large sage and
76
+ wasted space on a small one. It is now two and a half times the character's own
77
+ width: about 292 px small, 585 px medium, 878 px large, still clamped to the
78
+ viewport so it cannot overflow a narrow window.
79
+
80
+ **Citations name the book**
81
+
82
+ - Quotations are attributed by title only — 《务虚笔记》, 《我与地坛》,
83
+ 《病隙碎笔》, 《命若琴弦》 — because the title is what a reader needs in order
84
+ to find the passage. The author's name no longer appears in the interface; it
85
+ remains in the licence notice, where attribution belongs.
86
+ - The reading line supplied by the project owner is now attributed to
87
+ 《务虚笔记》, which resolves the source that 1.0.2 had recorded as unidentified.
88
+ The edition is still unpinned and the README says so rather than implying more
89
+ certainty than exists.
90
+
6
91
  ## 1.0.4
7
92
 
8
93
  **Fix: the plugin never activated**
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # INFJ Sage · 贤者桌宠
1
+ # INFJ Sage · 老绿
2
2
 
3
- **A pixel-art INFJ companion that lives inside your DeepSeek Harness window.**
3
+ **A pixel-art INFJ-A companion that lives inside your DeepSeek Harness window.**
4
4
 
5
5
  **Just here to accompany Qiangwei.**
6
6
 
7
- 一只住在 DSH 窗口里的 INFJ 贤者:陪你专注、等你决定,也陪你安静地打个盹。
7
+ 这是绿老头,不是brother:陪你专注、等你决定,也陪你安静地打个盹。
8
8
 
9
9
  <p align="center">
10
10
  <img src="assets/icon.png" width="120" alt="INFJ Sage icon">
@@ -23,61 +23,64 @@ card.
23
23
  While you work it sits and reads, turning its pages. It only runs when you pick
24
24
  it up.
25
25
 
26
- ---
26
+ \---
27
27
 
28
28
  ## What it does
29
29
 
30
- | Pose | When it appears | Trait it expresses |
31
- |---|---|---|
32
- | 静观 Observing | Nothing is running | 独处即充电 — recharged alone |
33
- | 阅读 Reading | At least one session is working, or a watched turn just finished | 先看模式,再看任务 — patterns before tasks |
34
- | 共情 Empathy | A session is waiting on your answer | 先接住情绪 — feelings first |
35
- | 充电 Recharging | Idle past the nap delay | 独处即充电 — the quiet half |
36
- | 受挫 Setback | A turn ended in an error | 过载时向内自责 — overload turns inward |
37
- | 带我去哪儿 Carried | While you are dragging it | 理想主义的续航 — carried, not chasing |
30
+ |Pose|When it appears|Trait it expresses|
31
+ |-|-|-|
32
+ |静观 Observing|Nothing is running|独处即充电 — recharged alone|
33
+ |阅读 Reading|At least one session is working, or a watched turn just finished|先看模式,再看任务 — patterns before tasks|
34
+ |共情 Empathy|A session is waiting on your answer|先接住情绪 — feelings first|
35
+ |充电 Recharging|Idle past the nap delay|独处即充电 — the quiet half|
36
+ |受挫 Setback|A turn ended in an error|过载时向内自责 — overload turns inward|
37
+ |带我去哪儿 Carried|While you are dragging it|理想主义的续航 — carried, not chasing|
38
38
 
39
39
  Around that core:
40
40
 
41
- - **Click** the sage and it says something in character.
42
- - **Drag** it anywhere; it snaps to the edge and remembers where you left it.
43
- - **It reads while you work** — an open book, eyes following the page, a quiet
44
- page turn. It never paces about on its own.
45
- - **It runs only when carried** — pick it up and it runs in the direction you
46
- take it, turning around at the screen edge so it never looks like it is
47
- running out of the window.
48
- - **Right-click** for settings: session scope, size, nap delay, palette, motion,
49
- idle glances, auto-tips, position reset, tuck away, and a trait panel.
50
- - **Idle glances** — while resting, the sage looks around using the sheet's
51
- sixteen drawn head poses.
52
- - Follows DSH's language, light/dark theme, and reduce-motion preference.
53
- - No build step to install, no runtime dependencies, no network calls, no model
54
- calls.
41
+ * **Click** the sage and it says something in character.
42
+ * **Drag** it anywhere; it snaps to the edge and remembers where you left it.
43
+ * **It reads while you work** — an open book, eyes following the page, a quiet
44
+ page turn. It never paces about on its own.
45
+ * **It runs only when carried** — pick it up and it runs in the direction you
46
+ take it, turning around at the screen edge so it never looks like it is
47
+ running out of the window.
48
+ * **Right-click** for settings: session scope, size, nap delay, palette, motion,
49
+ idle glances, auto-tips, position reset, tuck away, and a trait panel.
50
+ * **Idle glances** — while resting, the sage looks around using the sheet's
51
+ sixteen drawn head poses.
52
+ * Follows DSH's language, light/dark theme, and reduce-motion preference.
53
+ * No build step to install, no runtime dependencies, no network calls, no model
54
+ calls.
55
55
 
56
56
  This is an **in-window** companion, not an operating-system desktop overlay.
57
57
 
58
- ### Words borrowed from 史铁生 (Shi Tiesheng)
58
+ ### Quoted lines
59
59
 
60
- Several of the reading lines are his, and the sage is careful to say so: a
61
- quoted line renders its attribution directly under the text, in a smaller line,
62
- never as if the sage had written it.
60
+ Several reading lines are short quotations, attributed in the interface under the
61
+ line they belong to — the speech bubble cites the book, and this page records the
62
+ full source. They are brief, individually attributed, and used in a
63
+ non-commercial companion; the project claims no rights in them.
63
64
 
64
- | Line | Source |
65
- |---|---|
66
- | 如果我进来,走进你独处的时间……我只是来给你的窗上装好玻璃,冬天的风就要来了。 | 史铁生 — supplied by the project owner; publication not yet identified |
67
- | 死是一件不必急于求成的事。 | 史铁生《我与地坛》 |
68
- | 太阳,他每时每刻都是夕阳也都是旭日。 | 史铁生《我与地坛》 |
69
- | 我常觉得这中间有着宿命的味道。 | 史铁生《我与地坛》 |
65
+ |Line|Source|
66
+ |-|-|
67
+ |如果我进来,走进你独处的时间……我只是来给你的窗上装好玻璃,冬天的风就要来了。|《务虚笔记》 — named by the project owner; edition not yet pinned|
68
+ |死是一件不必急于求成的事。|《我与地坛》|
69
+ |太阳,他每时每刻都是夕阳也都是旭日。|《我与地坛》|
70
+ |我常觉得这中间有着宿命的味道。|《我与地坛》|
70
71
 
71
- Where a line could not be verified against a published source, this project says
72
- so rather than presenting a guess as a quotation — the first row above is
73
- recorded as unverified for exactly that reason. Titles are referenced for the
74
- remaining states; no passage is reproduced beyond the short lines listed here.
72
+ The sage cites the book, not the author's name: the title is what a reader needs
73
+ to find the passage, and it keeps the attribution to a fact rather than a
74
+ byline. Where a line could not be checked against a published edition, this
75
+ project says so — the first row above is recorded that way rather than being
76
+ presented as confirmed. Titles are referenced for the remaining states; no
77
+ passage is reproduced beyond the short lines listed here.
75
78
 
76
- ---
79
+ \---
77
80
 
78
81
  ## Install
79
82
 
80
- ### 1. From npm
83
+ ### 1\. From npm
81
84
 
82
85
  Package page: [dsh-plugin-infj-pet on npm](https://www.npmjs.com/package/dsh-plugin-infj-pet).
83
86
 
@@ -89,22 +92,22 @@ dsh-plugin-infj-pet
89
92
 
90
93
  To pin this release, use `dsh-plugin-infj-pet@1.0.1`.
91
94
 
92
- ### 2. From GitHub
95
+ ### 2\. From GitHub
93
96
 
94
97
  ```
95
- github:CN-Mg/dsh_infj_pet
98
+ github:CN-Mg/dsh\_infj\_pet
96
99
  ```
97
100
 
98
101
  Or with a pinned tag:
99
102
 
100
103
  ```
101
- github:CN-Mg/dsh_infj_pet#v1.0.1
104
+ github:CN-Mg/dsh\_infj\_pet#v1.0.1
102
105
  ```
103
106
 
104
107
  The committed source already contains the built client bundle, so a Git install
105
108
  needs no build step.
106
109
 
107
- ### 3. From a local folder
110
+ ### 3\. From a local folder
108
111
 
109
112
  Point the installer at the absolute path of this directory, or pack it first:
110
113
 
@@ -117,21 +120,21 @@ may not quit the application, and the browser bundle is only picked up on boot.
117
120
 
118
121
  To remove it: disable the row on the Plugins page, then uninstall the package.
119
122
 
120
- ---
123
+ \---
121
124
 
122
125
  ## Using it
123
126
 
124
- - **Click** — a line of dialogue, different each time.
125
- - **Drag** — reposition; release near a side and it snaps to that edge.
126
- - **Right-click** — settings and pose preview.
127
- - **Keyboard** — `Tab` to the sage, `Enter`/`Space` to poke, `Esc` to close.
128
- - **Session scope** — *All sessions* counts ordinary sessions on the current
129
- Host; *This session* follows only the one you are looking at. Child (subagent)
130
- sessions are never counted as your sessions, so a delegated burst does not look
131
- like your own work.
132
- - **Nap delay** — how long the sage stays quiet before it dozes off.
133
- - **Size** — 1×, 2×, or 3× the sprite's own pixels. Every size is a whole-number
134
- multiple, because pixel art resampled at a fractional scale turns to mush.
127
+ * **Click** — a line of dialogue, different each time.
128
+ * **Drag** — reposition; release near a side and it snaps to that edge.
129
+ * **Right-click** — settings and pose preview.
130
+ * **Keyboard** — `Tab` to the sage, `Enter`/`Space` to poke, `Esc` to close.
131
+ * **Session scope** — *All sessions* counts ordinary sessions on the current
132
+ Host; *This session* follows only the one you are looking at. Child (subagent)
133
+ sessions are never counted as your sessions, so a delegated burst does not look
134
+ like your own work.
135
+ * **Nap delay** — how long the sage stays quiet before it dozes off.
136
+ * **Size** — 1×, 2×, or 3× the sprite's own pixels. Every size is a whole-number
137
+ multiple, because pixel art resampled at a fractional scale turns to mush.
135
138
 
136
139
  ### Completion is never guessed
137
140
 
@@ -140,32 +143,32 @@ watched running**. A page that loads into an already-idle Host, a dropped
140
143
  connection, or a session that simply disappears never produces a celebration.
141
144
  Cancelled and failed turns are not successes.
142
145
 
143
- ---
146
+ \---
144
147
 
145
148
  ## Privacy and runtime boundaries
146
149
 
147
- - Reads session **identity and status** only — never message content, tool
148
- arguments, or transcripts.
149
- - Makes **no model calls**, opens **no ports**, sends **no telemetry**, and
150
- answers **no approvals** on your behalf.
151
- - Uses DSH's existing authenticated connection.
152
- - The sprite sheet is embedded in the bundle, so the plugin issues **no network
153
- request at all**.
154
- - Preferences live in `localStorage` under `dsh-plugin-infj-pet:v1`.
155
- - Writes only to its own DOM nodes and to one `<style>` element it owns.
156
- - Does not modify DSH itself, and replaces no official component.
150
+ * Reads session **identity and status** only — never message content, tool
151
+ arguments, or transcripts.
152
+ * Makes **no model calls**, opens **no ports**, sends **no telemetry**, and
153
+ answers **no approvals** on your behalf.
154
+ * Uses DSH's existing authenticated connection.
155
+ * The sprite sheet is embedded in the bundle, so the plugin issues **no network
156
+ request at all**.
157
+ * Preferences live in `localStorage` under `dsh-plugin-infj-pet:v1`.
158
+ * Writes only to its own DOM nodes and to one `<style>` element it owns.
159
+ * Does not modify DSH itself, and replaces no official component.
157
160
 
158
- ---
161
+ \---
159
162
 
160
163
  ## Compatibility
161
164
 
162
165
  Developed against **DSH Desktop 0.2.x** using the documented client plugin
163
166
  contract:
164
167
 
165
- - client bundle registered through `window.__ModuleLoader__.load({ id, factory })`
166
- - one `shell.overlay` slot occupant (`id: infj-pet`, `order: 92`)
167
- - injected shares: `slots`, `sessions`, `connection`, `locale`
168
- - only `react` and `react-dom/client` are required from the platform module table
168
+ * client bundle registered through `window.\_\_ModuleLoader\_\_.load({ id, factory })`
169
+ * one `shell.overlay` slot occupant (`id: infj-pet`, `order: 92`)
170
+ * injected shares: `slots`, `sessions`, `connection`, `locale`
171
+ * only `react` and `react-dom/client` are required from the platform module table
169
172
 
170
173
  The Host half declares **no `inject`**, on purpose. A Cordis `inject` entry names a
171
174
  service the plugin waits for, and the entry stays pending until that service is
@@ -178,7 +181,7 @@ DSH is evolving quickly. If a future release changes the slot or store contract,
178
181
  the sage is written to fail quietly: it falls back to a non-reactive pose rather
179
182
  than throwing into your session.
180
183
 
181
- ---
184
+ \---
182
185
 
183
186
  ## Development
184
187
 
@@ -253,36 +256,37 @@ tools/ generators, checker, preview builder, PNG reader
253
256
  tests/ Node test-runner suite with a small React harness
254
257
  ```
255
258
 
256
- `tools/sage_contact.py` and `tools/sage_measure.py` rasterise the sheet for
259
+ `tools/sage\_contact.py` and `tools/sage\_measure.py` rasterise the sheet for
257
260
  review outside DSH; they need Pillow and are development aids only.
258
261
 
259
262
  ### Design notes
260
263
 
261
- - **Fail soft.** A missing slot registry falls back to a direct DOM mount; a
262
- store hook that throws leaves a static but present companion.
263
- - **Never invent success.** The machine requires an observed rising edge before a
264
- falling edge can celebrate.
265
- - **One artwork source.** Frames, gaze cells, the icon and the bundle are all
266
- derived from the sheet, and every derivation is re-checked in CI.
267
- - **Integer pixels only.** Every frame offset and scale factor is a whole number,
268
- asserted by a test, because that is what keeps the drawing crisp.
269
- - **Own your nodes.** No global mutation beyond the plugin's own style element
270
- and its namespaced storage key.
264
+ * **Fail soft.** A missing slot registry falls back to a direct DOM mount; a
265
+ store hook that throws leaves a static but present companion.
266
+ * **Never invent success.** The machine requires an observed rising edge before a
267
+ falling edge can celebrate.
268
+ * **One artwork source.** Frames, gaze cells, the icon and the bundle are all
269
+ derived from the sheet, and every derivation is re-checked in CI.
270
+ * **Integer pixels only.** Every frame offset and scale factor is a whole number,
271
+ asserted by a test, because that is what keeps the drawing crisp.
272
+ * **Own your nodes.** No global mutation beyond the plugin's own style element
273
+ and its namespaced storage key.
271
274
 
272
- ---
275
+ \---
273
276
 
274
277
  ## Credits and licenses
275
278
 
276
- - **Code:** [MIT](LICENSE).
277
- - **Artwork:** [CC BY-NC-SA 4.0](ASSETS-LICENSE.md) — share and adapt
278
- non-commercially with attribution; separate from the code license.
279
- - The character was generated with a built-in image generator from the supplied
280
- INFJ reference card; `Sage/README.md` records the pet's provenance and stable
281
- id, and `Sage/prompts/` holds the production prompts.
282
- - No fonts are bundled; all text uses the platform UI font stack.
283
- - INFJ and *Advocate* / *Counselor* are popular-psychology vocabulary, not a
284
- clinical instrument. This plugin is decorative, assesses no one, and is
285
- affiliated with no personality-assessment publisher.
279
+ * **Code:** [MIT](LICENSE).
280
+ * **Artwork:** [CC BY-NC-SA 4.0](ASSETS-LICENSE.md) — share and adapt
281
+ non-commercially with attribution; separate from the code license.
282
+ * The character was generated with a built-in image generator from the supplied
283
+ INFJ reference card; `Sage/README.md` records the pet's provenance and stable
284
+ id, and `Sage/prompts/` holds the production prompts.
285
+ * No fonts are bundled; all text uses the platform UI font stack.
286
+ * INFJ and *Advocate* / *Counselor* are popular-psychology vocabulary, not a
287
+ clinical instrument. This plugin is decorative, assesses no one, and is
288
+ affiliated with no personality-assessment publisher.
286
289
 
287
290
  This is a personal project, not an official DeepSeek product. It does not
288
291
  represent DeepSeek's views or endorsement.
292
+
package/lib/client.js CHANGED
@@ -35,6 +35,17 @@ window.__ModuleLoader__.load({
35
35
  * ------------------------------------------------------------------ */
36
36
 
37
37
  var STORE_KEY = 'dsh-plugin-infj-pet:v1';
38
+
39
+ /**
40
+ * Bumped whenever the default resting place changes.
41
+ *
42
+ * A remembered position always wins over the default, which is right while the
43
+ * person is arranging their own desktop — but it also means a corrected
44
+ * default would never reach anyone who had ever dragged the sage. Recording
45
+ * the layout version alongside the position lets a default change take effect
46
+ * once, without discarding the position on every reload.
47
+ */
48
+ var LAYOUT_VERSION = 3;
38
49
  var STYLE_ID = 'dsh-plugin-infj-pet-styles';
39
50
  var EDGE_SNAP = 18;
40
51
 
@@ -127,12 +138,12 @@ window.__ModuleLoader__.load({
127
138
  {
128
139
  zh: '死是一件不必急于求成的事。',
129
140
  en: 'Dying is a thing that need not be hurried.',
130
- source: { zh: '史铁生《我与地坛》', en: 'Shi Tiesheng, "The Ditan and I"' }
141
+ source: { zh: '《我与地坛》', en: '"The Ditan and I"' }
131
142
  },
132
143
  {
133
144
  zh: '太阳,他每时每刻都是夕阳也都是旭日。',
134
145
  en: 'The sun is, at every moment, both the setting sun and the rising one.',
135
- source: { zh: '史铁生《我与地坛》', en: 'Shi Tiesheng, "The Ditan and I"' }
146
+ source: { zh: '《我与地坛》', en: '"The Ditan and I"' }
136
147
  },
137
148
  { zh: '我在,不急。', en: 'I am here. No rush.' }
138
149
  ],
@@ -142,7 +153,7 @@ window.__ModuleLoader__.load({
142
153
  {
143
154
  zh: '病隙里也有碎笔。',
144
155
  en: 'Fragments get written in the gaps between being ill.',
145
- source: { zh: '史铁生《病隙碎笔》', en: 'Shi Tiesheng, "Notes Written in the Gaps"' }
156
+ source: { zh: '《病隙碎笔》', en: '"Notes Written in the Gaps"' }
146
157
  }
147
158
  ],
148
159
  waiting: [
@@ -154,24 +165,24 @@ window.__ModuleLoader__.load({
154
165
  {
155
166
  zh: '命若琴弦,先别急着弹断它。',
156
167
  en: 'Life is like a string on the qin — do not rush to play it through.',
157
- source: { zh: '史铁生《命若琴弦》', en: 'Shi Tiesheng, "Life Is Like a String"' }
168
+ source: { zh: '《命若琴弦》', en: '"Life Is Like a String"' }
158
169
  }
159
170
  ],
160
171
  reading: [
161
172
  {
162
173
  zh: '如果我进来,走进你独处的时间,你只管你的沉思默想,不不,你不要慌忙起来,对,你想怎样待着就怎样待着,我只是来给你的窗上装好玻璃,冬天的风就要来了。',
163
174
  en: 'If I come in, into your private hour — go on with your thinking, no, do not get up in a hurry, yes, stay however you like. I have only come to fit the glass in your window. The winter wind is on its way.',
164
- source: { zh: '史铁生', en: 'Shi Tiesheng' }
175
+ source: { zh: '《务虚笔记》', en: '"Notes on a Fiction"' }
165
176
  },
166
177
  {
167
178
  zh: '太阳,他每时每刻都是夕阳也都是旭日。',
168
179
  en: 'The sun is, at every moment, both the setting sun and the rising one.',
169
- source: { zh: '史铁生《我与地坛》', en: 'Shi Tiesheng, "The Ditan and I"' }
180
+ source: { zh: '《我与地坛》', en: '"The Ditan and I"' }
170
181
  },
171
182
  {
172
183
  zh: '死是一件不必急于求成的事。',
173
184
  en: 'Dying is a thing that need not be hurried.',
174
- source: { zh: '史铁生《我与地坛》', en: 'Shi Tiesheng, "The Ditan and I"' }
185
+ source: { zh: '《我与地坛》', en: '"The Ditan and I"' }
175
186
  }
176
187
  ],
177
188
  sleeping: [
@@ -185,7 +196,7 @@ window.__ModuleLoader__.load({
185
196
  {
186
197
  zh: '我常觉得这中间有着宿命的味道。',
187
198
  en: 'I have often felt there was something of fate in all of it.',
188
- source: { zh: '史铁生《我与地坛》', en: 'Shi Tiesheng, "The Ditan and I"' }
199
+ source: { zh: '《我与地坛》', en: '"The Ditan and I"' }
189
200
  }
190
201
  ]
191
202
  };
@@ -454,18 +465,26 @@ window.__ModuleLoader__.load({
454
465
 
455
466
  var scale = opts.scale || 1;
456
467
  var art = SP.stage.art;
457
- var marginX = 12;
458
- var marginY = 8;
459
- // The box frames the artwork rather than the whole cell, so the sage is not
460
- // surrounded by the sheet's empty margin. Everything is an integer multiple
461
- // of the source pixels so nothing is resampled.
468
+ // The box is exactly the artwork, with no slop: any margin here would sit
469
+ // between the visible pixels and the box edge, and the bubble is anchored to
470
+ // the box, so the character would look offset from its own speech. The sheet's
471
+ // empty cell margin is already trimmed by using the measured art box, and
472
+ // everything stays an integer multiple of the source pixels so nothing is
473
+ // resampled. The little clearance under the feet is CSS padding on the
474
+ // container, which keeps the layout and the drawing in agreement.
475
+ //
476
+ // The background must be positioned by the artwork's origin, not the frame's:
477
+ // the artwork sits art.x/art.y into its cell, and the element shows only
478
+ // art.width x art.height. Subtracting the frame origin alone draws every
479
+ // frame art.x too far right and crops the excess off the element.
462
480
  return {
463
- width: (art.width + marginX) * scale,
464
- height: (art.height + marginY) * scale,
481
+ width: art.width * scale,
482
+ height: art.height * scale,
465
483
  backgroundSize: SP.sheet.width * scale + 'px ' + SP.sheet.height * scale + 'px',
466
- backgroundPosition: -Math.round(cell[0] * scale) + 'px ' + -Math.round(cell[1] * scale) + 'px',
467
- // How far the artwork's ground line sits above the box's bottom edge.
468
- bottomInset: (SP.stage.ground - (art.y + art.height) + marginY) * scale
484
+ backgroundPosition:
485
+ -Math.round((cell[0] + art.x) * scale) + 'px ' + -Math.round((cell[1] + art.y) * scale) + 'px',
486
+ // The ground line is the artwork's last row, so nothing hangs below it.
487
+ groundGap: (SP.stage.ground - (art.y + art.height)) * scale
469
488
  };
470
489
  }
471
490
 
@@ -712,7 +731,11 @@ window.__ModuleLoader__.load({
712
731
  '.infj-sprite{image-rendering:pixelated;image-rendering:crisp-edges;background-repeat:no-repeat;',
713
732
  'transform-origin:bottom center;transition:transform .16s ease-out}',
714
733
  '.infj-pet.is-poked .infj-sprite{transform:scale(1.06)}',
715
- '.infj-bubble{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);max-width:250px;',
734
+ // The bubble is anchored to the box, so its width is derived from the box
735
+ // rather than fixed: a larger sage gets a proportionally larger bubble, and
736
+ // the wrap stays comfortable at every size.
737
+ '.infj-bubble{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);',
738
+ 'box-sizing:border-box;width:calc(var(--s-bubble) * 1.875);max-width:min(88vw,var(--s-bubble) * 1.875);',
716
739
  'min-width:72px;padding:8px 12px;border-radius:14px;background:var(--s-panel);color:var(--s-ink);',
717
740
  'font-size:12.5px;font-weight:500;box-shadow:0 6px 22px rgba(16,34,16,0.18),0 1px 0 rgba(255,255,255,0.5) inset;',
718
741
  'border:1px solid var(--s-chip);pointer-events:none;animation:infj-bubble-in .22s ease-out;text-align:center;',
@@ -849,8 +872,16 @@ window.__ModuleLoader__.load({
849
872
  motion: saved.motion === 'on' || saved.motion === 'off' ? saved.motion : 'auto',
850
873
  hidden: saved.hidden === true,
851
874
  theme: saved.theme === 'light' || saved.theme === 'dark' ? saved.theme : null,
852
- x: typeof saved.x === 'number' && Number.isFinite(saved.x) ? saved.x : null,
853
- y: typeof saved.y === 'number' && Number.isFinite(saved.y) ? saved.y : null,
875
+ // A position saved under an older layout is ignored once, so a corrected
876
+ // default is actually seen.
877
+ x:
878
+ saved.layoutVersion === LAYOUT_VERSION && typeof saved.x === 'number' && Number.isFinite(saved.x)
879
+ ? saved.x
880
+ : null,
881
+ y:
882
+ saved.layoutVersion === LAYOUT_VERSION && typeof saved.y === 'number' && Number.isFinite(saved.y)
883
+ ? saved.y
884
+ : null,
854
885
  language: normalizeLanguage(saved.language)
855
886
  };
856
887
  }
@@ -1002,8 +1033,11 @@ window.__ModuleLoader__.load({
1002
1033
  }) || SIZES[1];
1003
1034
  var scale = size.scale;
1004
1035
  var art = SP.stage.art;
1005
- var boxWidth = (art.width + 12) * scale;
1006
- var boxHeight = (art.height + 8) * scale;
1036
+ // The box is the artwork itself; the sheet's own ground gap becomes padding
1037
+ // below it so the character is centred both ways inside its own box.
1038
+ var groundGap = (SP.stage.ground - (art.y + art.height)) * scale;
1039
+ var boxWidth = art.width * scale;
1040
+ var boxHeight = art.height * scale + groundGap;
1007
1041
 
1008
1042
  var rootRef = React.useRef(null);
1009
1043
  var dragRef = React.useRef(null);
@@ -1232,7 +1266,7 @@ window.__ModuleLoader__.load({
1232
1266
  y: Math.round(clamp(box.y, 0, limit.maxY))
1233
1267
  };
1234
1268
  setPosition(next);
1235
- store.write({ x: next.x, y: next.y });
1269
+ store.write({ x: next.x, y: next.y, layoutVersion: LAYOUT_VERSION });
1236
1270
  }
1237
1271
 
1238
1272
  function onContextMenu(event) {
@@ -1284,10 +1318,20 @@ window.__ModuleLoader__.load({
1284
1318
  var style = {
1285
1319
  width: boxWidth + 'px',
1286
1320
  height: boxHeight + 'px',
1321
+ // The bubble derives its width from the character's own size, so a
1322
+ // larger sage gets a proportionally larger bubble instead of the same
1323
+ // cramped box at every scale.
1324
+ '--s-bubble': boxWidth + 'px',
1325
+ paddingBottom: groundGap + 'px',
1326
+ // Anchored right, and up from the bottom. The companion is positioned
1327
+ // against the viewport, so it cannot see the shell's layout: a left inset
1328
+ // puts it under the sidebar (about 264px wide) and a top inset under the
1329
+ // title strip. The right edge is the one side that is always free, and
1330
+ // the insets keep it off the corner itself.
1287
1331
  left: position.x == null ? undefined : position.x + 'px',
1288
1332
  top: position.y == null ? undefined : position.y + 'px',
1289
- right: position.x == null ? '24px' : undefined,
1290
- bottom: position.x == null ? '24px' : undefined
1333
+ right: position.x == null ? '96px' : undefined,
1334
+ bottom: position.x == null ? '40px' : undefined
1291
1335
  };
1292
1336
 
1293
1337
  var children = [];
@@ -1344,8 +1388,7 @@ window.__ModuleLoader__.load({
1344
1388
  backgroundSize: spec.backgroundSize,
1345
1389
  backgroundPosition: spec.backgroundPosition,
1346
1390
  width: spec.width + 'px',
1347
- height: spec.height + 'px',
1348
- marginBottom: spec.bottomInset + 'px'
1391
+ height: spec.height + 'px'
1349
1392
  }
1350
1393
  })
1351
1394
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-infj-pet",
3
- "version": "1.0.4",
3
+ "version": "1.0.9",
4
4
  "description": "INFJ Sage — a pixel-art Advocate/Counselor companion that lives in your DSH window and reflects what your agents are doing.",
5
5
  "keywords": [
6
6
  "dsh",