@pablozaiden/webapp 1.2.0 → 1.3.0
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 +19 -0
- package/docs/ui-guidelines.md +1 -0
- package/package.json +1 -1
- package/src/web/WebAppRoot.tsx +5 -1
- package/src/web/app-shell.tsx +31 -17
- package/src/web/components/index.tsx +382 -19
- package/src/web/index.ts +2 -0
- package/src/web/motion.tsx +487 -0
- package/src/web/routing.ts +36 -5
- package/src/web/settings/resource-state.tsx +12 -7
- package/src/web/sidebar-tree.tsx +15 -4
- package/src/web/styles.css +349 -3
- package/src/web/toast.tsx +67 -20
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
|
package/docs/ui-guidelines.md
CHANGED
|
@@ -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
package/src/web/WebAppRoot.tsx
CHANGED
|
@@ -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
|
);
|
package/src/web/app-shell.tsx
CHANGED
|
@@ -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,25 @@ export function AppShell({
|
|
|
129
134
|
};
|
|
130
135
|
|
|
131
136
|
return (
|
|
132
|
-
<main className={`wapp-shell ${sidebarCollapsed ? "sidebar-collapsed" : ""} ${sidebarOpen ? "sidebar-open" : ""}
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
onClick={closeSidebar}
|
|
147
|
+
onKeyDown={(event) => {
|
|
148
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
closeSidebar();
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</Presence>
|
|
146
156
|
<aside id="wapp-sidebar" className="wapp-sidebar">
|
|
147
157
|
<div className="wapp-sidebar-header">
|
|
148
158
|
<button type="button" className="wapp-brand" onClick={() => navigateFromSidebarHeader(homeRoute)}>{appName}</button>
|
|
@@ -182,16 +192,20 @@ export function AppShell({
|
|
|
182
192
|
<header className="wapp-main-header">
|
|
183
193
|
<div className="wapp-main-header-title">
|
|
184
194
|
{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>
|
|
195
|
+
<h1 key={routeKey} className="wapp-route-fade">{headerTitle}</h1>
|
|
186
196
|
</div>
|
|
187
197
|
{primaryHeaderActions || headerActions.length ? (
|
|
188
|
-
<div className="wapp-main-header-actions">
|
|
198
|
+
<div key={routeKey} className="wapp-main-header-actions wapp-route-fade">
|
|
189
199
|
{primaryHeaderActions}
|
|
190
200
|
{headerActions.length ? <ActionMenu items={headerActions} ariaLabel={`Actions for ${headerActionLabel}`} /> : null}
|
|
191
201
|
</div>
|
|
192
202
|
) : null}
|
|
193
203
|
</header>
|
|
194
|
-
<div className="wapp-main-content">
|
|
204
|
+
<div className="wapp-main-content">
|
|
205
|
+
<div key={routeKey} className="wapp-route-view">
|
|
206
|
+
{view}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
195
209
|
</section>
|
|
196
210
|
</main>
|
|
197
211
|
);
|