@signalcore/ui 1.0.0-beta.3 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +79 -0
  2. package/dist/AgentChat.d.ts +19 -3
  3. package/dist/AgentChat.js +149 -27
  4. package/dist/AgentChat.js.map +1 -1
  5. package/dist/AgentSpace.d.ts +14 -2
  6. package/dist/AgentSpace.js +3 -1
  7. package/dist/AgentSpace.js.map +1 -1
  8. package/dist/View.d.ts +48 -0
  9. package/dist/View.js +101 -0
  10. package/dist/View.js.map +1 -0
  11. package/dist/blocks.d.ts +83 -2
  12. package/dist/client.d.ts +9 -1
  13. package/dist/client.js +1 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/es.js +12 -0
  16. package/dist/es.js.map +1 -1
  17. package/dist/index.d.ts +11 -5
  18. package/dist/index.js +7 -1
  19. package/dist/kit/Agenda.d.ts +25 -0
  20. package/dist/kit/Agenda.js +119 -0
  21. package/dist/kit/Agenda.js.map +1 -0
  22. package/dist/kit/Combobox.js +1 -1
  23. package/dist/kit/DateField.js +1 -1
  24. package/dist/kit/Kanban.d.ts +17 -0
  25. package/dist/kit/Kanban.js +157 -0
  26. package/dist/kit/Kanban.js.map +1 -0
  27. package/dist/kit/TimeField.js +1 -1
  28. package/dist/kit/Timeline.d.ts +14 -0
  29. package/dist/kit/Timeline.js +71 -0
  30. package/dist/kit/Timeline.js.map +1 -0
  31. package/dist/kit/context.d.ts +1 -1
  32. package/dist/kit/context.js.map +1 -1
  33. package/dist/kit/messages.d.ts +22 -0
  34. package/dist/kit/messages.js +10 -0
  35. package/dist/kit/messages.js.map +1 -1
  36. package/dist/surface/Placed.d.ts +20 -0
  37. package/dist/surface/Placed.js +70 -0
  38. package/dist/surface/Placed.js.map +1 -0
  39. package/dist/surface/Surface.d.ts +4 -2
  40. package/dist/surface/Surface.js +78 -2
  41. package/dist/surface/Surface.js.map +1 -1
  42. package/dist/surface/places.d.ts +16 -0
  43. package/dist/surface/places.js +15 -0
  44. package/dist/surface/places.js.map +1 -0
  45. package/dist/surface/scope.d.ts +17 -1
  46. package/dist/surface/scope.js +1 -0
  47. package/dist/surface/scope.js.map +1 -1
  48. package/dist/types.d.ts +12 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/widget.js +17 -17
  51. package/package.json +1 -1
  52. package/src/AgentChat.tsx +215 -34
  53. package/src/AgentSpace.tsx +17 -1
  54. package/src/View.tsx +117 -0
  55. package/src/blocks.ts +100 -2
  56. package/src/client.ts +16 -0
  57. package/src/contract.ts +166 -5
  58. package/src/es.ts +9 -0
  59. package/src/index.ts +27 -2
  60. package/src/kit/Agenda.tsx +161 -0
  61. package/src/kit/Kanban.tsx +182 -0
  62. package/src/kit/Timeline.tsx +58 -0
  63. package/src/kit/context.tsx +45 -1
  64. package/src/kit/messages.ts +31 -0
  65. package/src/styles.css +568 -0
  66. package/src/surface/Placed.tsx +98 -0
  67. package/src/surface/Surface.tsx +92 -1
  68. package/src/surface/places.ts +19 -0
  69. package/src/surface/scope.ts +24 -1
  70. package/src/types.ts +13 -0
  71. package/src/widget/element.tsx +41 -3
package/src/styles.css CHANGED
@@ -714,6 +714,124 @@
714
714
  height: 14px;
715
715
  }
716
716
 
717
+ /* Where the agent put what it showed apart: a quiet line in its reply, its glyph and words, that
718
+ opens it again; the one open reads in the ink. */
719
+ .sc-chat__placed {
720
+ display: inline-flex;
721
+ align-items: center;
722
+ gap: calc(var(--_space) * 2);
723
+ align-self: flex-start;
724
+ margin: calc(var(--_space) * 2) 0 0;
725
+ padding: 2px 0;
726
+ border: 0;
727
+ background: none;
728
+ color: var(--_muted);
729
+ font: inherit;
730
+ font-size: var(--_font-size-sm);
731
+ text-align: left;
732
+ white-space: normal;
733
+ cursor: pointer;
734
+ }
735
+
736
+ .sc-chat__placed:hover {
737
+ color: var(--_fg);
738
+ }
739
+
740
+ .sc-chat__placed:hover > span {
741
+ text-decoration: underline;
742
+ text-decoration-color: var(--_line-hover);
743
+ text-underline-offset: 3px;
744
+ }
745
+
746
+ .sc-chat__placed:focus-visible {
747
+ outline: 2px solid var(--_accent);
748
+ outline-offset: 2px;
749
+ }
750
+
751
+ .sc-chat__placed--open {
752
+ color: var(--_fg);
753
+ }
754
+
755
+ .sc-chat__placed > svg {
756
+ flex-shrink: 0;
757
+ width: 15px;
758
+ height: 15px;
759
+ }
760
+
761
+ /* A surface apart: its title and close over it, the surface scrolling under them. A panel stands
762
+ at the chat's right edge, the conversation beside it (under it, where the chat is narrow); a
763
+ modal over the page, on a dimmed backdrop. */
764
+ .sc-placed {
765
+ display: flex;
766
+ flex-direction: column;
767
+ min-height: 0;
768
+ color: var(--_fg);
769
+ background: var(--_surface);
770
+ }
771
+
772
+ .sc-placed__head {
773
+ display: flex;
774
+ flex-shrink: 0;
775
+ align-items: center;
776
+ justify-content: space-between;
777
+ gap: 12px;
778
+ padding: 10px 10px 10px 16px;
779
+ border-bottom: 1px solid var(--_hair);
780
+ }
781
+
782
+ .sc-placed__title {
783
+ margin: 0;
784
+ overflow: hidden;
785
+ font-size: var(--_font-size);
786
+ font-weight: var(--_font-weight-bold);
787
+ text-overflow: ellipsis;
788
+ white-space: nowrap;
789
+ }
790
+
791
+ .sc-placed__title::first-letter {
792
+ text-transform: uppercase;
793
+ }
794
+
795
+ .sc-placed__body {
796
+ flex: 1;
797
+ min-height: 0;
798
+ padding: 16px;
799
+ overflow-y: auto;
800
+ overscroll-behavior: contain;
801
+ }
802
+
803
+ .sc-placed--panel {
804
+ position: absolute;
805
+ inset: 0 0 0 auto;
806
+ z-index: 2;
807
+ width: min(440px, 100%);
808
+ border-left: 1px solid var(--_line);
809
+ }
810
+
811
+ /* The conversation beside an open panel: what is left of the chat's width (none, where the panel
812
+ takes it all). */
813
+ .sc-chat--placed {
814
+ padding-right: min(440px, 100%);
815
+ }
816
+
817
+ .sc-placed--modal {
818
+ width: min(600px, calc(100vw - 32px));
819
+ max-height: min(720px, calc(100dvh - 48px));
820
+ padding: 0;
821
+ overflow: hidden;
822
+ border: 1px solid var(--_line);
823
+ border-radius: var(--_radius);
824
+ box-shadow: var(--_shadow-overlay);
825
+ }
826
+
827
+ .sc-placed--modal[open] {
828
+ display: flex;
829
+ }
830
+
831
+ .sc-placed--modal::backdrop {
832
+ background: rgb(0 0 0 / 0.32);
833
+ }
834
+
717
835
  /* Where who answers the conversation changed: the same quiet line, what changed and why centred
718
836
  between the hairlines (a long reason wraps there), and when, quieter still. */
719
837
  .sc-chat__handover {
@@ -2283,6 +2401,439 @@ button.sc-tile__main:focus-visible {
2283
2401
  white-space: nowrap;
2284
2402
  }
2285
2403
 
2404
+ /* A calendar: its days one under another, each its weekday and number at the left of its events
2405
+ (today's in the accent), an empty day a single quiet line; wide enough, the week stands as
2406
+ seven columns. Each event its hours in figures that line up, its title, a quiet line of detail;
2407
+ one to open or press lights under the pointer. */
2408
+ .sc-agenda {
2409
+ min-width: 0;
2410
+ container-type: inline-size;
2411
+ font-variant-numeric: tabular-nums;
2412
+ }
2413
+
2414
+ .sc-agenda__days {
2415
+ display: flex;
2416
+ flex-direction: column;
2417
+ }
2418
+
2419
+ .sc-agenda__day {
2420
+ display: grid;
2421
+ grid-template-columns: 44px minmax(0, 1fr);
2422
+ gap: 12px;
2423
+ padding: 10px 0;
2424
+ }
2425
+
2426
+ .sc-agenda__day + .sc-agenda__day {
2427
+ border-top: 1px solid var(--_hair);
2428
+ }
2429
+
2430
+ .sc-agenda--day .sc-agenda__day {
2431
+ grid-template-columns: minmax(0, 1fr);
2432
+ padding: 0;
2433
+ }
2434
+
2435
+ .sc-agenda__date {
2436
+ display: flex;
2437
+ flex-direction: column;
2438
+ align-items: flex-start;
2439
+ margin: 0;
2440
+ font-weight: var(--_font-weight);
2441
+ line-height: 1.2;
2442
+ }
2443
+
2444
+ .sc-agenda__weekday {
2445
+ color: var(--_subtle);
2446
+ font-size: var(--_font-size-xs);
2447
+ text-transform: capitalize;
2448
+ }
2449
+
2450
+ .sc-agenda__number {
2451
+ color: var(--_fg);
2452
+ font-size: var(--_font-size-lg);
2453
+ font-weight: var(--_font-weight-medium);
2454
+ }
2455
+
2456
+ .sc-agenda__day--today .sc-agenda__weekday,
2457
+ .sc-agenda__day--today .sc-agenda__number {
2458
+ color: var(--_accent);
2459
+ }
2460
+
2461
+ .sc-agenda__events {
2462
+ display: flex;
2463
+ flex-direction: column;
2464
+ gap: 2px;
2465
+ min-width: 0;
2466
+ margin: 0;
2467
+ padding: 0;
2468
+ list-style: none;
2469
+ }
2470
+
2471
+ .sc-agenda__event {
2472
+ display: grid;
2473
+ grid-template-columns: calc(var(--_font-size) * 7) minmax(0, 1fr);
2474
+ gap: 12px;
2475
+ align-items: baseline;
2476
+ width: 100%;
2477
+ margin: 0 -8px;
2478
+ padding: 6px 8px;
2479
+ border: 0;
2480
+ border-radius: var(--_inner);
2481
+ background: none;
2482
+ color: inherit;
2483
+ font: inherit;
2484
+ text-align: left;
2485
+ text-decoration: none;
2486
+ box-sizing: content-box;
2487
+ }
2488
+
2489
+ :is(a, button).sc-agenda__event {
2490
+ cursor: pointer;
2491
+ transition: background-color 0.12s;
2492
+ }
2493
+
2494
+ :is(a, button).sc-agenda__event:hover:not(:disabled) {
2495
+ background: var(--_wash);
2496
+ }
2497
+
2498
+ :is(a, button).sc-agenda__event:focus-visible {
2499
+ outline: 2px solid var(--_accent);
2500
+ outline-offset: -2px;
2501
+ }
2502
+
2503
+ button.sc-agenda__event:disabled {
2504
+ cursor: default;
2505
+ }
2506
+
2507
+ .sc-agenda__time {
2508
+ color: var(--_muted);
2509
+ font-size: var(--_font-size-sm);
2510
+ }
2511
+
2512
+ .sc-agenda__time > span {
2513
+ white-space: nowrap;
2514
+ }
2515
+
2516
+ .sc-agenda__what {
2517
+ display: flex;
2518
+ flex-direction: column;
2519
+ min-width: 0;
2520
+ }
2521
+
2522
+ .sc-agenda__title {
2523
+ overflow-wrap: anywhere;
2524
+ font-weight: var(--_font-weight-medium);
2525
+ }
2526
+
2527
+ .sc-agenda__detail,
2528
+ .sc-agenda__empty {
2529
+ color: var(--_muted);
2530
+ font-size: var(--_font-size-sm);
2531
+ }
2532
+
2533
+ .sc-agenda__empty {
2534
+ margin: 0;
2535
+ padding: 2px 0;
2536
+ color: var(--_subtle);
2537
+ }
2538
+
2539
+ .sc-agenda__day--empty {
2540
+ align-items: center;
2541
+ }
2542
+
2543
+ @container (min-width: 680px) {
2544
+ .sc-agenda:not(.sc-agenda--day) > .sc-agenda__days {
2545
+ display: grid;
2546
+ grid-template-columns: repeat(7, minmax(0, 1fr));
2547
+ border: 1px solid var(--_hair);
2548
+ border-radius: var(--_inner);
2549
+ }
2550
+
2551
+ .sc-agenda:not(.sc-agenda--day) .sc-agenda__day {
2552
+ grid-template-columns: minmax(0, 1fr);
2553
+ align-content: start;
2554
+ gap: 8px;
2555
+ min-height: 140px;
2556
+ padding: 10px;
2557
+ border-top: 0;
2558
+ }
2559
+
2560
+ .sc-agenda:not(.sc-agenda--day) .sc-agenda__day + .sc-agenda__day {
2561
+ border-left: 1px solid var(--_hair);
2562
+ }
2563
+
2564
+ .sc-agenda:not(.sc-agenda--day) .sc-agenda__event {
2565
+ grid-template-columns: minmax(0, 1fr);
2566
+ gap: 0;
2567
+ margin: 0 -6px;
2568
+ padding: 4px 6px;
2569
+ }
2570
+
2571
+ .sc-agenda:not(.sc-agenda--day) .sc-agenda__time {
2572
+ font-size: var(--_font-size-xs);
2573
+ }
2574
+
2575
+ .sc-agenda:not(.sc-agenda--day) .sc-agenda__empty {
2576
+ display: none;
2577
+ }
2578
+ }
2579
+
2580
+ /* A timeline: a hairline down the left, a dot on it for each thing (hollow, or filled in its
2581
+ tone), its title with where it stands in a quiet word at its right, when under it, a line
2582
+ about it. */
2583
+ .sc-timeline {
2584
+ --_rail: 7px;
2585
+
2586
+ display: flex;
2587
+ flex-direction: column;
2588
+ min-width: 0;
2589
+ margin: 0;
2590
+ padding: 0;
2591
+ list-style: none;
2592
+ }
2593
+
2594
+ .sc-timeline__item {
2595
+ --_dot: var(--_line-hover);
2596
+
2597
+ position: relative;
2598
+ display: grid;
2599
+ grid-template-columns: 16px minmax(0, 1fr);
2600
+ gap: 10px;
2601
+ padding-bottom: 14px;
2602
+ }
2603
+
2604
+ .sc-timeline__item:last-child {
2605
+ padding-bottom: 0;
2606
+ }
2607
+
2608
+ .sc-timeline__item::before {
2609
+ content: "";
2610
+ position: absolute;
2611
+ top: 14px;
2612
+ bottom: -2px;
2613
+ left: var(--_rail);
2614
+ width: 1px;
2615
+ background: var(--_line);
2616
+ }
2617
+
2618
+ .sc-timeline__item:last-child::before {
2619
+ display: none;
2620
+ }
2621
+
2622
+ .sc-timeline__item--info {
2623
+ --_dot: var(--_accent);
2624
+ }
2625
+
2626
+ .sc-timeline__item--success {
2627
+ --_dot: var(--_success);
2628
+ }
2629
+
2630
+ .sc-timeline__item--warning {
2631
+ --_dot: var(--_warning);
2632
+ }
2633
+
2634
+ .sc-timeline__item--danger {
2635
+ --_dot: var(--_danger);
2636
+ }
2637
+
2638
+ .sc-timeline__dot {
2639
+ width: 9px;
2640
+ height: 9px;
2641
+ margin: 6px 0 0 3px;
2642
+ border: 2px solid var(--_dot);
2643
+ border-radius: var(--_radius-full);
2644
+ background: var(--_surface);
2645
+ }
2646
+
2647
+ [class*="sc-timeline__item--"] > .sc-timeline__dot {
2648
+ background: var(--_dot);
2649
+ }
2650
+
2651
+ .sc-timeline__body {
2652
+ display: flex;
2653
+ flex-direction: column;
2654
+ min-width: 0;
2655
+ }
2656
+
2657
+ .sc-timeline__head {
2658
+ display: flex;
2659
+ align-items: baseline;
2660
+ justify-content: space-between;
2661
+ gap: 12px;
2662
+ margin: 0;
2663
+ }
2664
+
2665
+ .sc-timeline__title {
2666
+ min-width: 0;
2667
+ overflow-wrap: anywhere;
2668
+ font-weight: var(--_font-weight-medium);
2669
+ }
2670
+
2671
+ .sc-timeline__status {
2672
+ flex-shrink: 0;
2673
+ color: var(--_muted);
2674
+ font-size: var(--_font-size-sm);
2675
+ }
2676
+
2677
+ .sc-timeline__when {
2678
+ color: var(--_subtle);
2679
+ font-size: var(--_font-size-xs);
2680
+ font-variant-numeric: tabular-nums;
2681
+ }
2682
+
2683
+ .sc-timeline__text {
2684
+ margin: 2px 0 0;
2685
+ color: var(--_muted);
2686
+ font-size: var(--_font-size-sm);
2687
+ }
2688
+
2689
+ /* A board: its columns side by side on a sunken ground, scrolling across inside their own box
2690
+ when they don't fit, each its title and count over its cards; a card lifts a hair off the
2691
+ column, a moving one fades where it was, and the column it would land in lights. */
2692
+ .sc-board {
2693
+ display: grid;
2694
+ grid-auto-columns: minmax(200px, 1fr);
2695
+ grid-auto-flow: column;
2696
+ gap: 8px;
2697
+ min-width: 0;
2698
+ overflow-x: auto;
2699
+ overscroll-behavior-x: contain;
2700
+ scroll-snap-type: x proximity;
2701
+ }
2702
+
2703
+ .sc-board__column {
2704
+ display: flex;
2705
+ flex-direction: column;
2706
+ gap: 8px;
2707
+ min-width: 0;
2708
+ padding: 10px;
2709
+ border: 1px solid transparent;
2710
+ border-radius: var(--_inner);
2711
+ background: var(--_sunken);
2712
+ scroll-snap-align: start;
2713
+ }
2714
+
2715
+ .sc-board__column--over {
2716
+ border-color: color-mix(in srgb, var(--_accent) 40%, transparent);
2717
+ background: var(--_picked);
2718
+ }
2719
+
2720
+ .sc-board__head {
2721
+ display: flex;
2722
+ align-items: baseline;
2723
+ justify-content: space-between;
2724
+ gap: 8px;
2725
+ margin: 0;
2726
+ padding: 0 2px;
2727
+ font-size: var(--_font-size-sm);
2728
+ font-weight: var(--_font-weight-medium);
2729
+ }
2730
+
2731
+ .sc-board__count {
2732
+ color: var(--_subtle);
2733
+ font-weight: var(--_font-weight);
2734
+ font-variant-numeric: tabular-nums;
2735
+ }
2736
+
2737
+ .sc-board__cards {
2738
+ display: flex;
2739
+ flex-direction: column;
2740
+ gap: 6px;
2741
+ margin: 0;
2742
+ padding: 0;
2743
+ list-style: none;
2744
+ }
2745
+
2746
+ .sc-board__card {
2747
+ position: relative;
2748
+ display: flex;
2749
+ flex-direction: column;
2750
+ gap: 2px;
2751
+ padding: 10px 12px;
2752
+ border: 1px solid var(--_line);
2753
+ border-radius: var(--_inner);
2754
+ background: var(--_surface);
2755
+ box-shadow: var(--_shadow);
2756
+ }
2757
+
2758
+ .sc-board__card[draggable="true"] {
2759
+ padding-right: 36px;
2760
+ cursor: grab;
2761
+ }
2762
+
2763
+ .sc-board__card--dragging {
2764
+ opacity: 0.45;
2765
+ }
2766
+
2767
+ .sc-board__card-title {
2768
+ overflow-wrap: anywhere;
2769
+ font-weight: var(--_font-weight-medium);
2770
+ }
2771
+
2772
+ .sc-board__card-subtitle {
2773
+ color: var(--_muted);
2774
+ font-size: var(--_font-size-sm);
2775
+ }
2776
+
2777
+ .sc-board__card-meta {
2778
+ margin-top: 4px;
2779
+ color: var(--_muted);
2780
+ font-size: var(--_font-size-xs);
2781
+ font-variant-numeric: tabular-nums;
2782
+ }
2783
+
2784
+ .sc-board__empty {
2785
+ margin: 0;
2786
+ padding: 2px;
2787
+ color: var(--_subtle);
2788
+ font-size: var(--_font-size-sm);
2789
+ }
2790
+
2791
+ .sc-board__move {
2792
+ position: absolute;
2793
+ top: 6px;
2794
+ right: 6px;
2795
+ display: grid;
2796
+ place-items: center;
2797
+ width: 26px;
2798
+ height: 26px;
2799
+ padding: 0;
2800
+ border: 0;
2801
+ border-radius: calc(var(--_inner) - 2px);
2802
+ background: none;
2803
+ color: var(--_subtle);
2804
+ cursor: pointer;
2805
+ }
2806
+
2807
+ .sc-board__move:hover,
2808
+ .sc-board__move[aria-expanded="true"] {
2809
+ background: var(--_wash);
2810
+ color: var(--_fg);
2811
+ }
2812
+
2813
+ .sc-board__move:focus-visible {
2814
+ outline: 2px solid var(--_accent);
2815
+ outline-offset: 0;
2816
+ }
2817
+
2818
+ .sc-board__move > svg {
2819
+ width: 15px;
2820
+ height: 15px;
2821
+ }
2822
+
2823
+ .sc-board__menu {
2824
+ display: flex;
2825
+ flex-direction: column;
2826
+ min-width: 160px;
2827
+ padding: 4px;
2828
+ }
2829
+
2830
+ .sc-board__menu > p {
2831
+ margin: 0;
2832
+ padding: 4px 8px 6px;
2833
+ color: var(--_subtle);
2834
+ font-size: var(--_font-size-xs);
2835
+ }
2836
+
2286
2837
  /* A note: its tone's mark and the words on a wash of it, edged by it. */
2287
2838
  .sc-note {
2288
2839
  --_tone: var(--_accent);
@@ -5211,6 +5762,23 @@ button.sc-attachment__main:focus-visible {
5211
5762
  color: var(--_fg);
5212
5763
  }
5213
5764
 
5765
+ /* A host's block this page doesn't draw: its words in a quiet hairline box where it would be. */
5766
+ .sc-surface__host {
5767
+ padding: 12px 14px;
5768
+ border: 1px solid var(--_line);
5769
+ border-radius: var(--_inner);
5770
+ background: var(--_surface);
5771
+ color: var(--_muted);
5772
+ }
5773
+
5774
+ .sc-surface__host > :first-child {
5775
+ margin-top: 0;
5776
+ }
5777
+
5778
+ .sc-surface__host > :last-child {
5779
+ margin-bottom: 0;
5780
+ }
5781
+
5214
5782
  /* An automation that is not there any more: a quiet line in a dashed box where it would be. */
5215
5783
  .sc-automation--gone {
5216
5784
  margin: 0;
@@ -0,0 +1,98 @@
1
+ import { XIcon } from "@phosphor-icons/react";
2
+ import { useEffect, useRef, type KeyboardEvent, type ReactNode } from "react";
3
+
4
+ import { classes } from "../classes";
5
+ import { Button } from "../kit/Button";
6
+
7
+ /** The parts of a surface shown apart from the conversation, by the class each carries. */
8
+ export type PlacedPart =
9
+ | "sc-placed"
10
+ | "sc-placed--panel"
11
+ | "sc-placed--modal"
12
+ | "sc-placed__head"
13
+ | "sc-placed__title"
14
+ | "sc-placed__body";
15
+
16
+ /**
17
+ * A surface the agent put apart from the conversation, tied to it all the same: in a `panel` that
18
+ * stands beside the conversation (over it where there is no room), or in a `modal` over the page,
19
+ * whose backdrop, Escape or cross close it. Its title heads it; closed, the conversation's line
20
+ * opens it again.
21
+ */
22
+ export function Placed({
23
+ where,
24
+ title,
25
+ closeLabel,
26
+ onClose,
27
+ classNames,
28
+ children,
29
+ }: {
30
+ where: "panel" | "modal";
31
+ title: string;
32
+ closeLabel: string;
33
+ onClose: () => void;
34
+ classNames: Partial<Record<PlacedPart, string>> | undefined;
35
+ children: ReactNode;
36
+ }) {
37
+ const dialog = useRef<HTMLDialogElement>(null);
38
+ useEffect(() => {
39
+ const opened = dialog.current;
40
+ if (opened === null) return undefined;
41
+ // jsdom has no top layer: a dialog there is only shown.
42
+ if (typeof opened.showModal === "function") opened.showModal();
43
+ else opened.setAttribute("open", "");
44
+ return () => {
45
+ if (typeof opened.close === "function") opened.close();
46
+ };
47
+ }, []);
48
+ const head = (
49
+ <header className={classes(classNames, "sc-placed__head")}>
50
+ <h2 className={classes(classNames, "sc-placed__title")}>{title}</h2>
51
+ <Button
52
+ tone="ghost"
53
+ size="sm"
54
+ icon={XIcon}
55
+ aria-label={closeLabel}
56
+ title={closeLabel}
57
+ onClick={onClose}
58
+ />
59
+ </header>
60
+ );
61
+ // Escape closes this, not the reply the conversation is writing.
62
+ const onKeyDown = (event: KeyboardEvent) => {
63
+ if (event.key !== "Escape") return;
64
+ event.stopPropagation();
65
+ if (where === "panel") onClose();
66
+ };
67
+ const body = <div className={classes(classNames, "sc-placed__body")}>{children}</div>;
68
+ if (where === "panel") {
69
+ return (
70
+ <aside
71
+ part="placed panel"
72
+ aria-label={title}
73
+ onKeyDown={onKeyDown}
74
+ className={classes(classNames, "sc-placed", "sc-placed--panel")}
75
+ >
76
+ {head}
77
+ {body}
78
+ </aside>
79
+ );
80
+ }
81
+ return (
82
+ <dialog
83
+ ref={dialog}
84
+ part="placed modal"
85
+ aria-label={title}
86
+ onKeyDown={onKeyDown}
87
+ onClose={onClose}
88
+ // A press on the backdrop (the dialog itself, outside its box) closes it.
89
+ onClick={(event) => {
90
+ if (event.target === event.currentTarget) onClose();
91
+ }}
92
+ className={`sc ${classes(classNames, "sc-placed", "sc-placed--modal")}`}
93
+ >
94
+ {head}
95
+ {body}
96
+ </dialog>
97
+ );
98
+ }