@mushi-mushi/web 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -216,294 +216,607 @@ function getAvailableLocales() {
216
216
  // src/styles.ts
217
217
  function getWidgetStyles(theme) {
218
218
  const isDark = theme === "dark";
219
- const accent = "#7c3aed";
220
- const accentHover = "#6d28d9";
221
- const accentBgLight = isDark ? "#2e2440" : "#f5f3ff";
222
- const accentBgSelected = isDark ? "#2e2440" : "#ede9fe";
223
- const border = isDark ? "#3f3f46" : "#e4e4e7";
224
- const bgSurface = isDark ? "#18181b" : "#ffffff";
225
- const bgMuted = isDark ? "#27272a" : "#fafafa";
226
- const textMuted = isDark ? "#a1a1aa" : "#71717a";
219
+ const paper = isDark ? "#0F0E0C" : "#F8F4ED";
220
+ const ink = isDark ? "#F2EBDD" : "#0E0D0B";
221
+ const inkMuted = isDark ? "#928B7E" : "#5C5852";
222
+ const inkFaint = isDark ? "#5A5650" : "#9A9489";
223
+ const rule = isDark ? "rgba(242,235,221,0.10)" : "rgba(14,13,11,0.10)";
224
+ const ruleStrong = isDark ? "rgba(242,235,221,0.18)" : "rgba(14,13,11,0.16)";
225
+ const vermillion = isDark ? "#FF5A47" : "#E03C2C";
226
+ const vermillionWash = isDark ? "rgba(255,90,71,0.12)" : "rgba(224,60,44,0.08)";
227
+ const vermillionInk = isDark ? "#FFE5E0" : "#7A1F15";
228
+ const fontDisplay = `'Iowan Old Style', 'Palatino Linotype', 'Palatino', 'Book Antiqua', 'Cambria', Georgia, 'Times New Roman', serif`;
229
+ const fontBody = `system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', sans-serif`;
230
+ const fontMono = `ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
231
+ const easeStamp = "cubic-bezier(0.22, 1, 0.36, 1)";
227
232
  return `
228
233
  :host {
229
234
  all: initial;
230
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
235
+ font-family: ${fontBody};
231
236
  font-size: 14px;
232
- line-height: 1.5;
233
- color: ${isDark ? "#e4e4e7" : "#18181b"};
234
- }
237
+ line-height: 1.55;
238
+ color: ${ink};
239
+ -webkit-font-smoothing: antialiased;
240
+ -moz-osx-font-smoothing: grayscale;
241
+ font-feature-settings: 'ss01', 'cv11'; /* nicer system-ui glyphs where supported */
242
+ }
243
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
244
+ button { font-family: inherit; }
235
245
 
236
- * {
237
- box-sizing: border-box;
238
- margin: 0;
239
- padding: 0;
240
- }
241
-
242
- /* Trigger button */
246
+ /* \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
247
+ A small "stamp card" \u2014 soft rounded square (4px radius), paper
248
+ background, vermillion bottom edge that reads as the inked face
249
+ of a real \u5370\u9451. A pulsing dot in the top-right hints there's a
250
+ channel here without needing a notification badge. */
243
251
  .mushi-trigger {
244
252
  position: fixed;
245
- width: 48px;
246
- height: 48px;
247
- border-radius: 50%;
248
- border: none;
253
+ width: 52px;
254
+ height: 52px;
255
+ border: 1px solid ${ruleStrong};
256
+ border-radius: 4px;
257
+ background: ${paper};
258
+ color: ${ink};
249
259
  cursor: pointer;
250
260
  display: flex;
251
261
  align-items: center;
252
262
  justify-content: center;
263
+ font-family: ${fontDisplay};
253
264
  font-size: 22px;
254
- background: ${bgMuted};
255
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
256
- transition: transform 0.2s ease, box-shadow 0.2s ease;
265
+ line-height: 1;
266
+ box-shadow:
267
+ 0 1px 0 ${rule},
268
+ 0 6px 14px -8px rgba(14,13,11,0.35),
269
+ inset 0 -3px 0 ${vermillion};
270
+ transition: transform 200ms ${easeStamp}, box-shadow 200ms ${easeStamp};
271
+ overflow: visible;
272
+ isolation: isolate;
273
+ }
274
+ .mushi-trigger::after {
275
+ content: '';
276
+ position: absolute;
277
+ top: 6px;
278
+ right: 6px;
279
+ width: 6px;
280
+ height: 6px;
281
+ border-radius: 50%;
282
+ background: ${vermillion};
283
+ box-shadow: 0 0 0 0 ${vermillion};
284
+ animation: mushi-pulse 2.4s ${easeStamp} infinite;
257
285
  }
258
286
  .mushi-trigger:hover {
259
- transform: scale(1.08);
260
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
287
+ transform: translateY(-2px) rotate(-1.5deg);
288
+ box-shadow:
289
+ 0 1px 0 ${rule},
290
+ 0 14px 24px -10px rgba(14,13,11,0.45),
291
+ inset 0 -3px 0 ${vermillion};
292
+ }
293
+ .mushi-trigger:active {
294
+ transform: translateY(0) rotate(0);
295
+ box-shadow:
296
+ 0 1px 0 ${rule},
297
+ 0 2px 4px -2px rgba(14,13,11,0.35),
298
+ inset 0 -2px 0 ${vermillion};
299
+ }
300
+ .mushi-trigger:focus-visible {
301
+ outline: 2px solid ${vermillion};
302
+ outline-offset: 3px;
303
+ }
304
+ .mushi-trigger.bottom-right { bottom: 24px; right: 24px; }
305
+ .mushi-trigger.bottom-left { bottom: 24px; left: 24px; }
306
+ .mushi-trigger.top-right { top: 24px; right: 24px; }
307
+ .mushi-trigger.top-left { top: 24px; left: 24px; }
308
+
309
+ @keyframes mushi-pulse {
310
+ 0% { box-shadow: 0 0 0 0 ${vermillion}; opacity: 1; }
311
+ 70% { box-shadow: 0 0 0 8px rgba(224,60,44,0); opacity: 0.5; }
312
+ 100% { box-shadow: 0 0 0 0 rgba(224,60,44,0); opacity: 1; }
261
313
  }
262
- .mushi-trigger.bottom-right { bottom: 20px; right: 20px; }
263
- .mushi-trigger.bottom-left { bottom: 20px; left: 20px; }
264
- .mushi-trigger.top-right { top: 20px; right: 20px; }
265
- .mushi-trigger.top-left { top: 20px; left: 20px; }
266
314
 
267
- /* Panel */
315
+ /* \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
316
+ Paper-card. Sharper corners (6px) than typical SaaS modals
317
+ (which default to 12-16px and read as plastic). Two-layer shadow:
318
+ one hairline that sells the paper edge, one diffuse that lifts
319
+ the panel off the underlying app. No backdrop-filter \u2014 we want
320
+ the widget to feel like it sits ON the page, not blur INTO it. */
268
321
  .mushi-panel {
269
322
  position: fixed;
270
- width: 380px;
271
- max-height: 560px;
272
- border-radius: 12px;
273
- background: ${bgSurface};
274
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
275
- border: 1px solid ${border};
323
+ width: 384px;
324
+ max-width: calc(100vw - 32px);
325
+ max-height: min(640px, calc(100vh - 120px));
326
+ background: ${paper};
327
+ border: 1px solid ${ruleStrong};
328
+ border-radius: 6px;
329
+ box-shadow:
330
+ 0 1px 0 ${rule},
331
+ 0 24px 56px -20px rgba(14,13,11,0.30),
332
+ 0 8px 16px -8px rgba(14,13,11,0.20);
276
333
  overflow: hidden;
277
334
  display: flex;
278
335
  flex-direction: column;
336
+ transform-origin: var(--mushi-origin, bottom right);
279
337
  }
280
- .mushi-panel.open {
281
- animation: mushi-slide-in 0.25s ease forwards;
282
- }
338
+ .mushi-panel.open { animation: mushi-stamp-in 320ms ${easeStamp} both; }
283
339
  .mushi-panel.closed { display: none; }
284
- .mushi-panel.bottom-right { bottom: 76px; right: 20px; }
285
- .mushi-panel.bottom-left { bottom: 76px; left: 20px; }
286
- .mushi-panel.top-right { top: 76px; right: 20px; }
287
- .mushi-panel.top-left { top: 76px; left: 20px; }
340
+ .mushi-panel.bottom-right { bottom: 88px; right: 24px; --mushi-origin: bottom right; }
341
+ .mushi-panel.bottom-left { bottom: 88px; left: 24px; --mushi-origin: bottom left; }
342
+ .mushi-panel.top-right { top: 88px; right: 24px; --mushi-origin: top right; }
343
+ .mushi-panel.top-left { top: 88px; left: 24px; --mushi-origin: top left; }
288
344
 
289
- @keyframes mushi-slide-in {
290
- from { opacity: 0; transform: translateY(8px) scale(0.98); }
291
- to { opacity: 1; transform: translateY(0) scale(1); }
345
+ @keyframes mushi-stamp-in {
346
+ 0% { opacity: 0; transform: scale(0.94) translateY(6px); }
347
+ 60% { opacity: 1; }
348
+ 100% { opacity: 1; transform: scale(1) translateY(0); }
292
349
  }
293
350
 
294
- /* Header */
351
+ /* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
352
+ Editorial masthead: small mono eyebrow ("MUSHI / REPORT") on top,
353
+ serif display headline below, mono step counter on the far right.
354
+ A single hairline separates header from body \u2014 no card stacking. */
295
355
  .mushi-header {
296
- padding: 14px 16px;
297
- border-bottom: 1px solid ${border};
298
- display: flex;
356
+ padding: 18px 20px 14px;
357
+ border-bottom: 1px solid ${rule};
358
+ display: grid;
359
+ grid-template-columns: auto 1fr auto;
360
+ align-items: end;
361
+ gap: 12px;
362
+ }
363
+ .mushi-header-mark {
364
+ display: inline-flex;
299
365
  align-items: center;
300
- gap: 8px;
366
+ justify-content: center;
367
+ width: 22px;
368
+ height: 22px;
369
+ border-radius: 3px;
370
+ background: ${vermillion};
371
+ color: #FAF7F0;
372
+ font-family: ${fontDisplay};
373
+ font-size: 14px;
374
+ font-weight: 600;
375
+ line-height: 1;
376
+ letter-spacing: -0.02em;
377
+ transform: rotate(-3deg);
378
+ flex-shrink: 0;
379
+ }
380
+ .mushi-header-titles {
381
+ min-width: 0;
382
+ display: flex;
383
+ flex-direction: column;
384
+ gap: 2px;
385
+ }
386
+ .mushi-header-eyebrow {
387
+ font-family: ${fontMono};
388
+ font-size: 10px;
389
+ letter-spacing: 0.18em;
390
+ text-transform: uppercase;
391
+ color: ${inkMuted};
301
392
  }
302
393
  .mushi-header h3 {
303
- font-size: 15px;
394
+ font-family: ${fontDisplay};
395
+ font-size: 19px;
396
+ font-weight: 500;
397
+ line-height: 1.15;
398
+ letter-spacing: -0.01em;
399
+ color: ${ink};
400
+ }
401
+ .mushi-header-meta {
402
+ align-self: start;
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 6px;
406
+ }
407
+ .mushi-step-counter {
408
+ font-family: ${fontMono};
409
+ font-size: 11px;
410
+ color: ${inkMuted};
411
+ letter-spacing: 0.06em;
412
+ tab-size: 2ch;
413
+ padding-top: 2px;
414
+ }
415
+ .mushi-step-counter b {
304
416
  font-weight: 600;
305
- flex: 1;
417
+ color: ${ink};
306
418
  }
307
419
  .mushi-close, .mushi-back {
308
420
  background: none;
309
421
  border: none;
310
422
  cursor: pointer;
311
- font-size: 18px;
312
- color: ${textMuted};
313
423
  padding: 4px;
314
- border-radius: 4px;
424
+ color: ${inkMuted};
425
+ font-family: ${fontBody};
426
+ font-size: 14px;
315
427
  line-height: 1;
428
+ border-radius: 3px;
429
+ transition: color 150ms ${easeStamp};
316
430
  }
317
- .mushi-close:hover, .mushi-back:hover {
318
- background: ${bgMuted};
431
+ .mushi-close:hover, .mushi-back:hover { color: ${vermillion}; }
432
+ .mushi-close:focus-visible, .mushi-back:focus-visible {
433
+ outline: 1.5px solid ${vermillion};
434
+ outline-offset: 2px;
319
435
  }
320
436
 
321
- /* Body */
437
+ /* \u2500\u2500 Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
438
+ Generous left/right padding (22px) so type breathes. Vertical
439
+ padding tighter at top because the header rule already creates
440
+ breathing room. */
322
441
  .mushi-body {
323
- padding: 12px 16px;
442
+ padding: 8px 22px 16px;
324
443
  overflow-y: auto;
325
444
  flex: 1;
445
+ scrollbar-width: thin;
446
+ scrollbar-color: ${inkFaint} transparent;
326
447
  }
448
+ .mushi-body::-webkit-scrollbar { width: 6px; }
449
+ .mushi-body::-webkit-scrollbar-thumb { background: ${inkFaint}; border-radius: 3px; }
327
450
 
328
- /* Step 1: Category options */
451
+ /* \u2500\u2500 Step 1: Categories as a contents-page list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
452
+ No boxes. Hairline rules between rows. Hovering a row pulls a
453
+ vermillion arrow in from the right and tints the row label \u2014
454
+ reads like flipping through an index card. */
329
455
  .mushi-option-btn {
330
- display: flex;
456
+ display: grid;
457
+ grid-template-columns: auto 1fr auto;
331
458
  align-items: center;
332
- gap: 12px;
459
+ gap: 14px;
333
460
  width: 100%;
334
- padding: 12px 14px;
335
- margin-bottom: 8px;
336
- border-radius: 10px;
337
- border: 1px solid ${border};
338
- background: ${bgMuted};
461
+ padding: 14px 0;
462
+ border: none;
463
+ border-bottom: 1px solid ${rule};
464
+ background: transparent;
339
465
  cursor: pointer;
340
- font-size: 14px;
341
466
  color: inherit;
342
467
  text-align: left;
343
- transition: border-color 0.15s, background 0.15s, transform 0.1s;
344
- }
345
- .mushi-option-btn:hover {
346
- border-color: ${isDark ? "#a78bfa" : accent};
347
- background: ${accentBgLight};
348
- transform: translateX(2px);
468
+ transition: padding 220ms ${easeStamp}, color 220ms ${easeStamp};
469
+ position: relative;
349
470
  }
471
+ .mushi-option-btn:last-child { border-bottom: none; }
472
+ .mushi-option-btn:hover { padding-left: 6px; color: ${vermillion}; }
473
+ .mushi-option-btn:hover .mushi-option-arrow { opacity: 1; transform: translateX(0); color: ${vermillion}; }
350
474
  .mushi-option-btn:focus-visible {
351
- outline: 2px solid ${accent};
352
- outline-offset: 2px;
475
+ outline: none;
476
+ padding-left: 6px;
477
+ box-shadow: inset 2px 0 0 ${vermillion};
478
+ }
479
+ .mushi-option-icon {
480
+ font-size: 18px;
481
+ line-height: 1;
482
+ flex-shrink: 0;
483
+ filter: ${isDark ? "none" : "grayscale(0.15)"};
484
+ }
485
+ .mushi-option-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
486
+ .mushi-option-label {
487
+ font-family: ${fontDisplay};
488
+ font-size: 16px;
489
+ font-weight: 500;
490
+ letter-spacing: -0.005em;
491
+ line-height: 1.2;
492
+ }
493
+ .mushi-option-desc {
494
+ font-size: 12px;
495
+ color: ${inkMuted};
496
+ letter-spacing: 0.005em;
497
+ }
498
+ .mushi-option-arrow {
499
+ font-family: ${fontMono};
500
+ font-size: 14px;
501
+ color: ${inkFaint};
502
+ opacity: 0;
503
+ transform: translateX(-4px);
504
+ transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
353
505
  }
354
- .mushi-option-icon { font-size: 20px; flex-shrink: 0; }
355
- .mushi-option-text { display: flex; flex-direction: column; gap: 2px; }
356
- .mushi-option-label { font-weight: 500; }
357
- .mushi-option-desc { font-size: 12px; color: ${textMuted}; }
358
506
 
359
- /* Step 2: Intent chips */
507
+ /* \u2500\u2500 Step 2: Selected-category breadcrumb + intent text-buttons \u2500
508
+ Breadcrumb is a thin chip with the kanji-stamp aesthetic carried
509
+ over (vermillion left rule). Intents are inline TEXT buttons
510
+ with vermillion underlines on hover \u2014 not pill-shaped chips,
511
+ which is the SaaS default and not what we are. */
360
512
  .mushi-selected-category {
361
- display: flex;
513
+ display: inline-flex;
362
514
  align-items: center;
363
515
  gap: 8px;
364
- padding: 8px 12px;
365
- border-radius: 8px;
366
- background: ${accentBgSelected};
367
- font-size: 13px;
368
- font-weight: 500;
369
- margin-bottom: 12px;
370
- }
516
+ padding: 6px 10px 6px 12px;
517
+ border-left: 2px solid ${vermillion};
518
+ background: ${vermillionWash};
519
+ color: ${vermillionInk};
520
+ font-family: ${fontMono};
521
+ font-size: 11px;
522
+ letter-spacing: 0.12em;
523
+ text-transform: uppercase;
524
+ margin: 4px 0 14px;
525
+ border-radius: 0 3px 3px 0;
526
+ }
527
+ .mushi-selected-category span:first-child { font-size: 14px; }
371
528
  .mushi-intents {
372
529
  display: flex;
373
- flex-wrap: wrap;
374
- gap: 8px;
530
+ flex-direction: column;
531
+ gap: 2px;
375
532
  }
376
533
  .mushi-intent-btn {
377
- padding: 8px 16px;
378
- border-radius: 20px;
379
- border: 1px solid ${border};
380
- background: ${bgMuted};
534
+ display: flex;
535
+ align-items: center;
536
+ justify-content: space-between;
537
+ gap: 12px;
538
+ padding: 12px 0;
539
+ border: none;
540
+ border-bottom: 1px solid ${rule};
541
+ background: transparent;
381
542
  cursor: pointer;
382
- font-size: 13px;
383
543
  color: inherit;
384
- transition: border-color 0.15s, background 0.15s;
385
- }
386
- .mushi-intent-btn:hover {
387
- border-color: ${isDark ? "#a78bfa" : accent};
388
- background: ${accentBgLight};
544
+ text-align: left;
545
+ font-family: ${fontDisplay};
546
+ font-size: 15px;
547
+ transition: padding 220ms ${easeStamp}, color 220ms ${easeStamp};
389
548
  }
549
+ .mushi-intent-btn::after {
550
+ content: '\u2192';
551
+ font-family: ${fontMono};
552
+ font-size: 13px;
553
+ color: ${inkFaint};
554
+ opacity: 0;
555
+ transform: translateX(-4px);
556
+ transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp};
557
+ }
558
+ .mushi-intent-btn:last-child { border-bottom: none; }
559
+ .mushi-intent-btn:hover { padding-left: 6px; color: ${vermillion}; }
560
+ .mushi-intent-btn:hover::after { opacity: 1; transform: translateX(0); color: ${vermillion}; }
390
561
  .mushi-intent-btn:focus-visible {
391
- outline: 2px solid ${accent};
392
- outline-offset: 2px;
562
+ outline: none;
563
+ padding-left: 6px;
564
+ box-shadow: inset 2px 0 0 ${vermillion};
393
565
  }
394
566
 
395
- /* Step 3: Details */
567
+ /* \u2500\u2500 Step 3: Borderless textarea + minimal attach pills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
568
+ The textarea has no box around it \u2014 just a hairline underline
569
+ that turns vermillion on focus. Encourages writing rather than
570
+ form-filling. */
396
571
  .mushi-textarea {
397
572
  width: 100%;
398
- min-height: 90px;
399
- padding: 10px 12px;
400
- border-radius: 8px;
401
- border: 1px solid ${border};
402
- background: ${bgMuted};
403
- color: inherit;
404
- font-family: inherit;
573
+ min-height: 96px;
574
+ padding: 8px 0 10px;
575
+ border: none;
576
+ border-bottom: 1px solid ${ruleStrong};
577
+ background: transparent;
578
+ color: ${ink};
579
+ font-family: ${fontBody};
405
580
  font-size: 14px;
581
+ line-height: 1.5;
406
582
  resize: vertical;
407
583
  outline: none;
408
- transition: border-color 0.15s, box-shadow 0.15s;
584
+ transition: border-color 200ms ${easeStamp};
409
585
  }
410
- .mushi-textarea:focus {
411
- border-color: ${isDark ? "#a78bfa" : accent};
412
- box-shadow: 0 0 0 2px ${isDark ? "rgba(167,139,250,0.2)" : "rgba(124,58,237,0.1)"};
586
+ .mushi-textarea::placeholder {
587
+ color: ${inkFaint};
588
+ font-style: italic;
413
589
  }
590
+ .mushi-textarea:focus { border-bottom-color: ${vermillion}; }
414
591
 
415
592
  .mushi-attachments {
416
593
  display: flex;
417
- gap: 8px;
418
- margin-top: 10px;
594
+ flex-wrap: wrap;
595
+ gap: 6px;
596
+ margin-top: 12px;
419
597
  }
420
598
  .mushi-attach-btn {
421
- padding: 6px 12px;
422
- border-radius: 6px;
423
- border: 1px solid ${border};
424
- background: none;
599
+ display: inline-flex;
600
+ align-items: center;
601
+ gap: 6px;
602
+ padding: 5px 10px;
603
+ border: 1px solid ${ruleStrong};
604
+ border-radius: 3px;
605
+ background: transparent;
606
+ color: ${inkMuted};
607
+ font-family: ${fontMono};
608
+ font-size: 11px;
609
+ letter-spacing: 0.04em;
610
+ text-transform: uppercase;
425
611
  cursor: pointer;
426
- font-size: 12px;
427
- color: ${textMuted};
428
- transition: border-color 0.15s, color 0.15s;
612
+ transition: color 180ms ${easeStamp}, border-color 180ms ${easeStamp}, background 180ms ${easeStamp};
429
613
  }
430
614
  .mushi-attach-btn:hover {
431
- border-color: ${isDark ? "#a78bfa" : accent};
432
- color: inherit;
615
+ color: ${ink};
616
+ border-color: ${ink};
433
617
  }
434
618
  .mushi-attach-btn.active {
435
- border-color: ${isDark ? "#a78bfa" : accent};
436
- color: ${isDark ? "#a78bfa" : accent};
437
- background: ${accentBgLight};
619
+ color: ${vermillion};
620
+ border-color: ${vermillion};
621
+ background: ${vermillionWash};
622
+ }
623
+ .mushi-attach-btn:focus-visible {
624
+ outline: 2px solid ${vermillion};
625
+ outline-offset: 2px;
438
626
  }
439
627
 
440
- /* Footer */
628
+ /* \u2500\u2500 Footer + submit (vermillion stamp) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
629
+ Submit button is the heaviest visual moment in the widget \u2014
630
+ vermillion fill, mono-caps label, send arrow. Holds an ink-
631
+ bloom pseudo-element that animates outward when pressed. */
441
632
  .mushi-footer {
442
- padding: 12px 16px;
443
- border-top: 1px solid ${border};
633
+ padding: 14px 22px 16px;
634
+ border-top: 1px solid ${rule};
444
635
  display: flex;
445
636
  align-items: center;
446
- justify-content: flex-end;
637
+ justify-content: space-between;
638
+ gap: 12px;
639
+ }
640
+ .mushi-footer-hint {
641
+ font-family: ${fontMono};
642
+ font-size: 10px;
643
+ letter-spacing: 0.10em;
644
+ text-transform: uppercase;
645
+ color: ${inkFaint};
447
646
  }
448
647
  .mushi-submit {
449
- padding: 8px 24px;
450
- border-radius: 8px;
451
- border: none;
452
- background: ${accent};
453
- color: #ffffff;
454
- font-size: 14px;
455
- font-weight: 500;
648
+ position: relative;
649
+ display: inline-flex;
650
+ align-items: center;
651
+ gap: 8px;
652
+ padding: 10px 18px;
653
+ border: 1px solid ${vermillion};
654
+ border-radius: 3px;
655
+ background: ${vermillion};
656
+ color: #FAF7F0;
657
+ font-family: ${fontMono};
658
+ font-size: 11px;
659
+ letter-spacing: 0.16em;
660
+ text-transform: uppercase;
456
661
  cursor: pointer;
457
- transition: background 0.15s;
662
+ overflow: hidden;
663
+ transition: transform 180ms ${easeStamp}, box-shadow 180ms ${easeStamp};
664
+ box-shadow: 0 2px 0 ${isDark ? "#7A1F15" : "#9A2A1E"};
665
+ }
666
+ .mushi-submit::after {
667
+ content: '';
668
+ position: absolute;
669
+ inset: 0;
670
+ background: radial-gradient(circle at center, rgba(255,255,255,0.35) 0%, transparent 60%);
671
+ opacity: 0;
672
+ transform: scale(0.4);
673
+ transition: opacity 280ms ${easeStamp}, transform 380ms ${easeStamp};
674
+ pointer-events: none;
675
+ }
676
+ .mushi-submit:hover {
677
+ transform: translateY(-1px);
678
+ box-shadow: 0 3px 0 ${isDark ? "#7A1F15" : "#9A2A1E"};
679
+ }
680
+ .mushi-submit:hover::after { opacity: 1; transform: scale(1.4); }
681
+ .mushi-submit:active { transform: translateY(1px); box-shadow: 0 1px 0 ${isDark ? "#7A1F15" : "#9A2A1E"}; }
682
+ .mushi-submit:disabled {
683
+ cursor: wait;
684
+ opacity: 0.7;
458
685
  }
459
- .mushi-submit:hover { background: ${accentHover}; }
460
- .mushi-submit:disabled { opacity: 0.5; cursor: not-allowed; }
461
686
  .mushi-submit:focus-visible {
462
- outline: 2px solid ${accent};
463
- outline-offset: 2px;
687
+ outline: 2px solid ${vermillion};
688
+ outline-offset: 3px;
689
+ }
690
+ .mushi-submit-arrow {
691
+ display: inline-block;
692
+ transition: transform 220ms ${easeStamp};
464
693
  }
694
+ .mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
465
695
 
466
- /* Step indicator dots */
696
+ /* \u2500\u2500 Step indicator (numeral ledger) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
697
+ Replaces the generic three-dots with a typographic series:
698
+ "01 \u2014 02 \u2014 03". The active step uses serif numerals, the
699
+ others use mono so the active one literally reads heavier. */
467
700
  .mushi-step-indicator {
468
701
  display: flex;
702
+ align-items: center;
469
703
  justify-content: center;
470
- gap: 6px;
471
- padding: 10px;
472
- }
473
- .mushi-dot {
474
- width: 6px;
475
- height: 6px;
476
- border-radius: 50%;
477
- background: ${border};
478
- transition: background 0.2s, transform 0.2s;
479
- }
480
- .mushi-dot.active {
481
- background: ${accent};
482
- transform: scale(1.3);
483
- }
484
- .mushi-dot.done {
485
- background: ${isDark ? "#a78bfa" : "#8b5cf6"};
704
+ gap: 8px;
705
+ padding: 10px 22px 14px;
706
+ color: ${inkFaint};
707
+ font-family: ${fontMono};
708
+ font-size: 11px;
709
+ letter-spacing: 0.10em;
710
+ }
711
+ .mushi-step-num {
712
+ display: inline-flex;
713
+ align-items: baseline;
714
+ gap: 4px;
715
+ transition: color 200ms ${easeStamp};
716
+ }
717
+ .mushi-step-num.done { color: ${inkMuted}; text-decoration: line-through; text-decoration-color: ${inkFaint}; }
718
+ .mushi-step-num.active {
719
+ color: ${vermillion};
720
+ font-family: ${fontDisplay};
721
+ font-size: 14px;
722
+ font-weight: 600;
723
+ letter-spacing: 0;
486
724
  }
725
+ .mushi-step-sep { width: 14px; height: 1px; background: ${rule}; }
487
726
 
488
- /* Success */
727
+ /* \u2500\u2500 Success: \u6731\u5370 stamp animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
728
+ The success state is the signature moment. A vermillion ring
729
+ scribes itself, then a "RECEIVED" mono-caps label fades in at
730
+ the centre, evoking a hanko being pressed onto the form. */
489
731
  .mushi-success {
490
732
  text-align: center;
491
- padding: 32px 16px;
733
+ padding: 28px 16px 20px;
734
+ }
735
+ .mushi-success-stamp {
736
+ position: relative;
737
+ width: 96px;
738
+ height: 96px;
739
+ margin: 0 auto 16px;
740
+ display: inline-flex;
741
+ align-items: center;
742
+ justify-content: center;
743
+ }
744
+ .mushi-success-stamp svg {
745
+ position: absolute;
746
+ inset: 0;
747
+ width: 100%;
748
+ height: 100%;
749
+ }
750
+ .mushi-success-stamp circle {
751
+ fill: none;
752
+ stroke: ${vermillion};
753
+ stroke-width: 3;
754
+ stroke-dasharray: 280;
755
+ stroke-dashoffset: 280;
756
+ transform: rotate(-90deg);
757
+ transform-origin: center;
758
+ animation: mushi-stamp-ring 700ms ${easeStamp} 80ms forwards;
759
+ }
760
+ .mushi-success-stamp-label {
761
+ font-family: ${fontDisplay};
762
+ font-size: 18px;
763
+ font-weight: 600;
764
+ color: ${vermillion};
765
+ letter-spacing: 0.04em;
766
+ transform: rotate(-6deg);
767
+ opacity: 0;
768
+ animation: mushi-stamp-press 360ms ${easeStamp} 600ms forwards;
769
+ }
770
+ .mushi-success-headline {
771
+ font-family: ${fontDisplay};
772
+ font-size: 18px;
773
+ font-weight: 500;
774
+ color: ${ink};
775
+ margin-bottom: 4px;
492
776
  }
493
- .mushi-success-icon {
494
- font-size: 40px;
495
- margin-bottom: 12px;
777
+ .mushi-success-meta {
778
+ font-family: ${fontMono};
779
+ font-size: 11px;
780
+ letter-spacing: 0.10em;
781
+ text-transform: uppercase;
782
+ color: ${inkMuted};
496
783
  }
497
- .mushi-success p {
498
- color: ${textMuted};
499
- font-size: 14px;
784
+
785
+ @keyframes mushi-stamp-ring {
786
+ to { stroke-dashoffset: 0; }
787
+ }
788
+ @keyframes mushi-stamp-press {
789
+ 0% { opacity: 0; transform: rotate(-6deg) scale(1.3); }
790
+ 60% { opacity: 1; transform: rotate(-6deg) scale(0.94); }
791
+ 100% { opacity: 1; transform: rotate(-6deg) scale(1); }
500
792
  }
501
793
 
502
- /* Error */
794
+ /* \u2500\u2500 Error \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
795
+ Inline editorial note rather than a red box. Vermillion left
796
+ rule keeps the same accent language. */
503
797
  .mushi-error {
504
- color: #ef4444;
798
+ margin-top: 10px;
799
+ padding: 8px 0 8px 10px;
800
+ border-left: 2px solid ${vermillion};
801
+ color: ${vermillion};
505
802
  font-size: 12px;
506
- margin-top: 8px;
803
+ font-family: ${fontMono};
804
+ letter-spacing: 0.02em;
805
+ }
806
+
807
+ /* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
808
+ Honour the OS preference: kill every transition + animation
809
+ except the focus underline (which is critical feedback). */
810
+ @media (prefers-reduced-motion: reduce) {
811
+ *,
812
+ *::before,
813
+ *::after {
814
+ animation-duration: 0.001ms !important;
815
+ animation-iteration-count: 1 !important;
816
+ transition-duration: 0.001ms !important;
817
+ }
818
+ .mushi-success-stamp circle { stroke-dashoffset: 0; }
819
+ .mushi-success-stamp-label { opacity: 1; }
507
820
  }
508
821
  `;
509
822
  }
@@ -516,6 +829,18 @@ var CATEGORY_ICONS = {
516
829
  confusing: "\u{1F615}",
517
830
  other: "\u{1F4DD}"
518
831
  };
832
+ function pad2(n) {
833
+ return n < 10 ? `0${n}` : String(n);
834
+ }
835
+ var TOTAL_STEPS = 3;
836
+ var STEP_NUMBER = {
837
+ category: 1,
838
+ intent: 2,
839
+ details: 3
840
+ };
841
+ function isSubmitShortcut(e) {
842
+ return (e.metaKey || e.ctrlKey) && e.key === "Enter";
843
+ }
519
844
  var MushiWidget = class {
520
845
  host;
521
846
  shadow;
@@ -529,11 +854,28 @@ var MushiWidget = class {
529
854
  screenshotAttached = false;
530
855
  elementSelected = false;
531
856
  submitting = false;
857
+ /** Captured at the moment of submit so the success ledger metadata
858
+ * ("REPORT · 14:23:07 JST") doesn't drift while the success step
859
+ * is on screen. */
860
+ submittedAt = null;
861
+ /** Pending success-state + auto-close timers. Tracked so destroy()
862
+ * can clear them — otherwise a host that unmounts mid-submit leaks
863
+ * this MushiWidget reference (and re-renders into a detached shadow
864
+ * root) for up to ~3.3s after destroy. */
865
+ successTimer = null;
866
+ autoCloseTimer = null;
532
867
  constructor(config = {}, callbacks) {
533
868
  this.config = {
534
869
  position: config.position ?? "bottom-right",
535
870
  theme: config.theme ?? "auto",
536
- triggerText: config.triggerText ?? "\u{1F41B}",
871
+ // Falsy-OR (NOT `??`) on purpose: `triggerText: ''` is semantically
872
+ // nonsense — it would render a labelless, glyphless trigger button
873
+ // that users can't see or aim at. Treat empty string the same as
874
+ // omitted so any caller that wires this to a cleared form input or
875
+ // pastes a legacy snippet that emitted `triggerText: ""` (see
876
+ // apps/admin/src/lib/sdkSnippets.ts widgetLines history) still gets
877
+ // the default 🐛 and a visible button.
878
+ triggerText: config.triggerText || "\u{1F41B}",
537
879
  expandedTitle: config.expandedTitle ?? "",
538
880
  mode: config.mode ?? "conversational",
539
881
  locale: config.locale ?? "auto",
@@ -555,6 +897,7 @@ var MushiWidget = class {
555
897
  this.screenshotAttached = false;
556
898
  this.elementSelected = false;
557
899
  this.submitting = false;
900
+ this.submittedAt = null;
558
901
  if (options?.category) {
559
902
  this.selectedCategory = options.category;
560
903
  this.selectedIntent = null;
@@ -585,6 +928,14 @@ var MushiWidget = class {
585
928
  if (this.isOpen) this.render();
586
929
  }
587
930
  destroy() {
931
+ if (this.successTimer !== null) {
932
+ clearTimeout(this.successTimer);
933
+ this.successTimer = null;
934
+ }
935
+ if (this.autoCloseTimer !== null) {
936
+ clearTimeout(this.autoCloseTimer);
937
+ this.autoCloseTimer = null;
938
+ }
588
939
  this.host.remove();
589
940
  }
590
941
  getTheme() {
@@ -606,6 +957,8 @@ var MushiWidget = class {
606
957
  trigger.className = `mushi-trigger ${pos}`;
607
958
  trigger.textContent = this.config.triggerText;
608
959
  trigger.setAttribute("aria-label", t.widget.trigger);
960
+ trigger.setAttribute("aria-haspopup", "dialog");
961
+ trigger.setAttribute("aria-expanded", String(this.isOpen));
609
962
  trigger.style.zIndex = String(this.config.zIndex);
610
963
  trigger.addEventListener("click", () => {
611
964
  if (this.isOpen) this.close();
@@ -615,6 +968,7 @@ var MushiWidget = class {
615
968
  const panel = document.createElement("div");
616
969
  panel.className = `mushi-panel ${pos}${this.isOpen ? " open" : " closed"}`;
617
970
  panel.setAttribute("role", "dialog");
971
+ panel.setAttribute("aria-modal", "true");
618
972
  panel.setAttribute("aria-label", t.widget.title);
619
973
  panel.style.zIndex = String(this.config.zIndex + 1);
620
974
  if (this.isOpen) {
@@ -636,37 +990,67 @@ var MushiWidget = class {
636
990
  return this.renderSuccessStep();
637
991
  }
638
992
  }
639
- renderHeader(title, showBack = false) {
993
+ /**
994
+ * Editorial masthead. Always carries:
995
+ * • the brand mark (虫 kanji on vermillion, "MUSHI" in mono above)
996
+ * • the page title (serif display)
997
+ * • the close affordance
998
+ *
999
+ * On sub-steps it additionally renders a back button (replacing the
1000
+ * "MUSHI" eyebrow with a "← BACK" mono link) and a step counter
1001
+ * ledger ("02 / 03") on the far right.
1002
+ */
1003
+ renderHeader(opts) {
640
1004
  const t = this.locale;
1005
+ const { title, showBack = false, step, eyebrow } = opts;
1006
+ const eyebrowHtml = showBack ? `<button type="button" class="mushi-back" data-action="back" aria-label="${t.widget.back}">\u2190 ${t.widget.back}</button>` : `<span class="mushi-header-eyebrow">${eyebrow ?? "Mushi \xB7 Report"}</span>`;
1007
+ const counterHtml = step ? `<span class="mushi-step-counter" aria-label="Step ${step} of ${TOTAL_STEPS}"><b>${pad2(step)}</b> / ${pad2(TOTAL_STEPS)}</span>` : "";
641
1008
  return `
642
1009
  <div class="mushi-header">
643
- ${showBack ? `<button class="mushi-back" data-action="back" aria-label="${t.widget.back}">\u2190</button>` : ""}
644
- <h3>${title}</h3>
645
- <button class="mushi-close" data-action="close" aria-label="${t.widget.close}">\u2715</button>
1010
+ <div class="mushi-header-mark" aria-hidden="true">\u866B</div>
1011
+ <div class="mushi-header-titles">
1012
+ ${eyebrowHtml}
1013
+ <h3>${title}</h3>
1014
+ </div>
1015
+ <div class="mushi-header-meta">
1016
+ ${counterHtml}
1017
+ <button type="button" class="mushi-close" data-action="close" aria-label="${t.widget.close}">\u2715</button>
1018
+ </div>
646
1019
  </div>
647
1020
  `;
648
1021
  }
1022
+ /**
1023
+ * Numeral step indicator: "01 — 02 — 03", with the active step in
1024
+ * vermillion serif and completed steps struck through in mono.
1025
+ * Replaces the original three-dot indicator (a generic SaaS pattern).
1026
+ */
1027
+ renderStepIndicator(currentStep) {
1028
+ const segments = [];
1029
+ for (let i = 1; i <= TOTAL_STEPS; i++) {
1030
+ const cls = i < currentStep ? "mushi-step-num done" : i === currentStep ? "mushi-step-num active" : "mushi-step-num";
1031
+ segments.push(`<span class="${cls}">${pad2(i)}</span>`);
1032
+ if (i < TOTAL_STEPS) segments.push('<span class="mushi-step-sep" aria-hidden="true"></span>');
1033
+ }
1034
+ return `<div class="mushi-step-indicator" aria-hidden="true">${segments.join("")}</div>`;
1035
+ }
649
1036
  renderCategoryStep() {
650
1037
  const t = this.locale;
651
1038
  const categories = ["bug", "slow", "visual", "confusing", "other"].map((id) => `
652
- <button class="mushi-option-btn" data-category="${id}" role="radio" aria-checked="false">
653
- <span class="mushi-option-icon">${CATEGORY_ICONS[id]}</span>
1039
+ <button type="button" class="mushi-option-btn" data-category="${id}" role="radio" aria-checked="false">
1040
+ <span class="mushi-option-icon" aria-hidden="true">${CATEGORY_ICONS[id]}</span>
654
1041
  <div class="mushi-option-text">
655
1042
  <span class="mushi-option-label">${t.step1.categories[id]}</span>
656
1043
  <span class="mushi-option-desc">${t.step1.categoryDescriptions[id]}</span>
657
1044
  </div>
1045
+ <span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
658
1046
  </button>
659
1047
  `).join("");
660
1048
  return `
661
- ${this.renderHeader(t.step1.heading)}
1049
+ ${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
662
1050
  <div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
663
1051
  ${categories}
664
1052
  </div>
665
- <div class="mushi-step-indicator">
666
- <span class="mushi-dot active"></span>
667
- <span class="mushi-dot"></span>
668
- <span class="mushi-dot"></span>
669
- </div>
1053
+ ${this.renderStepIndicator(STEP_NUMBER.category)}
670
1054
  `;
671
1055
  }
672
1056
  renderIntentStep() {
@@ -674,32 +1058,28 @@ var MushiWidget = class {
674
1058
  const cat = this.selectedCategory;
675
1059
  const intents = t.step2.intents[cat] || [];
676
1060
  const options = intents.map((intent) => `
677
- <button class="mushi-intent-btn" data-intent="${intent}">
1061
+ <button type="button" class="mushi-intent-btn" data-intent="${intent}">
678
1062
  ${intent}
679
1063
  </button>
680
1064
  `).join("");
681
1065
  return `
682
- ${this.renderHeader(t.step2.heading, true)}
1066
+ ${this.renderHeader({ title: t.step2.heading, showBack: true, step: STEP_NUMBER.intent })}
683
1067
  <div class="mushi-body">
684
1068
  <div class="mushi-selected-category">
685
- <span>${CATEGORY_ICONS[cat]}</span>
1069
+ <span aria-hidden="true">${CATEGORY_ICONS[cat]}</span>
686
1070
  <span>${t.step1.categories[cat]}</span>
687
1071
  </div>
688
1072
  <div class="mushi-intents">
689
1073
  ${options}
690
1074
  </div>
691
1075
  </div>
692
- <div class="mushi-step-indicator">
693
- <span class="mushi-dot done"></span>
694
- <span class="mushi-dot active"></span>
695
- <span class="mushi-dot"></span>
696
- </div>
1076
+ ${this.renderStepIndicator(STEP_NUMBER.intent)}
697
1077
  `;
698
1078
  }
699
1079
  renderDetailsStep() {
700
1080
  const t = this.locale;
701
1081
  return `
702
- ${this.renderHeader(t.step3.heading, true)}
1082
+ ${this.renderHeader({ title: t.step3.heading, showBack: true, step: STEP_NUMBER.details })}
703
1083
  <div class="mushi-body">
704
1084
  <textarea
705
1085
  class="mushi-textarea"
@@ -709,35 +1089,47 @@ var MushiWidget = class {
709
1089
  autofocus
710
1090
  ></textarea>
711
1091
  <div class="mushi-attachments">
712
- <button class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
1092
+ <button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
713
1093
  \u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
714
1094
  </button>
715
- <button class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
1095
+ <button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
716
1096
  \u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
717
1097
  </button>
718
1098
  </div>
719
1099
  <div class="mushi-error" style="display:none" role="alert"></div>
720
1100
  </div>
721
1101
  <div class="mushi-footer">
722
- <button class="mushi-submit" data-action="submit"${this.submitting ? " disabled" : ""}>
723
- ${this.submitting ? t.widget.submitting : t.widget.submit}
1102
+ <span class="mushi-footer-hint" aria-hidden="true">\u2318 + ENTER \u2192 send</span>
1103
+ <button type="button" class="mushi-submit" data-action="submit"${this.submitting ? " disabled" : ""}>
1104
+ <span>${this.submitting ? t.widget.submitting : t.widget.submit}</span>
1105
+ <span class="mushi-submit-arrow" aria-hidden="true">\u2192</span>
724
1106
  </button>
725
1107
  </div>
726
- <div class="mushi-step-indicator">
727
- <span class="mushi-dot done"></span>
728
- <span class="mushi-dot done"></span>
729
- <span class="mushi-dot active"></span>
730
- </div>
1108
+ ${this.renderStepIndicator(STEP_NUMBER.details)}
731
1109
  `;
732
1110
  }
1111
+ /**
1112
+ * Editorial success state: 朱印-style red stamp ring with the kanji
1113
+ * 受 ("received") at its centre, the localised "thank you" string
1114
+ * in serif below, and a mono ledger receipt ("REPORT · HH:MM:SS").
1115
+ * The ring + label animations are defined in styles.ts so this stays
1116
+ * pure markup and `prefers-reduced-motion` flips them to the final
1117
+ * frame instantly.
1118
+ */
733
1119
  renderSuccessStep() {
734
1120
  const t = this.locale;
1121
+ const stamp = this.submittedAt ?? /* @__PURE__ */ new Date();
1122
+ const time = stamp.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
735
1123
  return `
736
- ${this.renderHeader(t.widget.title)}
1124
+ ${this.renderHeader({ title: t.widget.title, eyebrow: "Mushi \xB7 Receipt" })}
737
1125
  <div class="mushi-body">
738
1126
  <div class="mushi-success">
739
- <div class="mushi-success-icon">\u2705</div>
740
- <p>${t.widget.submitted}</p>
1127
+ <div class="mushi-success-stamp" aria-hidden="true">
1128
+ <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><circle cx="50" cy="50" r="44"/></svg>
1129
+ <span class="mushi-success-stamp-label">\u53D7</span>
1130
+ </div>
1131
+ <div class="mushi-success-headline">${t.widget.submitted}</div>
1132
+ <div class="mushi-success-meta">REPORT \xB7 ${time}</div>
741
1133
  </div>
742
1134
  </div>
743
1135
  `;
@@ -775,7 +1167,7 @@ var MushiWidget = class {
775
1167
  panel.querySelector('[data-action="element"]')?.addEventListener("click", () => {
776
1168
  this.callbacks.onElementSelectorRequest?.();
777
1169
  });
778
- panel.querySelector('[data-action="submit"]')?.addEventListener("click", () => {
1170
+ const submitReport = () => {
779
1171
  const textarea = panel.querySelector(".mushi-textarea");
780
1172
  const description = textarea?.value?.trim() ?? "";
781
1173
  const errorEl = panel.querySelector(".mushi-error");
@@ -788,27 +1180,43 @@ var MushiWidget = class {
788
1180
  return;
789
1181
  }
790
1182
  this.submitting = true;
1183
+ this.submittedAt = /* @__PURE__ */ new Date();
791
1184
  this.render();
792
1185
  this.callbacks.onSubmit({
793
1186
  category: this.selectedCategory,
794
1187
  description,
795
1188
  intent: this.selectedIntent ?? void 0
796
1189
  });
797
- setTimeout(() => {
1190
+ this.successTimer = setTimeout(() => {
1191
+ this.successTimer = null;
798
1192
  this.submitting = false;
799
1193
  this.step = "success";
800
1194
  this.render();
801
- setTimeout(() => {
1195
+ this.autoCloseTimer = setTimeout(() => {
1196
+ this.autoCloseTimer = null;
802
1197
  if (this.step === "success") this.close();
803
- }, 2500);
1198
+ }, 2800);
804
1199
  }, 500);
805
- });
1200
+ };
1201
+ panel.querySelector('[data-action="submit"]')?.addEventListener("click", submitReport);
806
1202
  panel.addEventListener("keydown", (e) => {
807
- if (e.key === "Escape") this.close();
1203
+ if (e.key === "Escape") {
1204
+ this.close();
1205
+ return;
1206
+ }
1207
+ if (this.step === "details" && isSubmitShortcut(e)) {
1208
+ e.preventDefault();
1209
+ submitReport();
1210
+ }
808
1211
  });
809
1212
  }
810
1213
  trapFocus(panel) {
811
1214
  requestAnimationFrame(() => {
1215
+ const textarea = panel.querySelector("textarea");
1216
+ if (textarea) {
1217
+ textarea.focus();
1218
+ return;
1219
+ }
812
1220
  const focusable = panel.querySelectorAll("button, textarea, [tabindex]");
813
1221
  if (focusable.length > 0) focusable[0].focus();
814
1222
  });