@dsh-plus/secret-env 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.
@@ -1,260 +0,0 @@
1
- /**
2
- * 会话注入控件(conversation.input.right 官方插槽,session 作用域):
3
- * composer 工具行的钥匙胶囊,弹出本会话的会话级密钥面板(列表 + 增删)。
4
- * 会话 id 由插槽 inject 工厂直接供给(框架解析的严格 session 作用域)。
5
- * 窄屏(≤767px)popover 转底部抽屉;桌面点外部或 Esc 关闭。
6
- * @module secret-env/client/composer
7
- */
8
- import { type ReactElement, useEffect, useRef, useState } from 'react'
9
-
10
- import { fetchSecrets, type SessionWireEntry, setSession, unsetSession } from './api.ts'
11
- import { copyText, errorText } from './common.ts'
12
-
13
- export interface ComposerSecretProps {
14
- /** 插槽 inject 工厂注入的当前会话 id(严格 session 作用域,必存在)。 */
15
- sessionId: string
16
- t(key: string): string
17
- }
18
-
19
- interface FormState {
20
- name: string
21
- value: string
22
- description: string
23
- once: boolean
24
- }
25
-
26
- const EMPTY_FORM: FormState = { name: '', value: '', description: '', once: false }
27
-
28
- function SessionRow(props: {
29
- t: ComposerSecretProps['t']
30
- sessionId: string
31
- entry: SessionWireEntry
32
- onDeleted(): void
33
- onError(text: string): void
34
- }): ReactElement {
35
- const { t, entry } = props
36
- const [copied, setCopied] = useState(false)
37
- const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
38
-
39
- useEffect(
40
- () => () => {
41
- if (timer.current !== null) clearTimeout(timer.current)
42
- },
43
- [],
44
- )
45
-
46
- return (
47
- <div className="dse-row">
48
- <div className="dse-rowMain">
49
- <div className="dse-env">
50
- <span className="dse-envName">${entry.envName}</span>
51
- <button
52
- type="button"
53
- className="dse-iconBtn"
54
- title={t('copy')}
55
- aria-label={t('copy')}
56
- onClick={() => {
57
- void copyText(`$${entry.envName}`).then((ok) => {
58
- if (!ok) return
59
- setCopied(true)
60
- if (timer.current !== null) clearTimeout(timer.current)
61
- timer.current = setTimeout(() => setCopied(false), 1500)
62
- })
63
- }}
64
- >
65
- {copied ? '✓' : '⧉'}
66
- </button>
67
- </div>
68
- {entry.description !== '' ? <span className="dse-note">{entry.description}</span> : null}
69
- </div>
70
- <div className="dse-badges">
71
- {entry.once ? <span className="dse-badge">{t('scopeOnce')}</span> : null}
72
- <button
73
- type="button"
74
- className="dse-iconBtn dse-delBtn"
75
- title={t('delete')}
76
- aria-label={t('delete')}
77
- onClick={() => {
78
- unsetSession(props.sessionId, entry.name)
79
- .then(() => props.onDeleted())
80
- .catch((error: unknown) => props.onError(errorText(t, error)))
81
- }}
82
- >
83
-
84
- </button>
85
- </div>
86
- </div>
87
- )
88
- }
89
-
90
- export function ComposerSecret(props: ComposerSecretProps): ReactElement {
91
- const { sessionId, t } = props
92
- const [open, setOpen] = useState(false)
93
- const [items, setItems] = useState<SessionWireEntry[] | null>(null)
94
- const [form, setForm] = useState<FormState>(EMPTY_FORM)
95
- const [saving, setSaving] = useState(false)
96
- const [status, setStatus] = useState<string | null>(null)
97
- const wrapRef = useRef<HTMLSpanElement | null>(null)
98
-
99
- const load = (): void => {
100
- fetchSecrets(sessionId)
101
- .then((list) => setItems(list.session))
102
- .catch(() => setStatus(errorText(t, new Error('internal'))))
103
- }
104
-
105
- // 打开面板与会话切换时拉取;闭合计时清零。
106
- // biome-ignore lint/correctness/useExhaustiveDependencies: load 为闭包稳定函数,仅需随 open/sessionId 触发
107
- useEffect(() => {
108
- if (!open) return
109
- load()
110
- }, [open, sessionId])
111
-
112
- // 桌面:点外部 / Esc 关闭。
113
- useEffect(() => {
114
- if (!open) return
115
- const onDown = (event: MouseEvent): void => {
116
- if (wrapRef.current !== null && !wrapRef.current.contains(event.target as Node)) {
117
- setOpen(false)
118
- }
119
- }
120
- const onKey = (event: KeyboardEvent): void => {
121
- if (event.key === 'Escape') setOpen(false)
122
- }
123
- document.addEventListener('mousedown', onDown)
124
- document.addEventListener('keydown', onKey)
125
- return () => {
126
- document.removeEventListener('mousedown', onDown)
127
- document.removeEventListener('keydown', onKey)
128
- }
129
- }, [open])
130
-
131
- const onSave = (): void => {
132
- setSaving(true)
133
- setStatus(null)
134
- setSession(sessionId, form.name, form.value, form.description, form.once)
135
- .then(() => {
136
- setForm(EMPTY_FORM)
137
- load()
138
- })
139
- .catch((error: unknown) => setStatus(errorText(t, error)))
140
- .finally(() => setSaving(false))
141
- }
142
-
143
- const count = items?.length ?? 0
144
- const canSave = form.name.trim() !== '' && form.value !== '' && !saving
145
-
146
- return (
147
- <span className="dse-wrap" ref={wrapRef}>
148
- <button
149
- type="button"
150
- className="dse-chip"
151
- aria-expanded={open}
152
- aria-label={t('sessionSecrets')}
153
- title={t('sessionSecrets')}
154
- onClick={() => setOpen(!open)}
155
- >
156
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
157
- <path
158
- d="M14.5 2a7.5 7.5 0 0 0-7.36 9.04L2 20.19V22h1.81l1.5-1.5v-1.81h1.81v-1.81h1.81l1.22-1.22A7.5 7.5 0 1 0 14.5 2Zm2 5.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z"
159
- fill="currentColor"
160
- />
161
- </svg>
162
- {count > 0 ? <span className="dse-count">{count}</span> : null}
163
- </button>
164
- {open ? (
165
- <div className="dse-pop" role="dialog" aria-label={t('sessionSecrets')}>
166
- <div className="dse-popHead">
167
- <p className="dse-popTitle">{t('sessionSecrets')}</p>
168
- <button
169
- type="button"
170
- className="dse-iconBtn"
171
- title={t('refresh')}
172
- aria-label={t('refresh')}
173
- onClick={load}
174
- >
175
-
176
- </button>
177
- <button
178
- type="button"
179
- className="dse-iconBtn"
180
- title={t('close')}
181
- aria-label={t('close')}
182
- onClick={() => setOpen(false)}
183
- >
184
-
185
- </button>
186
- </div>
187
- <p className="dse-popHint">{t('sessionHint')}</p>
188
- <div className="dse-popList">
189
- {items === null || items.length === 0 ? (
190
- <p className="dse-empty">{t('sessionEmpty')}</p>
191
- ) : (
192
- items.map((entry) => (
193
- <SessionRow
194
- key={entry.name}
195
- t={t}
196
- sessionId={sessionId}
197
- entry={entry}
198
- onDeleted={load}
199
- onError={(text) => setStatus(text === '' ? null : text)}
200
- />
201
- ))
202
- )}
203
- </div>
204
- <div className="dse-popForm">
205
- <div className="dse-field">
206
- <div className="dse-head">
207
- <label className="dse-label" htmlFor="dse-s-name">
208
- {t('addSession')}
209
- </label>
210
- </div>
211
- <input
212
- id="dse-s-name"
213
- className="dse-input"
214
- placeholder="API_TOKEN"
215
- value={form.name}
216
- onChange={(event) => setForm({ ...form, name: event.target.value })}
217
- />
218
- <input
219
- className="dse-input"
220
- type="password"
221
- autoComplete="off"
222
- placeholder={t('valueLabel')}
223
- value={form.value}
224
- onChange={(event) => setForm({ ...form, value: event.target.value })}
225
- />
226
- <input
227
- className="dse-input"
228
- placeholder={t('descLabel')}
229
- value={form.description}
230
- onChange={(event) => setForm({ ...form, description: event.target.value })}
231
- />
232
- <div className="dse-checkRow">
233
- <input
234
- id="dse-s-once"
235
- type="checkbox"
236
- role="switch"
237
- aria-checked={form.once}
238
- checked={form.once}
239
- onChange={(event) => setForm({ ...form, once: event.target.checked })}
240
- />
241
- <label htmlFor="dse-s-once">{t('onceLabel')}</label>
242
- </div>
243
- </div>
244
- <div className="dse-foot">
245
- {status !== null ? <p className="dse-status dse-statusError">{status}</p> : null}
246
- <button
247
- type="button"
248
- className="dse-btn dse-btnPrimary"
249
- disabled={!canSave}
250
- onClick={onSave}
251
- >
252
- {saving ? t('saving') : t('save')}
253
- </button>
254
- </div>
255
- </div>
256
- </div>
257
- ) : null}
258
- </span>
259
- )
260
- }