@closerclick/closer-click-profile 0.1.1 → 0.2.1

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 +97 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closerclick/closer-click-profile",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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
  }
@@ -138,6 +151,9 @@ const STYLE = `
138
151
  --_muted: var(--ccp-muted, var(--muted, #8a7a66));
139
152
  --_accent: var(--ccp-accent, var(--accent, #c0392b));
140
153
  --_accent-2: var(--ccp-accent-2, var(--accent-2, #a93226));
154
+ /* Texto sobre el acento (botón primario). Default blanco; las apps con un
155
+ acento claro (p. ej. lima) lo ponen oscuro vía --ccp-accent-text. */
156
+ --_accent-text: var(--ccp-accent-text, #fff);
141
157
  --_gold: var(--ccp-gold, var(--gold, #d4a72c));
142
158
  --_derived: var(--ccp-derived, var(--derived, #a37a45));
143
159
  --_online: var(--ccp-online, var(--online, #5a8a3a));
@@ -211,6 +227,18 @@ const STYLE = `
211
227
  }
212
228
  .identity-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
213
229
  .name { font-family: var(--_font-headline); font-weight: 600; font-size: 17px; color: var(--_text); }
230
+ /* ----- Editor de tu propio nombre (mode="self") ----- */
231
+ .nick-edit { display: flex; flex-direction: column; gap: 6px; }
232
+ .nick-label { font-size: 12px; color: var(--_muted); }
233
+ .nick-row { display: flex; gap: 8px; align-items: center; }
234
+ .nick-input {
235
+ flex: 1; min-width: 0; font: inherit; font-size: 15px; color: var(--_text);
236
+ background: var(--ccp-input-bg, #fff); border: 1px solid var(--_border); border-radius: 8px;
237
+ padding: 8px 10px;
238
+ }
239
+ .nick-input:focus { outline: none; border-color: var(--_accent); }
240
+ .nick-save { white-space: nowrap; }
241
+ .nick-saved { font-size: 12px; color: var(--_online); }
214
242
  .pubkey {
215
243
  background: var(--_bg-3); padding: 2px 8px; border-radius: 6px;
216
244
  font-family: var(--_font-mono); font-size: 11.5px; color: var(--_muted);
@@ -284,7 +312,7 @@ const STYLE = `
284
312
  /* ----- Footer ----- */
285
313
  .foot { display: flex; gap: 10px; justify-content: flex-end; padding: 14px 24px; background: var(--_bg-2); border-top: 1px solid var(--_border); }
286
314
  .btn { font: inherit; font-weight: 600; padding: 9px 16px; border-radius: 10px; border: 1px solid transparent; cursor: pointer; }
287
- .btn.primary { background: var(--_accent); color: #fff; }
315
+ .btn.primary { background: var(--_accent); color: var(--_accent-text); }
288
316
  .btn.primary:hover:not(:disabled) { background: var(--_accent-2); }
289
317
  .btn.primary:disabled { opacity: 0.6; cursor: default; }
290
318
  .btn.secondary { background: transparent; color: var(--_text); border-color: var(--_border); }
@@ -309,6 +337,9 @@ class CloserClickProfile extends HTMLElement {
309
337
  this._hoverAfin = 0
310
338
  this._saving = false
311
339
  this._error = ''
340
+ this._savingName = false
341
+ this._nameSaved = false
342
+ this._nameErr = ''
312
343
  this._loadToken = 0
313
344
  this._onKeydown = this._onKeydown.bind(this)
314
345
  }
@@ -346,7 +377,12 @@ class CloserClickProfile extends HTMLElement {
346
377
  }
347
378
  get _t() { return I18N[this._lang] }
348
379
  get _pubkey() { return this.getAttribute('pubkey') || '' }
349
- get _editable() { return (this.getAttribute('mode') || 'edit') !== 'view' }
380
+ get _mode() { return (this.getAttribute('mode') || 'edit').toLowerCase() }
381
+ // Editor de calificación (confianza/afinidad/notas): solo en 'edit'.
382
+ get _editable() { return this._mode === 'edit' }
383
+ // 'self' = tu propio perfil: nombre editable (se escribe al vault), sin
384
+ // calificación (no te calificas a ti mismo).
385
+ get _self() { return this._mode === 'self' }
350
386
 
351
387
  _resetState() {
352
388
  this._my = { confianza: 0, afinidad: 0, notes: '' }
@@ -357,6 +393,8 @@ class CloserClickProfile extends HTMLElement {
357
393
  this._hover = 0
358
394
  this._hoverAfin = 0
359
395
  this._error = ''
396
+ this._nameSaved = false
397
+ this._nameErr = ''
360
398
  }
361
399
 
362
400
  /* ---- carga de datos vía provider ---- */
@@ -418,6 +456,32 @@ class CloserClickProfile extends HTMLElement {
418
456
  }
419
457
  }
420
458
 
459
+ // Guarda tu nombre visible (mode="self") en el vault vía provider.setMyName.
460
+ async _saveName() {
461
+ if (this._savingName) return
462
+ const p = this._provider
463
+ const input = this.shadowRoot.querySelector('.nick-input')
464
+ if (!input || !p || typeof p.setMyName !== 'function') return
465
+ const name = (input.value || '').trim()
466
+ if (!name) return
467
+ if (name === (this.getAttribute('name') || '')) { this._nameSaved = true; this._render(); return }
468
+ this._savingName = true
469
+ this._nameErr = ''
470
+ this._render()
471
+ try {
472
+ await p.setMyName(name)
473
+ this._savingName = false
474
+ this._nameSaved = true
475
+ this._emit('cc-profile-name', { pubkey: this._pubkey, name })
476
+ this.setAttribute('name', name) // refleja el nombre nuevo (re-render por attributeChangedCallback)
477
+ this._render()
478
+ } catch (e) {
479
+ this._savingName = false
480
+ this._nameErr = (e && e.message) || this._t.saveError
481
+ this._render()
482
+ }
483
+ }
484
+
421
485
  _close() { this._emit('cc-profile-close', { pubkey: this._pubkey }) }
422
486
  _refresh() { this._emit('cc-profile-refresh', { pubkey: this._pubkey }); this.reload() }
423
487
  _emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true })) }
@@ -471,7 +535,7 @@ class CloserClickProfile extends HTMLElement {
471
535
  const name = this.getAttribute('name') || t.contact
472
536
  const since = this.getAttribute('since')
473
537
  const online = this.hasAttribute('online') && this.getAttribute('online') !== 'false'
474
- const heading = this.getAttribute('heading') || (editable ? t.headingEdit : t.headingView)
538
+ const heading = this.getAttribute('heading') || (this._self ? t.headingSelf : editable ? t.headingEdit : t.headingView)
475
539
 
476
540
  const confLabel = t.labels[this._hover || this._my.confianza] || t.labels[0]
477
541
 
@@ -483,7 +547,16 @@ class CloserClickProfile extends HTMLElement {
483
547
  ${online ? '<span class="online-dot"></span>' : ''}
484
548
  </div>
485
549
  <div class="identity-text">
486
- <div class="name">${this._esc(name)}</div>
550
+ ${this._self ? `
551
+ <label class="nick-edit">
552
+ <span class="nick-label">${this._esc(t.editName)}</span>
553
+ <div class="nick-row">
554
+ <input class="nick-input" type="text" maxlength="40" value="${this._esc(name === t.contact ? '' : name)}" placeholder="${this._esc(t.nickPh)}" />
555
+ <button type="button" class="btn primary nick-save" data-savename ${this._savingName ? 'disabled' : ''}>${this._esc(this._savingName ? t.saving : t.saveName)}</button>
556
+ </div>
557
+ ${this._nameSaved ? `<span class="nick-saved">${this._esc(t.nameSaved)}</span>` : ''}
558
+ ${this._nameErr ? `<span class="error">${this._esc(this._nameErr)}</span>` : ''}
559
+ </label>` : `<div class="name">${this._esc(name)}</div>`}
487
560
  <code class="pubkey">${this._esc(this._shortKey(pk))}</code>
488
561
  ${since ? `<div class="since">${this._esc(t.knownSince)} ${this._esc(this._fmtDate(since))}</div>` : ''}
489
562
  </div>
@@ -632,6 +705,15 @@ class CloserClickProfile extends HTMLElement {
632
705
  const refresh = q('[data-refresh]'); if (refresh) refresh.addEventListener('click', () => this._refresh())
633
706
  const reload = q('[data-reload]'); if (reload) reload.addEventListener('click', () => this.reload())
634
707
 
708
+ if (this._self) {
709
+ const savename = q('[data-savename]'); if (savename) savename.addEventListener('click', () => this._saveName())
710
+ const nickInput = q('.nick-input')
711
+ if (nickInput) {
712
+ nickInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._saveName() } })
713
+ nickInput.addEventListener('input', () => { this._nameSaved = false; this._nameErr = '' })
714
+ }
715
+ }
716
+
635
717
  if (this._editable) {
636
718
  qa('[data-star]').forEach(btn => {
637
719
  const kind = btn.getAttribute('data-star')
@@ -734,6 +816,16 @@ export function createVaultProfileProvider({ identity, reputation } = {}) {
734
816
  async rate(pubkey, indicators, notes) {
735
817
  return reputation.rate(pubkey, indicators, { notes })
736
818
  },
819
+
820
+ // --- Tu propia identidad (para mode="self") ---
821
+ myPubkey,
822
+ getMyName() { return (identity && identity.me && identity.me.nickname) || null },
823
+ async setMyName(name) {
824
+ if (!identity || typeof identity.setMyNickname !== 'function') {
825
+ throw new Error('closer-click-profile: identity.setMyNickname no disponible')
826
+ }
827
+ return identity.setMyNickname(name)
828
+ },
737
829
  }
738
830
  }
739
831