@pablozaiden/webapp 1.2.0 → 1.3.1

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.
@@ -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 160ms ease-out;
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
- .wapp-toast {
1759
- animation: none;
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,17 @@ 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;
2243
+ }
2244
+
2245
+ .wapp-confirm-dialog-backdrop {
2246
+ display: flex;
2247
+ align-items: center;
2248
+ justify-content: center;
2249
+ min-width: 0;
2250
+ overflow-x: hidden;
2251
+ overflow-y: auto;
2252
+ padding: max(0.75rem, var(--wapp-safe-area-top)) max(0.75rem, var(--wapp-safe-area-right)) max(0.75rem, var(--wapp-safe-area-bottom)) max(0.75rem, var(--wapp-safe-area-left));
1943
2253
  }
1944
2254
 
1945
2255
  .wapp-dialog {
@@ -1955,6 +2265,77 @@ textarea::placeholder {
1955
2265
  background: var(--wapp-surface);
1956
2266
  padding: 0;
1957
2267
  box-shadow: 0 24px 55px rgb(0 0 0 / 0.38);
2268
+ animation: wapp-dialog-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
2269
+ }
2270
+
2271
+ .wapp-dialog.wapp-motion-exit,
2272
+ .wapp-modal.wapp-motion-exit {
2273
+ animation-name: wapp-dialog-exit;
2274
+ }
2275
+
2276
+ @keyframes wapp-dialog-enter {
2277
+ from {
2278
+ opacity: 0;
2279
+ transform: translate(-50%, calc(-50% + 0.5rem)) scale(0.98);
2280
+ }
2281
+
2282
+ to {
2283
+ opacity: 1;
2284
+ transform: translate(-50%, -50%) scale(1);
2285
+ }
2286
+ }
2287
+
2288
+ @keyframes wapp-dialog-exit {
2289
+ from {
2290
+ opacity: 1;
2291
+ transform: translate(-50%, -50%) scale(1);
2292
+ }
2293
+
2294
+ to {
2295
+ opacity: 0;
2296
+ transform: translate(-50%, calc(-50% + 0.5rem)) scale(0.98);
2297
+ }
2298
+ }
2299
+
2300
+ .wapp-confirm-dialog-backdrop > .wapp-confirm-dialog {
2301
+ position: relative;
2302
+ top: auto;
2303
+ left: auto;
2304
+ width: min(28rem, 100%);
2305
+ max-width: 100%;
2306
+ max-height: calc(var(--wapp-viewport-height) - 1.5rem - var(--wapp-safe-area-top) - var(--wapp-safe-area-bottom));
2307
+ transform: none;
2308
+ display: flex;
2309
+ flex-direction: column;
2310
+ animation-name: wapp-confirm-dialog-enter;
2311
+ }
2312
+
2313
+ .wapp-confirm-dialog-backdrop > .wapp-confirm-dialog.wapp-motion-exit {
2314
+ animation-name: wapp-confirm-dialog-exit;
2315
+ }
2316
+
2317
+ @keyframes wapp-confirm-dialog-enter {
2318
+ from {
2319
+ opacity: 0;
2320
+ transform: translateY(0.5rem) scale(0.98);
2321
+ }
2322
+
2323
+ to {
2324
+ opacity: 1;
2325
+ transform: translateY(0) scale(1);
2326
+ }
2327
+ }
2328
+
2329
+ @keyframes wapp-confirm-dialog-exit {
2330
+ from {
2331
+ opacity: 1;
2332
+ transform: translateY(0) scale(1);
2333
+ }
2334
+
2335
+ to {
2336
+ opacity: 0;
2337
+ transform: translateY(0.5rem) scale(0.98);
2338
+ }
1958
2339
  }
1959
2340
 
1960
2341
  .wapp-dialog-title {
@@ -2002,6 +2383,13 @@ textarea::placeholder {
2002
2383
  padding: 1.25rem 1.5rem;
2003
2384
  }
2004
2385
 
2386
+ .wapp-confirm-dialog .wapp-dialog-body {
2387
+ min-width: 0;
2388
+ flex: 1 1 auto;
2389
+ overflow-x: hidden;
2390
+ overflow-y: auto;
2391
+ }
2392
+
2005
2393
  .wapp-dialog-body p {
2006
2394
  margin: 0;
2007
2395
  color: color-mix(in srgb, var(--wapp-text), var(--wapp-muted) 28%);
@@ -2015,6 +2403,17 @@ textarea::placeholder {
2015
2403
  padding: 1rem 1.5rem;
2016
2404
  }
2017
2405
 
2406
+ @media (max-width: 639px) {
2407
+ .wapp-confirm-dialog .wapp-dialog-actions {
2408
+ flex-direction: column-reverse;
2409
+ align-items: stretch;
2410
+ }
2411
+
2412
+ .wapp-confirm-dialog .wapp-dialog-actions .wapp-button {
2413
+ width: 100%;
2414
+ }
2415
+ }
2416
+
2018
2417
  .wapp-modal-layer {
2019
2418
  position: fixed;
2020
2419
  inset: 0;
@@ -2026,6 +2425,13 @@ textarea::placeholder {
2026
2425
  overflow-x: hidden;
2027
2426
  overflow-y: auto;
2028
2427
  padding: 0.5rem;
2428
+ animation: wapp-fade-in var(--wapp-motion-fast) var(--wapp-motion-ease) both;
2429
+ }
2430
+
2431
+ .wapp-modal-layer.wapp-motion-exit,
2432
+ .wapp-dialog-backdrop.wapp-motion-exit {
2433
+ animation: wapp-fade-out var(--wapp-motion-fast) var(--wapp-motion-ease) both;
2434
+ pointer-events: none;
2029
2435
  }
2030
2436
 
2031
2437
  .wapp-modal-overlay {
@@ -2048,6 +2454,7 @@ textarea::placeholder {
2048
2454
  background: var(--wapp-surface);
2049
2455
  box-shadow: 0 24px 55px rgb(0 0 0 / 0.38);
2050
2456
  outline: none;
2457
+ animation: wapp-dialog-enter var(--wapp-motion-fast) var(--wapp-motion-ease) both;
2051
2458
  }
2052
2459
 
2053
2460
  .wapp-modal-sm {
@@ -2205,10 +2612,18 @@ textarea::placeholder {
2205
2612
  transform: translateX(0);
2206
2613
  }
2207
2614
 
2615
+ :root[data-wapp-mobile] .wapp-shell:not(.sidebar-open) .wapp-sidebar {
2616
+ pointer-events: none;
2617
+ }
2618
+
2208
2619
  :root[data-wapp-mobile] .wapp-shell.sidebar-open .wapp-mobile-backdrop {
2209
2620
  display: block;
2210
2621
  }
2211
2622
 
2623
+ :root[data-wapp-mobile] .wapp-mobile-backdrop.wapp-motion-exit {
2624
+ display: block;
2625
+ }
2626
+
2212
2627
  :root[data-wapp-mobile] .wapp-main-header {
2213
2628
  padding: 0.875rem 1rem;
2214
2629
  }
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
- if (toasts.length === 0) {
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
- {toasts.map((toast) => {
78
- const isError = toast.variant === "error";
79
- return (
80
- <div
81
- key={toast.id}
82
- className={`wapp-toast wapp-toast-${toast.variant}`}
83
- data-toast-variant={toast.variant}
84
- role={isError ? "alert" : "status"}
85
- aria-live={isError ? "assertive" : "polite"}
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
  );