@linxin666/dsh-pet 0.1.1 → 0.1.2

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.
@@ -0,0 +1,198 @@
1
+ //#region src/affinity.ts
2
+ const AFFINITY_MAX = 100;
3
+ /** Affinity ranks by points; the pet visibly grows with its rank. */
4
+ const AFFINITY_RANKS = [
5
+ {
6
+ min: 0,
7
+ name: "幼鲸",
8
+ emoji: "🐣"
9
+ },
10
+ {
11
+ min: 25,
12
+ name: "伙伴",
13
+ emoji: "🐬"
14
+ },
15
+ {
16
+ min: 50,
17
+ name: "挚友",
18
+ emoji: "🐳"
19
+ },
20
+ {
21
+ min: 80,
22
+ name: "深海羁绊",
23
+ emoji: "💙"
24
+ }
25
+ ];
26
+ const defaultAffinityConfig = {
27
+ turnReward: 1,
28
+ petReward: 1,
29
+ petCooldownMs: 1e4,
30
+ feedReward: 5,
31
+ feedCooldownMs: 3e4
32
+ };
33
+ function emptyAffinity() {
34
+ return {
35
+ points: 0,
36
+ lastPetAt: 0,
37
+ lastFeedAt: 0,
38
+ pets: 0,
39
+ feeds: 0,
40
+ turns: 0
41
+ };
42
+ }
43
+ /** Rank for a point total. */
44
+ function rankOf(points) {
45
+ let rank = AFFINITY_RANKS[0];
46
+ for (const candidate of AFFINITY_RANKS) if (points >= candidate.min) rank = candidate;
47
+ return rank;
48
+ }
49
+ function clamp(points) {
50
+ return Math.min(100, Math.max(0, points));
51
+ }
52
+ /**
53
+ * Apply one interaction to a copy of the state (immutable style: returns a
54
+ * new object; the caller replaces the persisted state). Cooldowns only
55
+ * apply once the pet has been interacted with at least once (last*At === 0
56
+ * means "never", so the first pet/feed always lands).
57
+ */
58
+ function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
59
+ const next = { ...state };
60
+ if (kind === "pet") {
61
+ if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) return {
62
+ affinity: state,
63
+ delta: 0,
64
+ reaction: "摸过头啦,让鲸鱼娘歇口气~",
65
+ accepted: false
66
+ };
67
+ next.lastPetAt = nowMs;
68
+ next.pets += 1;
69
+ next.points = clamp(state.points + config.petReward);
70
+ return {
71
+ affinity: next,
72
+ delta: config.petReward,
73
+ reaction: "咕噜咕噜~被摸摸好舒服!",
74
+ accepted: true
75
+ };
76
+ }
77
+ if (kind === "feed") {
78
+ if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) return {
79
+ affinity: state,
80
+ delta: 0,
81
+ reaction: "吃饱啦,晚点再喂~",
82
+ accepted: false
83
+ };
84
+ next.lastFeedAt = nowMs;
85
+ next.feeds += 1;
86
+ next.points = clamp(state.points + config.feedReward);
87
+ return {
88
+ affinity: next,
89
+ delta: config.feedReward,
90
+ reaction: "呜哇!小鱼干好好吃!",
91
+ accepted: true
92
+ };
93
+ }
94
+ return {
95
+ affinity: state,
96
+ delta: 0,
97
+ reaction: "",
98
+ accepted: false
99
+ };
100
+ }
101
+ /** Reward one completed turn (called by the host on `done`). */
102
+ function applyTurnReward(state, config = defaultAffinityConfig) {
103
+ const next = { ...state };
104
+ next.turns += 1;
105
+ next.points = clamp(state.points + config.turnReward);
106
+ return next;
107
+ }
108
+ //#endregion
109
+ //#region src/state.ts
110
+ const defaultPetStateConfig = { celebrateMs: 2400 };
111
+ /**
112
+ * Map one activity phase onto the animation contract.
113
+ * - thinking / tool → `running` (focused work), with `running-right` as the
114
+ * side-alternating variant the client may use for tool activity.
115
+ * - waiting → `waiting` (expectant pose, needs user input).
116
+ * - done → `jumping` (celebration), then back to `idle` after the window.
117
+ * - idle → `idle` (calm breathing loop).
118
+ * `failed` has no DSH phase source yet; the machine keeps the mapping table
119
+ * so a future error event can light it up.
120
+ */
121
+ function animationForPhase(phase) {
122
+ switch (phase) {
123
+ case "thinking": return "running";
124
+ case "tool": return "running-right";
125
+ case "waiting": return "waiting";
126
+ case "done": return "jumping";
127
+ case "idle": return "idle";
128
+ }
129
+ }
130
+ /** The spritesheet row index for one animation track. */
131
+ function rowOf(animation) {
132
+ return {
133
+ "idle": 0,
134
+ "running-right": 1,
135
+ "running-left": 2,
136
+ "waving": 3,
137
+ "jumping": 4,
138
+ "failed": 5,
139
+ "waiting": 6,
140
+ "running": 7,
141
+ "review": 8
142
+ }[animation];
143
+ }
144
+ /**
145
+ * PetStateMachine — one instance per host process. Holds only the latest
146
+ * input snapshot and the celebration timing; no storage, no side effects.
147
+ */
148
+ var PetStateMachine = class {
149
+ config;
150
+ now;
151
+ phase = "idle";
152
+ line;
153
+ phrase;
154
+ sessionActive = false;
155
+ doneAt;
156
+ constructor(config = defaultPetStateConfig, now = Date.now) {
157
+ this.config = config;
158
+ this.now = now;
159
+ }
160
+ /** Consume one `activity/status` session event. */
161
+ onActivityStatus(input) {
162
+ this.phase = input.phase;
163
+ this.line = input.line;
164
+ this.phrase = input.phrase;
165
+ if (input.phase === "done") this.doneAt = this.now();
166
+ }
167
+ /** A session became the active one (or a fresh session started). */
168
+ onSessionActive() {
169
+ this.sessionActive = true;
170
+ }
171
+ /** The active session was disposed (or none left). */
172
+ onSessionDisposed() {
173
+ this.sessionActive = false;
174
+ this.phase = "idle";
175
+ this.line = void 0;
176
+ this.phrase = void 0;
177
+ this.doneAt = void 0;
178
+ }
179
+ /** Render the current animation decision. */
180
+ render() {
181
+ const nowMs = this.now();
182
+ let animation = animationForPhase(this.phase);
183
+ if (this.phase === "done" && this.doneAt !== void 0) {
184
+ if (nowMs - this.doneAt < this.config.celebrateMs) animation = "jumping";
185
+ else animation = "idle";
186
+ }
187
+ const bubble = this.phrase ?? this.line;
188
+ return {
189
+ animation,
190
+ ...bubble === void 0 ? {} : { bubble },
191
+ animationStartedAt: nowMs,
192
+ phase: this.phase,
193
+ sessionActive: this.sessionActive
194
+ };
195
+ }
196
+ };
197
+ //#endregion
198
+ export { AFFINITY_MAX as a, applyTurnReward as c, rankOf as d, rowOf as i, defaultAffinityConfig as l, animationForPhase as n, AFFINITY_RANKS as o, defaultPetStateConfig as r, applyInteraction as s, PetStateMachine as t, emptyAffinity as u };
@@ -0,0 +1,198 @@
1
+ //#region src/affinity.ts
2
+ const AFFINITY_MAX = 100;
3
+ /** Affinity ranks by points; the pet visibly grows with its rank.
4
+ * Marker glyphs are plain ASCII (the repo bans all emoji characters);
5
+ * they read as a growing star trail alongside the rank name. */
6
+ const AFFINITY_RANKS = [
7
+ {
8
+ min: 0,
9
+ name: "幼鲸",
10
+ emoji: "*"
11
+ },
12
+ {
13
+ min: 25,
14
+ name: "伙伴",
15
+ emoji: "**"
16
+ },
17
+ {
18
+ min: 50,
19
+ name: "挚友",
20
+ emoji: "***"
21
+ },
22
+ {
23
+ min: 80,
24
+ name: "深海羁绊",
25
+ emoji: "****"
26
+ }
27
+ ];
28
+ const defaultAffinityConfig = {
29
+ turnReward: 1,
30
+ petReward: 1,
31
+ petCooldownMs: 1e4,
32
+ feedReward: 5,
33
+ feedCooldownMs: 3e4
34
+ };
35
+ function emptyAffinity() {
36
+ return {
37
+ points: 0,
38
+ lastPetAt: 0,
39
+ lastFeedAt: 0,
40
+ pets: 0,
41
+ feeds: 0,
42
+ turns: 0
43
+ };
44
+ }
45
+ /** Rank for a point total. */
46
+ function rankOf(points) {
47
+ let rank = AFFINITY_RANKS[0];
48
+ for (const candidate of AFFINITY_RANKS) if (points >= candidate.min) rank = candidate;
49
+ return rank;
50
+ }
51
+ function clamp(points) {
52
+ return Math.min(100, Math.max(0, points));
53
+ }
54
+ /**
55
+ * Apply one interaction to a copy of the state (immutable style: returns a
56
+ * new object; the caller replaces the persisted state). Cooldowns only
57
+ * apply once the pet has been interacted with at least once (last*At === 0
58
+ * means "never", so the first pet/feed always lands).
59
+ */
60
+ function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
61
+ const next = { ...state };
62
+ if (kind === "pet") {
63
+ if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) return {
64
+ affinity: state,
65
+ delta: 0,
66
+ reaction: "摸过头啦,让鲸鱼娘歇口气~",
67
+ accepted: false
68
+ };
69
+ next.lastPetAt = nowMs;
70
+ next.pets += 1;
71
+ next.points = clamp(state.points + config.petReward);
72
+ return {
73
+ affinity: next,
74
+ delta: config.petReward,
75
+ reaction: "咕噜咕噜~被摸摸好舒服!",
76
+ accepted: true
77
+ };
78
+ }
79
+ if (kind === "feed") {
80
+ if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) return {
81
+ affinity: state,
82
+ delta: 0,
83
+ reaction: "吃饱啦,晚点再喂~",
84
+ accepted: false
85
+ };
86
+ next.lastFeedAt = nowMs;
87
+ next.feeds += 1;
88
+ next.points = clamp(state.points + config.feedReward);
89
+ return {
90
+ affinity: next,
91
+ delta: config.feedReward,
92
+ reaction: "呜哇!小鱼干好好吃!",
93
+ accepted: true
94
+ };
95
+ }
96
+ return {
97
+ affinity: state,
98
+ delta: 0,
99
+ reaction: "",
100
+ accepted: false
101
+ };
102
+ }
103
+ /** Reward one completed turn (called by the host on `done`). */
104
+ function applyTurnReward(state, config = defaultAffinityConfig) {
105
+ const next = { ...state };
106
+ next.turns += 1;
107
+ next.points = clamp(state.points + config.turnReward);
108
+ return next;
109
+ }
110
+ //#endregion
111
+ //#region src/state.ts
112
+ const defaultPetStateConfig = { celebrateMs: 2400 };
113
+ /**
114
+ * Map one activity phase onto the animation contract.
115
+ * - thinking / tool → `running` (focused work), with `running-right` as the
116
+ * side-alternating variant the client may use for tool activity.
117
+ * - waiting → `waiting` (expectant pose, needs user input).
118
+ * - done → `jumping` (celebration), then back to `idle` after the window.
119
+ * - idle → `idle` (calm breathing loop).
120
+ * `failed` has no DSH phase source yet; the machine keeps the mapping table
121
+ * so a future error event can light it up.
122
+ */
123
+ function animationForPhase(phase) {
124
+ switch (phase) {
125
+ case "thinking": return "running";
126
+ case "tool": return "running-right";
127
+ case "waiting": return "waiting";
128
+ case "done": return "jumping";
129
+ case "idle": return "idle";
130
+ }
131
+ }
132
+ /** The spritesheet row index for one animation track. */
133
+ function rowOf(animation) {
134
+ return {
135
+ "idle": 0,
136
+ "running-right": 1,
137
+ "running-left": 2,
138
+ "waving": 3,
139
+ "jumping": 4,
140
+ "failed": 5,
141
+ "waiting": 6,
142
+ "running": 7,
143
+ "review": 8
144
+ }[animation];
145
+ }
146
+ /**
147
+ * PetStateMachine — one instance per host process. Holds only the latest
148
+ * input snapshot and the celebration timing; no storage, no side effects.
149
+ */
150
+ var PetStateMachine = class {
151
+ config;
152
+ now;
153
+ phase = "idle";
154
+ line;
155
+ phrase;
156
+ sessionActive = false;
157
+ doneAt;
158
+ constructor(config = defaultPetStateConfig, now = Date.now) {
159
+ this.config = config;
160
+ this.now = now;
161
+ }
162
+ /** Consume one `activity/status` session event. */
163
+ onActivityStatus(input) {
164
+ this.phase = input.phase;
165
+ this.line = input.line;
166
+ this.phrase = input.phrase;
167
+ if (input.phase === "done") this.doneAt = this.now();
168
+ }
169
+ /** A session became the active one (or a fresh session started). */
170
+ onSessionActive() {
171
+ this.sessionActive = true;
172
+ }
173
+ /** The active session was disposed (or none left). */
174
+ onSessionDisposed() {
175
+ this.sessionActive = false;
176
+ this.phase = "idle";
177
+ this.line = void 0;
178
+ this.phrase = void 0;
179
+ this.doneAt = void 0;
180
+ }
181
+ /** Render the current animation decision. */
182
+ render() {
183
+ const nowMs = this.now();
184
+ let animation = animationForPhase(this.phase);
185
+ if (this.phase === "done" && this.doneAt !== void 0) if (nowMs - this.doneAt < this.config.celebrateMs) animation = "jumping";
186
+ else animation = "idle";
187
+ const bubble = this.phrase ?? this.line;
188
+ return {
189
+ animation,
190
+ ...bubble === void 0 ? {} : { bubble },
191
+ animationStartedAt: nowMs,
192
+ phase: this.phase,
193
+ sessionActive: this.sessionActive
194
+ };
195
+ }
196
+ };
197
+ //#endregion
198
+ export { AFFINITY_MAX as a, applyTurnReward as c, rankOf as d, rowOf as i, defaultAffinityConfig as l, animationForPhase as n, AFFINITY_RANKS as o, defaultPetStateConfig as r, applyInteraction as s, PetStateMachine as t, emptyAffinity as u };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Affinity score — pure, clock-injected. The pet grows closer the more you
3
+ * work together and care for it: every completed turn earns a small reward,
4
+ * petting earns a tiny one (cooldown-gated), feeding earns the most.
5
+ * Persistence lives in the service; this module only computes transitions.
6
+ * @module @linxin666/dsh-pet/affinity
7
+ */
8
+ export const AFFINITY_MAX = 100;
9
+ /** Affinity ranks by points; the pet visibly grows with its rank.
10
+ * Marker glyphs are plain ASCII (the repo bans all emoji characters);
11
+ * they read as a growing star trail alongside the rank name. */
12
+ export const AFFINITY_RANKS = [
13
+ { min: 0, name: '幼鲸', emoji: '*' },
14
+ { min: 25, name: '伙伴', emoji: '**' },
15
+ { min: 50, name: '挚友', emoji: '***' },
16
+ { min: 80, name: '深海羁绊', emoji: '****' },
17
+ ];
18
+ export const defaultAffinityConfig = {
19
+ turnReward: 1,
20
+ petReward: 1,
21
+ petCooldownMs: 10_000,
22
+ feedReward: 5,
23
+ feedCooldownMs: 30_000,
24
+ };
25
+ export function emptyAffinity() {
26
+ return { points: 0, lastPetAt: 0, lastFeedAt: 0, pets: 0, feeds: 0, turns: 0 };
27
+ }
28
+ /** Rank for a point total. */
29
+ export function rankOf(points) {
30
+ let rank = AFFINITY_RANKS[0];
31
+ for (const candidate of AFFINITY_RANKS) {
32
+ if (points >= candidate.min)
33
+ rank = candidate;
34
+ }
35
+ return rank;
36
+ }
37
+ function clamp(points) {
38
+ return Math.min(AFFINITY_MAX, Math.max(0, points));
39
+ }
40
+ /**
41
+ * Apply one interaction to a copy of the state (immutable style: returns a
42
+ * new object; the caller replaces the persisted state). Cooldowns only
43
+ * apply once the pet has been interacted with at least once (last*At === 0
44
+ * means "never", so the first pet/feed always lands).
45
+ */
46
+ export function applyInteraction(state, kind, nowMs, config = defaultAffinityConfig) {
47
+ const next = { ...state };
48
+ if (kind === 'pet') {
49
+ if (state.lastPetAt !== 0 && nowMs - state.lastPetAt < config.petCooldownMs) {
50
+ return { affinity: state, delta: 0, reaction: '摸过头啦,让鲸鱼娘歇口气~', accepted: false };
51
+ }
52
+ next.lastPetAt = nowMs;
53
+ next.pets += 1;
54
+ next.points = clamp(state.points + config.petReward);
55
+ return {
56
+ affinity: next,
57
+ delta: config.petReward,
58
+ reaction: '咕噜咕噜~被摸摸好舒服!',
59
+ accepted: true,
60
+ };
61
+ }
62
+ if (kind === 'feed') {
63
+ if (state.lastFeedAt !== 0 && nowMs - state.lastFeedAt < config.feedCooldownMs) {
64
+ return { affinity: state, delta: 0, reaction: '吃饱啦,晚点再喂~', accepted: false };
65
+ }
66
+ next.lastFeedAt = nowMs;
67
+ next.feeds += 1;
68
+ next.points = clamp(state.points + config.feedReward);
69
+ return {
70
+ affinity: next,
71
+ delta: config.feedReward,
72
+ reaction: '呜哇!小鱼干好好吃!',
73
+ accepted: true,
74
+ };
75
+ }
76
+ return { affinity: state, delta: 0, reaction: '', accepted: false };
77
+ }
78
+ /** Reward one completed turn (called by the host on `done`). */
79
+ export function applyTurnReward(state, config = defaultAffinityConfig) {
80
+ const next = { ...state };
81
+ next.turns += 1;
82
+ next.points = clamp(state.points + config.turnReward);
83
+ return next;
84
+ }
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Dock anchor inside `conversation.input.selector.context`: the input
4
+ * selector row mounts in EVERY conversation phase (no-session cold start,
5
+ * the blank-session hero, and the active seat), so the floating pet stays on
6
+ * screen on the new-conversation screen too. While visible it mounts the
7
+ * floating WhalePet (portal); while hidden it renders the summon button.
8
+ * @module @linxin666/dsh-pet/client/PetDockEntry
9
+ */
10
+ import { useEffect, useSyncExternalStore } from 'react';
11
+ import { WhalePet } from "./WhalePet.js";
12
+ import styles from './pet.module.css';
13
+ const DEFAULT_DISPLAY = { visible: true, size: 160, right: 24, bottom: 20 };
14
+ /**
15
+ * Dock entry: while the pet is visible, mount the floating WhalePet (it
16
+ * portals itself onto document.body); while hidden, render the summon
17
+ * button so the pet can always come back. The store is the plugin-owned
18
+ * single instance — the slot system provides none because the pet is
19
+ * host-global, not session-scoped.
20
+ */
21
+ export function PetDockEntry(props) {
22
+ const { store, ensure } = props;
23
+ const ui = useSyncExternalStore(store.subscribe, store.getSnapshot);
24
+ const snapshot = ui.snapshot;
25
+ const feedback = ui.feedback;
26
+ const visible = snapshot?.display.visible ?? true;
27
+ useEffect(() => {
28
+ ensure();
29
+ }, [ensure]);
30
+ if (visible) {
31
+ return (_jsx("span", { "data-pet-dock": true, "data-testid": "pet-dock", children: _jsx(WhalePet, { snapshot: snapshot, display: snapshot?.display ?? DEFAULT_DISPLAY, feedback: feedback, onPet: props.pet, onFeed: props.feed, onHide: props.hide, onDragEnd: props.dragEnd, onRename: props.rename, onFeedbackDone: props.feedbackDone, t: props.t }) }));
32
+ }
33
+ return (_jsx("button", { type: "button", className: styles.summon, onClick: props.summon, "data-testid": "pet-summon", children: props.t('pet.summon', { name: snapshot?.name ?? '鲸鱼娘' }) }));
34
+ }
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PluginSettingsCard, ValueField, BooleanField } from "./PluginSettingsCard.js";
3
+ import { CardForm, booleanField, numberField, textField } from "./settings-form.js";
4
+ /** Bridges the `pet` scope onto the card's staged form. */
5
+ export class PetSettingsCardController {
6
+ form;
7
+ store;
8
+ /** @param scope - the bound settings scope for the `pet` namespace. */
9
+ constructor(scope) {
10
+ this.form = new CardForm(scope, [
11
+ booleanField('enabled'),
12
+ booleanField('visible'),
13
+ numberField('size'),
14
+ numberField('right'),
15
+ numberField('bottom'),
16
+ textField('name'),
17
+ ]);
18
+ this.store = this.form.bind(() => this.projection());
19
+ }
20
+ projection() {
21
+ return {
22
+ ...this.form.shell(),
23
+ enabled: this.form.field('enabled'),
24
+ visible: this.form.field('visible'),
25
+ size: this.form.field('size'),
26
+ right: this.form.field('right'),
27
+ bottom: this.form.field('bottom'),
28
+ name: this.form.field('name'),
29
+ };
30
+ }
31
+ /**
32
+ * Build the face the card's slot registration injects.
33
+ * @returns the card's snapshot and its form actions.
34
+ */
35
+ inject() {
36
+ return { hooks: { petSettingsCard: this.store }, ...this.form.actions() };
37
+ }
38
+ }
39
+ /**
40
+ * Render the pet settings card.
41
+ * @param props - locale copy, the card snapshot, and its form actions.
42
+ * @returns the card.
43
+ */
44
+ export function PetSettingsCard(props) {
45
+ const { t } = props;
46
+ const state = props.usePetSettingsCard(snapshot => snapshot);
47
+ const disabled = !state.writable;
48
+ const fieldProps = {
49
+ overriddenLabel: t('settings.overridden'),
50
+ resetLabel: t('settings.reset'),
51
+ invalidLabel: t('settings.invalidNumber'),
52
+ disabled,
53
+ };
54
+ return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-pet-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(BooleanField, { id: "settings-pet-visible", label: t('settings.visible'), hint: t('settings.visibleHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.visible, onEdit: (text) => { props.edit('visible', text); }, onReset: () => { props.resetField('visible'); } }), _jsx(ValueField, { id: "settings-pet-size", label: t('settings.size'), hint: t('settings.sizeHint'), numeric: true, ...fieldProps, ...state.size, onEdit: (text) => { props.edit('size', text); }, onReset: () => { props.resetField('size'); } }), _jsx(ValueField, { id: "settings-pet-right", label: t('settings.right'), hint: t('settings.rightHint'), numeric: true, ...fieldProps, ...state.right, onEdit: (text) => { props.edit('right', text); }, onReset: () => { props.resetField('right'); } }), _jsx(ValueField, { id: "settings-pet-bottom", label: t('settings.bottom'), hint: t('settings.bottomHint'), numeric: true, ...fieldProps, ...state.bottom, onEdit: (text) => { props.edit('bottom', text); }, onReset: () => { props.resetField('bottom'); } }), _jsx(ValueField, { id: "settings-pet-name", label: t('settings.name'), hint: t('settings.nameHint'), ...fieldProps, ...state.name, onEdit: (text) => { props.edit('name', text); }, onReset: () => { props.resetField('name'); } })] }));
55
+ }
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Shared chrome for the plugin settings card: a disclosure header naming the
4
+ * plugin and what its settings govern, the controls inside, and the save that
5
+ * writes them. Renders nothing while the namespace is unavailable — a
6
+ * deployment that does not compose the owning plugin should show no trace of
7
+ * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
8
+ * slice (this package must not depend on a sibling UI package).
9
+ */
10
+ import { useState } from 'react';
11
+ import css from './settings-card.module.css';
12
+ /**
13
+ * Render one plugin settings card.
14
+ * @param props - the plugin's copy keys, its form state, and its controls.
15
+ * @returns the card, or nothing when the namespace is unavailable.
16
+ */
17
+ export function PluginSettingsCard(props) {
18
+ const [open, setOpen] = useState(false);
19
+ const { state } = props;
20
+ if (!state.available)
21
+ return null;
22
+ const title = props.t(props.titleKey);
23
+ const blocked = !state.dirty || state.invalid || state.saving;
24
+ return (_jsxs("li", { className: css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: title }), _jsx("span", { className: css.description, children: props.t(props.descriptionKey) })] }), state.dirty ? _jsx("span", { className: css.pending, children: props.t('settings.unsaved') }) : null, _jsx("span", { className: open ? css.chevronOpen : css.chevron, children: "\u25BE" })] }), open
25
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, _jsxs("div", { className: css.footer, children: [state.failed ? _jsx("p", { className: css.failed, role: "status", children: props.t('settings.saveFailed') }) : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] })] }))
26
+ : null] }));
27
+ }
28
+ /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
29
+ export function ValueField(props) {
30
+ return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
31
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
32
+ : null] }), _jsx("input", { id: props.id, className: props.invalid ? css.inputInvalid : css.input, type: "text", ...props.numeric === true ? { inputMode: 'numeric' } : {}, ...props.invalid ? { 'aria-invalid': true } : {}, value: props.text, placeholder: props.placeholder ?? '', disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); } }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
33
+ }
34
+ /** A staged boolean field: 继承 / 开 / 关. */
35
+ export function BooleanField(props) {
36
+ return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
37
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
38
+ : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), _jsx("option", { value: "true", children: props.onLabel }), _jsx("option", { value: "false", children: props.offLabel })] }), _jsx("p", { className: css.hint, children: props.hint })] }));
39
+ }