@pablozaiden/webapp 0.3.8 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/webapp",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Opinionated Bun + React webapp framework for single-server apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
2
- import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
2
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
3
3
  import type { ApiKeySummary, AuthSessionSummary, CreatedApiKeyResponse, CreatedUserResponse, DeviceVerificationDetails, PasskeyAuthStatusResponse, ThemePreference, UserSetupDetails, WebAppConfigResponse, WebAppUserRole, WebAppUserSummary } from "../contracts";
4
4
  import { ActionMenu, Badge, Button, ConfirmDialog, ContextMenu, DangerZone, Dialog, EmptyState, FormSection, IconButton, Panel, SelectField, TextField, type ContextMenuPosition } from "./components";
5
5
  import type { ActionMenuItem, SidebarAction, SidebarBuildContext, SidebarNode, WebAppRoute } from "./sidebar/types";
@@ -1047,6 +1047,8 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1047
1047
  const { route, navigate } = useRoute(homeRoute);
1048
1048
  const { theme, setTheme } = useTheme();
1049
1049
  const [search, setSearch] = useState("");
1050
+ const sidebarSearchId = useId();
1051
+ const sidebarSearchInputRef = useRef<HTMLInputElement>(null);
1050
1052
  const [sidebarOpen, setSidebarOpen] = useState(false);
1051
1053
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
1052
1054
  const sidebarTreeState = useSidebarCollapsedState(appName);
@@ -1112,6 +1114,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1112
1114
  ...augmented,
1113
1115
  ];
1114
1116
  }, [augmentPinningActions, baseNodes, currentPins, filteredNodes, pinningEnabled, sidebar.pinning, sidebarSearchActive]);
1117
+ const activeActionNodes = useMemo(() => augmentPinningActions(baseNodes), [augmentPinningActions, baseNodes]);
1115
1118
 
1116
1119
  useEffect(() => {
1117
1120
  if (!config?.currentUser) return;
@@ -1176,7 +1179,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1176
1179
  const topActions = sidebar.topActions?.slice(0, 2) ?? [];
1177
1180
  const defaultTitle = route.view === "settings" ? "Settings" : route.view === homeRoute.view ? appName : route.view.replace(/-/g, " ");
1178
1181
  const headerContext = { route, defaultTitle };
1179
- const activeSidebarNode = flattenSidebarItems(nodes).find((node) => routeMatches(node.route, route));
1182
+ const activeSidebarNode = flattenSidebarItems(activeActionNodes).find((node) => routeMatches(node.route, route));
1180
1183
  const activeSidebarActions = activeSidebarNode?.actions ?? [];
1181
1184
  const headerActions = [
1182
1185
  ...(header?.getActions?.(headerContext) ?? []),
@@ -1212,7 +1215,27 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1212
1215
  </div>
1213
1216
  </div>
1214
1217
  <div className="wapp-sidebar-scroll">
1215
- {sidebarSearchEnabled ? <label className="wapp-search"><span className="sr-only">Search</span><input value={search} onInput={(event) => setSearch(event.currentTarget.value)} placeholder="Search" /></label> : null}
1218
+ {sidebarSearchEnabled ? (
1219
+ <div className="wapp-search">
1220
+ <label className="sr-only" htmlFor={sidebarSearchId}>Search</label>
1221
+ <div className={`wapp-search-input-wrap${search.length > 0 ? " wapp-search-input-wrap--clearable" : ""}`}>
1222
+ <input id={sidebarSearchId} ref={sidebarSearchInputRef} value={search} onInput={(event) => setSearch(event.currentTarget.value)} placeholder="Search" />
1223
+ {search.length > 0 ? (
1224
+ <button
1225
+ type="button"
1226
+ className="wapp-search-clear"
1227
+ aria-label="Clear search"
1228
+ onClick={() => {
1229
+ setSearch("");
1230
+ sidebarSearchInputRef.current?.focus();
1231
+ }}
1232
+ >
1233
+ ×
1234
+ </button>
1235
+ ) : null}
1236
+ </div>
1237
+ </div>
1238
+ ) : null}
1216
1239
  <SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} collapsed={sidebarTreeState.collapsed} toggleCollapsed={sidebarTreeState.toggleCollapsed} searchActive={sidebarSearchActive} />
1217
1240
  <div className="wapp-sidebar-footer">v{effectiveVersion}<button type="button" aria-label="Reload" onClick={() => window.location.reload()}><Icon name="refresh" /></button></div>
1218
1241
  </div>
@@ -282,10 +282,17 @@ textarea::placeholder {
282
282
  scroll-padding-bottom: var(--wapp-mobile-bottom-clearance);
283
283
  }
284
284
 
285
+ .wapp-search {
286
+ margin-bottom: 1.5rem;
287
+ }
288
+
289
+ .wapp-search-input-wrap {
290
+ position: relative;
291
+ }
292
+
285
293
  .wapp-search input {
286
294
  width: 100%;
287
295
  height: 2.625rem;
288
- margin-bottom: 1.5rem;
289
296
  border: 1px solid var(--wapp-border);
290
297
  border-radius: 0.75rem;
291
298
  background: var(--wapp-surface);
@@ -296,11 +303,41 @@ textarea::placeholder {
296
303
  font-size: 0.875rem;
297
304
  }
298
305
 
306
+ .wapp-search-input-wrap--clearable input {
307
+ padding-right: 2.5rem;
308
+ }
309
+
299
310
  .wapp-search input:focus {
300
311
  border-color: var(--wapp-muted);
301
312
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--wapp-muted), transparent 70%);
302
313
  }
303
314
 
315
+ .wapp-search-clear {
316
+ position: absolute;
317
+ top: 50%;
318
+ right: 0.375rem;
319
+ width: 1.875rem;
320
+ height: 1.875rem;
321
+ display: inline-flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ transform: translateY(-50%);
325
+ border: 0;
326
+ border-radius: 999px;
327
+ background: transparent;
328
+ color: var(--wapp-muted);
329
+ cursor: pointer;
330
+ font-size: 1.125rem;
331
+ line-height: 1;
332
+ }
333
+
334
+ .wapp-search-clear:hover,
335
+ .wapp-search-clear:focus-visible {
336
+ background: color-mix(in srgb, var(--wapp-muted), transparent 88%);
337
+ color: var(--wapp-text);
338
+ outline: none;
339
+ }
340
+
304
341
  .wapp-sidebar-section.top {
305
342
  margin-bottom: 1.5rem;
306
343
  }