@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.
package/README.md CHANGED
@@ -28,6 +28,25 @@ Each application package must declare `react` and `react-dom`; they are peer dep
28
28
  | `@pablozaiden/webapp/cli` | One-binary command helpers, device/environment auth credentials and generic API CLI caller |
29
29
  | `@pablozaiden/webapp/build` | Bun single-binary compile helper |
30
30
 
31
+ ## Motion primitives
32
+
33
+ The web export includes `Presence`, `Collapsible`, `AsyncState`, `AnimatedList`,
34
+ `Tabs`, `TabPanels`, and `TabPanel`. Use stable React keys with `AnimatedList`;
35
+ it preserves removed keyed children for the exit duration and marks them
36
+ inaccessible while they leave. The primitives honor
37
+ `prefers-reduced-motion` and skip visual movement when it is enabled.
38
+
39
+ ```tsx
40
+ <AnimatedList>
41
+ {records.map((record) => (
42
+ <DataListRow key={record.id} title={record.title} />
43
+ ))}
44
+ </AnimatedList>
45
+ ```
46
+
47
+ Use `layout="contents"` when animated children should participate directly in
48
+ the surrounding layout rather than through the list wrapper.
49
+
31
50
  See `docs/getting-started.md` for the minimum app shape and `examples/notes-todo` for a realistic app. Use `docs/github-actions.md` when adding CI, Docker and release workflows to an app built with the framework. Release/publishing details for this package are in `docs/release.md`.
32
51
 
33
52
  CLI API callers can use a stored device session or the stateless
@@ -43,6 +43,7 @@ Use these first:
43
43
  | `Toolbar` | Page title/actions inside main content |
44
44
  | `Panel` | Cards/sections; use `actions` for a top-right menu/action area |
45
45
  | `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 |
46
+ | `FloatingPanel` | Anchored arbitrary popup content; use it instead of app-owned portals when the content is not a simple `ActionMenu` |
46
47
  | `Button` / `IconButton` | Form submission and true inline controls; prefer action menus for entity/app commands |
47
48
  | `Badge` | Generic status/count labels; preserves the supplied text casing |
48
49
  | `StatusBadge` | Status labels with the shared uppercase and letter-spacing treatment |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/webapp",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Opinionated Bun + React webapp framework for single-server apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,7 @@ import { AppShell } from "./app-shell";
4
4
  import { DeviceVerificationScreen, PasskeyAuthScreen, UserSetupScreen } from "./auth-screens";
5
5
  import { EmptyState, Panel } from "./components";
6
6
  import { useMobileBreakpoint, useMobileSidebarSwipe, useMobileViewportHeight } from "./mobile-hooks";
7
- import { useRoute } from "./routing";
7
+ import { routeToHash, supportsViewTransitions, useRoute } from "./routing";
8
8
  import { flattenSidebarItems, toStoredPin, useSidebarCollapsedState, useSidebarPins } from "./sidebar-state";
9
9
  import { SettingsView } from "./settings/settings-view";
10
10
  import type { HeaderContext, WebAppRootProps } from "./root-types";
@@ -12,6 +12,7 @@ import type { ActionMenuItem, SidebarNode, WebAppRoute } from "./sidebar/types";
12
12
  import { ThemeProvider } from "./theme";
13
13
  import { WebAppConfigProvider, useWebAppConfig } from "./webapp-config";
14
14
  import { setLogLevel } from "./logger";
15
+ import { useReducedMotion } from "./motion";
15
16
 
16
17
  export { replaceHashRoute, replaceWebAppRoute, routeToHash } from "./routing";
17
18
  export type {
@@ -48,6 +49,7 @@ function WebAppRootContent({
48
49
  refresh: () => Promise<void>;
49
50
  }) {
50
51
  const isMobile = useMobileBreakpoint();
52
+ const reducedMotion = useReducedMotion();
51
53
  useMobileViewportHeight(isMobile);
52
54
  const { route, navigate } = useRoute(homeRoute);
53
55
  const [search, setSearch] = useState("");
@@ -189,6 +191,8 @@ function WebAppRootContent({
189
191
  headerActionLabel={headerActionLabel}
190
192
  primaryHeaderActions={primaryHeaderActions}
191
193
  headerActions={headerActions}
194
+ routeKey={routeToHash(route)}
195
+ nativeRouteTransitions={supportsViewTransitions() && !reducedMotion}
192
196
  view={view}
193
197
  />
194
198
  );
@@ -1,5 +1,6 @@
1
1
  import { useEffect, type ReactNode, type RefObject } from "react";
2
2
  import { ActionMenu, IconButton } from "./components";
3
+ import { Presence } from "./motion";
3
4
  import { SidebarTree } from "./sidebar-tree";
4
5
  import type { SidebarCollapsedState } from "./sidebar-state";
5
6
  import type { ActionMenuItem, SidebarAction, SidebarNode, WebAppRoute } from "./sidebar/types";
@@ -61,6 +62,8 @@ export interface AppShellProps {
61
62
  headerActionLabel: string;
62
63
  primaryHeaderActions?: ReactNode;
63
64
  headerActions: ActionMenuItem[];
65
+ routeKey: string;
66
+ nativeRouteTransitions?: boolean;
64
67
  view: ReactNode;
65
68
  }
66
69
 
@@ -88,6 +91,8 @@ export function AppShell({
88
91
  headerActionLabel,
89
92
  primaryHeaderActions,
90
93
  headerActions,
94
+ routeKey,
95
+ nativeRouteTransitions = false,
91
96
  view,
92
97
  }: AppShellProps) {
93
98
  useEffect(() => {
@@ -129,20 +134,35 @@ export function AppShell({
129
134
  };
130
135
 
131
136
  return (
132
- <main className={`wapp-shell ${sidebarCollapsed ? "sidebar-collapsed" : ""} ${sidebarOpen ? "sidebar-open" : ""}`}>
133
- <div
134
- className="wapp-mobile-backdrop"
135
- role="button"
136
- tabIndex={0}
137
- aria-label="Close sidebar"
138
- onClick={closeSidebar}
139
- onKeyDown={(event) => {
140
- if (event.key === "Enter" || event.key === " ") {
141
- event.preventDefault();
142
- closeSidebar();
143
- }
144
- }}
145
- />
137
+ <main className={`wapp-shell ${sidebarCollapsed ? "sidebar-collapsed" : ""} ${sidebarOpen ? "sidebar-open" : ""} ${nativeRouteTransitions ? "wapp-native-route-transitions" : ""}`.trim()}>
138
+ <Presence present={sidebarOpen}>
139
+ {(state) => (
140
+ <div
141
+ className={`wapp-mobile-backdrop wapp-motion-${state}`}
142
+ role="button"
143
+ tabIndex={0}
144
+ aria-label="Close sidebar"
145
+ aria-hidden={state === "exit" ? true : undefined}
146
+ onPointerDown={(event) => {
147
+ if (event.button === 0) {
148
+ event.preventDefault();
149
+ closeSidebar();
150
+ }
151
+ }}
152
+ onClick={(event) => {
153
+ if (event.detail === 0) {
154
+ closeSidebar();
155
+ }
156
+ }}
157
+ onKeyDown={(event) => {
158
+ if (event.key === "Enter" || event.key === " ") {
159
+ event.preventDefault();
160
+ closeSidebar();
161
+ }
162
+ }}
163
+ />
164
+ )}
165
+ </Presence>
146
166
  <aside id="wapp-sidebar" className="wapp-sidebar">
147
167
  <div className="wapp-sidebar-header">
148
168
  <button type="button" className="wapp-brand" onClick={() => navigateFromSidebarHeader(homeRoute)}>{appName}</button>
@@ -182,16 +202,20 @@ export function AppShell({
182
202
  <header className="wapp-main-header">
183
203
  <div className="wapp-main-header-title">
184
204
  {sidebarCollapsed ? <IconButton className="wapp-sidebar-top-button" aria-label={sidebarToggleLabel} title={sidebarToggleLabel} aria-expanded={!sidebarCollapsed} aria-controls="wapp-sidebar" onClick={toggleSidebarCollapsed}><Icon name="sidebar" /></IconButton> : <IconButton className="wapp-mobile-only wapp-sidebar-top-button" aria-label="Show sidebar" title="Show sidebar" aria-expanded={sidebarOpen} aria-controls="wapp-sidebar" onClick={() => setSidebarOpen(true)}><Icon name="sidebar" /></IconButton>}
185
- <h1>{headerTitle}</h1>
205
+ <h1 key={routeKey} className="wapp-route-fade">{headerTitle}</h1>
186
206
  </div>
187
207
  {primaryHeaderActions || headerActions.length ? (
188
- <div className="wapp-main-header-actions">
208
+ <div key={routeKey} className="wapp-main-header-actions wapp-route-fade">
189
209
  {primaryHeaderActions}
190
210
  {headerActions.length ? <ActionMenu items={headerActions} ariaLabel={`Actions for ${headerActionLabel}`} /> : null}
191
211
  </div>
192
212
  ) : null}
193
213
  </header>
194
- <div className="wapp-main-content">{view}</div>
214
+ <div className="wapp-main-content">
215
+ <div key={routeKey} className="wapp-route-view">
216
+ {view}
217
+ </div>
218
+ </div>
195
219
  </section>
196
220
  </main>
197
221
  );