@pablozaiden/webapp 0.5.6 → 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/getting-started.md +3 -1
- package/docs/server.md +3 -1
- package/docs/sidebar.md +2 -0
- package/package.json +1 -1
- package/src/server/create-web-app-server.ts +1 -1
- package/src/web/WebAppRoot.tsx +87 -5
- package/src/web/styles.css +5 -0
package/docs/getting-started.md
CHANGED
|
@@ -41,7 +41,7 @@ const app = createWebAppServer({
|
|
|
41
41
|
await app.runFromCli();
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
The framework generates the HTML document, React mount point, viewport metadata, PWA manifest, default SVG icons, and the theme prepaint script. By default it uses `./web/main.tsx` relative to the Bun entry file as the frontend entrypoint, so apps only need to create that file. Override document defaults only when the app needs different metadata:
|
|
44
|
+
The framework generates the HTML document, React mount point, fixed-scale viewport metadata, PWA manifest, default SVG icons, and the theme prepaint script. On iPhone, iPad, and other mobile browsers that honor viewport scaling tokens, the generated viewport prevents pinch-to-zoom while preserving normal scrolling; clients that ignore those tokens are unaffected. By default it uses `./web/main.tsx` relative to the Bun entry file as the frontend entrypoint, so apps only need to create that file. Override document defaults only when the app needs different metadata:
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
createWebAppServer({
|
|
@@ -67,6 +67,8 @@ createWebAppServer({
|
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Do not create an app-owned `index.html` or add global touch handlers to control zoom. The framework owns the viewport policy so the same metadata is emitted for development, compiled clients, and installed PWAs without disabling scrolling.
|
|
71
|
+
|
|
70
72
|
PWA support is on by default, with generated initials icons unless the app provides `web.icons`. Icon paths are relative to the app package root; production apps should provide at least 192x192 and 512x512 PNG manifest icons plus an Apple touch icon. Set `web.pwa: false` only for apps that deliberately should not be installable. The standard theme preference key is `webapp.theme`; apps should not use app-specific theme storage keys.
|
|
71
73
|
|
|
72
74
|
Apps should stay one app and one binary. Use subcommands for different modes:
|
package/docs/server.md
CHANGED
|
@@ -96,7 +96,9 @@ Built-in endpoints include:
|
|
|
96
96
|
|
|
97
97
|
## Framework-owned web document and PWA metadata
|
|
98
98
|
|
|
99
|
-
`createWebAppServer` owns the browser document. Apps do not provide `index.html`; the framework generates a Bun `HTMLBundle` internally so Bun hot reload and asset rewriting keep working. By default the frontend entrypoint is `./web/main.tsx` relative to the Bun entry file.
|
|
99
|
+
`createWebAppServer` owns the browser document. Apps do not provide `index.html`; the framework generates a Bun `HTMLBundle` internally so Bun hot reload and asset rewriting keep working. By default the frontend entrypoint is `./web/main.tsx` relative to the Bun entry file. The generated viewport keeps the app at `initial-scale=1` with `maximum-scale=1` and `user-scalable=no`; clients and mobile browsers that honor those viewport scaling tokens, including iPhone and iPad, cannot change the app scale with pinch-to-zoom while normal scrolling remains enabled. Clients that ignore the tokens are unaffected.
|
|
100
|
+
|
|
101
|
+
Do not replace this behavior with global touch event handlers, `preventDefault()` calls, or `touch-action: none`: those approaches can disable scrolling and other touch interactions. Applications should keep the document framework-owned rather than adding an app-owned `index.html`.
|
|
100
102
|
|
|
101
103
|
```ts
|
|
102
104
|
createWebAppServer({
|
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
|
@@ -510,7 +510,7 @@ function generatedHtml(
|
|
|
510
510
|
<html lang="${escapeAttribute(web.lang ?? "en")}">
|
|
511
511
|
<head>
|
|
512
512
|
<meta charset="utf-8" />
|
|
513
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
513
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
|
514
514
|
${themeMetaTag}\
|
|
515
515
|
${manifestTags} <title>${title}</title>
|
|
516
516
|
<script>${themeBootScript(themeColor)}</script>
|
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;
|