@pablozaiden/webapp 0.3.9 → 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.9",
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);
@@ -1213,7 +1215,27 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1213
1215
  </div>
1214
1216
  </div>
1215
1217
  <div className="wapp-sidebar-scroll">
1216
- {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}
1217
1239
  <SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} collapsed={sidebarTreeState.collapsed} toggleCollapsed={sidebarTreeState.toggleCollapsed} searchActive={sidebarSearchActive} />
1218
1240
  <div className="wapp-sidebar-footer">v{effectiveVersion}<button type="button" aria-label="Reload" onClick={() => window.location.reload()}><Icon name="refresh" /></button></div>
1219
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
  }