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