@linxin666/dsh-pet 0.1.15 → 0.1.16
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.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +270 -334
- package/lib/client.js.map +1 -1
- package/lib/index.js +641 -87
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -1
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +30 -12
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +62 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +61 -12
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +4 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +11 -4
- package/lib/types/persist.d.ts +10 -3
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +32 -5
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +46 -13
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +73 -16
- package/package.json +2 -2
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +7 -1
- package/src/client/index.ts +36 -14
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +3 -0
- package/src/client/settings-form.ts +114 -12
- package/src/client/spritesheet.ts +16 -92
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +5 -3
- package/src/ledger.ts +11 -4
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +42 -8
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +102 -20
- package/lib/types/client/WhalePet.d.ts.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-pet browser half — mounts the
|
|
3
|
-
* and drives it from the host's same-origin
|
|
4
|
-
* the host snapshot (~2 s),
|
|
5
|
-
* The pet is host-global (no
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* dsh-pet browser half — mounts the selected pet as a global floating
|
|
3
|
+
* surface and drives it from the host's same-origin '/api/pet/*' JSON
|
|
4
|
+
* endpoints: fetch the registry list once, poll the host snapshot (~2 s),
|
|
5
|
+
* forward interactions, persist drag positions. The pet is host-global (no
|
|
6
|
+
* session dimension), so it mounts directly onto 'document.body' via a
|
|
7
|
+
* single React root rather than a session-scoped slot — on the
|
|
8
|
+
* new-conversation screen no session exists, and a dock-mounted pet would
|
|
9
|
+
* vanish there (issue #48). When the pet is hidden the entry becomes a
|
|
9
10
|
* fixed-position summon button.
|
|
10
11
|
* @module @linxin666/dsh-pet/client
|
|
11
12
|
*/
|
|
@@ -14,13 +15,15 @@ import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-
|
|
|
14
15
|
export declare const inject: string[];
|
|
15
16
|
/** Re-exported for consumers that type against the injected face. */
|
|
16
17
|
export type { PetInjected, PetDockEntryProps } from './PetDockEntry.tsx';
|
|
18
|
+
export type { PetSpriteProps } from './PetSprite.tsx';
|
|
17
19
|
export type { PetUiState, PetFeedback } from './pet-store.ts';
|
|
18
20
|
export type { PetSettingsCardFace, PetSettingsCardState } from './PetSettingsCard.tsx';
|
|
21
|
+
export type { PetDefinition } from '../registry.ts';
|
|
19
22
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
20
23
|
interface SlotMap {
|
|
21
24
|
/**
|
|
22
25
|
* The child slot the Web UI plugin group declares; this card registers
|
|
23
|
-
* into the group instead of the top-level
|
|
26
|
+
* into the group instead of the top-level 'settings.plugin.item' list.
|
|
24
27
|
* Spelled here with the same shape so this package can register without
|
|
25
28
|
* depending on the sibling UI package.
|
|
26
29
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6D7G,yBAAyB;AACzB,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,sEAAsE;AACtE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmL9C"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-pet browser half — mounts the
|
|
3
|
-
* and drives it from the host's same-origin
|
|
4
|
-
* the host snapshot (~2 s),
|
|
5
|
-
* The pet is host-global (no
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* dsh-pet browser half — mounts the selected pet as a global floating
|
|
3
|
+
* surface and drives it from the host's same-origin '/api/pet/*' JSON
|
|
4
|
+
* endpoints: fetch the registry list once, poll the host snapshot (~2 s),
|
|
5
|
+
* forward interactions, persist drag positions. The pet is host-global (no
|
|
6
|
+
* session dimension), so it mounts directly onto 'document.body' via a
|
|
7
|
+
* single React root rather than a session-scoped slot — on the
|
|
8
|
+
* new-conversation screen no session exists, and a dock-mounted pet would
|
|
9
|
+
* vanish there (issue #48). When the pet is hidden the entry becomes a
|
|
9
10
|
* fixed-position summon button.
|
|
10
11
|
* @module @linxin666/dsh-pet/client
|
|
11
12
|
*/
|
|
@@ -25,17 +26,19 @@ async function petFetch(path, body) {
|
|
|
25
26
|
body: JSON.stringify(body),
|
|
26
27
|
});
|
|
27
28
|
if (!response.ok) {
|
|
28
|
-
throw new Error(
|
|
29
|
+
throw new Error('pet ' + path + ' failed: ' + response.status);
|
|
29
30
|
}
|
|
30
31
|
return (await response.json());
|
|
31
32
|
}
|
|
32
33
|
/** The live host API instance (always defined; failures surface per call). */
|
|
33
34
|
const petApi = {
|
|
34
35
|
state: () => petFetch('/api/pet/state'),
|
|
36
|
+
pets: () => petFetch('/api/pet/pets'),
|
|
35
37
|
interact: (kind) => petFetch('/api/pet/interact', { kind }),
|
|
36
38
|
setVisible: (visible) => petFetch('/api/pet/set-visible', { visible }),
|
|
37
39
|
setConfig: (patch) => petFetch('/api/pet/set-config', patch),
|
|
38
40
|
setName: (name) => petFetch('/api/pet/set-name', { name }),
|
|
41
|
+
setPet: (petId) => petFetch('/api/pet/set-pet', { petId }),
|
|
39
42
|
};
|
|
40
43
|
/** Poll interval for the host snapshot. */
|
|
41
44
|
const POLL_MS = 2000;
|
|
@@ -59,8 +62,9 @@ export function apply(ctx) {
|
|
|
59
62
|
? snapshot.value?.enabled ?? true
|
|
60
63
|
: snapshot.status === 'unavailable';
|
|
61
64
|
};
|
|
62
|
-
// Plugin configuration card: one staged form over the
|
|
63
|
-
// namespace, contributed to the Web UI plugin group.
|
|
65
|
+
// Plugin configuration card: one staged form over the 'pet' settings
|
|
66
|
+
// namespace, contributed to the Web UI plugin group. The controller loads
|
|
67
|
+
// the petId choices from the registry endpoint itself.
|
|
64
68
|
const petSettings = new PetSettingsCardController(settingsScope);
|
|
65
69
|
ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
|
|
66
70
|
name: 'web-ui.plugin.item',
|
|
@@ -81,9 +85,23 @@ export function apply(ctx) {
|
|
|
81
85
|
// it stateless on the new-conversation screen (no session to scope by).
|
|
82
86
|
const petStore = createPetStore().create();
|
|
83
87
|
const setSnapshot = petStore.actions.setSnapshot;
|
|
88
|
+
const setPets = petStore.actions.setPets;
|
|
84
89
|
const setState = petStore.actions.setState;
|
|
85
90
|
const setFeedback = petStore.actions.setFeedback;
|
|
91
|
+
// The registry list is fetched lazily with retries baked into the poll
|
|
92
|
+
// cycle: until it lands, the dock entry renders nothing and every 2s
|
|
93
|
+
// tick tries again. After it lands, one list feeds both the sprite and
|
|
94
|
+
// the settings card's choices.
|
|
95
|
+
let petsLoaded = false;
|
|
86
96
|
const pollNow = () => {
|
|
97
|
+
if (!petsLoaded) {
|
|
98
|
+
petApi.pets().then((list) => {
|
|
99
|
+
petsLoaded = true;
|
|
100
|
+
setPets(list);
|
|
101
|
+
}, () => {
|
|
102
|
+
// Retry on the next poll tick.
|
|
103
|
+
});
|
|
104
|
+
}
|
|
87
105
|
petApi.state().then((snapshot) => {
|
|
88
106
|
setSnapshot(snapshot);
|
|
89
107
|
}, () => {
|
|
@@ -187,8 +205,8 @@ export function apply(ctx) {
|
|
|
187
205
|
// for a global floating surface — the dock is session-scoped, so a pet
|
|
188
206
|
// mounted there would vanish on the new-conversation screen (issue #48).
|
|
189
207
|
// The entry therefore mounts straight onto document.body via a single
|
|
190
|
-
// React root for the page lifetime:
|
|
191
|
-
// visible, and the hidden-state summon button is fixed-positioned.
|
|
208
|
+
// React root for the page lifetime: PetSprite portals itself to body
|
|
209
|
+
// when visible, and the hidden-state summon button is fixed-positioned.
|
|
192
210
|
const container = document.createElement('div');
|
|
193
211
|
container.dataset.dshPetRoot = '';
|
|
194
212
|
document.body.appendChild(container);
|
|
@@ -15,10 +15,12 @@ export declare const zh: {
|
|
|
15
15
|
readonly 'pet.rank': "亲密度 {rank}";
|
|
16
16
|
readonly 'pet.points': "{points} 点";
|
|
17
17
|
readonly 'pet.treats': "小鱼干 ×{n}";
|
|
18
|
-
readonly 'pet.state.loading': "
|
|
19
|
-
readonly 'pet.state.error': "
|
|
18
|
+
readonly 'pet.state.loading': "宠物正在赶来…";
|
|
19
|
+
readonly 'pet.state.error': "宠物迷路了(连接失败)";
|
|
20
20
|
readonly 'settings.title': "宠物";
|
|
21
|
-
readonly 'settings.description': "
|
|
21
|
+
readonly 'settings.description': "选择宠物并调整它的显示布局。";
|
|
22
|
+
readonly 'settings.pet': "宠物";
|
|
23
|
+
readonly 'settings.petHint': "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。";
|
|
22
24
|
readonly 'settings.enabled': "启用宠物";
|
|
23
25
|
readonly 'settings.enabledHint': "关闭后隐藏宠物并停止轮询,可在设置里重新启用。";
|
|
24
26
|
readonly 'settings.visible': "显示宠物";
|
|
@@ -29,8 +31,6 @@ export declare const zh: {
|
|
|
29
31
|
readonly 'settings.rightHint': "距视口右边缘的水平内缩距离。";
|
|
30
32
|
readonly 'settings.bottom': "距底部(px)";
|
|
31
33
|
readonly 'settings.bottomHint': "距视口底边的垂直内缩距离。";
|
|
32
|
-
readonly 'settings.name': "名字";
|
|
33
|
-
readonly 'settings.nameHint': "宠物显示名,1–20 个字符。";
|
|
34
34
|
readonly 'settings.inherit': "继承";
|
|
35
35
|
readonly 'settings.on': "开";
|
|
36
36
|
readonly 'settings.off': "关";
|
|
@@ -58,10 +58,12 @@ export declare const en: {
|
|
|
58
58
|
readonly 'pet.rank': "Affinity {rank}";
|
|
59
59
|
readonly 'pet.points': "{points} pts";
|
|
60
60
|
readonly 'pet.treats': "Treats ×{n}";
|
|
61
|
-
readonly 'pet.state.loading': "The
|
|
62
|
-
readonly 'pet.state.error': "The
|
|
61
|
+
readonly 'pet.state.loading': "The pet is on its way…";
|
|
62
|
+
readonly 'pet.state.error': "The pet is lost (connection failed)";
|
|
63
63
|
readonly 'settings.title': "Pet";
|
|
64
|
-
readonly 'settings.description': "
|
|
64
|
+
readonly 'settings.description': "Pick a pet and tune its display layout.";
|
|
65
|
+
readonly 'settings.pet': "Pet";
|
|
66
|
+
readonly 'settings.petHint': "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.";
|
|
65
67
|
readonly 'settings.enabled': "Enable the pet";
|
|
66
68
|
readonly 'settings.enabledHint': "When off, the pet hides and polling stops; re-enable it here.";
|
|
67
69
|
readonly 'settings.visible': "Show the pet";
|
|
@@ -72,8 +74,6 @@ export declare const en: {
|
|
|
72
74
|
readonly 'settings.rightHint': "Horizontal inset from the viewport right edge.";
|
|
73
75
|
readonly 'settings.bottom': "Bottom inset (px)";
|
|
74
76
|
readonly 'settings.bottomHint': "Vertical inset from the viewport bottom edge.";
|
|
75
|
-
readonly 'settings.name': "Name";
|
|
76
|
-
readonly 'settings.nameHint': "The pet’s display name, 1–20 characters.";
|
|
77
77
|
readonly 'settings.inherit': "Inherit";
|
|
78
78
|
readonly 'settings.on': "On";
|
|
79
79
|
readonly 'settings.off': "Off";
|
|
@@ -15,11 +15,13 @@ export const zh = {
|
|
|
15
15
|
'pet.rank': '亲密度 {rank}',
|
|
16
16
|
'pet.points': '{points} 点',
|
|
17
17
|
'pet.treats': '小鱼干 ×{n}',
|
|
18
|
-
'pet.state.loading': '
|
|
19
|
-
'pet.state.error': '
|
|
18
|
+
'pet.state.loading': '宠物正在赶来…',
|
|
19
|
+
'pet.state.error': '宠物迷路了(连接失败)',
|
|
20
20
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
21
21
|
'settings.title': '宠物',
|
|
22
|
-
'settings.description': '
|
|
22
|
+
'settings.description': '选择宠物并调整它的显示布局。',
|
|
23
|
+
'settings.pet': '宠物',
|
|
24
|
+
'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。',
|
|
23
25
|
'settings.enabled': '启用宠物',
|
|
24
26
|
'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。',
|
|
25
27
|
'settings.visible': '显示宠物',
|
|
@@ -30,8 +32,6 @@ export const zh = {
|
|
|
30
32
|
'settings.rightHint': '距视口右边缘的水平内缩距离。',
|
|
31
33
|
'settings.bottom': '距底部(px)',
|
|
32
34
|
'settings.bottomHint': '距视口底边的垂直内缩距离。',
|
|
33
|
-
'settings.name': '名字',
|
|
34
|
-
'settings.nameHint': '宠物显示名,1–20 个字符。',
|
|
35
35
|
'settings.inherit': '继承',
|
|
36
36
|
'settings.on': '开',
|
|
37
37
|
'settings.off': '关',
|
|
@@ -59,11 +59,13 @@ export const en = {
|
|
|
59
59
|
'pet.rank': 'Affinity {rank}',
|
|
60
60
|
'pet.points': '{points} pts',
|
|
61
61
|
'pet.treats': 'Treats ×{n}',
|
|
62
|
-
'pet.state.loading': 'The
|
|
63
|
-
'pet.state.error': 'The
|
|
62
|
+
'pet.state.loading': 'The pet is on its way…',
|
|
63
|
+
'pet.state.error': 'The pet is lost (connection failed)',
|
|
64
64
|
// Plugin settings card (the `settings.plugin.item` seat).
|
|
65
65
|
'settings.title': 'Pet',
|
|
66
|
-
'settings.description': '
|
|
66
|
+
'settings.description': 'Pick a pet and tune its display layout.',
|
|
67
|
+
'settings.pet': 'Pet',
|
|
68
|
+
'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.',
|
|
67
69
|
'settings.enabled': 'Enable the pet',
|
|
68
70
|
'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.',
|
|
69
71
|
'settings.visible': 'Show the pet',
|
|
@@ -74,8 +76,6 @@ export const en = {
|
|
|
74
76
|
'settings.rightHint': 'Horizontal inset from the viewport right edge.',
|
|
75
77
|
'settings.bottom': 'Bottom inset (px)',
|
|
76
78
|
'settings.bottomHint': 'Vertical inset from the viewport bottom edge.',
|
|
77
|
-
'settings.name': 'Name',
|
|
78
|
-
'settings.nameHint': 'The pet\u2019s display name, 1\u201320 characters.',
|
|
79
79
|
'settings.inherit': 'Inherit',
|
|
80
80
|
'settings.on': 'On',
|
|
81
81
|
'settings.off': 'Off',
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
9
|
import type { PetStateView } from '../service.ts';
|
|
10
10
|
import type { PetInteraction } from '../affinity.ts';
|
|
11
|
+
import type { PetDefinition } from '../registry.ts';
|
|
11
12
|
/** One transient reaction bubble on the pet. */
|
|
12
13
|
export interface PetFeedback {
|
|
13
14
|
/** Bubble copy. */
|
|
@@ -21,6 +22,8 @@ export interface PetFeedback {
|
|
|
21
22
|
export interface PetUiState {
|
|
22
23
|
/** Latest host snapshot; null before the first successful fetch. */
|
|
23
24
|
snapshot: PetStateView | null;
|
|
25
|
+
/** The registry list the host serves (atlas URLs + geometry + tracks). */
|
|
26
|
+
pets: PetDefinition[];
|
|
24
27
|
/** Fetch lifecycle. */
|
|
25
28
|
state: 'loading' | 'ready' | 'error';
|
|
26
29
|
/** Transport error message (for the debug surface), when any. */
|
|
@@ -32,6 +35,8 @@ export interface PetUiState {
|
|
|
32
35
|
export type PetUiActions = {
|
|
33
36
|
/** Replace the host snapshot (poll result). */
|
|
34
37
|
setSnapshot: (draft: PetUiState, snapshot: PetStateView) => void;
|
|
38
|
+
/** Replace the registry list. */
|
|
39
|
+
setPets: (draft: PetUiState, pets: PetDefinition[]) => void;
|
|
35
40
|
/** Mark the fetch lifecycle. */
|
|
36
41
|
setState: (draft: PetUiState, state: PetUiState['state'], error: string | null) => void;
|
|
37
42
|
/** Show a reaction bubble. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,uBAAuB;IACvB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAC7B;AAED,uBAAuB;AACvB,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IAChE,iCAAiC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC3D,gCAAgC;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvF,8BAA8B;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CA2B5E;AAED,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA"}
|
|
@@ -11,6 +11,7 @@ export function createPetStore() {
|
|
|
11
11
|
return defineStore({
|
|
12
12
|
init: () => ({
|
|
13
13
|
snapshot: null,
|
|
14
|
+
pets: [],
|
|
14
15
|
state: 'loading',
|
|
15
16
|
error: null,
|
|
16
17
|
feedback: null,
|
|
@@ -21,6 +22,9 @@ export function createPetStore() {
|
|
|
21
22
|
draft.state = 'ready';
|
|
22
23
|
draft.error = null;
|
|
23
24
|
},
|
|
25
|
+
setPets: (draft, pets) => {
|
|
26
|
+
draft.pets = pets;
|
|
27
|
+
},
|
|
24
28
|
setState: (draft, state, error) => {
|
|
25
29
|
draft.state = state;
|
|
26
30
|
draft.error = error;
|
|
@@ -18,6 +18,14 @@ export type FieldWrite = {
|
|
|
18
18
|
export interface FieldSpec {
|
|
19
19
|
/** Field name inside the namespace section. */
|
|
20
20
|
field: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the Host treats this field as a secret and redacts its value from
|
|
23
|
+
* the read-back (role('secret') in the section schema). Redacted secrets are
|
|
24
|
+
* never compared against the draft on save; the field lands when the scope
|
|
25
|
+
* reports the write succeeded (its secret-set marker under the bridge), so
|
|
26
|
+
* a successful secret save is not misreported as failed.
|
|
27
|
+
*/
|
|
28
|
+
secret?: boolean;
|
|
21
29
|
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
22
30
|
format: (value: unknown) => string;
|
|
23
31
|
/**
|
|
@@ -57,6 +65,12 @@ export interface CardShell {
|
|
|
57
65
|
saving: boolean;
|
|
58
66
|
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
59
67
|
failed: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The rejection code/message the Host returned for the last failed save,
|
|
70
|
+
* surfaced next to the generic failure text. Undefined while no save has
|
|
71
|
+
* failed (or the failure carried no server reason).
|
|
72
|
+
*/
|
|
73
|
+
failedReason?: string;
|
|
60
74
|
}
|
|
61
75
|
/** The write actions the card's slot entry injects. */
|
|
62
76
|
export interface CardActions {
|
|
@@ -69,6 +83,37 @@ export interface CardActions {
|
|
|
69
83
|
/** Drop every staged edit. */
|
|
70
84
|
discard: () => void;
|
|
71
85
|
}
|
|
86
|
+
/** One durable write a batched settings scope performs. */
|
|
87
|
+
export interface BatchedWrite {
|
|
88
|
+
/** Field this entry writes. */
|
|
89
|
+
field: string;
|
|
90
|
+
/** set stores a value; unset drops the leaf. */
|
|
91
|
+
op: 'set' | 'unset';
|
|
92
|
+
/** Value for op set (absent for unset). */
|
|
93
|
+
value?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/** Per-field outcome of one batched scope write. */
|
|
96
|
+
export interface BatchedFieldResult {
|
|
97
|
+
/** Field this entry writes. */
|
|
98
|
+
field: string;
|
|
99
|
+
/** Whether the Host accepted this field's write (per the read-back view). */
|
|
100
|
+
landed: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Result of a batched scope write. The bridge scope posts every planned write
|
|
104
|
+
* in one /mutate so the Host validate hook judges baseURL+model together; a
|
|
105
|
+
* batched refusal fails the whole save rather than per-field.
|
|
106
|
+
*/
|
|
107
|
+
export interface BatchResult {
|
|
108
|
+
/** Whether the whole mutate was accepted. */
|
|
109
|
+
ok: boolean;
|
|
110
|
+
/** Per-field success, in the request order (always present when ok). */
|
|
111
|
+
fields: BatchedFieldResult[];
|
|
112
|
+
/** Host rejection code (mutate refused). */
|
|
113
|
+
code?: string;
|
|
114
|
+
/** Host rejection message (mutate refused). */
|
|
115
|
+
message?: string;
|
|
116
|
+
}
|
|
72
117
|
/** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
|
|
73
118
|
export interface NumberConstraints {
|
|
74
119
|
/** The accepted value must be a whole number. */
|
|
@@ -80,6 +125,13 @@ export interface NumberConstraints {
|
|
|
80
125
|
export declare function numberField(field: string, constraints?: NumberConstraints): FieldSpec;
|
|
81
126
|
/** A free-text field. An empty draft clears the field. */
|
|
82
127
|
export declare function textField(field: string): FieldSpec;
|
|
128
|
+
/**
|
|
129
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
130
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
131
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
132
|
+
* reporting the write landed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function secretField(field: string): FieldSpec;
|
|
83
135
|
/** A boolean field, edited through true/false draft text. */
|
|
84
136
|
export declare function booleanField(field: string): FieldSpec;
|
|
85
137
|
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
@@ -99,6 +151,7 @@ export declare class CardForm<T> {
|
|
|
99
151
|
private readonly listeners;
|
|
100
152
|
private saving;
|
|
101
153
|
private failed;
|
|
154
|
+
private failedReason;
|
|
102
155
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
103
156
|
constructor(scope: SettingsScope<T>, specs: FieldSpec[]);
|
|
104
157
|
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
@@ -111,9 +164,18 @@ export declare class CardForm<T> {
|
|
|
111
164
|
actions(): CardActions;
|
|
112
165
|
/**
|
|
113
166
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
167
|
+
*
|
|
168
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
169
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
170
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
171
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
172
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
173
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
114
174
|
* @returns settlement after every write and the read-back.
|
|
115
175
|
*/
|
|
116
176
|
save(): Promise<void>;
|
|
177
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
178
|
+
private batchedScope;
|
|
117
179
|
/**
|
|
118
180
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
119
181
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAyB,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAGjH,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAA;IACb,2FAA2F;IAC3F,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAClC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAChD;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,6 +40,15 @@ export function textField(field) {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
45
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
46
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
47
|
+
* reporting the write landed.
|
|
48
|
+
*/
|
|
49
|
+
export function secretField(field) {
|
|
50
|
+
return { ...textField(field), secret: true };
|
|
51
|
+
}
|
|
43
52
|
/** A boolean field, edited through true/false draft text. */
|
|
44
53
|
export function booleanField(field) {
|
|
45
54
|
return {
|
|
@@ -84,6 +93,7 @@ export class CardForm {
|
|
|
84
93
|
listeners = new Set();
|
|
85
94
|
saving = false;
|
|
86
95
|
failed = false;
|
|
96
|
+
failedReason;
|
|
87
97
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
88
98
|
constructor(scope, specs) {
|
|
89
99
|
this.scope = scope;
|
|
@@ -108,6 +118,7 @@ export class CardForm {
|
|
|
108
118
|
invalid: plan.some(item => item.run === undefined),
|
|
109
119
|
saving: this.saving,
|
|
110
120
|
failed: this.failed,
|
|
121
|
+
...this.failedReason === undefined ? {} : { failedReason: this.failedReason },
|
|
111
122
|
};
|
|
112
123
|
}
|
|
113
124
|
/** Read one field's state from the effective section and its staged draft. */
|
|
@@ -137,37 +148,68 @@ export class CardForm {
|
|
|
137
148
|
return;
|
|
138
149
|
this.staged.clear();
|
|
139
150
|
this.failed = false;
|
|
151
|
+
this.failedReason = undefined;
|
|
140
152
|
this.publish();
|
|
141
153
|
},
|
|
142
154
|
};
|
|
143
155
|
}
|
|
144
156
|
/**
|
|
145
157
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
158
|
+
*
|
|
159
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
160
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
161
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
162
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
163
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
164
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
146
165
|
* @returns settlement after every write and the read-back.
|
|
147
166
|
*/
|
|
148
167
|
async save() {
|
|
149
168
|
const plan = this.plan();
|
|
150
|
-
const
|
|
151
|
-
if (plan.length === 0 || this.saving ||
|
|
169
|
+
const valid = plan.filter(item => item.run !== undefined);
|
|
170
|
+
if (plan.length === 0 || this.saving || valid.length !== plan.length)
|
|
152
171
|
return;
|
|
172
|
+
const plannedWrites = valid.map(item => item.op);
|
|
153
173
|
// Snapshot the fields this save writes, so edits staged while it is in
|
|
154
174
|
// flight survive: only the staged keys this save actually wrote are cleared.
|
|
155
175
|
const fields = new Set(plan.map(item => item.field));
|
|
156
176
|
this.saving = true;
|
|
157
177
|
this.failed = false;
|
|
178
|
+
this.failedReason = undefined;
|
|
158
179
|
this.publish();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
const landed = new Set();
|
|
181
|
+
const batch = this.batchedScope();
|
|
182
|
+
if (batch !== undefined) {
|
|
183
|
+
const result = await batch.mutate(plannedWrites);
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
for (const field of result.fields) {
|
|
186
|
+
if (field.landed)
|
|
187
|
+
landed.add(field.field);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.failedReason = result.message;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
for (const item of valid) {
|
|
196
|
+
if (await item.run())
|
|
197
|
+
landed.add(item.field);
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
for (const field of fields) {
|
|
201
|
+
if (landed.has(field))
|
|
165
202
|
this.staged.delete(field);
|
|
166
203
|
}
|
|
167
204
|
this.saving = false;
|
|
168
|
-
this.failed =
|
|
205
|
+
this.failed = landed.size !== fields.size;
|
|
169
206
|
this.publish();
|
|
170
207
|
}
|
|
208
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
209
|
+
batchedScope() {
|
|
210
|
+
const candidate = this.scope;
|
|
211
|
+
return typeof candidate?.mutate === 'function' ? candidate : undefined;
|
|
212
|
+
}
|
|
171
213
|
/**
|
|
172
214
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
173
215
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -181,18 +223,18 @@ export class CardForm {
|
|
|
181
223
|
const spec = this.specOf(field);
|
|
182
224
|
if (staged.clear) {
|
|
183
225
|
if (this.stored(field))
|
|
184
|
-
plan.push({ field, run: () => this.clear(field) });
|
|
226
|
+
plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) });
|
|
185
227
|
continue;
|
|
186
228
|
}
|
|
187
229
|
if (staged.text === spec.format(this.sectionValue(field)))
|
|
188
230
|
continue;
|
|
189
231
|
const write = spec.parse(staged.text);
|
|
190
232
|
if (write === undefined)
|
|
191
|
-
plan.push({ field, run: undefined });
|
|
233
|
+
plan.push({ field, op: { field, op: 'unset' }, run: undefined });
|
|
192
234
|
else if (write.kind === 'clear')
|
|
193
|
-
plan.push({ field, run: () => this.clear(field) });
|
|
235
|
+
plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) });
|
|
194
236
|
else
|
|
195
|
-
plan.push({ field, run: () => this.store(field, write.value) });
|
|
237
|
+
plan.push({ field, op: { field, op: 'set', value: write.value }, run: () => this.store(field, write.value) });
|
|
196
238
|
}
|
|
197
239
|
return plan;
|
|
198
240
|
}
|
|
@@ -202,11 +244,18 @@ export class CardForm {
|
|
|
202
244
|
}
|
|
203
245
|
async store(field, value) {
|
|
204
246
|
await this.scope.set(field, value);
|
|
247
|
+
// A redacted secret never appears in the user layer read-back; judging it
|
|
248
|
+
// by value would misreport a successful secret save as failed. The bridge
|
|
249
|
+
// reports secret writes through its secret-set markers (batch path); on
|
|
250
|
+
// the per-field path the scope resolved, so the write is landed.
|
|
251
|
+
if (this.specOf(field).secret)
|
|
252
|
+
return true;
|
|
205
253
|
return this.userLayer()?.[field] === value;
|
|
206
254
|
}
|
|
207
255
|
stage(field, edit) {
|
|
208
256
|
this.staged.set(field, edit);
|
|
209
257
|
this.failed = false;
|
|
258
|
+
this.failedReason = undefined;
|
|
210
259
|
this.publish();
|
|
211
260
|
}
|
|
212
261
|
specOf(field) {
|