@closerclick/closer-click-profile 0.1.0 → 0.2.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +170 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closerclick/closer-click-profile",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Web Component (custom element) <closer-click-profile> reutilizable por cualquier app del ecosistema Closer Click: tarjeta de perfil + reputación (confianza/afinidad, web-of-trust, reputación de la red). Autohosteado, Shadow DOM, temable por CSS vars.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -33,7 +33,9 @@
33
33
  * name nombre/nick a mostrar
34
34
  * since timestamp ms del primer contacto ("conocido desde")
35
35
  * online booleano: muestra el punto de en-línea
36
- * mode 'edit' (default) | 'view' (solo lectura, sin editor ni footer)
36
+ * mode 'edit' (default, calificar a un peer) | 'view' (solo lectura)
37
+ * | 'self' (TU perfil: nombre editable que se guarda en el vault,
38
+ * sin calificación; conserva los paneles de reputación)
37
39
  * modal booleano: envuelve la tarjeta en backdrop + header/footer
38
40
  * heading título del header (override)
39
41
  * lang 'es' | 'en' | 'auto' (default 'auto')
@@ -42,6 +44,7 @@
42
44
  * Métodos: el.reload()
43
45
  * Eventos (bubbles, composed):
44
46
  * 'cc-profile-rate' detail { pubkey, indicators, notes } (tras guardar)
47
+ * 'cc-profile-name' detail { pubkey, name } (mode="self", tras guardar tu nombre)
45
48
  * 'cc-profile-close'
46
49
  * 'cc-profile-refresh' detail { pubkey } (botón ↻ del web-of-trust)
47
50
  */
@@ -78,6 +81,11 @@ const I18N = {
78
81
  saving: 'Guardando…',
79
82
  close: 'Cerrar',
80
83
  saveError: 'Error al guardar',
84
+ headingSelf: 'Mi perfil',
85
+ editName: 'Tu nombre visible',
86
+ nickPh: 'Tu nombre',
87
+ saveName: 'Guardar',
88
+ nameSaved: '✓ Guardado',
81
89
  labels: ['Sin calificar', 'Sospechoso', 'Dudoso', 'Confiable', 'Muy confiable', 'De total confianza'],
82
90
  },
83
91
  en: {
@@ -111,6 +119,11 @@ const I18N = {
111
119
  saving: 'Saving…',
112
120
  close: 'Close',
113
121
  saveError: 'Save error',
122
+ headingSelf: 'My profile',
123
+ editName: 'Your display name',
124
+ nickPh: 'Your name',
125
+ saveName: 'Save',
126
+ nameSaved: '✓ Saved',
114
127
  labels: ['Unrated', 'Suspicious', 'Doubtful', 'Trustworthy', 'Very trustworthy', 'Fully trusted'],
115
128
  },
116
129
  }
@@ -123,27 +136,33 @@ const AVATAR_PALETTE = [
123
136
 
124
137
  const STYLE = `
125
138
  :host {
126
- /* ----- Tema (override desde la app) ----- */
127
- --ccp-bg: var(--bg-1, #faf3e7);
128
- --ccp-bg-2: var(--bg-2, #f5ede0);
129
- --ccp-bg-3: var(--bg-3, #ede2cf);
130
- --ccp-bg-4: var(--bg-4, #e0d3ba);
131
- --ccp-border: var(--border, #d4c4a8);
132
- --ccp-text: var(--text, #2b211a);
133
- --ccp-muted: var(--muted, #8a7a66);
134
- --ccp-accent: var(--accent, #c0392b);
135
- --ccp-accent-2: var(--accent-2, #a93226);
136
- --ccp-gold: var(--gold, #d4a72c);
137
- --ccp-derived: var(--derived, #a37a45);
138
- --ccp-online: var(--online, #5a8a3a);
139
- --ccp-affinity: var(--affinity, #2dd4bf);
140
- --ccp-radius: var(--ccp-radius, 16px);
141
- --ccp-font: var(--font-body, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif);
142
- --ccp-font-headline: var(--font-headline, var(--ccp-font));
143
- --ccp-font-mono: var(--font-mono, ui-monospace, Menlo, Consolas, monospace);
144
-
145
- font-family: var(--ccp-font);
146
- color: var(--ccp-text);
139
+ /* ----- Tema -----
140
+ * API pública = --ccp-* (la app las setea por :root, selector o inline).
141
+ * Tokens internos = --_* (los usa el componente). Cada uno resuelve:
142
+ * override --ccp-* → var de tema del messenger → literal.
143
+ * NO declaramos --ccp-* acá (eso ganaría por especificidad al override de la
144
+ * app); así un simple selector de la app (o en :root) funciona. */
145
+ --_bg: var(--ccp-bg, var(--bg-1, #faf3e7));
146
+ --_bg-2: var(--ccp-bg-2, var(--bg-2, #f5ede0));
147
+ --_bg-3: var(--ccp-bg-3, var(--bg-3, #ede2cf));
148
+ --_bg-4: var(--ccp-bg-4, var(--bg-4, #e0d3ba));
149
+ --_border: var(--ccp-border, var(--border, #d4c4a8));
150
+ --_text: var(--ccp-text, var(--text, #2b211a));
151
+ --_muted: var(--ccp-muted, var(--muted, #8a7a66));
152
+ --_accent: var(--ccp-accent, var(--accent, #c0392b));
153
+ --_accent-2: var(--ccp-accent-2, var(--accent-2, #a93226));
154
+ --_gold: var(--ccp-gold, var(--gold, #d4a72c));
155
+ --_derived: var(--ccp-derived, var(--derived, #a37a45));
156
+ --_online: var(--ccp-online, var(--online, #5a8a3a));
157
+ --_affinity: var(--ccp-affinity, var(--affinity, #2dd4bf));
158
+ --_input-bg: var(--ccp-input-bg, var(--input-bg, #fff));
159
+ --_radius: var(--ccp-radius, 16px);
160
+ --_font: var(--ccp-font, var(--font-body, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif));
161
+ --_font-headline: var(--ccp-font-headline, var(--font-headline, var(--_font)));
162
+ --_font-mono: var(--ccp-font-mono, var(--font-mono, ui-monospace, Menlo, Consolas, monospace));
163
+
164
+ font-family: var(--_font);
165
+ color: var(--_text);
147
166
  }
148
167
  * { box-sizing: border-box; }
149
168
 
@@ -156,9 +175,9 @@ const STYLE = `
156
175
  z-index: 2147483000;
157
176
  }
158
177
  .modal {
159
- background: var(--ccp-bg);
160
- border: 1px solid var(--ccp-border);
161
- border-radius: var(--ccp-radius);
178
+ background: var(--_bg);
179
+ border: 1px solid var(--_border);
180
+ border-radius: var(--_radius);
162
181
  width: 100%; max-width: 460px;
163
182
  max-height: 92vh;
164
183
  display: flex; flex-direction: column;
@@ -168,18 +187,18 @@ const STYLE = `
168
187
  .head {
169
188
  display: flex; align-items: center; justify-content: space-between;
170
189
  padding: 18px 24px;
171
- border-bottom: 1px solid var(--ccp-border);
190
+ border-bottom: 1px solid var(--_border);
172
191
  }
173
192
  .head h2 {
174
- margin: 0; font-family: var(--ccp-font-headline);
175
- font-size: 18px; font-weight: 600; color: var(--ccp-text);
193
+ margin: 0; font-family: var(--_font-headline);
194
+ font-size: 18px; font-weight: 600; color: var(--_text);
176
195
  }
177
196
  .x {
178
197
  background: transparent; border: 0;
179
- font-size: 24px; cursor: pointer; color: var(--ccp-muted);
198
+ font-size: 24px; cursor: pointer; color: var(--_muted);
180
199
  width: 32px; height: 32px; border-radius: 8px; line-height: 1;
181
200
  }
182
- .x:hover { background: var(--ccp-bg-3); color: var(--ccp-text); }
201
+ .x:hover { background: var(--_bg-3); color: var(--_text); }
183
202
 
184
203
  .body { padding: 20px 24px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; }
185
204
  :host([modal]) .body { max-height: 70vh; }
@@ -187,102 +206,114 @@ const STYLE = `
187
206
  /* ----- Identity ----- */
188
207
  .identity {
189
208
  display: flex; gap: 14px; align-items: center;
190
- background: var(--ccp-bg-2);
191
- border: 1px solid var(--ccp-border);
209
+ background: var(--_bg-2);
210
+ border: 1px solid var(--_border);
192
211
  border-radius: 12px; padding: 14px;
193
212
  }
194
213
  .avatar-wrap { position: relative; flex-shrink: 0; }
195
214
  .avatar {
196
215
  width: 56px; height: 56px; border-radius: 50%;
197
216
  display: flex; align-items: center; justify-content: center;
198
- color: #fff; font-family: var(--ccp-font-headline);
217
+ color: #fff; font-family: var(--_font-headline);
199
218
  font-weight: 600; font-size: 18px;
200
219
  }
201
220
  .online-dot {
202
221
  position: absolute; right: 0; bottom: 0;
203
222
  width: 14px; height: 14px; border-radius: 50%;
204
- background: var(--ccp-online); border: 2px solid var(--ccp-bg-2);
223
+ background: var(--_online); border: 2px solid var(--_bg-2);
205
224
  }
206
225
  .identity-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
207
- .name { font-family: var(--ccp-font-headline); font-weight: 600; font-size: 17px; color: var(--ccp-text); }
226
+ .name { font-family: var(--_font-headline); font-weight: 600; font-size: 17px; color: var(--_text); }
227
+ /* ----- Editor de tu propio nombre (mode="self") ----- */
228
+ .nick-edit { display: flex; flex-direction: column; gap: 6px; }
229
+ .nick-label { font-size: 12px; color: var(--_muted); }
230
+ .nick-row { display: flex; gap: 8px; align-items: center; }
231
+ .nick-input {
232
+ flex: 1; min-width: 0; font: inherit; font-size: 15px; color: var(--_text);
233
+ background: var(--ccp-input-bg, #fff); border: 1px solid var(--_border); border-radius: 8px;
234
+ padding: 8px 10px;
235
+ }
236
+ .nick-input:focus { outline: none; border-color: var(--_accent); }
237
+ .nick-save { white-space: nowrap; }
238
+ .nick-saved { font-size: 12px; color: var(--_online); }
208
239
  .pubkey {
209
- background: var(--ccp-bg-3); padding: 2px 8px; border-radius: 6px;
210
- font-family: var(--ccp-font-mono); font-size: 11.5px; color: var(--ccp-muted);
240
+ background: var(--_bg-3); padding: 2px 8px; border-radius: 6px;
241
+ font-family: var(--_font-mono); font-size: 11.5px; color: var(--_muted);
211
242
  width: fit-content;
212
243
  }
213
- .since { font-size: 12px; color: var(--ccp-muted); }
244
+ .since { font-size: 12px; color: var(--_muted); }
214
245
 
215
246
  /* ----- Sections ----- */
216
247
  .section { display: flex; flex-direction: column; gap: 6px; position: relative; }
217
- .section-label { font-size: 13px; font-weight: 500; color: var(--ccp-muted); }
248
+ .section-label { font-size: 13px; font-weight: 500; color: var(--_muted); }
218
249
  .section-label small { font-weight: 400; }
219
250
  .stars-row { display: flex; gap: 6px; align-items: center; }
220
251
  .star-btn {
221
252
  background: transparent; border: 0; font-size: 36px;
222
- color: var(--ccp-bg-4); cursor: pointer; padding: 0; line-height: 1;
253
+ color: var(--_bg-4); cursor: pointer; padding: 0; line-height: 1;
223
254
  transition: color 100ms ease-out, transform 100ms ease-out;
224
255
  }
225
256
  .star-btn[disabled] { cursor: default; }
226
257
  .star-btn:not([disabled]):hover { transform: scale(1.1); }
227
- .star-btn.filled { color: var(--ccp-gold); text-shadow: 0 1px 2px rgba(212, 167, 44, 0.35); }
228
- .star-btn.afin.filled { color: var(--ccp-affinity); text-shadow: none; }
258
+ .star-btn.filled { color: var(--_gold); text-shadow: 0 1px 2px rgba(212, 167, 44, 0.35); }
259
+ .star-btn.afin.filled { color: var(--_affinity); text-shadow: none; }
229
260
 
230
- .rating-meta { display: flex; gap: 8px; align-items: center; font-size: 14px; color: var(--ccp-text); margin-top: 4px; }
261
+ .rating-meta { display: flex; gap: 8px; align-items: center; font-size: 14px; color: var(--_text); margin-top: 4px; }
231
262
  .rating-num { font-weight: 600; }
232
- .rating-label { color: var(--ccp-muted); }
263
+ .rating-label { color: var(--_muted); }
233
264
  .clear {
234
- background: transparent; border: 0; color: var(--ccp-muted); cursor: pointer;
265
+ background: transparent; border: 0; color: var(--_muted); cursor: pointer;
235
266
  font-size: 12px; margin-left: auto; text-decoration: underline;
236
267
  }
237
- .clear:hover { color: var(--ccp-accent); }
268
+ .clear:hover { color: var(--_accent); }
238
269
 
239
270
  textarea {
240
- width: 100%; resize: vertical; font: inherit; color: var(--ccp-text);
241
- background: #fff; border: 1px solid var(--ccp-border); border-radius: 8px;
271
+ width: 100%; resize: vertical; font: inherit; color: var(--_text);
272
+ background: var(--ccp-input-bg, #fff); border: 1px solid var(--_border); border-radius: 8px;
242
273
  padding: 8px 10px;
243
274
  }
244
- textarea:focus { outline: none; border-color: var(--ccp-accent); }
245
- .counter { position: absolute; right: 8px; bottom: 8px; font-size: 11px; color: var(--ccp-muted); background: #fff; padding: 0 4px; }
275
+ textarea:focus { outline: none; border-color: var(--_accent); }
276
+ .counter { position: absolute; right: 8px; bottom: 8px; font-size: 11px; color: var(--_muted); background: var(--ccp-input-bg, #fff); padding: 0 4px; }
246
277
 
247
278
  /* ----- Web of Trust + Cloud ----- */
248
- .panel { background: var(--ccp-bg-3); border-radius: 12px; padding: 14px; }
279
+ .panel { background: var(--_bg-3); border-radius: 12px; padding: 14px; }
249
280
  .panel + .panel { margin-top: 12px; }
250
281
  .panel-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
251
- .panel-title { font-size: 12px; font-weight: 600; color: var(--ccp-text); text-transform: uppercase; letter-spacing: 0.05em; }
252
- .refresh { background: transparent; border: 0; color: var(--ccp-muted); cursor: pointer; font-size: 16px; width: 24px; height: 24px; border-radius: 6px; }
253
- .refresh:hover { background: var(--ccp-bg-4); color: var(--ccp-text); }
254
- .empty { font-size: 13px; color: var(--ccp-muted); font-style: italic; }
255
- .weak { font-size: 13px; color: var(--ccp-muted); }
282
+ .panel-title { font-size: 12px; font-weight: 600; color: var(--_text); text-transform: uppercase; letter-spacing: 0.05em; }
283
+ .refresh { background: transparent; border: 0; color: var(--_muted); cursor: pointer; font-size: 16px; width: 24px; height: 24px; border-radius: 6px; }
284
+ .refresh:hover { background: var(--_bg-4); color: var(--_text); }
285
+ .empty { font-size: 13px; color: var(--_muted); font-style: italic; }
286
+ .weak { font-size: 13px; color: var(--_muted); }
256
287
  .summary { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
257
288
  .summary:last-child { margin-bottom: 0; }
258
289
  .stars { font-size: 16px; letter-spacing: 1px; }
259
- .stars.derived { color: var(--ccp-derived); }
260
- .stars.afin { color: var(--ccp-affinity); }
261
- .num { font-family: var(--ccp-font-headline); font-weight: 600; font-size: 14px; color: var(--ccp-text); }
262
- .count { font-size: 12px; color: var(--ccp-muted); }
263
- .ind { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--ccp-muted); min-width: 64px; }
290
+ .stars.derived { color: var(--_derived); }
291
+ .stars.afin { color: var(--_affinity); }
292
+ .num { font-family: var(--_font-headline); font-weight: 600; font-size: 14px; color: var(--_text); }
293
+ .count { font-size: 12px; color: var(--_muted); }
294
+ .ind { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--_muted); min-width: 64px; }
264
295
 
265
296
  .endorsements { list-style: none; padding: 0; margin: 0; max-height: 130px; overflow-y: auto; }
266
297
  .endorsements li {
267
298
  display: flex; gap: 8px; align-items: center; font-size: 12.5px;
268
- padding: 6px 0; border-bottom: 1px solid var(--ccp-border);
299
+ padding: 6px 0; border-bottom: 1px solid var(--_border);
269
300
  }
270
301
  .endorsements li:last-child { border-bottom: 0; }
271
- .endorsements .key { background: var(--ccp-bg-2); padding: 1px 6px; border-radius: 4px; font-family: var(--ccp-font-mono); font-size: 11px; color: var(--ccp-muted); }
272
- .endorsements .r { color: var(--ccp-derived); }
273
- .endorsements .when { color: var(--ccp-muted); margin-left: auto; font-size: 11px; }
302
+ .endorsements .key { background: var(--_bg-2); padding: 1px 6px; border-radius: 4px; font-family: var(--_font-mono); font-size: 11px; color: var(--_muted); }
303
+ .endorsements .r { color: var(--_derived); }
304
+ .endorsements .when { color: var(--_muted); margin-left: auto; font-size: 11px; }
274
305
 
275
- .privacy { margin: 0; font-size: 12px; color: var(--ccp-muted); text-align: center; line-height: 1.5; }
276
- .error { margin: 0; font-size: 13px; color: var(--ccp-accent); font-weight: 500; }
306
+ .privacy { margin: 0; font-size: 12px; color: var(--_muted); text-align: center; line-height: 1.5; }
307
+ .error { margin: 0; font-size: 13px; color: var(--_accent); font-weight: 500; }
277
308
 
278
309
  /* ----- Footer ----- */
279
- .foot { display: flex; gap: 10px; justify-content: flex-end; padding: 14px 24px; background: var(--ccp-bg-2); border-top: 1px solid var(--ccp-border); }
310
+ .foot { display: flex; gap: 10px; justify-content: flex-end; padding: 14px 24px; background: var(--_bg-2); border-top: 1px solid var(--_border); }
280
311
  .btn { font: inherit; font-weight: 600; padding: 9px 16px; border-radius: 10px; border: 1px solid transparent; cursor: pointer; }
281
- .btn.primary { background: var(--ccp-accent); color: #fff; }
282
- .btn.primary:hover:not(:disabled) { background: var(--ccp-accent-2); }
312
+ .btn.primary { background: var(--_accent); color: #fff; }
313
+ .btn.primary:hover:not(:disabled) { background: var(--_accent-2); }
283
314
  .btn.primary:disabled { opacity: 0.6; cursor: default; }
284
- .btn.secondary { background: transparent; color: var(--ccp-text); border-color: var(--ccp-border); }
285
- .btn.secondary:hover { background: var(--ccp-bg-3); }
315
+ .btn.secondary { background: transparent; color: var(--_text); border-color: var(--_border); }
316
+ .btn.secondary:hover { background: var(--_bg-3); }
286
317
  `
287
318
 
288
319
  class CloserClickProfile extends HTMLElement {
@@ -303,6 +334,9 @@ class CloserClickProfile extends HTMLElement {
303
334
  this._hoverAfin = 0
304
335
  this._saving = false
305
336
  this._error = ''
337
+ this._savingName = false
338
+ this._nameSaved = false
339
+ this._nameErr = ''
306
340
  this._loadToken = 0
307
341
  this._onKeydown = this._onKeydown.bind(this)
308
342
  }
@@ -340,7 +374,12 @@ class CloserClickProfile extends HTMLElement {
340
374
  }
341
375
  get _t() { return I18N[this._lang] }
342
376
  get _pubkey() { return this.getAttribute('pubkey') || '' }
343
- get _editable() { return (this.getAttribute('mode') || 'edit') !== 'view' }
377
+ get _mode() { return (this.getAttribute('mode') || 'edit').toLowerCase() }
378
+ // Editor de calificación (confianza/afinidad/notas): solo en 'edit'.
379
+ get _editable() { return this._mode === 'edit' }
380
+ // 'self' = tu propio perfil: nombre editable (se escribe al vault), sin
381
+ // calificación (no te calificas a ti mismo).
382
+ get _self() { return this._mode === 'self' }
344
383
 
345
384
  _resetState() {
346
385
  this._my = { confianza: 0, afinidad: 0, notes: '' }
@@ -351,6 +390,8 @@ class CloserClickProfile extends HTMLElement {
351
390
  this._hover = 0
352
391
  this._hoverAfin = 0
353
392
  this._error = ''
393
+ this._nameSaved = false
394
+ this._nameErr = ''
354
395
  }
355
396
 
356
397
  /* ---- carga de datos vía provider ---- */
@@ -412,6 +453,32 @@ class CloserClickProfile extends HTMLElement {
412
453
  }
413
454
  }
414
455
 
456
+ // Guarda tu nombre visible (mode="self") en el vault vía provider.setMyName.
457
+ async _saveName() {
458
+ if (this._savingName) return
459
+ const p = this._provider
460
+ const input = this.shadowRoot.querySelector('.nick-input')
461
+ if (!input || !p || typeof p.setMyName !== 'function') return
462
+ const name = (input.value || '').trim()
463
+ if (!name) return
464
+ if (name === (this.getAttribute('name') || '')) { this._nameSaved = true; this._render(); return }
465
+ this._savingName = true
466
+ this._nameErr = ''
467
+ this._render()
468
+ try {
469
+ await p.setMyName(name)
470
+ this._savingName = false
471
+ this._nameSaved = true
472
+ this._emit('cc-profile-name', { pubkey: this._pubkey, name })
473
+ this.setAttribute('name', name) // refleja el nombre nuevo (re-render por attributeChangedCallback)
474
+ this._render()
475
+ } catch (e) {
476
+ this._savingName = false
477
+ this._nameErr = (e && e.message) || this._t.saveError
478
+ this._render()
479
+ }
480
+ }
481
+
415
482
  _close() { this._emit('cc-profile-close', { pubkey: this._pubkey }) }
416
483
  _refresh() { this._emit('cc-profile-refresh', { pubkey: this._pubkey }); this.reload() }
417
484
  _emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true })) }
@@ -465,7 +532,7 @@ class CloserClickProfile extends HTMLElement {
465
532
  const name = this.getAttribute('name') || t.contact
466
533
  const since = this.getAttribute('since')
467
534
  const online = this.hasAttribute('online') && this.getAttribute('online') !== 'false'
468
- const heading = this.getAttribute('heading') || (editable ? t.headingEdit : t.headingView)
535
+ const heading = this.getAttribute('heading') || (this._self ? t.headingSelf : editable ? t.headingEdit : t.headingView)
469
536
 
470
537
  const confLabel = t.labels[this._hover || this._my.confianza] || t.labels[0]
471
538
 
@@ -477,7 +544,16 @@ class CloserClickProfile extends HTMLElement {
477
544
  ${online ? '<span class="online-dot"></span>' : ''}
478
545
  </div>
479
546
  <div class="identity-text">
480
- <div class="name">${this._esc(name)}</div>
547
+ ${this._self ? `
548
+ <label class="nick-edit">
549
+ <span class="nick-label">${this._esc(t.editName)}</span>
550
+ <div class="nick-row">
551
+ <input class="nick-input" type="text" maxlength="40" value="${this._esc(name === t.contact ? '' : name)}" placeholder="${this._esc(t.nickPh)}" />
552
+ <button type="button" class="btn primary nick-save" data-savename ${this._savingName ? 'disabled' : ''}>${this._esc(this._savingName ? t.saving : t.saveName)}</button>
553
+ </div>
554
+ ${this._nameSaved ? `<span class="nick-saved">${this._esc(t.nameSaved)}</span>` : ''}
555
+ ${this._nameErr ? `<span class="error">${this._esc(this._nameErr)}</span>` : ''}
556
+ </label>` : `<div class="name">${this._esc(name)}</div>`}
481
557
  <code class="pubkey">${this._esc(this._shortKey(pk))}</code>
482
558
  ${since ? `<div class="since">${this._esc(t.knownSince)} ${this._esc(this._fmtDate(since))}</div>` : ''}
483
559
  </div>
@@ -626,6 +702,15 @@ class CloserClickProfile extends HTMLElement {
626
702
  const refresh = q('[data-refresh]'); if (refresh) refresh.addEventListener('click', () => this._refresh())
627
703
  const reload = q('[data-reload]'); if (reload) reload.addEventListener('click', () => this.reload())
628
704
 
705
+ if (this._self) {
706
+ const savename = q('[data-savename]'); if (savename) savename.addEventListener('click', () => this._saveName())
707
+ const nickInput = q('.nick-input')
708
+ if (nickInput) {
709
+ nickInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._saveName() } })
710
+ nickInput.addEventListener('input', () => { this._nameSaved = false; this._nameErr = '' })
711
+ }
712
+ }
713
+
629
714
  if (this._editable) {
630
715
  qa('[data-star]').forEach(btn => {
631
716
  const kind = btn.getAttribute('data-star')
@@ -728,6 +813,16 @@ export function createVaultProfileProvider({ identity, reputation } = {}) {
728
813
  async rate(pubkey, indicators, notes) {
729
814
  return reputation.rate(pubkey, indicators, { notes })
730
815
  },
816
+
817
+ // --- Tu propia identidad (para mode="self") ---
818
+ myPubkey,
819
+ getMyName() { return (identity && identity.me && identity.me.nickname) || null },
820
+ async setMyName(name) {
821
+ if (!identity || typeof identity.setMyNickname !== 'function') {
822
+ throw new Error('closer-click-profile: identity.setMyNickname no disponible')
823
+ }
824
+ return identity.setMyNickname(name)
825
+ },
731
826
  }
732
827
  }
733
828