@pablozaiden/webapp 0.5.7 → 0.5.8
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/sidebar.md +2 -0
- package/package.json +1 -1
- package/src/web/WebAppRoot.tsx +87 -5
- package/src/web/styles.css +5 -0
package/docs/sidebar.md
CHANGED
|
@@ -38,6 +38,8 @@ Sidebar nodes support:
|
|
|
38
38
|
|
|
39
39
|
Search is intentionally app-defined: `getNodes({ search })` receives raw search text and returns the tree that should be rendered. Set `sidebar.search: false` when an app has a small fixed navigation tree and should not show the sidebar search box.
|
|
40
40
|
|
|
41
|
+
On mobile widths, the drawer can be opened with a horizontal swipe starting within the first 24px of the viewport, in addition to the header button. The gesture must move at least 64px to the right, stay within 48px of vertical displacement, and remain more horizontal than vertical.
|
|
42
|
+
|
|
41
43
|
Use `actions` when an entity needs commands in the sidebar. `WebAppRoot` finds the active route-backed sidebar node and automatically renders its `ActionMenuItem[]` in the title-bar three-line menu, so the sidebar right-click menu and header menu stay consistent from one source of truth. Use `header.getActions` only for extra route-level actions that are not represented by the active sidebar node.
|
|
42
44
|
|
|
43
45
|
```tsx
|
package/package.json
CHANGED
package/src/web/WebAppRoot.tsx
CHANGED
|
@@ -190,7 +190,7 @@ function useMobileViewportHeight() {
|
|
|
190
190
|
timers.add(timer);
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
-
const
|
|
193
|
+
const handleViewportTransition = () => {
|
|
194
194
|
scheduleSync();
|
|
195
195
|
scheduleDelayedSync(120);
|
|
196
196
|
scheduleDelayedSync(320);
|
|
@@ -200,9 +200,10 @@ function useMobileViewportHeight() {
|
|
|
200
200
|
viewport?.addEventListener("resize", scheduleSync);
|
|
201
201
|
viewport?.addEventListener("scroll", scheduleSync);
|
|
202
202
|
window.addEventListener("resize", scheduleSync);
|
|
203
|
+
window.addEventListener("orientationchange", handleViewportTransition);
|
|
203
204
|
mobileQuery.addEventListener("change", scheduleSync);
|
|
204
|
-
document.addEventListener("focusin",
|
|
205
|
-
document.addEventListener("focusout",
|
|
205
|
+
document.addEventListener("focusin", handleViewportTransition);
|
|
206
|
+
document.addEventListener("focusout", handleViewportTransition);
|
|
206
207
|
|
|
207
208
|
return () => {
|
|
208
209
|
if (frame) {
|
|
@@ -214,14 +215,94 @@ function useMobileViewportHeight() {
|
|
|
214
215
|
viewport?.removeEventListener("resize", scheduleSync);
|
|
215
216
|
viewport?.removeEventListener("scroll", scheduleSync);
|
|
216
217
|
window.removeEventListener("resize", scheduleSync);
|
|
218
|
+
window.removeEventListener("orientationchange", handleViewportTransition);
|
|
217
219
|
mobileQuery.removeEventListener("change", scheduleSync);
|
|
218
|
-
document.removeEventListener("focusin",
|
|
219
|
-
document.removeEventListener("focusout",
|
|
220
|
+
document.removeEventListener("focusin", handleViewportTransition);
|
|
221
|
+
document.removeEventListener("focusout", handleViewportTransition);
|
|
220
222
|
clearViewportHeight();
|
|
221
223
|
};
|
|
222
224
|
}, []);
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
const MOBILE_SIDEBAR_BREAKPOINT = 900;
|
|
228
|
+
const SIDEBAR_SWIPE_EDGE_WIDTH = 24;
|
|
229
|
+
const SIDEBAR_SWIPE_DISTANCE = 64;
|
|
230
|
+
const SIDEBAR_SWIPE_VERTICAL_TOLERANCE = 48;
|
|
231
|
+
|
|
232
|
+
function useMobileSidebarSwipe(sidebarOpen: boolean, setSidebarOpen: (open: boolean) => void) {
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let tracking = false;
|
|
239
|
+
let startX = 0;
|
|
240
|
+
let startY = 0;
|
|
241
|
+
|
|
242
|
+
const reset = () => {
|
|
243
|
+
tracking = false;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const handleTouchStart = (event: TouchEvent) => {
|
|
247
|
+
if (sidebarOpen || window.innerWidth > MOBILE_SIDEBAR_BREAKPOINT || event.touches.length !== 1) {
|
|
248
|
+
reset();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const touch = event.touches[0];
|
|
253
|
+
if (!touch || touch.clientX > SIDEBAR_SWIPE_EDGE_WIDTH) {
|
|
254
|
+
reset();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
tracking = true;
|
|
259
|
+
startX = touch.clientX;
|
|
260
|
+
startY = touch.clientY;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleTouchMove = (event: TouchEvent) => {
|
|
264
|
+
if (!tracking) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (event.touches.length !== 1) {
|
|
268
|
+
reset();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const touch = event.touches[0];
|
|
273
|
+
if (!touch) {
|
|
274
|
+
reset();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const deltaX = touch.clientX - startX;
|
|
279
|
+
const deltaY = Math.abs(touch.clientY - startY);
|
|
280
|
+
if (deltaX <= 0 || deltaY > SIDEBAR_SWIPE_VERTICAL_TOLERANCE || deltaY > deltaX) {
|
|
281
|
+
reset();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (deltaX < SIDEBAR_SWIPE_DISTANCE) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
setSidebarOpen(true);
|
|
290
|
+
reset();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
document.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
294
|
+
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
295
|
+
document.addEventListener("touchend", reset);
|
|
296
|
+
document.addEventListener("touchcancel", reset);
|
|
297
|
+
return () => {
|
|
298
|
+
document.removeEventListener("touchstart", handleTouchStart);
|
|
299
|
+
document.removeEventListener("touchmove", handleTouchMove);
|
|
300
|
+
document.removeEventListener("touchend", reset);
|
|
301
|
+
document.removeEventListener("touchcancel", reset);
|
|
302
|
+
};
|
|
303
|
+
}, [setSidebarOpen, sidebarOpen]);
|
|
304
|
+
}
|
|
305
|
+
|
|
225
306
|
async function json<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
226
307
|
const response = await fetch(path, {
|
|
227
308
|
...init,
|
|
@@ -1077,6 +1158,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
1077
1158
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
1078
1159
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
1079
1160
|
const sidebarTreeState = useSidebarCollapsedState(appName);
|
|
1161
|
+
useMobileSidebarSwipe(sidebarOpen, setSidebarOpen);
|
|
1080
1162
|
const toggleSidebarCollapsed = useCallback(() => {
|
|
1081
1163
|
setSidebarCollapsed((current) => {
|
|
1082
1164
|
const nextCollapsed = !current;
|
package/src/web/styles.css
CHANGED
|
@@ -108,6 +108,7 @@ textarea::placeholder {
|
|
|
108
108
|
height: var(--wapp-viewport-height);
|
|
109
109
|
min-height: var(--wapp-viewport-height);
|
|
110
110
|
overflow: hidden;
|
|
111
|
+
overscroll-behavior-x: none;
|
|
111
112
|
background: var(--wapp-bg);
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -1896,6 +1897,10 @@ textarea::placeholder {
|
|
|
1896
1897
|
padding: 0.875rem 1rem;
|
|
1897
1898
|
}
|
|
1898
1899
|
|
|
1900
|
+
.wapp-main {
|
|
1901
|
+
touch-action: pan-y;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1899
1904
|
.wapp-main-content {
|
|
1900
1905
|
overflow: auto;
|
|
1901
1906
|
overflow-x: hidden;
|