@pablozaiden/webapp 0.3.2 → 0.3.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.
- package/docs/getting-started.md +7 -2
- package/docs/ui-guidelines.md +4 -0
- package/package.json +1 -1
- package/src/web/WebAppRoot.tsx +126 -2
- package/src/web/styles.css +78 -13
package/docs/getting-started.md
CHANGED
|
@@ -57,11 +57,15 @@ See `docs/cli.md` for framework CLI helpers and generic API command support.
|
|
|
57
57
|
Frontend entrypoints should use `renderWebApp` so Bun/browser hot reload reuses the existing React root instead of calling `createRoot()` twice. Import the framework CSS explicitly so Bun hot reload observes style changes:
|
|
58
58
|
|
|
59
59
|
```tsx
|
|
60
|
-
import { WebAppRoot, renderWebApp } from "@pablozaiden/webapp/web";
|
|
60
|
+
import { Page, Panel, WebAppRoot, renderWebApp } from "@pablozaiden/webapp/web";
|
|
61
61
|
import "@pablozaiden/webapp/web/styles.css";
|
|
62
62
|
|
|
63
63
|
function Home() {
|
|
64
|
-
return
|
|
64
|
+
return (
|
|
65
|
+
<Page>
|
|
66
|
+
<Panel>Hello</Panel>
|
|
67
|
+
</Page>
|
|
68
|
+
);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
renderWebApp(
|
|
@@ -75,6 +79,7 @@ renderWebApp(
|
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
`renderWebApp` renders into `#root` by default and reuses the existing React root across hot reloads. Pass a custom element id or `Element` only when the app uses a different mount point.
|
|
82
|
+
`WebAppRoot` owns the shell and `.wapp-main-content`; each route component should return a `Page` wrapper so standard content margins, mobile padding and scroll behavior stay consistent.
|
|
78
83
|
|
|
79
84
|
Recommended dev script:
|
|
80
85
|
|
package/docs/ui-guidelines.md
CHANGED
|
@@ -9,6 +9,7 @@ The framework intentionally provides a consistent base UI:
|
|
|
9
9
|
- Settings and collapse controls are framework-owned.
|
|
10
10
|
- Version is always visible at the bottom of the sidebar.
|
|
11
11
|
- Main content should prefer panels, toolbars, badges and simple forms over custom one-off layouts.
|
|
12
|
+
- `WebAppRoot` owns the fixed main title bar and `.wapp-main-content`; app routes should not render or style those shell elements directly.
|
|
12
13
|
|
|
13
14
|
## Main content primitives
|
|
14
15
|
|
|
@@ -16,6 +17,7 @@ Use these first:
|
|
|
16
17
|
|
|
17
18
|
| Component | Use |
|
|
18
19
|
| --- | --- |
|
|
20
|
+
| `Page` | Required top-level wrapper for route content rendered by `WebAppRoot.routes`; provides standard margins/padding and mobile spacing |
|
|
19
21
|
| `Toolbar` | Page title/actions inside main content |
|
|
20
22
|
| `Panel` | Cards/sections; use `actions` for a top-right menu/action area |
|
|
21
23
|
| `ActionMenu` | Three-line action menu for secondary surfaces; entity-level shell menus should usually come from `SidebarNode.actions` so the framework renders them in the sidebar context menu and fixed title bar |
|
|
@@ -34,6 +36,8 @@ Use these first:
|
|
|
34
36
|
|
|
35
37
|
For entity actions, prefer the framework title bar: define one `ActionMenuItem[]` builder and attach it to `SidebarNode.actions` for the route-backed node. The framework reuses those actions for both sidebar right-click and the active route title-bar menu. Use `WebAppRoot.header.getActions` only for additional actions not owned by a sidebar node.
|
|
36
38
|
|
|
39
|
+
Every route component rendered by `WebAppRoot.routes` should return `Page` at the top level, including loading/error/empty states. Do not render a `Panel`, `DataList`, `EmptyState` or custom div directly into `WebAppRoot`; that skips the standard content margins and recreates the visual bug where cards touch the main content edge. Use `EntityHeader` only when the page needs a content-specific heading distinct from the fixed framework title bar; do not duplicate the active route title immediately below the header.
|
|
40
|
+
|
|
37
41
|
## Visual validation captures
|
|
38
42
|
|
|
39
43
|
Generated screenshots live in `artifacts/screenshots`:
|
package/package.json
CHANGED
package/src/web/WebAppRoot.tsx
CHANGED
|
@@ -70,6 +70,18 @@ type StoredSidebarPin = {
|
|
|
70
70
|
route: WebAppRoute;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
function isSidebarShortcutEditableTarget(target: EventTarget | null): boolean {
|
|
74
|
+
if (!(target instanceof HTMLElement)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (target.isContentEditable || target.closest("[contenteditable=''], [contenteditable='true']")) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return target instanceof HTMLInputElement
|
|
81
|
+
|| target instanceof HTMLTextAreaElement
|
|
82
|
+
|| target instanceof HTMLSelectElement;
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
function routeToHash(route: WebAppRoute): string {
|
|
74
86
|
const params = new URLSearchParams();
|
|
75
87
|
for (const [key, value] of Object.entries(route)) {
|
|
@@ -102,6 +114,87 @@ function useRoute(defaultRoute: WebAppRoute) {
|
|
|
102
114
|
return { route, navigate };
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
function useMobileViewportHeight() {
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (typeof window === "undefined") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const root = document.documentElement;
|
|
124
|
+
const mobileQuery = window.matchMedia("(max-width: 900px)");
|
|
125
|
+
const viewport = window.visualViewport;
|
|
126
|
+
const timers = new Set<number>();
|
|
127
|
+
let frame = 0;
|
|
128
|
+
|
|
129
|
+
const clearViewportHeight = () => {
|
|
130
|
+
root.style.removeProperty("--wapp-viewport-height");
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const sync = () => {
|
|
134
|
+
frame = 0;
|
|
135
|
+
if (!mobileQuery.matches) {
|
|
136
|
+
clearViewportHeight();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const height = Math.round(viewport?.height ?? window.innerHeight);
|
|
141
|
+
if (height > 0) {
|
|
142
|
+
root.style.setProperty("--wapp-viewport-height", `${height}px`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const scrollingElement = document.scrollingElement;
|
|
146
|
+
if (scrollingElement && scrollingElement.scrollTop !== 0) {
|
|
147
|
+
scrollingElement.scrollTop = 0;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const scheduleSync = () => {
|
|
152
|
+
if (frame) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
frame = requestAnimationFrame(sync);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const scheduleDelayedSync = (delay: number) => {
|
|
159
|
+
const timer = window.setTimeout(() => {
|
|
160
|
+
timers.delete(timer);
|
|
161
|
+
scheduleSync();
|
|
162
|
+
}, delay);
|
|
163
|
+
timers.add(timer);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleKeyboardBoundary = () => {
|
|
167
|
+
scheduleSync();
|
|
168
|
+
scheduleDelayedSync(120);
|
|
169
|
+
scheduleDelayedSync(320);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
scheduleSync();
|
|
173
|
+
viewport?.addEventListener("resize", scheduleSync);
|
|
174
|
+
viewport?.addEventListener("scroll", scheduleSync);
|
|
175
|
+
window.addEventListener("resize", scheduleSync);
|
|
176
|
+
mobileQuery.addEventListener("change", scheduleSync);
|
|
177
|
+
document.addEventListener("focusin", handleKeyboardBoundary);
|
|
178
|
+
document.addEventListener("focusout", handleKeyboardBoundary);
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
if (frame) {
|
|
182
|
+
cancelAnimationFrame(frame);
|
|
183
|
+
}
|
|
184
|
+
for (const timer of timers) {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
}
|
|
187
|
+
viewport?.removeEventListener("resize", scheduleSync);
|
|
188
|
+
viewport?.removeEventListener("scroll", scheduleSync);
|
|
189
|
+
window.removeEventListener("resize", scheduleSync);
|
|
190
|
+
mobileQuery.removeEventListener("change", scheduleSync);
|
|
191
|
+
document.removeEventListener("focusin", handleKeyboardBoundary);
|
|
192
|
+
document.removeEventListener("focusout", handleKeyboardBoundary);
|
|
193
|
+
clearViewportHeight();
|
|
194
|
+
};
|
|
195
|
+
}, []);
|
|
196
|
+
}
|
|
197
|
+
|
|
105
198
|
async function json<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
106
199
|
const response = await fetch(path, {
|
|
107
200
|
...init,
|
|
@@ -892,12 +985,20 @@ function SettingsView({ config, refresh, customSections, theme, setTheme }: { co
|
|
|
892
985
|
}
|
|
893
986
|
|
|
894
987
|
export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRouteChange, settings, version }: WebAppRootProps) {
|
|
988
|
+
useMobileViewportHeight();
|
|
895
989
|
const { config, error, refresh } = useConfig();
|
|
896
990
|
const { route, navigate } = useRoute(homeRoute);
|
|
897
991
|
const { theme, setTheme } = useTheme();
|
|
898
992
|
const [search, setSearch] = useState("");
|
|
899
993
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
900
994
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
995
|
+
const toggleSidebarCollapsed = useCallback(() => {
|
|
996
|
+
setSidebarCollapsed((current) => {
|
|
997
|
+
const nextCollapsed = !current;
|
|
998
|
+
setSidebarOpen(!nextCollapsed);
|
|
999
|
+
return nextCollapsed;
|
|
1000
|
+
});
|
|
1001
|
+
}, []);
|
|
901
1002
|
const sidebarSearchEnabled = sidebar.search !== false;
|
|
902
1003
|
const pinningEnabled = sidebar.pinning !== false;
|
|
903
1004
|
const sidebarPins = useSidebarPins(appName, sidebar.pinning ? sidebar.pinning.storageKey : undefined);
|
|
@@ -959,6 +1060,28 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
959
1060
|
.catch(() => undefined);
|
|
960
1061
|
}, [config?.currentUser?.id, setTheme]);
|
|
961
1062
|
|
|
1063
|
+
useEffect(() => {
|
|
1064
|
+
function handleSidebarShortcut(event: KeyboardEvent) {
|
|
1065
|
+
if (
|
|
1066
|
+
event.key.toLowerCase() !== "b"
|
|
1067
|
+
|| event.altKey
|
|
1068
|
+
|| event.shiftKey
|
|
1069
|
+
|| event.ctrlKey === event.metaKey
|
|
1070
|
+
|| event.isComposing
|
|
1071
|
+
|| event.repeat
|
|
1072
|
+
|| isSidebarShortcutEditableTarget(event.target)
|
|
1073
|
+
) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
event.preventDefault();
|
|
1078
|
+
toggleSidebarCollapsed();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
document.addEventListener("keydown", handleSidebarShortcut);
|
|
1082
|
+
return () => document.removeEventListener("keydown", handleSidebarShortcut);
|
|
1083
|
+
}, [toggleSidebarCollapsed]);
|
|
1084
|
+
|
|
962
1085
|
useEffect(() => {
|
|
963
1086
|
onRouteChange?.(route);
|
|
964
1087
|
}, [onRouteChange, route]);
|
|
@@ -1002,6 +1125,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
1002
1125
|
const headerTitle = header?.renderTitle?.(headerContext) ?? defaultTitle;
|
|
1003
1126
|
const primaryHeaderActions = header?.renderActions?.(headerContext);
|
|
1004
1127
|
const headerActionLabel = typeof headerTitle === "string" ? headerTitle : defaultTitle;
|
|
1128
|
+
const sidebarToggleLabel = sidebarCollapsed ? "Show sidebar" : "Collapse sidebar";
|
|
1005
1129
|
const navigateFromSidebarHeader = (nextRoute: WebAppRoute) => {
|
|
1006
1130
|
navigate(nextRoute);
|
|
1007
1131
|
setSidebarOpen(false);
|
|
@@ -1024,7 +1148,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
1024
1148
|
<div className="wapp-sidebar-actions">
|
|
1025
1149
|
{topActions.map((action) => <IconButton key={action.id} className="wapp-sidebar-top-button" title={action.title} aria-label={action.title} onClick={() => runSidebarHeaderAction(action)}><ActionIcon icon={action.icon} /></IconButton>)}
|
|
1026
1150
|
<IconButton className="wapp-sidebar-top-button" title="Settings" aria-label="Open settings" active={route.view === "settings"} onClick={() => navigateFromSidebarHeader({ view: "settings" })}><Icon name="settings" /></IconButton>
|
|
1027
|
-
<IconButton className="wapp-sidebar-top-button" title=
|
|
1151
|
+
<IconButton className="wapp-sidebar-top-button" title={sidebarToggleLabel} aria-label={sidebarToggleLabel} onClick={toggleSidebarCollapsed}><Icon name="sidebar" /></IconButton>
|
|
1028
1152
|
</div>
|
|
1029
1153
|
</div>
|
|
1030
1154
|
<div className="wapp-sidebar-scroll">
|
|
@@ -1036,7 +1160,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
1036
1160
|
<section className="wapp-main">
|
|
1037
1161
|
<header className="wapp-main-header">
|
|
1038
1162
|
<div className="wapp-main-header-title">
|
|
1039
|
-
{sidebarCollapsed ? <IconButton className="wapp-sidebar-top-button" aria-label=
|
|
1163
|
+
{sidebarCollapsed ? <IconButton className="wapp-sidebar-top-button" aria-label={sidebarToggleLabel} title={sidebarToggleLabel} onClick={toggleSidebarCollapsed}><Icon name="sidebar" /></IconButton> : <IconButton className="wapp-mobile-only wapp-sidebar-top-button" aria-label="Show sidebar" title="Show sidebar" onClick={() => setSidebarOpen(true)}><Icon name="sidebar" /></IconButton>}
|
|
1040
1164
|
<h1>{headerTitle}</h1>
|
|
1041
1165
|
</div>
|
|
1042
1166
|
{primaryHeaderActions || headerActions.length ? (
|
package/src/web/styles.css
CHANGED
|
@@ -13,9 +13,29 @@
|
|
|
13
13
|
--wapp-danger-bg: #991b1b;
|
|
14
14
|
--wapp-primary: #111827;
|
|
15
15
|
--wapp-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
16
|
+
--wapp-overlay-bg: rgb(0 0 0 / 0.5);
|
|
17
|
+
--wapp-overlay-blur: blur(4px);
|
|
18
|
+
--wapp-safe-area-top: env(safe-area-inset-top, 0px);
|
|
19
|
+
--wapp-safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
|
20
|
+
--wapp-safe-area-left: env(safe-area-inset-left, 0px);
|
|
21
|
+
--wapp-safe-area-right: env(safe-area-inset-right, 0px);
|
|
22
|
+
--wapp-mobile-bottom-clearance: max(var(--wapp-safe-area-bottom), 0.75rem);
|
|
23
|
+
--wapp-viewport-height: 100vh;
|
|
16
24
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
@supports (height: 100svh) {
|
|
28
|
+
:root {
|
|
29
|
+
--wapp-viewport-height: 100svh;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@supports (height: 100dvh) {
|
|
34
|
+
:root {
|
|
35
|
+
--wapp-viewport-height: 100dvh;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
:root.dark {
|
|
20
40
|
color-scheme: dark;
|
|
21
41
|
--wapp-bg: var(--wapp-shell-bg);
|
|
@@ -41,6 +61,7 @@ html,
|
|
|
41
61
|
body,
|
|
42
62
|
#root {
|
|
43
63
|
height: 100%;
|
|
64
|
+
min-height: 100%;
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
body {
|
|
@@ -84,8 +105,8 @@ textarea::placeholder {
|
|
|
84
105
|
|
|
85
106
|
.wapp-shell {
|
|
86
107
|
display: flex;
|
|
87
|
-
height:
|
|
88
|
-
min-height:
|
|
108
|
+
height: var(--wapp-viewport-height);
|
|
109
|
+
min-height: var(--wapp-viewport-height);
|
|
89
110
|
overflow: hidden;
|
|
90
111
|
background: var(--wapp-bg);
|
|
91
112
|
}
|
|
@@ -94,7 +115,7 @@ textarea::placeholder {
|
|
|
94
115
|
width: 20rem;
|
|
95
116
|
min-width: 20rem;
|
|
96
117
|
min-height: 0;
|
|
97
|
-
height:
|
|
118
|
+
height: var(--wapp-viewport-height);
|
|
98
119
|
display: flex;
|
|
99
120
|
flex-direction: column;
|
|
100
121
|
overflow: hidden;
|
|
@@ -258,6 +279,7 @@ textarea::placeholder {
|
|
|
258
279
|
overflow-y: auto;
|
|
259
280
|
overflow-x: hidden;
|
|
260
281
|
padding: 1rem 0.75rem;
|
|
282
|
+
scroll-padding-bottom: var(--wapp-mobile-bottom-clearance);
|
|
261
283
|
}
|
|
262
284
|
|
|
263
285
|
.wapp-search input {
|
|
@@ -501,7 +523,7 @@ textarea::placeholder {
|
|
|
501
523
|
align-items: center;
|
|
502
524
|
justify-content: space-between;
|
|
503
525
|
min-height: 2rem;
|
|
504
|
-
padding: 0.25rem 0.25rem
|
|
526
|
+
padding: 0.25rem 0.25rem var(--wapp-safe-area-bottom);
|
|
505
527
|
color: var(--wapp-muted-2);
|
|
506
528
|
font-size: 0.75rem;
|
|
507
529
|
}
|
|
@@ -527,7 +549,7 @@ textarea::placeholder {
|
|
|
527
549
|
flex: 1;
|
|
528
550
|
min-width: 0;
|
|
529
551
|
min-height: 0;
|
|
530
|
-
height:
|
|
552
|
+
height: var(--wapp-viewport-height);
|
|
531
553
|
display: flex;
|
|
532
554
|
flex-direction: column;
|
|
533
555
|
}
|
|
@@ -551,12 +573,53 @@ textarea::placeholder {
|
|
|
551
573
|
overflow: auto;
|
|
552
574
|
overflow-x: hidden;
|
|
553
575
|
padding: 0;
|
|
576
|
+
scroll-padding-bottom: var(--wapp-mobile-bottom-clearance);
|
|
554
577
|
}
|
|
555
578
|
|
|
556
579
|
.wapp-page {
|
|
557
580
|
width: 100%;
|
|
558
581
|
min-width: 0;
|
|
559
|
-
|
|
582
|
+
max-width: 100%;
|
|
583
|
+
padding: 1.5rem 2rem max(2rem, var(--wapp-mobile-bottom-clearance));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.wapp-page,
|
|
587
|
+
.wapp-panel,
|
|
588
|
+
.wapp-modal-body {
|
|
589
|
+
overflow-wrap: anywhere;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.wapp-page :where(*):not(table, thead, tbody, tfoot, tr, th, td),
|
|
593
|
+
.wapp-panel :where(*):not(table, thead, tbody, tfoot, tr, th, td),
|
|
594
|
+
.wapp-modal-body :where(*):not(table, thead, tbody, tfoot, tr, th, td) {
|
|
595
|
+
min-width: 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.wapp-page :where(img, video, canvas, iframe, svg),
|
|
599
|
+
.wapp-panel :where(img, video, canvas, iframe, svg),
|
|
600
|
+
.wapp-modal-body :where(img, video, canvas, iframe, svg) {
|
|
601
|
+
max-width: 100%;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.wapp-page :where(pre),
|
|
605
|
+
.wapp-panel :where(pre),
|
|
606
|
+
.wapp-modal-body :where(pre) {
|
|
607
|
+
max-width: 100%;
|
|
608
|
+
overflow-x: auto;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.wapp-page :where(table),
|
|
612
|
+
.wapp-panel :where(table),
|
|
613
|
+
.wapp-modal-body :where(table) {
|
|
614
|
+
display: block;
|
|
615
|
+
max-width: 100%;
|
|
616
|
+
overflow-x: auto;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.wapp-page :where(code, kbd, samp),
|
|
620
|
+
.wapp-panel :where(code, kbd, samp),
|
|
621
|
+
.wapp-modal-body :where(code, kbd, samp) {
|
|
622
|
+
overflow-wrap: anywhere;
|
|
560
623
|
}
|
|
561
624
|
|
|
562
625
|
.wapp-panel {
|
|
@@ -1488,7 +1551,8 @@ textarea::placeholder {
|
|
|
1488
1551
|
.wapp-mobile-backdrop {
|
|
1489
1552
|
position: fixed;
|
|
1490
1553
|
inset: 0;
|
|
1491
|
-
background:
|
|
1554
|
+
background: var(--wapp-overlay-bg);
|
|
1555
|
+
backdrop-filter: var(--wapp-overlay-blur);
|
|
1492
1556
|
z-index: 50;
|
|
1493
1557
|
}
|
|
1494
1558
|
|
|
@@ -1589,8 +1653,8 @@ textarea::placeholder {
|
|
|
1589
1653
|
.wapp-modal-overlay {
|
|
1590
1654
|
position: absolute;
|
|
1591
1655
|
inset: 0;
|
|
1592
|
-
background:
|
|
1593
|
-
backdrop-filter: blur
|
|
1656
|
+
background: var(--wapp-overlay-bg);
|
|
1657
|
+
backdrop-filter: var(--wapp-overlay-blur);
|
|
1594
1658
|
}
|
|
1595
1659
|
|
|
1596
1660
|
.wapp-modal {
|
|
@@ -1599,7 +1663,7 @@ textarea::placeholder {
|
|
|
1599
1663
|
flex-direction: column;
|
|
1600
1664
|
width: 100%;
|
|
1601
1665
|
min-width: 0;
|
|
1602
|
-
max-height: calc(
|
|
1666
|
+
max-height: calc(var(--wapp-viewport-height) - 1.5rem - var(--wapp-safe-area-bottom));
|
|
1603
1667
|
overflow: hidden;
|
|
1604
1668
|
border: 1px solid var(--wapp-border);
|
|
1605
1669
|
border-radius: 0.5rem;
|
|
@@ -1682,6 +1746,7 @@ textarea::placeholder {
|
|
|
1682
1746
|
overflow-x: hidden;
|
|
1683
1747
|
overflow-y: auto;
|
|
1684
1748
|
padding: 0.75rem 1rem;
|
|
1749
|
+
scroll-padding-bottom: var(--wapp-mobile-bottom-clearance);
|
|
1685
1750
|
}
|
|
1686
1751
|
|
|
1687
1752
|
.wapp-modal-footer {
|
|
@@ -1692,7 +1757,7 @@ textarea::placeholder {
|
|
|
1692
1757
|
justify-content: flex-end;
|
|
1693
1758
|
gap: 0.5rem;
|
|
1694
1759
|
border-top: 1px solid var(--wapp-border);
|
|
1695
|
-
padding: 0.75rem 1rem;
|
|
1760
|
+
padding: 0.75rem 1rem max(0.75rem, var(--wapp-mobile-bottom-clearance));
|
|
1696
1761
|
}
|
|
1697
1762
|
|
|
1698
1763
|
@media (min-width: 640px) {
|
|
@@ -1701,7 +1766,7 @@ textarea::placeholder {
|
|
|
1701
1766
|
}
|
|
1702
1767
|
|
|
1703
1768
|
.wapp-modal {
|
|
1704
|
-
max-height: calc(
|
|
1769
|
+
max-height: calc(var(--wapp-viewport-height) - 2.5rem - var(--wapp-safe-area-bottom));
|
|
1705
1770
|
}
|
|
1706
1771
|
|
|
1707
1772
|
.wapp-modal-header {
|
|
@@ -1776,7 +1841,7 @@ textarea::placeholder {
|
|
|
1776
1841
|
}
|
|
1777
1842
|
|
|
1778
1843
|
.wapp-page {
|
|
1779
|
-
padding: 1rem;
|
|
1844
|
+
padding: 1rem 1rem max(1rem, var(--wapp-mobile-bottom-clearance));
|
|
1780
1845
|
}
|
|
1781
1846
|
|
|
1782
1847
|
.wapp-panel-header,
|