@pablozaiden/webapp 1.1.1 → 1.3.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/README.md +19 -0
- package/docs/getting-started.md +27 -34
- package/docs/server.md +54 -3
- package/docs/settings.md +6 -5
- package/docs/ui-guidelines.md +1 -0
- package/package.json +2 -1
- package/src/contracts/index.ts +25 -1
- package/src/server/create-web-app-server.ts +2 -1
- package/src/server/index.ts +11 -0
- package/src/server/logger.ts +107 -42
- package/src/server/request-schemas.ts +2 -1
- package/src/server/runtime-config.ts +8 -6
- package/src/web/WebAppRoot.tsx +13 -1
- package/src/web/app-shell.tsx +31 -17
- package/src/web/components/index.tsx +382 -19
- package/src/web/index.ts +3 -0
- package/src/web/logger.ts +44 -0
- package/src/web/motion.tsx +487 -0
- package/src/web/routing.ts +36 -5
- package/src/web/settings/account-section.tsx +3 -5
- package/src/web/settings/resource-state.tsx +12 -7
- package/src/web/sidebar-tree.tsx +15 -4
- package/src/web/styles.css +349 -3
- package/src/web/toast.tsx +67 -20
- package/src/web/webapp-config.tsx +2 -2
package/src/web/styles.css
CHANGED
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
--wapp-safe-area-right: env(safe-area-inset-right, 0px);
|
|
39
39
|
--wapp-mobile-bottom-clearance: max(var(--wapp-safe-area-bottom), 0.75rem);
|
|
40
40
|
--wapp-viewport-height: 100vh;
|
|
41
|
+
--wapp-motion-fast: 350ms;
|
|
42
|
+
--wapp-motion-normal: 350ms;
|
|
43
|
+
--wapp-motion-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
44
|
+
--wapp-motion-distance: 0.25rem;
|
|
41
45
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -638,6 +642,7 @@ textarea::placeholder {
|
|
|
638
642
|
}
|
|
639
643
|
|
|
640
644
|
.wapp-main-content {
|
|
645
|
+
view-transition-name: wapp-main-content;
|
|
641
646
|
display: flex;
|
|
642
647
|
flex-direction: column;
|
|
643
648
|
flex: 1;
|
|
@@ -648,6 +653,132 @@ textarea::placeholder {
|
|
|
648
653
|
scroll-padding-bottom: var(--wapp-mobile-bottom-clearance);
|
|
649
654
|
}
|
|
650
655
|
|
|
656
|
+
.wapp-route-view,
|
|
657
|
+
.wapp-route-fade {
|
|
658
|
+
min-width: 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.wapp-route-view {
|
|
662
|
+
display: flex;
|
|
663
|
+
flex: 1;
|
|
664
|
+
min-height: 0;
|
|
665
|
+
flex-direction: column;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.wapp-route-view,
|
|
669
|
+
.wapp-route-fade {
|
|
670
|
+
animation: wapp-fade-in var(--wapp-motion-normal) var(--wapp-motion-ease) both;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.wapp-native-route-transitions .wapp-route-view {
|
|
674
|
+
animation: none;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
::view-transition-old(wapp-main-content) {
|
|
678
|
+
animation: wapp-view-transition-old var(--wapp-motion-normal) var(--wapp-motion-ease) both;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
::view-transition-new(wapp-main-content) {
|
|
682
|
+
animation: wapp-view-transition-new var(--wapp-motion-normal) var(--wapp-motion-ease) both;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
@keyframes wapp-view-transition-old {
|
|
686
|
+
to {
|
|
687
|
+
opacity: 0;
|
|
688
|
+
transform: translateY(calc(var(--wapp-motion-distance) * -1));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
@keyframes wapp-view-transition-new {
|
|
693
|
+
from {
|
|
694
|
+
opacity: 0;
|
|
695
|
+
transform: translateY(var(--wapp-motion-distance));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.wapp-motion-enter {
|
|
700
|
+
animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.wapp-motion-exit {
|
|
704
|
+
animation: wapp-fade-out var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
705
|
+
pointer-events: none;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@keyframes wapp-fade-in {
|
|
709
|
+
from {
|
|
710
|
+
opacity: 0;
|
|
711
|
+
transform: translateY(var(--wapp-motion-distance));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
to {
|
|
715
|
+
opacity: 1;
|
|
716
|
+
transform: translateY(0);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@keyframes wapp-fade-out {
|
|
721
|
+
from {
|
|
722
|
+
opacity: 1;
|
|
723
|
+
transform: translateY(0);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
to {
|
|
727
|
+
opacity: 0;
|
|
728
|
+
transform: translateY(calc(var(--wapp-motion-distance) * -1));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.wapp-streaming-text-chunk {
|
|
733
|
+
animation: wapp-streaming-text-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@keyframes wapp-streaming-text-enter {
|
|
737
|
+
from {
|
|
738
|
+
opacity: 0;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
to {
|
|
742
|
+
opacity: 1;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.wapp-collapsible {
|
|
747
|
+
display: grid;
|
|
748
|
+
grid-template-rows: 1fr;
|
|
749
|
+
transition: grid-template-rows var(--wapp-motion-normal) var(--wapp-motion-ease), opacity var(--wapp-motion-normal) var(--wapp-motion-ease);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.wapp-collapsible-inner {
|
|
753
|
+
min-height: 0;
|
|
754
|
+
overflow: hidden;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.wapp-collapsible-enter {
|
|
758
|
+
grid-template-rows: 0fr;
|
|
759
|
+
opacity: 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.wapp-collapsible-idle {
|
|
763
|
+
grid-template-rows: 1fr;
|
|
764
|
+
opacity: 1;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.wapp-collapsible-exit {
|
|
768
|
+
grid-template-rows: 0fr;
|
|
769
|
+
opacity: 0;
|
|
770
|
+
pointer-events: none;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.wapp-async-state {
|
|
774
|
+
animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.wapp-async-state-refreshing {
|
|
778
|
+
color: var(--wapp-muted);
|
|
779
|
+
font-size: 0.75rem;
|
|
780
|
+
}
|
|
781
|
+
|
|
651
782
|
.wapp-page {
|
|
652
783
|
width: 100%;
|
|
653
784
|
min-width: 0;
|
|
@@ -1208,6 +1339,77 @@ textarea::placeholder {
|
|
|
1208
1339
|
box-shadow: 0 16px 36px rgb(15 23 42 / 0.18);
|
|
1209
1340
|
}
|
|
1210
1341
|
|
|
1342
|
+
.wapp-floating-panel {
|
|
1343
|
+
z-index: 1000;
|
|
1344
|
+
transform-origin: top left;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.wapp-floating-panel.wapp-motion-enter {
|
|
1348
|
+
animation-name: wapp-floating-panel-enter;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
.wapp-floating-panel.wapp-motion-exit {
|
|
1352
|
+
animation-name: wapp-floating-panel-exit;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
@keyframes wapp-floating-panel-enter {
|
|
1356
|
+
from {
|
|
1357
|
+
opacity: 0;
|
|
1358
|
+
transform: translateY(0.25rem) scale(0.98);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
to {
|
|
1362
|
+
opacity: 1;
|
|
1363
|
+
transform: translateY(0) scale(1);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
@keyframes wapp-floating-panel-exit {
|
|
1368
|
+
from {
|
|
1369
|
+
opacity: 1;
|
|
1370
|
+
transform: translateY(0) scale(1);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
to {
|
|
1374
|
+
opacity: 0;
|
|
1375
|
+
transform: translateY(0.25rem) scale(0.98);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.wapp-action-menu.wapp-motion-enter {
|
|
1380
|
+
transform-origin: top right;
|
|
1381
|
+
animation-name: wapp-menu-enter;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.wapp-action-menu.wapp-motion-exit {
|
|
1385
|
+
transform-origin: top right;
|
|
1386
|
+
animation-name: wapp-menu-exit;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
@keyframes wapp-menu-enter {
|
|
1390
|
+
from {
|
|
1391
|
+
opacity: 0;
|
|
1392
|
+
transform: translateY(-0.25rem) scale(0.98);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
to {
|
|
1396
|
+
opacity: 1;
|
|
1397
|
+
transform: translateY(0) scale(1);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
@keyframes wapp-menu-exit {
|
|
1402
|
+
from {
|
|
1403
|
+
opacity: 1;
|
|
1404
|
+
transform: translateY(0) scale(1);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
to {
|
|
1408
|
+
opacity: 0;
|
|
1409
|
+
transform: translateY(-0.25rem) scale(0.98);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1211
1413
|
:root.dark .wapp-action-menu {
|
|
1212
1414
|
border-color: #334155;
|
|
1213
1415
|
background: #262626;
|
|
@@ -1684,7 +1886,7 @@ textarea::placeholder {
|
|
|
1684
1886
|
padding: 0.75rem;
|
|
1685
1887
|
box-shadow: 0 8px 24px rgb(0 0 0 / 0.14);
|
|
1686
1888
|
pointer-events: auto;
|
|
1687
|
-
animation: wapp-toast-enter
|
|
1889
|
+
animation: wapp-toast-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
1688
1890
|
}
|
|
1689
1891
|
|
|
1690
1892
|
.wapp-toast-success {
|
|
@@ -1754,9 +1956,30 @@ textarea::placeholder {
|
|
|
1754
1956
|
}
|
|
1755
1957
|
}
|
|
1756
1958
|
|
|
1959
|
+
.wapp-toast.wapp-motion-enter {
|
|
1960
|
+
animation-name: wapp-toast-enter;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
.wapp-toast.wapp-motion-exit {
|
|
1964
|
+
animation-name: wapp-toast-exit;
|
|
1965
|
+
pointer-events: none;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
@keyframes wapp-toast-exit {
|
|
1969
|
+
to {
|
|
1970
|
+
opacity: 0;
|
|
1971
|
+
transform: translateY(0.25rem);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1757
1975
|
@media (prefers-reduced-motion: reduce) {
|
|
1758
|
-
|
|
1759
|
-
|
|
1976
|
+
*,
|
|
1977
|
+
*::before,
|
|
1978
|
+
*::after {
|
|
1979
|
+
animation-duration: 0.01ms !important;
|
|
1980
|
+
animation-iteration-count: 1 !important;
|
|
1981
|
+
scroll-behavior: auto !important;
|
|
1982
|
+
transition-duration: 0.01ms !important;
|
|
1760
1983
|
}
|
|
1761
1984
|
}
|
|
1762
1985
|
|
|
@@ -1841,6 +2064,76 @@ textarea::placeholder {
|
|
|
1841
2064
|
box-shadow: none;
|
|
1842
2065
|
}
|
|
1843
2066
|
|
|
2067
|
+
.wapp-tabs {
|
|
2068
|
+
display: flex;
|
|
2069
|
+
align-items: flex-end;
|
|
2070
|
+
gap: 0.25rem;
|
|
2071
|
+
border-bottom: 1px solid var(--wapp-border-soft);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
.wapp-tab {
|
|
2075
|
+
position: relative;
|
|
2076
|
+
border: 0;
|
|
2077
|
+
background: transparent;
|
|
2078
|
+
color: var(--wapp-muted);
|
|
2079
|
+
cursor: pointer;
|
|
2080
|
+
padding: 0.625rem 0.75rem;
|
|
2081
|
+
transition: color 120ms ease;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
.wapp-tab::after {
|
|
2085
|
+
position: absolute;
|
|
2086
|
+
right: 0.75rem;
|
|
2087
|
+
bottom: -1px;
|
|
2088
|
+
left: 0.75rem;
|
|
2089
|
+
height: 2px;
|
|
2090
|
+
background: var(--wapp-primary);
|
|
2091
|
+
content: "";
|
|
2092
|
+
opacity: 0;
|
|
2093
|
+
transform: scaleX(0.35);
|
|
2094
|
+
transition: opacity 120ms ease, transform 160ms var(--wapp-motion-ease);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.wapp-tab:hover,
|
|
2098
|
+
.wapp-tab.active {
|
|
2099
|
+
color: var(--wapp-text);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.wapp-tab.active::after {
|
|
2103
|
+
opacity: 1;
|
|
2104
|
+
transform: scaleX(1);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
.wapp-tab:disabled {
|
|
2108
|
+
cursor: not-allowed;
|
|
2109
|
+
opacity: 0.5;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
.wapp-tab:focus-visible {
|
|
2113
|
+
outline: 2px solid var(--wapp-primary);
|
|
2114
|
+
outline-offset: -2px;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
.wapp-tab-panels {
|
|
2118
|
+
position: relative;
|
|
2119
|
+
min-width: 0;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
.wapp-tab-panel {
|
|
2123
|
+
min-width: 0;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
.wapp-tab-panel.wapp-motion-exit {
|
|
2127
|
+
position: absolute;
|
|
2128
|
+
inset: 0;
|
|
2129
|
+
overflow: hidden;
|
|
2130
|
+
width: 100%;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
.wapp-animated-list-contents {
|
|
2134
|
+
display: contents;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1844
2137
|
.wapp-list {
|
|
1845
2138
|
margin-top: 0.75rem;
|
|
1846
2139
|
border: 1px solid var(--wapp-border-soft);
|
|
@@ -1932,6 +2225,12 @@ textarea::placeholder {
|
|
|
1932
2225
|
background: var(--wapp-overlay-bg);
|
|
1933
2226
|
backdrop-filter: var(--wapp-overlay-blur);
|
|
1934
2227
|
z-index: 50;
|
|
2228
|
+
animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
.wapp-mobile-backdrop.wapp-motion-exit {
|
|
2232
|
+
animation-name: wapp-fade-out;
|
|
2233
|
+
pointer-events: none;
|
|
1935
2234
|
}
|
|
1936
2235
|
|
|
1937
2236
|
.wapp-dialog-backdrop {
|
|
@@ -1940,6 +2239,7 @@ textarea::placeholder {
|
|
|
1940
2239
|
z-index: 80;
|
|
1941
2240
|
background: rgb(0 0 0 / 0.58);
|
|
1942
2241
|
backdrop-filter: blur(10px);
|
|
2242
|
+
animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
1943
2243
|
}
|
|
1944
2244
|
|
|
1945
2245
|
.wapp-dialog {
|
|
@@ -1955,6 +2255,36 @@ textarea::placeholder {
|
|
|
1955
2255
|
background: var(--wapp-surface);
|
|
1956
2256
|
padding: 0;
|
|
1957
2257
|
box-shadow: 0 24px 55px rgb(0 0 0 / 0.38);
|
|
2258
|
+
animation: wapp-dialog-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
.wapp-dialog.wapp-motion-exit,
|
|
2262
|
+
.wapp-modal.wapp-motion-exit {
|
|
2263
|
+
animation-name: wapp-dialog-exit;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
@keyframes wapp-dialog-enter {
|
|
2267
|
+
from {
|
|
2268
|
+
opacity: 0;
|
|
2269
|
+
transform: translate(-50%, calc(-50% + 0.5rem)) scale(0.98);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
to {
|
|
2273
|
+
opacity: 1;
|
|
2274
|
+
transform: translate(-50%, -50%) scale(1);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
@keyframes wapp-dialog-exit {
|
|
2279
|
+
from {
|
|
2280
|
+
opacity: 1;
|
|
2281
|
+
transform: translate(-50%, -50%) scale(1);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
to {
|
|
2285
|
+
opacity: 0;
|
|
2286
|
+
transform: translate(-50%, calc(-50% + 0.5rem)) scale(0.98);
|
|
2287
|
+
}
|
|
1958
2288
|
}
|
|
1959
2289
|
|
|
1960
2290
|
.wapp-dialog-title {
|
|
@@ -2026,6 +2356,13 @@ textarea::placeholder {
|
|
|
2026
2356
|
overflow-x: hidden;
|
|
2027
2357
|
overflow-y: auto;
|
|
2028
2358
|
padding: 0.5rem;
|
|
2359
|
+
animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
.wapp-modal-layer.wapp-motion-exit,
|
|
2363
|
+
.wapp-dialog-backdrop.wapp-motion-exit {
|
|
2364
|
+
animation: wapp-fade-out var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
2365
|
+
pointer-events: none;
|
|
2029
2366
|
}
|
|
2030
2367
|
|
|
2031
2368
|
.wapp-modal-overlay {
|
|
@@ -2048,6 +2385,7 @@ textarea::placeholder {
|
|
|
2048
2385
|
background: var(--wapp-surface);
|
|
2049
2386
|
box-shadow: 0 24px 55px rgb(0 0 0 / 0.38);
|
|
2050
2387
|
outline: none;
|
|
2388
|
+
animation: wapp-dialog-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
|
|
2051
2389
|
}
|
|
2052
2390
|
|
|
2053
2391
|
.wapp-modal-sm {
|
|
@@ -2205,10 +2543,18 @@ textarea::placeholder {
|
|
|
2205
2543
|
transform: translateX(0);
|
|
2206
2544
|
}
|
|
2207
2545
|
|
|
2546
|
+
:root[data-wapp-mobile] .wapp-shell:not(.sidebar-open) .wapp-sidebar {
|
|
2547
|
+
pointer-events: none;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2208
2550
|
:root[data-wapp-mobile] .wapp-shell.sidebar-open .wapp-mobile-backdrop {
|
|
2209
2551
|
display: block;
|
|
2210
2552
|
}
|
|
2211
2553
|
|
|
2554
|
+
:root[data-wapp-mobile] .wapp-mobile-backdrop.wapp-motion-exit {
|
|
2555
|
+
display: block;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2212
2558
|
:root[data-wapp-mobile] .wapp-main-header {
|
|
2213
2559
|
padding: 0.875rem 1rem;
|
|
2214
2560
|
}
|
package/src/web/toast.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
+
import { MOTION_FAST_MS, usePresence } from "./motion";
|
|
3
4
|
|
|
4
5
|
export type ToastVariant = "success" | "error" | "warning" | "info";
|
|
5
6
|
export type ToastId = string;
|
|
@@ -67,31 +68,77 @@ function normalizeDuration(duration: number | undefined): number {
|
|
|
67
68
|
return duration;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function ToastItem({
|
|
72
|
+
toast,
|
|
73
|
+
present,
|
|
74
|
+
onDismiss,
|
|
75
|
+
onExitComplete,
|
|
76
|
+
}: {
|
|
77
|
+
toast: Toast;
|
|
78
|
+
present: boolean;
|
|
79
|
+
onDismiss: (id: ToastId) => void;
|
|
80
|
+
onExitComplete: (id: ToastId) => void;
|
|
81
|
+
}) {
|
|
82
|
+
const presence = usePresence(present, { duration: MOTION_FAST_MS });
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!present && !presence.mounted) {
|
|
86
|
+
onExitComplete(toast.id);
|
|
87
|
+
}
|
|
88
|
+
}, [onExitComplete, presence.mounted, present, toast.id]);
|
|
89
|
+
|
|
90
|
+
if (!presence.mounted) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isError = toast.variant === "error";
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={`wapp-toast wapp-toast-${toast.variant} wapp-motion-${presence.state}`}
|
|
98
|
+
data-toast-variant={toast.variant}
|
|
99
|
+
role={isError ? "alert" : "status"}
|
|
100
|
+
aria-live={isError ? "assertive" : "polite"}
|
|
101
|
+
aria-atomic="true"
|
|
102
|
+
aria-hidden={present ? undefined : true}
|
|
103
|
+
>
|
|
104
|
+
<span className="wapp-toast-message">{toast.message}</span>
|
|
105
|
+
<button type="button" className="wapp-toast-dismiss" aria-label="Dismiss notification" onClick={() => onDismiss(toast.id)}>
|
|
106
|
+
×
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
70
112
|
function ToastViewport({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: ToastId) => void }) {
|
|
71
|
-
|
|
113
|
+
const [renderedToasts, setRenderedToasts] = useState<Toast[]>(toasts);
|
|
114
|
+
const activeIds = useMemo(() => new Set(toasts.map((toast) => toast.id)), [toasts]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
setRenderedToasts((current) => {
|
|
118
|
+
const exiting = current.filter((toast) => !activeIds.has(toast.id));
|
|
119
|
+
return [...toasts, ...exiting];
|
|
120
|
+
});
|
|
121
|
+
}, [activeIds, toasts]);
|
|
122
|
+
|
|
123
|
+
const removeExited = useCallback((id: ToastId) => {
|
|
124
|
+
setRenderedToasts((current) => current.filter((toast) => toast.id !== id));
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
if (renderedToasts.length === 0) {
|
|
72
128
|
return null;
|
|
73
129
|
}
|
|
74
130
|
|
|
75
131
|
return createPortal(
|
|
76
|
-
<div className="wapp-toast-viewport" role="region" aria-label="Notifications">
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
aria-atomic="true"
|
|
87
|
-
>
|
|
88
|
-
<span className="wapp-toast-message">{toast.message}</span>
|
|
89
|
-
<button type="button" className="wapp-toast-dismiss" aria-label="Dismiss notification" onClick={() => onDismiss(toast.id)}>
|
|
90
|
-
×
|
|
91
|
-
</button>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
})}
|
|
132
|
+
<div className="wapp-toast-viewport" role="region" aria-label="Notifications" aria-hidden={toasts.length === 0 ? true : undefined}>
|
|
133
|
+
{renderedToasts.map((toast) => (
|
|
134
|
+
<ToastItem
|
|
135
|
+
key={toast.id}
|
|
136
|
+
toast={toast}
|
|
137
|
+
present={activeIds.has(toast.id)}
|
|
138
|
+
onDismiss={onDismiss}
|
|
139
|
+
onExitComplete={removeExited}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
95
142
|
</div>,
|
|
96
143
|
document.body,
|
|
97
144
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { VALID_LOG_LEVELS, type WebAppConfigResponse } from "../contracts";
|
|
3
3
|
import { appJson } from "./api-client";
|
|
4
4
|
|
|
5
5
|
export interface WebAppConfigState {
|
|
@@ -16,7 +16,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function isLogLevelName(value: unknown): boolean {
|
|
19
|
-
return
|
|
19
|
+
return typeof value === "string" && VALID_LOG_LEVELS.includes(value as WebAppConfigResponse["logLevel"]["level"]);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function isCurrentUser(value: unknown): boolean {
|