@nuniapp/widget 0.1.6 → 0.1.7

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/dist/index.js CHANGED
@@ -165,7 +165,7 @@ function buildCommentPrompt(comment, options = {}) {
165
165
  }
166
166
  //#endregion
167
167
  //#region src/config.ts
168
- const VERSION = "0.1.6";
168
+ const VERSION = "0.1.7";
169
169
  function resolveConfig(options) {
170
170
  return {
171
171
  project: options.project,
@@ -5567,14 +5567,15 @@ const EN = {
5567
5567
  yourName: "Your name",
5568
5568
  leaveComment: "Leave a comment",
5569
5569
  notYou: "Not you?",
5570
- screenshotAlt: "Screenshot that will be attached",
5571
- screenshotNote: "Screenshot attached. Only the site owner sees it.",
5572
- remove: "Remove",
5573
5570
  cancel: "Cancel",
5574
5571
  post: "Post",
5575
5572
  posting: "Posting…",
5576
5573
  save: "Save",
5577
5574
  send: "{key} + Enter",
5575
+ charsLeft: {
5576
+ one: "{count} character left",
5577
+ other: "{count} characters left"
5578
+ },
5578
5579
  pinLabel: "Comment by {name}: {body}",
5579
5580
  commentBy: "Comment by {name}",
5580
5581
  openScreenshot: "Open the screenshot",
@@ -5687,6 +5688,8 @@ const ICONS = {
5687
5688
  send: s("<path d=\"m22 2-7 20-4-9-9-4z\"/><path d=\"M22 2 11 13\"/>"),
5688
5689
  bot: s("<path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/>"),
5689
5690
  smile: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01\"/>"),
5691
+ user: s("<circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/>"),
5692
+ arrowUp: s("<path d=\"M12 19V5M5 12l7-7 7 7\"/>"),
5690
5693
  target: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>"),
5691
5694
  github: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 .5a11.5 11.5 0 0 0-3.6 22.4c.6.1.8-.3.8-.6v-2c-3.2.7-3.9-1.5-3.9-1.5-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.9 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A11.5 11.5 0 0 0 12 .5z"/></svg>`,
5692
5695
  link: s("<path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>")
@@ -6226,59 +6229,34 @@ button { cursor: pointer; }
6226
6229
  /* Cards (composer + thread) */
6227
6230
  .card {
6228
6231
  position: fixed;
6229
- width: 320px;
6232
+ width: 340px;
6230
6233
  max-width: calc(100vw - 24px);
6231
6234
  background: var(--n-bg);
6232
6235
  color: var(--n-fg);
6233
6236
  border: 1px solid var(--n-border);
6234
- border-radius: var(--n-radius);
6237
+ border-radius: 14px;
6235
6238
  box-shadow: var(--n-shadow);
6236
6239
  pointer-events: auto;
6237
6240
  overflow: hidden;
6238
6241
  }
6239
- .card-body { padding: 12px; display: grid; gap: 10px; }
6242
+ .card-body { padding: 12px 14px; display: grid; gap: 10px; }
6240
6243
  .card[data-card="thread"] { display: flex; flex-direction: column; max-height: calc(100vh - 24px); }
6241
- .thread { border-top: 1px solid var(--n-border); display: grid; min-height: 0; }
6242
- .replies { display: grid; gap: 12px; padding: 12px; overflow-y: auto; max-height: 45vh; }
6243
- .reply { display: grid; gap: 6px; }
6244
- .avatar-sm { width: 20px; height: 20px; font-size: 9px; }
6245
- .badge-owner { background: var(--n-accent-soft); color: var(--n-accent); }
6246
- .reply-form { display: flex; align-items: flex-end; gap: 8px; padding: 8px 12px 12px; flex-wrap: wrap; }
6247
- .reply-form .field { flex: 1 1 100%; }
6248
- /* A fixed size: growing on focus would move the Send button mid-click. */
6249
- .reply-form .field-reply { flex: 1 1 0; min-height: 56px; height: 56px; resize: none; }
6250
- .reactions { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; position: relative; }
6251
- .reaction {
6252
- display: inline-flex;
6253
- align-items: center;
6254
- gap: 4px;
6255
- height: 24px;
6256
- padding: 0 8px;
6257
- border: 1px solid var(--n-border);
6258
- border-radius: 999px;
6259
- background: var(--n-bg);
6244
+ .card .quote {
6245
+ margin: 0;
6246
+ padding: 6px 10px;
6247
+ border-left: 0;
6248
+ border-radius: 8px;
6249
+ background: var(--n-accent-soft);
6260
6250
  color: var(--n-fg);
6261
- font-size: 12px;
6262
- }
6263
- .reaction:hover { background: var(--n-bg-subtle); }
6264
- .reaction[aria-pressed="true"] { border-color: var(--n-accent); background: var(--n-accent-soft); }
6265
- .reaction-add { color: var(--n-fg-muted); padding: 0 6px; }
6266
- .reaction-picker {
6267
- display: flex;
6268
- gap: 2px;
6269
- padding: 4px;
6270
- border: 1px solid var(--n-border);
6271
- border-radius: 999px;
6272
- background: var(--n-bg);
6273
- box-shadow: var(--n-shadow);
6251
+ font-style: normal;
6252
+ font-size: 13px;
6253
+ box-shadow: inset 2px 0 0 var(--n-accent);
6274
6254
  }
6275
- .reaction-picker .reaction { border-color: transparent; font-size: 14px; }
6276
- .btn-xs { width: 24px; height: 24px; }
6277
- .card-head { display: flex; align-items: center; gap: 8px; }
6255
+ .card-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
6278
6256
  .card-head .spacer { flex: 1; }
6279
6257
  .avatar {
6280
- width: 24px;
6281
- height: 24px;
6258
+ width: 26px;
6259
+ height: 26px;
6282
6260
  border-radius: 999px;
6283
6261
  color: #fff;
6284
6262
  font-size: 10px;
@@ -6288,8 +6266,8 @@ button { cursor: pointer; }
6288
6266
  justify-content: center;
6289
6267
  flex: none;
6290
6268
  }
6291
- .author { font-weight: 600; }
6292
- .meta { color: var(--n-fg-muted); font-size: 12px; }
6269
+ .author { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6270
+ .meta { color: var(--n-fg-muted); font-size: 12px; white-space: nowrap; }
6293
6271
  .badge {
6294
6272
  display: inline-flex;
6295
6273
  align-items: center;
@@ -6304,28 +6282,147 @@ button { cursor: pointer; }
6304
6282
  text-overflow: ellipsis;
6305
6283
  white-space: nowrap;
6306
6284
  }
6285
+ .badge .icon { width: 12px; height: 12px; }
6307
6286
  .badge-ok { background: rgba(31,157,85,0.14); color: var(--n-ok); }
6308
- .comment-body { white-space: pre-wrap; word-break: break-word; }
6287
+ .badge-owner { background: var(--n-accent-soft); color: var(--n-accent); }
6288
+ .comment-body { white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
6309
6289
  .shot {
6310
6290
  display: block;
6311
6291
  border: 1px solid var(--n-border);
6312
- border-radius: 8px;
6292
+ border-radius: 10px;
6313
6293
  overflow: hidden;
6314
6294
  background: var(--n-bg-subtle);
6315
6295
  }
6316
6296
  .shot img { display: block; width: 100%; max-height: 160px; object-fit: cover; object-position: top; }
6317
- .shot-preview { display: grid; gap: 4px; }
6318
- .shot-preview img {
6319
- display: block;
6320
- width: 100%;
6321
- max-height: 200px;
6322
- /* The whole image, so the commenter sees everything that is attached. */
6323
- object-fit: contain;
6324
- border: 1px solid var(--n-border);
6297
+ .icon-btn {
6298
+ display: inline-grid;
6299
+ place-items: center;
6300
+ width: 28px;
6301
+ height: 28px;
6302
+ padding: 0;
6303
+ border: 0;
6325
6304
  border-radius: 8px;
6326
- background: var(--n-bg-subtle);
6305
+ background: none;
6306
+ color: var(--n-fg-muted);
6307
+ flex: none;
6308
+ transition: background 120ms ease, color 120ms ease;
6327
6309
  }
6328
- .shot-note { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--n-fg-muted); }
6310
+ .icon-btn:hover { background: var(--n-bg-subtle); color: var(--n-fg); }
6311
+ .icon-btn:disabled { opacity: 0.4; cursor: default; background: none; }
6312
+ .icon-btn-danger:hover { background: color-mix(in srgb, var(--n-danger) 12%, transparent); color: var(--n-danger); }
6313
+ .icon-btn.is-resolved, .icon-btn.is-resolved:hover { background: rgba(31,157,85,0.14); color: var(--n-ok); }
6314
+
6315
+ /* A message (the comment or a reply): text lines up with the name. */
6316
+ .msg { gap: 2px; padding: 12px 8px 8px 14px; }
6317
+ .msg-main { display: grid; gap: 8px; padding: 0 6px 0 34px; min-width: 0; }
6318
+ .msg .card-head { min-height: 28px; }
6319
+ .badges { display: flex; flex-wrap: wrap; gap: 4px; }
6320
+ .msg-foot { display: flex; align-items: center; gap: 2px; margin-right: -6px; }
6321
+ .msg-foot .spacer { flex: 1; }
6322
+ /* A reply's tools float over its header on hover, so names never truncate. */
6323
+ .msg-tools:empty { display: none; }
6324
+ .msg-tools {
6325
+ position: absolute;
6326
+ top: 4px;
6327
+ right: 8px;
6328
+ display: flex;
6329
+ gap: 2px;
6330
+ padding: 2px;
6331
+ border: 1px solid var(--n-border);
6332
+ border-radius: 10px;
6333
+ background: var(--n-bg);
6334
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.08);
6335
+ opacity: 0;
6336
+ pointer-events: none;
6337
+ transition: opacity 120ms ease;
6338
+ }
6339
+ .reply:hover .msg-tools, .reply:focus-within .msg-tools { opacity: 1; pointer-events: auto; }
6340
+ @media (hover: none) {
6341
+ .msg-tools { position: static; opacity: 1; pointer-events: auto; padding: 0; border: 0; box-shadow: none; background: none; }
6342
+ }
6343
+
6344
+ /* Replies */
6345
+ .thread { border-top: 1px solid var(--n-border); display: grid; min-height: 0; }
6346
+ .replies { display: grid; padding: 4px 0; overflow-y: auto; max-height: 45vh; }
6347
+ .reply.msg { position: relative; display: grid; padding: 8px 8px 8px 14px; }
6348
+ .reply-form {
6349
+ display: grid;
6350
+ margin: 4px 12px 12px;
6351
+ border: 1px solid var(--n-border);
6352
+ border-radius: 12px;
6353
+ background: var(--n-bg);
6354
+ transition: box-shadow 140ms ease, border-color 140ms ease;
6355
+ }
6356
+ .reply-form:focus-within {
6357
+ border-color: color-mix(in srgb, var(--n-accent) 45%, var(--n-border));
6358
+ box-shadow: 0 0 0 3px var(--n-accent-soft);
6359
+ }
6360
+ .reply-name {
6361
+ padding: 8px 12px 6px;
6362
+ border: 0;
6363
+ border-bottom: 1px solid var(--n-border);
6364
+ background: none;
6365
+ outline: none;
6366
+ font-weight: 600;
6367
+ }
6368
+ .reply-row { display: flex; align-items: flex-end; gap: 4px; padding: 4px; }
6369
+ .reply-text {
6370
+ flex: 1;
6371
+ min-width: 0;
6372
+ min-height: 32px;
6373
+ max-height: 160px;
6374
+ padding: 6px 8px;
6375
+ border: 0;
6376
+ background: none;
6377
+ outline: none;
6378
+ resize: none;
6379
+ line-height: 1.4;
6380
+ }
6381
+ .reply-name::placeholder, .reply-text::placeholder { color: var(--n-fg-muted); opacity: 1; font-weight: 400; }
6382
+ .reply-send {
6383
+ display: inline-grid;
6384
+ place-items: center;
6385
+ width: 32px;
6386
+ height: 32px;
6387
+ border: 0;
6388
+ border-radius: 999px;
6389
+ background: var(--n-accent);
6390
+ color: var(--n-accent-fg);
6391
+ flex: none;
6392
+ }
6393
+ .reply-send:hover { filter: brightness(1.08); }
6394
+ .reply-send:disabled { opacity: 0.4; cursor: default; filter: none; }
6395
+
6396
+ /* Reactions */
6397
+ .reactions { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; position: relative; }
6398
+ .reaction {
6399
+ display: inline-flex;
6400
+ align-items: center;
6401
+ gap: 4px;
6402
+ height: 26px;
6403
+ padding: 0 8px;
6404
+ border: 1px solid var(--n-border);
6405
+ border-radius: 999px;
6406
+ background: var(--n-bg);
6407
+ color: var(--n-fg);
6408
+ font-size: 12px;
6409
+ }
6410
+ .reaction:hover { background: var(--n-bg-subtle); }
6411
+ .reaction[aria-pressed="true"] { border-color: color-mix(in srgb, var(--n-accent) 50%, transparent); background: var(--n-accent-soft); }
6412
+ .reaction-add { color: var(--n-fg-muted); padding: 0 6px; border-color: transparent; }
6413
+ .reaction-add:hover, .reaction-add[aria-expanded="true"] { color: var(--n-fg); }
6414
+ .reaction-picker {
6415
+ display: flex;
6416
+ gap: 2px;
6417
+ padding: 4px;
6418
+ border: 1px solid var(--n-border);
6419
+ border-radius: 999px;
6420
+ background: var(--n-bg);
6421
+ box-shadow: var(--n-shadow);
6422
+ }
6423
+ .reaction-picker .reaction { border-color: transparent; font-size: 14px; }
6424
+
6425
+ /* Shared controls */
6329
6426
  .field {
6330
6427
  width: 100%;
6331
6428
  padding: 8px 10px;
@@ -6357,11 +6454,90 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
6357
6454
  .btn-ghost { border-color: transparent; background: transparent; }
6358
6455
  .btn-icon { width: 32px; padding: 0; justify-content: center; }
6359
6456
  .btn-danger { color: var(--n-danger); }
6360
- .kbd { color: var(--n-fg-muted); font-size: 11px; }
6361
6457
  .error { color: var(--n-danger); font-size: 12px; }
6362
6458
  .divider { height: 1px; background: var(--n-border); }
6363
- .actions { display: flex; gap: 4px; padding: 8px 12px; border-top: 1px solid var(--n-border); background: var(--n-bg-subtle); }
6364
- .actions .spacer { flex: 1; }
6459
+
6460
+ /* Composer: one surface, the card itself is the input. */
6461
+ .composer {
6462
+ display: grid;
6463
+ transition: box-shadow 140ms ease, border-color 140ms ease;
6464
+ }
6465
+ .composer:focus-within {
6466
+ border-color: color-mix(in srgb, var(--n-accent) 45%, var(--n-border));
6467
+ box-shadow: 0 0 0 4px var(--n-accent-soft), var(--n-shadow);
6468
+ }
6469
+ .composer.sheet:focus-within { border-color: var(--n-border); box-shadow: var(--n-shadow); }
6470
+ .composer .spacer { flex: 1; }
6471
+ .cmp-head { display: flex; align-items: center; gap: 8px; min-height: 48px; padding: 10px 44px 4px 14px; }
6472
+ .avatar-empty { background: var(--n-bg-subtle); color: var(--n-fg-muted); }
6473
+ .avatar-empty .icon { width: 14px; height: 14px; }
6474
+ .cmp-name {
6475
+ flex: 1;
6476
+ min-width: 0;
6477
+ padding: 0;
6478
+ border: 0;
6479
+ background: none;
6480
+ outline: none;
6481
+ font-weight: 600;
6482
+ }
6483
+ .cmp-link {
6484
+ border: 0;
6485
+ background: none;
6486
+ padding: 2px 4px;
6487
+ border-radius: 6px;
6488
+ color: var(--n-fg-muted);
6489
+ font-size: 12px;
6490
+ white-space: nowrap;
6491
+ }
6492
+ .cmp-link:hover { color: var(--n-fg); }
6493
+ .composer .quote { margin: 6px 14px 0; }
6494
+ .cmp-text {
6495
+ display: block;
6496
+ width: 100%;
6497
+ min-height: 76px;
6498
+ max-height: 240px;
6499
+ padding: 8px 14px;
6500
+ border: 0;
6501
+ background: none;
6502
+ outline: none;
6503
+ resize: none;
6504
+ line-height: 1.5;
6505
+ }
6506
+ .cmp-name::placeholder, .cmp-text::placeholder { color: var(--n-fg-muted); opacity: 1; }
6507
+ .cmp-name::placeholder { font-weight: 500; }
6508
+ .cmp-error { padding: 0 14px 4px; }
6509
+ .cmp-foot { display: flex; align-items: center; gap: 8px; padding: 6px 8px 8px 14px; }
6510
+ .cmp-keys { display: inline-flex; gap: 3px; }
6511
+ .cmp-keys kbd {
6512
+ display: inline-grid;
6513
+ place-items: center;
6514
+ min-width: 20px;
6515
+ height: 20px;
6516
+ padding: 0 4px;
6517
+ border: 1px solid var(--n-border);
6518
+ border-bottom-width: 2px;
6519
+ border-radius: 5px;
6520
+ color: var(--n-fg-muted);
6521
+ font: 500 11px/1 var(--n-font);
6522
+ }
6523
+ .cmp-count { color: var(--n-fg-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
6524
+ .cmp-post {
6525
+ display: inline-flex;
6526
+ align-items: center;
6527
+ gap: 6px;
6528
+ height: 32px;
6529
+ padding: 0 10px 0 14px;
6530
+ border: 0;
6531
+ border-radius: 999px;
6532
+ background: var(--n-accent);
6533
+ color: var(--n-accent-fg);
6534
+ font-weight: 600;
6535
+ font-size: 13px;
6536
+ }
6537
+ .cmp-post:hover { filter: brightness(1.08); }
6538
+ .cmp-post:disabled { opacity: 0.4; cursor: default; filter: none; }
6539
+ .cmp-close { position: absolute; top: 13px; right: 8px; }
6540
+ @media (hover: none) { .cmp-keys { display: none; } }
6365
6541
 
6366
6542
  /* Panel */
6367
6543
  .panel {
@@ -6509,13 +6685,11 @@ function isTypingTarget(target) {
6509
6685
  const el = target;
6510
6686
  return el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT";
6511
6687
  }
6512
- function dataUrl(blob) {
6513
- return new Promise((resolve, reject) => {
6514
- const reader = new FileReader();
6515
- reader.onload = () => resolve(String(reader.result));
6516
- reader.onerror = () => reject(reader.error);
6517
- reader.readAsDataURL(blob);
6518
- });
6688
+ function avatar(name) {
6689
+ return h("span", {
6690
+ class: "avatar",
6691
+ style: `background:${colorFor(name)}`
6692
+ }, initials(name));
6519
6693
  }
6520
6694
  function describeElement(el) {
6521
6695
  const tag = el.tagName.toLowerCase();
@@ -6599,6 +6773,7 @@ var NuniWidget = class {
6599
6773
  _defineProperty(this, "layoutFrame", 0);
6600
6774
  _defineProperty(this, "pinPositions", /* @__PURE__ */ new Map());
6601
6775
  _defineProperty(this, "resolveTimer", 0);
6776
+ _defineProperty(this, "shownPlacements", "");
6602
6777
  _defineProperty(this, "i18n", void 0);
6603
6778
  _defineProperty(this, "isIgnored", (el) => el === this.host || this.host.contains(el));
6604
6779
  _defineProperty(this, "onFrameKey", (e) => this.onKeyDown(e));
@@ -6960,10 +7135,18 @@ var NuniWidget = class {
6960
7135
  this.renderPins();
6961
7136
  this.layout();
6962
7137
  if (queue.length) whenIdle(slice);
6963
- else if ((onlyStale || run > 1) && this.panelOpen) this.render();
7138
+ else if ((onlyStale || run > 1) && this.panelOpen && this.placementSummary() !== this.shownPlacements) this.render();
6964
7139
  };
6965
7140
  slice();
6966
7141
  }
7142
+ /** What the panel and cards show from the placements: found, approximate or lost. */
7143
+ placementSummary() {
7144
+ return this.comments.map((c) => {
7145
+ const placement = this.placements.get(c._id);
7146
+ if (!placement?.element) return "-";
7147
+ return placement.confidence === "low" ? "~" : "+";
7148
+ }).join("");
7149
+ }
6967
7150
  /**
6968
7151
  * Pins inside shadow roots and iframes: the page's observers don't see
6969
7152
  * changes or scrolling in there, so watch each of those scopes too.
@@ -7245,12 +7428,12 @@ var NuniWidget = class {
7245
7428
  this.activeId = null;
7246
7429
  this.render();
7247
7430
  this.focusComposer();
7248
- this.takeScreenshot(draft);
7431
+ draft.screenshot = this.takeScreenshot(draft);
7249
7432
  }
7250
7433
  /** Capture while the person types, so posting stays instant. */
7251
7434
  async takeScreenshot(draft) {
7252
7435
  const load = this.runtime.loadScreenshot;
7253
- if (!load || draft.element.ownerDocument !== document) return;
7436
+ if (!load || draft.element.ownerDocument !== document) return null;
7254
7437
  const accent = getComputedStyle(this.host).getPropertyValue("--n-accent").trim() || "#d6246e";
7255
7438
  try {
7256
7439
  await new Promise((resolve) => setTimeout(resolve, 60));
@@ -7265,23 +7448,14 @@ var NuniWidget = class {
7265
7448
  bottom: box.top + box.height
7266
7449
  };
7267
7450
  } : void 0;
7268
- const blob = capture ? await capture(element, {
7451
+ return capture ? await capture(element, {
7269
7452
  exclude: this.host,
7270
7453
  accent,
7271
7454
  focus
7272
7455
  }) : null;
7273
- if (!blob || !this.isComposing(draft) || draft.sending) return;
7274
- const preview = await dataUrl(blob);
7275
- if (!this.isComposing(draft) || draft.sending) return;
7276
- draft.screenshot = {
7277
- blob,
7278
- preview
7279
- };
7280
- this.render();
7281
- } catch {}
7282
- }
7283
- isComposing(draft) {
7284
- return this.card?.kind === "composer" && this.card.draft === draft;
7456
+ } catch {
7457
+ return null;
7458
+ }
7285
7459
  }
7286
7460
  collectContext(draft) {
7287
7461
  const { collectors } = this.runtime;
@@ -7293,9 +7467,10 @@ var NuniWidget = class {
7293
7467
  if (draft.dom) context.dom = draft.dom;
7294
7468
  return Object.keys(context).length ? context : void 0;
7295
7469
  }
7296
- async attachScreenshot(id, image) {
7470
+ async attachScreenshot(id, shot) {
7297
7471
  try {
7298
- await this.api.uploadScreenshot(id, this.secret, image);
7472
+ const image = await shot;
7473
+ if (image) await this.api.uploadScreenshot(id, this.secret, image);
7299
7474
  } catch {}
7300
7475
  }
7301
7476
  copyForAgent(comment) {
@@ -7595,7 +7770,7 @@ var NuniWidget = class {
7595
7770
  },
7596
7771
  context: this.collectContext(draft)
7597
7772
  });
7598
- if (shot) this.attachScreenshot(id, shot.blob);
7773
+ if (shot) this.attachScreenshot(id, shot);
7599
7774
  this.placements.set(id, {
7600
7775
  element: draft.element,
7601
7776
  confidence: "exact",
@@ -7766,7 +7941,7 @@ var NuniWidget = class {
7766
7941
  kind: "thread",
7767
7942
  id
7768
7943
  };
7769
- this.focusNext = "[data-card=\"thread\"] .card-head button";
7944
+ this.focusNext = "[data-card=\"thread\"] .card-close";
7770
7945
  }
7771
7946
  render() {
7772
7947
  this.syncOwnerDetail();
@@ -7829,6 +8004,7 @@ var NuniWidget = class {
7829
8004
  }
7830
8005
  }
7831
8006
  renderUi() {
8007
+ this.shownPlacements = this.placementSummary();
7832
8008
  const nodes = [this.renderToolbar()];
7833
8009
  if (this.panelOpen) nodes.push(this.renderPanel());
7834
8010
  const card = this.renderCard();
@@ -7973,80 +8149,112 @@ var NuniWidget = class {
7973
8149
  onclick: () => this.openClaim()
7974
8150
  }, icon(ICONS.github), this.t("claim"))));
7975
8151
  }
8152
+ renderComposer(draft, cls) {
8153
+ const needsName = !this.name;
8154
+ const avatar = h("span", {
8155
+ class: "avatar cmp-avatar",
8156
+ "aria-hidden": "true"
8157
+ });
8158
+ const paintAvatar = (name) => {
8159
+ const clean = name.trim();
8160
+ avatar.classList.toggle("avatar-empty", !clean);
8161
+ if (clean) {
8162
+ avatar.textContent = initials(clean);
8163
+ avatar.style.background = colorFor(clean);
8164
+ } else {
8165
+ avatar.replaceChildren(icon(ICONS.user));
8166
+ avatar.style.background = "";
8167
+ }
8168
+ };
8169
+ const nameInput = h("input", {
8170
+ class: "cmp-name",
8171
+ placeholder: this.t("yourName"),
8172
+ value: this.name,
8173
+ maxlength: LIMITS.nameMaxLength,
8174
+ autocomplete: "name",
8175
+ "data-focus-key": "name",
8176
+ "aria-label": this.t("yourName")
8177
+ });
8178
+ const textarea = h("textarea", {
8179
+ class: "cmp-text",
8180
+ placeholder: this.t("leaveComment"),
8181
+ maxlength: LIMITS.bodyMaxLength,
8182
+ rows: 3,
8183
+ "data-focus-key": "body",
8184
+ "aria-label": this.t("comment")
8185
+ });
8186
+ const prev = this.uiLayer.querySelector("[data-focus-key=\"body\"]");
8187
+ if (prev) textarea.value = prev.value;
8188
+ const prevName = this.uiLayer.querySelector("[data-focus-key=\"name\"]");
8189
+ if (prevName) nameInput.value = prevName.value;
8190
+ paintAvatar(needsName ? nameInput.value : this.name);
8191
+ nameInput.addEventListener("input", () => paintAvatar(nameInput.value));
8192
+ const counter = h("span", {
8193
+ class: "cmp-count",
8194
+ "aria-live": "polite"
8195
+ });
8196
+ const post = h("button", {
8197
+ class: "cmp-post",
8198
+ type: "submit",
8199
+ disabled: Boolean(draft.sending) || !textarea.value.trim()
8200
+ }, draft.sending ? this.t("posting") : this.t("post"), icon(ICONS.arrowUp));
8201
+ const update = () => {
8202
+ post.disabled = Boolean(draft.sending) || !textarea.value.trim();
8203
+ const left = LIMITS.bodyMaxLength - textarea.value.length;
8204
+ counter.textContent = left <= LIMITS.bodyMaxLength * .1 ? this.t("charsLeft", { count: left }) : "";
8205
+ textarea.style.height = "auto";
8206
+ textarea.style.height = `${textarea.scrollHeight + 2}px`;
8207
+ };
8208
+ textarea.addEventListener("input", () => {
8209
+ update();
8210
+ this.positionCard();
8211
+ });
8212
+ requestAnimationFrame(() => {
8213
+ if (!textarea.isConnected) return;
8214
+ update();
8215
+ this.positionCard();
8216
+ });
8217
+ const submit = () => void this.submitDraft(draft, textarea.value, needsName ? nameInput.value : this.name);
8218
+ textarea.addEventListener("keydown", (e) => {
8219
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
8220
+ e.preventDefault();
8221
+ submit();
8222
+ }
8223
+ });
8224
+ return h("form", {
8225
+ class: `${cls} composer`,
8226
+ "data-card": "composer",
8227
+ onsubmit: (e) => {
8228
+ e.preventDefault();
8229
+ submit();
8230
+ }
8231
+ }, h("div", { class: "cmp-head" }, avatar, needsName ? nameInput : h("span", { class: "author" }, this.name), h("span", { class: "spacer" }), needsName ? null : h("button", {
8232
+ class: "cmp-link",
8233
+ type: "button",
8234
+ onclick: () => {
8235
+ this.name = "";
8236
+ this.render();
8237
+ this.focusComposer();
8238
+ }
8239
+ }, this.t("notYou"))), draft.anchor.quote ? h("blockquote", { class: "quote" }, draft.anchor.quote.exact) : null, textarea, draft.error ? h("div", {
8240
+ class: "error cmp-error",
8241
+ role: "alert"
8242
+ }, draft.error) : null, h("div", { class: "cmp-foot" }, h("span", {
8243
+ class: "cmp-keys",
8244
+ title: this.t("send", { key: this.i18n.mod })
8245
+ }, h("kbd", {}, this.i18n.mod), h("kbd", {}, "↵")), counter, h("span", { class: "spacer" }), post), h("button", {
8246
+ class: "icon-btn cmp-close",
8247
+ type: "button",
8248
+ "aria-label": this.t("cancel"),
8249
+ title: this.t("cancel"),
8250
+ onclick: () => this.closeCard()
8251
+ }, icon(ICONS.close)));
8252
+ }
7976
8253
  renderCard() {
7977
8254
  const card = this.card;
7978
8255
  if (!card) return null;
7979
8256
  const cls = `card${window.matchMedia(MOBILE_QUERY).matches ? " sheet" : ""}`;
7980
- if (card.kind === "composer") {
7981
- const { draft } = card;
7982
- const needsName = !this.name;
7983
- const nameInput = h("input", {
7984
- class: "field",
7985
- placeholder: this.t("yourName"),
7986
- value: this.name,
7987
- maxlength: LIMITS.nameMaxLength,
7988
- autocomplete: "name",
7989
- "data-focus-key": "name",
7990
- "aria-label": this.t("yourName")
7991
- });
7992
- const textarea = h("textarea", {
7993
- class: "field",
7994
- placeholder: this.t("leaveComment"),
7995
- maxlength: LIMITS.bodyMaxLength,
7996
- "data-focus-key": "body",
7997
- "aria-label": this.t("comment")
7998
- });
7999
- const prev = this.uiLayer.querySelector("[data-focus-key=\"body\"]");
8000
- if (prev) textarea.value = prev.value;
8001
- const prevName = this.uiLayer.querySelector("[data-focus-key=\"name\"]");
8002
- if (prevName) nameInput.value = prevName.value;
8003
- const submit = () => void this.submitDraft(draft, textarea.value, needsName ? nameInput.value : this.name);
8004
- textarea.addEventListener("keydown", (e) => {
8005
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
8006
- e.preventDefault();
8007
- submit();
8008
- }
8009
- });
8010
- return h("form", {
8011
- class: cls,
8012
- "data-card": "composer",
8013
- onsubmit: (e) => {
8014
- e.preventDefault();
8015
- submit();
8016
- }
8017
- }, h("div", { class: "card-body" }, needsName ? nameInput : h("div", { class: "row" }, h("span", {
8018
- class: "avatar",
8019
- style: `background:${colorFor(this.name)}`
8020
- }, initials(this.name)), h("span", { class: "author" }, this.name), h("span", { class: "spacer" }), h("button", {
8021
- class: "link",
8022
- type: "button",
8023
- onclick: () => {
8024
- this.name = "";
8025
- this.render();
8026
- }
8027
- }, this.t("notYou"))), draft.anchor.quote ? h("blockquote", { class: "quote" }, draft.anchor.quote.exact) : null, textarea, draft.screenshot ? h("div", { class: "shot-preview" }, h("img", {
8028
- src: draft.screenshot.preview,
8029
- alt: this.t("screenshotAlt")
8030
- }), h("div", { class: "shot-note" }, h("span", {}, this.t("screenshotNote")), h("span", { class: "spacer" }), h("button", {
8031
- class: "link",
8032
- type: "button",
8033
- onclick: () => {
8034
- draft.screenshot = void 0;
8035
- this.render();
8036
- }
8037
- }, this.t("remove")))) : null, draft.error ? h("div", {
8038
- class: "error",
8039
- role: "alert"
8040
- }, draft.error) : null, h("div", { class: "row" }, h("span", { class: "kbd" }, this.t("send", { key: this.i18n.mod })), h("span", { class: "spacer" }), h("button", {
8041
- class: "btn btn-ghost",
8042
- type: "button",
8043
- onclick: () => this.closeCard()
8044
- }, this.t("cancel")), h("button", {
8045
- class: "btn btn-primary",
8046
- type: "submit",
8047
- disabled: Boolean(draft.sending)
8048
- }, icon(ICONS.send), draft.sending ? this.t("posting") : this.t("post")))));
8049
- }
8257
+ if (card.kind === "composer") return this.renderComposer(card.draft, cls);
8050
8258
  const comment = this.comments.find((c) => c._id === card.id);
8051
8259
  if (!comment) return null;
8052
8260
  const mine = this.isMine(comment);
@@ -8074,61 +8282,55 @@ var NuniWidget = class {
8074
8282
  disabled: Boolean(card.busy),
8075
8283
  onclick: () => this.saveEdit(comment, textarea.value)
8076
8284
  }, this.t("save"))));
8077
- } else body = h("div", { class: "card-body" }, h("div", { class: "card-head" }, h("span", {
8078
- class: "avatar",
8079
- style: `background:${colorFor(comment.authorName)}`
8080
- }, initials(comment.authorName)), h("span", { class: "author" }, comment.authorName), h("span", { class: "meta" }, this.i18n.timeAgo(comment.createdAt) + (comment.editedAt ? ` · ${this.t("edited")}` : "")), h("span", { class: "spacer" }), h("button", {
8081
- class: "btn btn-ghost btn-icon",
8082
- type: "button",
8083
- "aria-label": this.t("close"),
8084
- onclick: () => this.closeCard()
8085
- }, icon(ICONS.close))), comment.anchor.quote ? h("blockquote", { class: "quote" }, comment.anchor.quote.exact) : null, h("div", { class: "comment-body" }, comment.body), this.ownerDetail?._id === comment._id && this.ownerDetail.screenshotUrl ? h("a", {
8086
- class: "shot",
8087
- href: this.ownerDetail.screenshotUrl,
8088
- target: "_blank",
8089
- rel: "noreferrer",
8090
- title: this.t("openScreenshot")
8091
- }, h("img", {
8092
- src: this.ownerDetail.screenshotUrl,
8093
- alt: this.t("screenshotTaken")
8094
- })) : null, h("div", { class: "row" }, comment.status === "resolved" ? h("span", { class: "badge badge-ok" }, icon(ICONS.check), this.t("resolved")) : null, comment.page.origin !== location.origin ? h("span", {
8095
- class: "badge",
8096
- title: this.t("leftOn", { origin: comment.page.origin })
8097
- }, new URL(comment.page.origin).host) : null, placement?.confidence === "low" ? h("span", {
8098
- class: "badge",
8099
- title: this.t("approximateHint")
8100
- }, this.t("approximate")) : null, !placement?.element ? h("span", {
8101
- class: "badge",
8102
- title: this.t("originallyOn", { text: comment.anchor.text || comment.anchor.tag })
8103
- }, this.t("notFound")) : null), this.renderReactions(comment, comment._id), card.error ? h("div", {
8104
- class: "error",
8105
- role: "alert"
8106
- }, card.error) : null);
8107
- const actions = [];
8108
- if (!card.editing) {
8109
- if (owner) actions.push(comment.status === "open" ? h("button", {
8110
- class: "btn",
8285
+ } else {
8286
+ const badges = [
8287
+ comment.status === "resolved" ? h("span", { class: "badge badge-ok" }, icon(ICONS.check), this.t("resolved")) : null,
8288
+ comment.page.origin !== location.origin ? h("span", {
8289
+ class: "badge",
8290
+ title: this.t("leftOn", { origin: comment.page.origin })
8291
+ }, new URL(comment.page.origin).host) : null,
8292
+ placement?.confidence === "low" ? h("span", {
8293
+ class: "badge",
8294
+ title: this.t("approximateHint")
8295
+ }, this.t("approximate")) : null,
8296
+ !placement?.element ? h("span", {
8297
+ class: "badge",
8298
+ title: this.t("originallyOn", { text: comment.anchor.text || comment.anchor.tag })
8299
+ }, this.t("notFound")) : null
8300
+ ].filter((badge) => badge !== null);
8301
+ const contextPending = owner && !(this.ownerDetailLoaded && this.ownerDetailId === comment._id);
8302
+ const resolved = comment.status === "resolved";
8303
+ body = h("div", { class: "card-body msg" }, h("div", { class: "card-head" }, avatar(comment.authorName), h("span", { class: "author" }, comment.authorName), h("span", { class: "meta" }, this.i18n.timeAgo(comment.createdAt) + (comment.editedAt ? ` · ${this.t("edited")}` : "")), h("span", { class: "spacer" }), owner ? h("button", {
8304
+ class: `icon-btn${resolved ? " is-resolved" : ""}`,
8111
8305
  type: "button",
8306
+ "aria-label": this.t(resolved ? "reopen" : "resolve"),
8307
+ title: this.t(resolved ? "reopen" : "resolve"),
8112
8308
  disabled: Boolean(card.busy),
8113
- onclick: () => this.resolveComment(comment)
8114
- }, icon(ICONS.check), this.t("resolve")) : h("button", {
8115
- class: "btn",
8309
+ onclick: () => resolved ? this.reopenComment(comment) : this.resolveComment(comment)
8310
+ }, icon(ICONS.check)) : null, h("button", {
8311
+ class: "icon-btn card-close",
8116
8312
  type: "button",
8117
- disabled: Boolean(card.busy),
8118
- onclick: () => this.reopenComment(comment)
8119
- }, icon(ICONS.undo), this.t("reopen")));
8120
- actions.push(h("span", { class: "spacer" }));
8121
- const contextPending = owner && !(this.ownerDetailLoaded && this.ownerDetailId === comment._id);
8122
- actions.push(h("button", {
8123
- class: "btn btn-ghost btn-icon",
8313
+ "aria-label": this.t("close"),
8314
+ title: this.t("close"),
8315
+ onclick: () => this.closeCard()
8316
+ }, icon(ICONS.close))), h("div", { class: "msg-main" }, comment.anchor.quote ? h("blockquote", { class: "quote" }, comment.anchor.quote.exact) : null, h("div", { class: "comment-body" }, comment.body), this.ownerDetail?._id === comment._id && this.ownerDetail.screenshotUrl ? h("a", {
8317
+ class: "shot",
8318
+ href: this.ownerDetail.screenshotUrl,
8319
+ target: "_blank",
8320
+ rel: "noreferrer",
8321
+ title: this.t("openScreenshot")
8322
+ }, h("img", {
8323
+ src: this.ownerDetail.screenshotUrl,
8324
+ alt: this.t("screenshotTaken")
8325
+ })) : null, badges.length ? h("div", { class: "badges" }, ...badges) : null, h("div", { class: "msg-foot" }, this.renderReactions(comment, comment._id), h("span", { class: "spacer" }), h("button", {
8326
+ class: "icon-btn",
8124
8327
  type: "button",
8125
8328
  "aria-label": this.t("copyForAgent"),
8126
8329
  title: contextPending ? this.t("loadingContext") : this.t("copyForAgentHint"),
8127
8330
  disabled: contextPending,
8128
8331
  onclick: () => this.copyForAgent(comment)
8129
- }, icon(ICONS.bot)));
8130
- if (mine) actions.push(h("button", {
8131
- class: "btn btn-ghost btn-icon",
8332
+ }, icon(ICONS.bot)), mine ? h("button", {
8333
+ class: "icon-btn",
8132
8334
  type: "button",
8133
8335
  "aria-label": this.t("edit"),
8134
8336
  title: this.t("edit"),
@@ -8136,30 +8338,36 @@ var NuniWidget = class {
8136
8338
  card.editing = true;
8137
8339
  this.render();
8138
8340
  }
8139
- }, icon(ICONS.edit)));
8140
- if (mine || owner) actions.push(h("button", {
8141
- class: "btn btn-ghost btn-icon btn-danger",
8341
+ }, icon(ICONS.edit)) : null, mine || owner ? h("button", {
8342
+ class: "icon-btn icon-btn-danger",
8142
8343
  type: "button",
8143
8344
  "aria-label": this.t("delete"),
8144
8345
  title: this.t("delete"),
8145
8346
  "data-focus-key": "delete",
8146
8347
  disabled: Boolean(card.busy),
8147
8348
  onclick: () => void this.deleteComment(comment)
8148
- }, icon(ICONS.trash)));
8349
+ }, icon(ICONS.trash)) : null), card.error ? h("div", {
8350
+ class: "error",
8351
+ role: "alert"
8352
+ }, card.error) : null));
8149
8353
  }
8150
8354
  return h("div", {
8151
8355
  class: cls,
8152
8356
  "data-card": "thread",
8153
8357
  role: "dialog",
8154
8358
  "aria-label": this.t("commentBy", { name: comment.authorName })
8155
- }, body, actions.length ? h("div", { class: "actions" }, ...actions) : null, card.editing ? null : this.renderThread(comment));
8359
+ }, body, card.editing ? null : this.renderThread(comment));
8156
8360
  }
8157
- /** Emoji chips for one message, plus a picker to add one. */
8158
- renderReactions(comment, targetId) {
8361
+ /**
8362
+ * Emoji chips for one message and the picker to add one. Replies keep
8363
+ * their add button with their other tools, so `withAdd` is off for them.
8364
+ */
8365
+ renderReactions(comment, targetId, withAdd = true) {
8159
8366
  const card = this.card;
8160
8367
  if (card?.kind !== "thread") return null;
8161
8368
  const summaries = this.threadId === comment._id && this.thread ? this.thread.reactions.filter((r) => r.targetId === targetId) : [];
8162
8369
  const open = card.picker === targetId;
8370
+ if (!withAdd && !summaries.length && !open) return null;
8163
8371
  return h("div", { class: "reactions" }, ...summaries.map((r) => {
8164
8372
  const mine = r.authorKeyHashes.includes(this.secretHash);
8165
8373
  return h("button", {
@@ -8169,17 +8377,7 @@ var NuniWidget = class {
8169
8377
  "aria-label": `${r.emoji} ${r.count}${mine ? ", including you" : ""}`,
8170
8378
  onclick: () => this.toggleReaction(comment, targetId, r.emoji)
8171
8379
  }, `${r.emoji} ${r.count}`);
8172
- }), h("button", {
8173
- class: "reaction reaction-add",
8174
- type: "button",
8175
- "aria-label": this.t("addReaction"),
8176
- "aria-expanded": String(open),
8177
- title: this.t("addReaction"),
8178
- onclick: () => {
8179
- card.picker = open ? null : targetId;
8180
- this.render();
8181
- }
8182
- }, icon(ICONS.smile)), open ? h("div", {
8380
+ }), withAdd ? this.reactionAdd(targetId, "reaction reaction-add") : null, open ? h("div", {
8183
8381
  class: "reaction-picker",
8184
8382
  role: "group",
8185
8383
  "aria-label": this.t("reactions")
@@ -8194,6 +8392,22 @@ var NuniWidget = class {
8194
8392
  }
8195
8393
  }, emoji))) : null);
8196
8394
  }
8395
+ reactionAdd(targetId, cls) {
8396
+ const card = this.card;
8397
+ if (card?.kind !== "thread") return null;
8398
+ const open = card.picker === targetId;
8399
+ return h("button", {
8400
+ class: cls,
8401
+ type: "button",
8402
+ "aria-label": this.t("addReaction"),
8403
+ "aria-expanded": String(open),
8404
+ title: this.t("addReaction"),
8405
+ onclick: () => {
8406
+ card.picker = open ? null : targetId;
8407
+ this.render();
8408
+ }
8409
+ }, icon(ICONS.smile));
8410
+ }
8197
8411
  /** The replies under a comment and the box to add one. */
8198
8412
  renderThread(comment) {
8199
8413
  const card = this.card;
@@ -8226,13 +8440,10 @@ var NuniWidget = class {
8226
8440
  }, this.t("save"))));
8227
8441
  }
8228
8442
  return h("div", {
8229
- class: "reply",
8443
+ class: "reply msg",
8230
8444
  "data-reply": reply._id
8231
- }, h("div", { class: "card-head" }, h("span", {
8232
- class: "avatar avatar-sm",
8233
- style: `background:${colorFor(reply.authorName)}`
8234
- }, initials(reply.authorName)), h("span", { class: "author" }, reply.authorName), reply.isOwner ? h("span", { class: "badge badge-owner" }, this.t("owner")) : null, h("span", { class: "meta" }, this.i18n.timeAgo(reply.createdAt) + (reply.editedAt ? ` · ${this.t("edited")}` : "")), h("span", { class: "spacer" }), mine ? h("button", {
8235
- class: "btn btn-ghost btn-icon btn-xs",
8445
+ }, h("div", { class: "card-head" }, avatar(reply.authorName), h("span", { class: "author" }, reply.authorName), reply.isOwner ? h("span", { class: "badge badge-owner" }, this.t("owner")) : null, h("span", { class: "meta" }, this.i18n.timeAgo(reply.createdAt) + (reply.editedAt ? ` · ${this.t("edited")}` : "")), h("span", { class: "spacer" }), h("span", { class: "msg-tools" }, this.reactionAdd(reply._id, "icon-btn"), mine ? h("button", {
8446
+ class: "icon-btn",
8236
8447
  type: "button",
8237
8448
  "aria-label": this.t("editReply"),
8238
8449
  title: this.t("edit"),
@@ -8241,18 +8452,18 @@ var NuniWidget = class {
8241
8452
  this.render();
8242
8453
  }
8243
8454
  }, icon(ICONS.edit)) : null, mine || owner ? h("button", {
8244
- class: "btn btn-ghost btn-icon btn-xs btn-danger",
8455
+ class: "icon-btn icon-btn-danger",
8245
8456
  type: "button",
8246
8457
  "aria-label": this.t("deleteReply"),
8247
8458
  title: this.t("delete"),
8248
8459
  "data-focus-key": `delete-${reply._id}`,
8249
8460
  disabled: Boolean(card.busy),
8250
8461
  onclick: () => void this.deleteReply(comment, reply)
8251
- }, icon(ICONS.trash)) : null), h("div", { class: "comment-body" }, reply.body), this.renderReactions(comment, reply._id));
8462
+ }, icon(ICONS.trash)) : null)), h("div", { class: "msg-main" }, h("div", { class: "comment-body" }, reply.body), this.renderReactions(comment, reply._id, false)));
8252
8463
  });
8253
8464
  const needsName = !owner && !this.name;
8254
8465
  const nameInput = h("input", {
8255
- class: "field",
8466
+ class: "reply-name",
8256
8467
  placeholder: this.t("yourName"),
8257
8468
  value: this.name,
8258
8469
  maxlength: LIMITS.nameMaxLength,
@@ -8263,14 +8474,31 @@ var NuniWidget = class {
8263
8474
  const prevName = this.uiLayer.querySelector("[data-focus-key=\"reply-name\"]");
8264
8475
  if (prevName) nameInput.value = prevName.value;
8265
8476
  const box = h("textarea", {
8266
- class: "field field-reply",
8477
+ class: "reply-text",
8267
8478
  placeholder: replies.length ? this.t("reply") : this.t("replyFirst"),
8268
8479
  maxlength: LIMITS.bodyMaxLength,
8480
+ rows: 1,
8269
8481
  "data-focus-key": "reply",
8270
8482
  "aria-label": this.t("reply")
8271
8483
  });
8272
8484
  const prev = this.uiLayer.querySelector("[data-focus-key=\"reply\"]");
8273
8485
  if (prev) box.value = prev.value;
8486
+ const sendButton = h("button", {
8487
+ class: "reply-send",
8488
+ type: "submit",
8489
+ "aria-label": this.t("sendReply"),
8490
+ title: this.t("sendReply"),
8491
+ disabled: Boolean(card.sendingReply) || !box.value.trim()
8492
+ }, icon(ICONS.arrowUp));
8493
+ const grow = () => {
8494
+ sendButton.disabled = Boolean(card.sendingReply) || !box.value.trim();
8495
+ box.style.height = "auto";
8496
+ box.style.height = `${box.scrollHeight}px`;
8497
+ };
8498
+ box.addEventListener("input", grow);
8499
+ requestAnimationFrame(() => {
8500
+ if (box.isConnected) grow();
8501
+ });
8274
8502
  const send = () => void this.sendReply(comment, box.value, needsName ? nameInput.value : this.name);
8275
8503
  box.addEventListener("keydown", (e) => {
8276
8504
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -8291,13 +8519,7 @@ var NuniWidget = class {
8291
8519
  e.preventDefault();
8292
8520
  send();
8293
8521
  }
8294
- }, needsName ? nameInput : null, box, h("button", {
8295
- class: "btn btn-primary btn-icon",
8296
- type: "submit",
8297
- "aria-label": this.t("sendReply"),
8298
- title: this.t("sendReply"),
8299
- disabled: Boolean(card.sendingReply)
8300
- }, icon(ICONS.send))));
8522
+ }, needsName ? nameInput : null, h("div", { class: "reply-row" }, box, sendButton)));
8301
8523
  }
8302
8524
  positionCard() {
8303
8525
  const el = this.uiLayer.querySelector("[data-card]");