@nuniapp/widget 0.1.5 → 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.5";
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\"/>")
@@ -5869,6 +5872,57 @@ function sha256(data) {
5869
5872
  return out;
5870
5873
  }
5871
5874
  //#endregion
5875
+ //#region src/scroll-to-pin.ts
5876
+ /** Reveal the pin itself, including scroll containers and same-origin frames. */
5877
+ function scrollToPin(element, point) {
5878
+ element.scrollIntoView({
5879
+ block: "center",
5880
+ inline: "center",
5881
+ behavior: "instant"
5882
+ });
5883
+ let node = element;
5884
+ while (node) {
5885
+ const parent = node.parentElement;
5886
+ if (parent) {
5887
+ const style = parent.ownerDocument.defaultView?.getComputedStyle(parent);
5888
+ const scrollX = style && /auto|scroll|hidden/.test(style.overflowX);
5889
+ const scrollY = style && /auto|scroll|hidden/.test(style.overflowY);
5890
+ if (parent !== parent.ownerDocument.scrollingElement && (scrollX || scrollY)) {
5891
+ const rect = viewportRect(parent, document);
5892
+ const pin = point();
5893
+ parent.scrollBy({
5894
+ left: scrollX ? pin.x - rect.left - parent.clientLeft - parent.clientWidth / 2 : 0,
5895
+ top: scrollY ? pin.y - rect.top - parent.clientTop - parent.clientHeight / 2 : 0,
5896
+ behavior: "instant"
5897
+ });
5898
+ }
5899
+ node = parent;
5900
+ continue;
5901
+ }
5902
+ const root = node.getRootNode();
5903
+ if (isShadowRoot(root)) {
5904
+ node = root.host;
5905
+ continue;
5906
+ }
5907
+ const doc = node.ownerDocument;
5908
+ const view = doc.defaultView;
5909
+ const frame = frameElementOf(doc);
5910
+ if (view) {
5911
+ const pin = point();
5912
+ const rect = frame ? viewportRect(frame, document) : {
5913
+ left: 0,
5914
+ top: 0
5915
+ };
5916
+ view.scrollBy({
5917
+ left: pin.x - rect.left - (frame?.clientLeft ?? 0) - view.innerWidth / 2,
5918
+ top: pin.y - rect.top - (frame?.clientTop ?? 0) - view.innerHeight / 2,
5919
+ behavior: "instant"
5920
+ });
5921
+ }
5922
+ node = frame;
5923
+ }
5924
+ }
5925
+ //#endregion
5872
5926
  //#region src/storage.ts
5873
5927
  /** localStorage that never throws (private mode, blocked storage, sandboxed iframes). */
5874
5928
  const memory = /* @__PURE__ */ new Map();
@@ -6175,59 +6229,34 @@ button { cursor: pointer; }
6175
6229
  /* Cards (composer + thread) */
6176
6230
  .card {
6177
6231
  position: fixed;
6178
- width: 320px;
6232
+ width: 340px;
6179
6233
  max-width: calc(100vw - 24px);
6180
6234
  background: var(--n-bg);
6181
6235
  color: var(--n-fg);
6182
6236
  border: 1px solid var(--n-border);
6183
- border-radius: var(--n-radius);
6237
+ border-radius: 14px;
6184
6238
  box-shadow: var(--n-shadow);
6185
6239
  pointer-events: auto;
6186
6240
  overflow: hidden;
6187
6241
  }
6188
- .card-body { padding: 12px; display: grid; gap: 10px; }
6242
+ .card-body { padding: 12px 14px; display: grid; gap: 10px; }
6189
6243
  .card[data-card="thread"] { display: flex; flex-direction: column; max-height: calc(100vh - 24px); }
6190
- .thread { border-top: 1px solid var(--n-border); display: grid; min-height: 0; }
6191
- .replies { display: grid; gap: 12px; padding: 12px; overflow-y: auto; max-height: 45vh; }
6192
- .reply { display: grid; gap: 6px; }
6193
- .avatar-sm { width: 20px; height: 20px; font-size: 9px; }
6194
- .badge-owner { background: var(--n-accent-soft); color: var(--n-accent); }
6195
- .reply-form { display: flex; align-items: flex-end; gap: 8px; padding: 8px 12px 12px; flex-wrap: wrap; }
6196
- .reply-form .field { flex: 1 1 100%; }
6197
- /* A fixed size: growing on focus would move the Send button mid-click. */
6198
- .reply-form .field-reply { flex: 1 1 0; min-height: 56px; height: 56px; resize: none; }
6199
- .reactions { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; position: relative; }
6200
- .reaction {
6201
- display: inline-flex;
6202
- align-items: center;
6203
- gap: 4px;
6204
- height: 24px;
6205
- padding: 0 8px;
6206
- border: 1px solid var(--n-border);
6207
- border-radius: 999px;
6208
- 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);
6209
6250
  color: var(--n-fg);
6210
- font-size: 12px;
6211
- }
6212
- .reaction:hover { background: var(--n-bg-subtle); }
6213
- .reaction[aria-pressed="true"] { border-color: var(--n-accent); background: var(--n-accent-soft); }
6214
- .reaction-add { color: var(--n-fg-muted); padding: 0 6px; }
6215
- .reaction-picker {
6216
- display: flex;
6217
- gap: 2px;
6218
- padding: 4px;
6219
- border: 1px solid var(--n-border);
6220
- border-radius: 999px;
6221
- background: var(--n-bg);
6222
- box-shadow: var(--n-shadow);
6251
+ font-style: normal;
6252
+ font-size: 13px;
6253
+ box-shadow: inset 2px 0 0 var(--n-accent);
6223
6254
  }
6224
- .reaction-picker .reaction { border-color: transparent; font-size: 14px; }
6225
- .btn-xs { width: 24px; height: 24px; }
6226
- .card-head { display: flex; align-items: center; gap: 8px; }
6255
+ .card-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
6227
6256
  .card-head .spacer { flex: 1; }
6228
6257
  .avatar {
6229
- width: 24px;
6230
- height: 24px;
6258
+ width: 26px;
6259
+ height: 26px;
6231
6260
  border-radius: 999px;
6232
6261
  color: #fff;
6233
6262
  font-size: 10px;
@@ -6237,8 +6266,8 @@ button { cursor: pointer; }
6237
6266
  justify-content: center;
6238
6267
  flex: none;
6239
6268
  }
6240
- .author { font-weight: 600; }
6241
- .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; }
6242
6271
  .badge {
6243
6272
  display: inline-flex;
6244
6273
  align-items: center;
@@ -6253,28 +6282,147 @@ button { cursor: pointer; }
6253
6282
  text-overflow: ellipsis;
6254
6283
  white-space: nowrap;
6255
6284
  }
6285
+ .badge .icon { width: 12px; height: 12px; }
6256
6286
  .badge-ok { background: rgba(31,157,85,0.14); color: var(--n-ok); }
6257
- .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; }
6258
6289
  .shot {
6259
6290
  display: block;
6260
6291
  border: 1px solid var(--n-border);
6261
- border-radius: 8px;
6292
+ border-radius: 10px;
6262
6293
  overflow: hidden;
6263
6294
  background: var(--n-bg-subtle);
6264
6295
  }
6265
6296
  .shot img { display: block; width: 100%; max-height: 160px; object-fit: cover; object-position: top; }
6266
- .shot-preview { display: grid; gap: 4px; }
6267
- .shot-preview img {
6268
- display: block;
6269
- width: 100%;
6270
- max-height: 200px;
6271
- /* The whole image, so the commenter sees everything that is attached. */
6272
- object-fit: contain;
6273
- 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;
6274
6304
  border-radius: 8px;
6275
- 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;
6309
+ }
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);
6276
6422
  }
6277
- .shot-note { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--n-fg-muted); }
6423
+ .reaction-picker .reaction { border-color: transparent; font-size: 14px; }
6424
+
6425
+ /* Shared controls */
6278
6426
  .field {
6279
6427
  width: 100%;
6280
6428
  padding: 8px 10px;
@@ -6306,11 +6454,90 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
6306
6454
  .btn-ghost { border-color: transparent; background: transparent; }
6307
6455
  .btn-icon { width: 32px; padding: 0; justify-content: center; }
6308
6456
  .btn-danger { color: var(--n-danger); }
6309
- .kbd { color: var(--n-fg-muted); font-size: 11px; }
6310
6457
  .error { color: var(--n-danger); font-size: 12px; }
6311
6458
  .divider { height: 1px; background: var(--n-border); }
6312
- .actions { display: flex; gap: 4px; padding: 8px 12px; border-top: 1px solid var(--n-border); background: var(--n-bg-subtle); }
6313
- .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; } }
6314
6541
 
6315
6542
  /* Panel */
6316
6543
  .panel {
@@ -6458,13 +6685,11 @@ function isTypingTarget(target) {
6458
6685
  const el = target;
6459
6686
  return el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT";
6460
6687
  }
6461
- function dataUrl(blob) {
6462
- return new Promise((resolve, reject) => {
6463
- const reader = new FileReader();
6464
- reader.onload = () => resolve(String(reader.result));
6465
- reader.onerror = () => reject(reader.error);
6466
- reader.readAsDataURL(blob);
6467
- });
6688
+ function avatar(name) {
6689
+ return h("span", {
6690
+ class: "avatar",
6691
+ style: `background:${colorFor(name)}`
6692
+ }, initials(name));
6468
6693
  }
6469
6694
  function describeElement(el) {
6470
6695
  const tag = el.tagName.toLowerCase();
@@ -6473,6 +6698,21 @@ function describeElement(el) {
6473
6698
  const text = elementText(el, 40);
6474
6699
  return `${tag}${id}${cls}${text ? ` "${text}"` : ""}`;
6475
6700
  }
6701
+ /**
6702
+ * Build an "Other pages" link target, or null when it would leave the
6703
+ * current origin. The stored path comes from anonymous comment submissions;
6704
+ * one that does not start with "/" (".evil.com/x", "@evil.com/") would
6705
+ * extend or replace the host once the concatenated string is parsed as a
6706
+ * URL, sending the visitor - and the page's query string - to another site.
6707
+ */
6708
+ function otherPageHref(path, loc) {
6709
+ const candidate = /[?#]/.test(path) ? loc.origin + path : loc.origin + path + loc.search;
6710
+ try {
6711
+ return new URL(candidate).origin === loc.origin ? candidate : null;
6712
+ } catch {
6713
+ return null;
6714
+ }
6715
+ }
6476
6716
  var NuniWidget = class {
6477
6717
  constructor(config, runtime) {
6478
6718
  this.config = config;
@@ -6533,6 +6773,7 @@ var NuniWidget = class {
6533
6773
  _defineProperty(this, "layoutFrame", 0);
6534
6774
  _defineProperty(this, "pinPositions", /* @__PURE__ */ new Map());
6535
6775
  _defineProperty(this, "resolveTimer", 0);
6776
+ _defineProperty(this, "shownPlacements", "");
6536
6777
  _defineProperty(this, "i18n", void 0);
6537
6778
  _defineProperty(this, "isIgnored", (el) => el === this.host || this.host.contains(el));
6538
6779
  _defineProperty(this, "onFrameKey", (e) => this.onKeyDown(e));
@@ -6894,10 +7135,18 @@ var NuniWidget = class {
6894
7135
  this.renderPins();
6895
7136
  this.layout();
6896
7137
  if (queue.length) whenIdle(slice);
6897
- else if ((onlyStale || run > 1) && this.panelOpen) this.render();
7138
+ else if ((onlyStale || run > 1) && this.panelOpen && this.placementSummary() !== this.shownPlacements) this.render();
6898
7139
  };
6899
7140
  slice();
6900
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
+ }
6901
7150
  /**
6902
7151
  * Pins inside shadow roots and iframes: the page's observers don't see
6903
7152
  * changes or scrolling in there, so watch each of those scopes too.
@@ -7179,12 +7428,12 @@ var NuniWidget = class {
7179
7428
  this.activeId = null;
7180
7429
  this.render();
7181
7430
  this.focusComposer();
7182
- this.takeScreenshot(draft);
7431
+ draft.screenshot = this.takeScreenshot(draft);
7183
7432
  }
7184
7433
  /** Capture while the person types, so posting stays instant. */
7185
7434
  async takeScreenshot(draft) {
7186
7435
  const load = this.runtime.loadScreenshot;
7187
- if (!load || draft.element.ownerDocument !== document) return;
7436
+ if (!load || draft.element.ownerDocument !== document) return null;
7188
7437
  const accent = getComputedStyle(this.host).getPropertyValue("--n-accent").trim() || "#d6246e";
7189
7438
  try {
7190
7439
  await new Promise((resolve) => setTimeout(resolve, 60));
@@ -7199,23 +7448,14 @@ var NuniWidget = class {
7199
7448
  bottom: box.top + box.height
7200
7449
  };
7201
7450
  } : void 0;
7202
- const blob = capture ? await capture(element, {
7451
+ return capture ? await capture(element, {
7203
7452
  exclude: this.host,
7204
7453
  accent,
7205
7454
  focus
7206
7455
  }) : null;
7207
- if (!blob || !this.isComposing(draft) || draft.sending) return;
7208
- const preview = await dataUrl(blob);
7209
- if (!this.isComposing(draft) || draft.sending) return;
7210
- draft.screenshot = {
7211
- blob,
7212
- preview
7213
- };
7214
- this.render();
7215
- } catch {}
7216
- }
7217
- isComposing(draft) {
7218
- return this.card?.kind === "composer" && this.card.draft === draft;
7456
+ } catch {
7457
+ return null;
7458
+ }
7219
7459
  }
7220
7460
  collectContext(draft) {
7221
7461
  const { collectors } = this.runtime;
@@ -7227,9 +7467,10 @@ var NuniWidget = class {
7227
7467
  if (draft.dom) context.dom = draft.dom;
7228
7468
  return Object.keys(context).length ? context : void 0;
7229
7469
  }
7230
- async attachScreenshot(id, image) {
7470
+ async attachScreenshot(id, shot) {
7231
7471
  try {
7232
- await this.api.uploadScreenshot(id, this.secret, image);
7472
+ const image = await shot;
7473
+ if (image) await this.api.uploadScreenshot(id, this.secret, image);
7233
7474
  } catch {}
7234
7475
  }
7235
7476
  copyForAgent(comment) {
@@ -7490,10 +7731,9 @@ var NuniWidget = class {
7490
7731
  this.focusComment(comment._id);
7491
7732
  }
7492
7733
  focusComment(id) {
7493
- this.placements.get(id)?.element?.scrollIntoView({
7494
- block: "center",
7495
- behavior: "smooth"
7496
- });
7734
+ const placement = this.placements.get(id);
7735
+ const comment = this.comments.find((c) => c._id === id);
7736
+ if (placement?.element && comment) scrollToPin(placement.element, () => this.anchorPoint(comment.anchor, placement.element, placement.range));
7497
7737
  this.openThread(id);
7498
7738
  if (window.matchMedia(MOBILE_QUERY).matches) this.panelOpen = false;
7499
7739
  this.render();
@@ -7530,7 +7770,7 @@ var NuniWidget = class {
7530
7770
  },
7531
7771
  context: this.collectContext(draft)
7532
7772
  });
7533
- if (shot) this.attachScreenshot(id, shot.blob);
7773
+ if (shot) this.attachScreenshot(id, shot);
7534
7774
  this.placements.set(id, {
7535
7775
  element: draft.element,
7536
7776
  confidence: "exact",
@@ -7701,7 +7941,7 @@ var NuniWidget = class {
7701
7941
  kind: "thread",
7702
7942
  id
7703
7943
  };
7704
- this.focusNext = "[data-card=\"thread\"] .card-head button";
7944
+ this.focusNext = "[data-card=\"thread\"] .card-close";
7705
7945
  }
7706
7946
  render() {
7707
7947
  this.syncOwnerDetail();
@@ -7764,6 +8004,7 @@ var NuniWidget = class {
7764
8004
  }
7765
8005
  }
7766
8006
  renderUi() {
8007
+ this.shownPlacements = this.placementSummary();
7767
8008
  const nodes = [this.renderToolbar()];
7768
8009
  if (this.panelOpen) nodes.push(this.renderPanel());
7769
8010
  const card = this.renderCard();
@@ -7848,8 +8089,8 @@ var NuniWidget = class {
7848
8089
  if (otherPages.length) {
7849
8090
  children.push(h("div", { class: "section-label" }, this.t("otherPages")));
7850
8091
  for (const p of otherPages.slice(0, 20)) {
7851
- const href = /[?#]/.test(p.path) ? location.origin + p.path : location.origin + p.path + location.search;
7852
- children.push(h("a", {
8092
+ const href = otherPageHref(p.path, location);
8093
+ children.push(h(href === null ? "div" : "a", href === null ? { class: "item" } : {
7853
8094
  class: "item",
7854
8095
  href
7855
8096
  }, h("div", { class: "row" }, h("span", { class: "author" }, p.path), h("span", { class: "spacer" }), h("span", { class: "badge" }, this.t("openOnPage", { count: p.count })))));
@@ -7908,80 +8149,112 @@ var NuniWidget = class {
7908
8149
  onclick: () => this.openClaim()
7909
8150
  }, icon(ICONS.github), this.t("claim"))));
7910
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
+ }
7911
8253
  renderCard() {
7912
8254
  const card = this.card;
7913
8255
  if (!card) return null;
7914
8256
  const cls = `card${window.matchMedia(MOBILE_QUERY).matches ? " sheet" : ""}`;
7915
- if (card.kind === "composer") {
7916
- const { draft } = card;
7917
- const needsName = !this.name;
7918
- const nameInput = h("input", {
7919
- class: "field",
7920
- placeholder: this.t("yourName"),
7921
- value: this.name,
7922
- maxlength: LIMITS.nameMaxLength,
7923
- autocomplete: "name",
7924
- "data-focus-key": "name",
7925
- "aria-label": this.t("yourName")
7926
- });
7927
- const textarea = h("textarea", {
7928
- class: "field",
7929
- placeholder: this.t("leaveComment"),
7930
- maxlength: LIMITS.bodyMaxLength,
7931
- "data-focus-key": "body",
7932
- "aria-label": this.t("comment")
7933
- });
7934
- const prev = this.uiLayer.querySelector("[data-focus-key=\"body\"]");
7935
- if (prev) textarea.value = prev.value;
7936
- const prevName = this.uiLayer.querySelector("[data-focus-key=\"name\"]");
7937
- if (prevName) nameInput.value = prevName.value;
7938
- const submit = () => void this.submitDraft(draft, textarea.value, needsName ? nameInput.value : this.name);
7939
- textarea.addEventListener("keydown", (e) => {
7940
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
7941
- e.preventDefault();
7942
- submit();
7943
- }
7944
- });
7945
- return h("form", {
7946
- class: cls,
7947
- "data-card": "composer",
7948
- onsubmit: (e) => {
7949
- e.preventDefault();
7950
- submit();
7951
- }
7952
- }, h("div", { class: "card-body" }, needsName ? nameInput : h("div", { class: "row" }, h("span", {
7953
- class: "avatar",
7954
- style: `background:${colorFor(this.name)}`
7955
- }, initials(this.name)), h("span", { class: "author" }, this.name), h("span", { class: "spacer" }), h("button", {
7956
- class: "link",
7957
- type: "button",
7958
- onclick: () => {
7959
- this.name = "";
7960
- this.render();
7961
- }
7962
- }, 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", {
7963
- src: draft.screenshot.preview,
7964
- alt: this.t("screenshotAlt")
7965
- }), h("div", { class: "shot-note" }, h("span", {}, this.t("screenshotNote")), h("span", { class: "spacer" }), h("button", {
7966
- class: "link",
7967
- type: "button",
7968
- onclick: () => {
7969
- draft.screenshot = void 0;
7970
- this.render();
7971
- }
7972
- }, this.t("remove")))) : null, draft.error ? h("div", {
7973
- class: "error",
7974
- role: "alert"
7975
- }, draft.error) : null, h("div", { class: "row" }, h("span", { class: "kbd" }, this.t("send", { key: this.i18n.mod })), h("span", { class: "spacer" }), h("button", {
7976
- class: "btn btn-ghost",
7977
- type: "button",
7978
- onclick: () => this.closeCard()
7979
- }, this.t("cancel")), h("button", {
7980
- class: "btn btn-primary",
7981
- type: "submit",
7982
- disabled: Boolean(draft.sending)
7983
- }, icon(ICONS.send), draft.sending ? this.t("posting") : this.t("post")))));
7984
- }
8257
+ if (card.kind === "composer") return this.renderComposer(card.draft, cls);
7985
8258
  const comment = this.comments.find((c) => c._id === card.id);
7986
8259
  if (!comment) return null;
7987
8260
  const mine = this.isMine(comment);
@@ -8009,61 +8282,55 @@ var NuniWidget = class {
8009
8282
  disabled: Boolean(card.busy),
8010
8283
  onclick: () => this.saveEdit(comment, textarea.value)
8011
8284
  }, this.t("save"))));
8012
- } else body = h("div", { class: "card-body" }, h("div", { class: "card-head" }, h("span", {
8013
- class: "avatar",
8014
- style: `background:${colorFor(comment.authorName)}`
8015
- }, 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", {
8016
- class: "btn btn-ghost btn-icon",
8017
- type: "button",
8018
- "aria-label": this.t("close"),
8019
- onclick: () => this.closeCard()
8020
- }, 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", {
8021
- class: "shot",
8022
- href: this.ownerDetail.screenshotUrl,
8023
- target: "_blank",
8024
- rel: "noreferrer",
8025
- title: this.t("openScreenshot")
8026
- }, h("img", {
8027
- src: this.ownerDetail.screenshotUrl,
8028
- alt: this.t("screenshotTaken")
8029
- })) : 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", {
8030
- class: "badge",
8031
- title: this.t("leftOn", { origin: comment.page.origin })
8032
- }, new URL(comment.page.origin).host) : null, placement?.confidence === "low" ? h("span", {
8033
- class: "badge",
8034
- title: this.t("approximateHint")
8035
- }, this.t("approximate")) : null, !placement?.element ? h("span", {
8036
- class: "badge",
8037
- title: this.t("originallyOn", { text: comment.anchor.text || comment.anchor.tag })
8038
- }, this.t("notFound")) : null), this.renderReactions(comment, comment._id), card.error ? h("div", {
8039
- class: "error",
8040
- role: "alert"
8041
- }, card.error) : null);
8042
- const actions = [];
8043
- if (!card.editing) {
8044
- if (owner) actions.push(comment.status === "open" ? h("button", {
8045
- 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" : ""}`,
8046
8305
  type: "button",
8306
+ "aria-label": this.t(resolved ? "reopen" : "resolve"),
8307
+ title: this.t(resolved ? "reopen" : "resolve"),
8047
8308
  disabled: Boolean(card.busy),
8048
- onclick: () => this.resolveComment(comment)
8049
- }, icon(ICONS.check), this.t("resolve")) : h("button", {
8050
- class: "btn",
8309
+ onclick: () => resolved ? this.reopenComment(comment) : this.resolveComment(comment)
8310
+ }, icon(ICONS.check)) : null, h("button", {
8311
+ class: "icon-btn card-close",
8051
8312
  type: "button",
8052
- disabled: Boolean(card.busy),
8053
- onclick: () => this.reopenComment(comment)
8054
- }, icon(ICONS.undo), this.t("reopen")));
8055
- actions.push(h("span", { class: "spacer" }));
8056
- const contextPending = owner && !(this.ownerDetailLoaded && this.ownerDetailId === comment._id);
8057
- actions.push(h("button", {
8058
- 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",
8059
8327
  type: "button",
8060
8328
  "aria-label": this.t("copyForAgent"),
8061
8329
  title: contextPending ? this.t("loadingContext") : this.t("copyForAgentHint"),
8062
8330
  disabled: contextPending,
8063
8331
  onclick: () => this.copyForAgent(comment)
8064
- }, icon(ICONS.bot)));
8065
- if (mine) actions.push(h("button", {
8066
- class: "btn btn-ghost btn-icon",
8332
+ }, icon(ICONS.bot)), mine ? h("button", {
8333
+ class: "icon-btn",
8067
8334
  type: "button",
8068
8335
  "aria-label": this.t("edit"),
8069
8336
  title: this.t("edit"),
@@ -8071,30 +8338,36 @@ var NuniWidget = class {
8071
8338
  card.editing = true;
8072
8339
  this.render();
8073
8340
  }
8074
- }, icon(ICONS.edit)));
8075
- if (mine || owner) actions.push(h("button", {
8076
- class: "btn btn-ghost btn-icon btn-danger",
8341
+ }, icon(ICONS.edit)) : null, mine || owner ? h("button", {
8342
+ class: "icon-btn icon-btn-danger",
8077
8343
  type: "button",
8078
8344
  "aria-label": this.t("delete"),
8079
8345
  title: this.t("delete"),
8080
8346
  "data-focus-key": "delete",
8081
8347
  disabled: Boolean(card.busy),
8082
8348
  onclick: () => void this.deleteComment(comment)
8083
- }, icon(ICONS.trash)));
8349
+ }, icon(ICONS.trash)) : null), card.error ? h("div", {
8350
+ class: "error",
8351
+ role: "alert"
8352
+ }, card.error) : null));
8084
8353
  }
8085
8354
  return h("div", {
8086
8355
  class: cls,
8087
8356
  "data-card": "thread",
8088
8357
  role: "dialog",
8089
8358
  "aria-label": this.t("commentBy", { name: comment.authorName })
8090
- }, body, actions.length ? h("div", { class: "actions" }, ...actions) : null, card.editing ? null : this.renderThread(comment));
8359
+ }, body, card.editing ? null : this.renderThread(comment));
8091
8360
  }
8092
- /** Emoji chips for one message, plus a picker to add one. */
8093
- 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) {
8094
8366
  const card = this.card;
8095
8367
  if (card?.kind !== "thread") return null;
8096
8368
  const summaries = this.threadId === comment._id && this.thread ? this.thread.reactions.filter((r) => r.targetId === targetId) : [];
8097
8369
  const open = card.picker === targetId;
8370
+ if (!withAdd && !summaries.length && !open) return null;
8098
8371
  return h("div", { class: "reactions" }, ...summaries.map((r) => {
8099
8372
  const mine = r.authorKeyHashes.includes(this.secretHash);
8100
8373
  return h("button", {
@@ -8104,17 +8377,7 @@ var NuniWidget = class {
8104
8377
  "aria-label": `${r.emoji} ${r.count}${mine ? ", including you" : ""}`,
8105
8378
  onclick: () => this.toggleReaction(comment, targetId, r.emoji)
8106
8379
  }, `${r.emoji} ${r.count}`);
8107
- }), h("button", {
8108
- class: "reaction reaction-add",
8109
- type: "button",
8110
- "aria-label": this.t("addReaction"),
8111
- "aria-expanded": String(open),
8112
- title: this.t("addReaction"),
8113
- onclick: () => {
8114
- card.picker = open ? null : targetId;
8115
- this.render();
8116
- }
8117
- }, icon(ICONS.smile)), open ? h("div", {
8380
+ }), withAdd ? this.reactionAdd(targetId, "reaction reaction-add") : null, open ? h("div", {
8118
8381
  class: "reaction-picker",
8119
8382
  role: "group",
8120
8383
  "aria-label": this.t("reactions")
@@ -8129,6 +8392,22 @@ var NuniWidget = class {
8129
8392
  }
8130
8393
  }, emoji))) : null);
8131
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
+ }
8132
8411
  /** The replies under a comment and the box to add one. */
8133
8412
  renderThread(comment) {
8134
8413
  const card = this.card;
@@ -8161,13 +8440,10 @@ var NuniWidget = class {
8161
8440
  }, this.t("save"))));
8162
8441
  }
8163
8442
  return h("div", {
8164
- class: "reply",
8443
+ class: "reply msg",
8165
8444
  "data-reply": reply._id
8166
- }, h("div", { class: "card-head" }, h("span", {
8167
- class: "avatar avatar-sm",
8168
- style: `background:${colorFor(reply.authorName)}`
8169
- }, 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", {
8170
- 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",
8171
8447
  type: "button",
8172
8448
  "aria-label": this.t("editReply"),
8173
8449
  title: this.t("edit"),
@@ -8176,18 +8452,18 @@ var NuniWidget = class {
8176
8452
  this.render();
8177
8453
  }
8178
8454
  }, icon(ICONS.edit)) : null, mine || owner ? h("button", {
8179
- class: "btn btn-ghost btn-icon btn-xs btn-danger",
8455
+ class: "icon-btn icon-btn-danger",
8180
8456
  type: "button",
8181
8457
  "aria-label": this.t("deleteReply"),
8182
8458
  title: this.t("delete"),
8183
8459
  "data-focus-key": `delete-${reply._id}`,
8184
8460
  disabled: Boolean(card.busy),
8185
8461
  onclick: () => void this.deleteReply(comment, reply)
8186
- }, 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)));
8187
8463
  });
8188
8464
  const needsName = !owner && !this.name;
8189
8465
  const nameInput = h("input", {
8190
- class: "field",
8466
+ class: "reply-name",
8191
8467
  placeholder: this.t("yourName"),
8192
8468
  value: this.name,
8193
8469
  maxlength: LIMITS.nameMaxLength,
@@ -8198,14 +8474,31 @@ var NuniWidget = class {
8198
8474
  const prevName = this.uiLayer.querySelector("[data-focus-key=\"reply-name\"]");
8199
8475
  if (prevName) nameInput.value = prevName.value;
8200
8476
  const box = h("textarea", {
8201
- class: "field field-reply",
8477
+ class: "reply-text",
8202
8478
  placeholder: replies.length ? this.t("reply") : this.t("replyFirst"),
8203
8479
  maxlength: LIMITS.bodyMaxLength,
8480
+ rows: 1,
8204
8481
  "data-focus-key": "reply",
8205
8482
  "aria-label": this.t("reply")
8206
8483
  });
8207
8484
  const prev = this.uiLayer.querySelector("[data-focus-key=\"reply\"]");
8208
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
+ });
8209
8502
  const send = () => void this.sendReply(comment, box.value, needsName ? nameInput.value : this.name);
8210
8503
  box.addEventListener("keydown", (e) => {
8211
8504
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -8226,13 +8519,7 @@ var NuniWidget = class {
8226
8519
  e.preventDefault();
8227
8520
  send();
8228
8521
  }
8229
- }, needsName ? nameInput : null, box, h("button", {
8230
- class: "btn btn-primary btn-icon",
8231
- type: "submit",
8232
- "aria-label": this.t("sendReply"),
8233
- title: this.t("sendReply"),
8234
- disabled: Boolean(card.sendingReply)
8235
- }, icon(ICONS.send))));
8522
+ }, needsName ? nameInput : null, h("div", { class: "reply-row" }, box, sendButton)));
8236
8523
  }
8237
8524
  positionCard() {
8238
8525
  const el = this.uiLayer.querySelector("[data-card]");