@pablozaiden/webapp 0.3.6 → 0.3.7

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.6",
3
+ "version": "0.3.7",
4
4
  "description": "Opinionated Bun + React webapp framework for single-server apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -531,6 +531,17 @@ function DeviceVerificationScreen() {
531
531
 
532
532
  type SidebarTreeParentKind = "root" | "section" | "item";
533
533
 
534
+ type SidebarTreeProps = {
535
+ nodes: SidebarNode[];
536
+ route: WebAppRoute;
537
+ navigate: (route: WebAppRoute) => void;
538
+ collapsed: SidebarCollapsedState;
539
+ toggleCollapsed: (id: string, isCollapsed: boolean) => void;
540
+ searchActive: boolean;
541
+ level?: number;
542
+ parentKind?: SidebarTreeParentKind;
543
+ };
544
+
534
545
  function sidebarIndentStyle(level: number, parentKind: SidebarTreeParentKind): { marginLeft?: string } | undefined {
535
546
  if (level <= 0) {
536
547
  return undefined;
@@ -540,19 +551,26 @@ function sidebarIndentStyle(level: number, parentKind: SidebarTreeParentKind): {
540
551
  return { marginLeft: `${baseIndentRem + nestedSectionIndentRem}rem` };
541
552
  }
542
553
 
543
- function SidebarTree({ nodes, route, navigate, collapsed, toggleCollapsed, level = 0, parentKind = "root" }: { nodes: SidebarNode[]; route: WebAppRoute; navigate: (route: WebAppRoute) => void; collapsed: SidebarCollapsedState; toggleCollapsed: (id: string, isCollapsed: boolean) => void; level?: number; parentKind?: SidebarTreeParentKind }) {
554
+ function SidebarTree({ nodes, route, navigate, collapsed, toggleCollapsed, searchActive, level = 0, parentKind = "root" }: SidebarTreeProps) {
544
555
  const [contextMenu, setContextMenu] = useState<{ position: ContextMenuPosition; items: ActionMenuItem[]; title: string } | null>(null);
545
556
  return (
546
557
  <>
547
558
  {nodes.map((node) => {
548
559
  const hasChildren = Boolean(node.children?.length);
549
- const isCollapsed = collapsed[node.id] ?? node.defaultCollapsed ?? false;
560
+ const storedIsCollapsed = collapsed[node.id] ?? node.defaultCollapsed ?? false;
561
+ const isCollapsed = searchActive && hasChildren ? false : storedIsCollapsed;
562
+ const toggleAriaLabel = searchActive ? `Toggling unavailable during search for ${node.title}` : `${isCollapsed ? "Expand" : "Collapse"} ${node.title}`;
563
+ const toggleNodeCollapsed = () => {
564
+ if (!searchActive) {
565
+ toggleCollapsed(node.id, storedIsCollapsed);
566
+ }
567
+ };
550
568
  if (node.type === "section") {
551
569
  return (
552
570
  <section className={`wapp-sidebar-section ${level === 0 ? "top" : "nested"}`} key={node.id}>
553
571
  <div className="wapp-sidebar-section-title" style={sidebarIndentStyle(level, parentKind)}>
554
572
  {hasChildren ? (
555
- <button type="button" aria-expanded={!isCollapsed} aria-label={`${isCollapsed ? "Expand" : "Collapse"} ${node.title}`} onClick={() => toggleCollapsed(node.id, isCollapsed)}>
573
+ <button type="button" aria-expanded={!isCollapsed} aria-label={toggleAriaLabel} disabled={searchActive} onClick={toggleNodeCollapsed}>
556
574
  <span>{isCollapsed ? "▶" : "▼"}</span>{node.title}
557
575
  </button>
558
576
  ) : (
@@ -560,7 +578,7 @@ function SidebarTree({ nodes, route, navigate, collapsed, toggleCollapsed, level
560
578
  )}
561
579
  {node.action ? <button type="button" className="wapp-sidebar-action" title={node.action.title} aria-label={node.action.title} onClick={node.action.onAction ?? (() => node.action?.route && navigate(node.action.route))}>{node.action.label ?? "New"}</button> : null}
562
580
  </div>
563
- {!isCollapsed && hasChildren ? <SidebarTree nodes={node.children ?? []} route={route} navigate={navigate} collapsed={collapsed} toggleCollapsed={toggleCollapsed} level={level + 1} parentKind="section" /> : null}
581
+ {!isCollapsed && hasChildren ? <SidebarTree nodes={node.children ?? []} route={route} navigate={navigate} collapsed={collapsed} toggleCollapsed={toggleCollapsed} searchActive={searchActive} level={level + 1} parentKind="section" /> : null}
564
582
  {!isCollapsed && !hasChildren && level === 0 ? <div className="wapp-sidebar-empty">No items.</div> : null}
565
583
  </section>
566
584
  );
@@ -568,7 +586,7 @@ function SidebarTree({ nodes, route, navigate, collapsed, toggleCollapsed, level
568
586
  const active = node.route?.view === route.view && Object.entries(node.route).every(([key, value]) => key === "view" || route[key] === value);
569
587
  return (
570
588
  <div className={`wapp-sidebar-item-wrap ${hasChildren ? "has-toggle" : ""}`} key={node.id} style={sidebarIndentStyle(level, parentKind)}>
571
- {hasChildren ? <button type="button" className="wapp-tree-toggle" aria-expanded={!isCollapsed} aria-label={`${isCollapsed ? "Expand" : "Collapse"} ${node.title}`} onClick={() => toggleCollapsed(node.id, isCollapsed)}>{isCollapsed ? "▶" : "▼"}</button> : null}
589
+ {hasChildren ? <button type="button" className="wapp-tree-toggle" aria-expanded={!isCollapsed} aria-label={toggleAriaLabel} disabled={searchActive} onClick={toggleNodeCollapsed}>{isCollapsed ? "▶" : "▼"}</button> : null}
572
590
  <button
573
591
  type="button"
574
592
  className={`wapp-sidebar-item ${active ? "active" : ""}`}
@@ -585,7 +603,7 @@ function SidebarTree({ nodes, route, navigate, collapsed, toggleCollapsed, level
585
603
  </span>
586
604
  {node.badge ? <Badge variant={node.badgeVariant} className="wapp-sidebar-badge" title={node.badge} aria-label={node.badge}> </Badge> : null}
587
605
  </button>
588
- {!isCollapsed && node.children ? <div className="wapp-sidebar-children"><SidebarTree nodes={node.children} route={route} navigate={navigate} collapsed={collapsed} toggleCollapsed={toggleCollapsed} level={level + 1} parentKind="item" /></div> : null}
606
+ {!isCollapsed && node.children ? <div className="wapp-sidebar-children"><SidebarTree nodes={node.children} route={route} navigate={navigate} collapsed={collapsed} toggleCollapsed={toggleCollapsed} searchActive={searchActive} level={level + 1} parentKind="item" /></div> : null}
589
607
  </div>
590
608
  );
591
609
  })}
@@ -1040,10 +1058,12 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1040
1058
  });
1041
1059
  }, []);
1042
1060
  const sidebarSearchEnabled = sidebar.search !== false;
1061
+ const normalizedSidebarSearch = sidebarSearchEnabled ? search.trim() : "";
1062
+ const sidebarSearchActive = normalizedSidebarSearch.length > 0;
1043
1063
  const pinningEnabled = sidebar.pinning !== false;
1044
1064
  const sidebarPins = useSidebarPins(appName, sidebar.pinning ? sidebar.pinning.storageKey : undefined);
1045
1065
  const baseNodes = useMemo(() => sidebar.getNodes({ search: "" }), [sidebar]);
1046
- const filteredNodes = useMemo(() => sidebar.getNodes({ search: sidebarSearchEnabled ? search : "" }), [sidebar, search, sidebarSearchEnabled]);
1066
+ const filteredNodes = useMemo(() => sidebar.getNodes({ search: normalizedSidebarSearch }), [sidebar, normalizedSidebarSearch]);
1047
1067
  const allPinnableItems = useMemo(() => flattenSidebarItems(baseNodes).filter((node) => node.pinnable && node.route), [baseNodes]);
1048
1068
  const currentPins = useMemo(() => {
1049
1069
  const byId = new Map(allPinnableItems.map((node) => [node.pinId ?? node.id, node]));
@@ -1071,7 +1091,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1071
1091
  }), [pinningActionFor]);
1072
1092
  const nodes = useMemo(() => {
1073
1093
  const augmented = augmentPinningActions(filteredNodes);
1074
- if (!pinningEnabled || (sidebarSearchEnabled && search.trim()) || currentPins.length === 0) return augmented;
1094
+ if (!pinningEnabled || sidebarSearchActive || currentPins.length === 0) return augmented;
1075
1095
  const augmentedByPinId = new Map(flattenSidebarItems(augmentPinningActions(baseNodes)).map((node) => [node.pinId ?? node.id, node]));
1076
1096
  const pinnedChildren = currentPins.map((pin) => ({
1077
1097
  ...(augmentedByPinId.get(pin.id) ?? {
@@ -1091,7 +1111,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1091
1111
  { type: "section" as const, id: "framework:pinned", title: sidebar.pinning ? sidebar.pinning.sectionTitle ?? "Pinned" : "Pinned", children: pinnedChildren },
1092
1112
  ...augmented,
1093
1113
  ];
1094
- }, [augmentPinningActions, baseNodes, currentPins, filteredNodes, pinningEnabled, search, sidebar.pinning, sidebarSearchEnabled]);
1114
+ }, [augmentPinningActions, baseNodes, currentPins, filteredNodes, pinningEnabled, sidebar.pinning, sidebarSearchActive]);
1095
1115
 
1096
1116
  useEffect(() => {
1097
1117
  if (!config?.currentUser) return;
@@ -1192,8 +1212,8 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1192
1212
  </div>
1193
1213
  </div>
1194
1214
  <div className="wapp-sidebar-scroll">
1195
- {sidebarSearchEnabled ? <label className="wapp-search"><span className="sr-only">Search</span><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search" /></label> : null}
1196
- <SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} collapsed={sidebarTreeState.collapsed} toggleCollapsed={sidebarTreeState.toggleCollapsed} />
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}
1216
+ <SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} collapsed={sidebarTreeState.collapsed} toggleCollapsed={sidebarTreeState.toggleCollapsed} searchActive={sidebarSearchActive} />
1197
1217
  <div className="wapp-sidebar-footer">v{effectiveVersion}<button type="button" aria-label="Reload" onClick={() => window.location.reload()}><Icon name="refresh" /></button></div>
1198
1218
  </div>
1199
1219
  </aside>