@pablozaiden/webapp 0.3.5 → 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.5",
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>
@@ -1072,9 +1072,10 @@ textarea::placeholder {
1072
1072
  }
1073
1073
 
1074
1074
  .wapp-badge {
1075
+ --wapp-badge-fg: var(--wapp-muted);
1075
1076
  border-radius: 999px;
1076
1077
  background: color-mix(in srgb, var(--wapp-muted), transparent 86%);
1077
- color: var(--wapp-muted);
1078
+ color: var(--wapp-badge-fg);
1078
1079
  font-size: 0.6875rem;
1079
1080
  font-weight: 700;
1080
1081
  padding: 0.125rem 0.5rem;
@@ -1093,61 +1094,73 @@ textarea::placeholder {
1093
1094
 
1094
1095
  .wapp-badge-success,
1095
1096
  .wapp-badge-completed {
1097
+ --wapp-badge-fg: rgb(22 101 52);
1096
1098
  background: rgb(220 252 231);
1097
- color: rgb(22 101 52);
1099
+ color: var(--wapp-badge-fg);
1098
1100
  }
1099
1101
  .wapp-badge-warning,
1100
1102
  .wapp-badge-plan_ready {
1103
+ --wapp-badge-fg: rgb(146 64 14);
1101
1104
  background: rgb(254 243 199);
1102
- color: rgb(146 64 14);
1105
+ color: var(--wapp-badge-fg);
1103
1106
  }
1104
1107
  .wapp-badge-error,
1105
1108
  .wapp-badge-failed {
1109
+ --wapp-badge-fg: rgb(153 27 27);
1106
1110
  background: rgb(254 226 226);
1107
- color: rgb(153 27 27);
1111
+ color: var(--wapp-badge-fg);
1108
1112
  }
1109
1113
  .wapp-badge-info,
1110
1114
  .wapp-badge-running {
1115
+ --wapp-badge-fg: rgb(30 64 175);
1111
1116
  background: rgb(219 234 254);
1112
- color: rgb(30 64 175);
1117
+ color: var(--wapp-badge-fg);
1113
1118
  }
1114
1119
  .wapp-badge-disabled,
1115
1120
  .wapp-badge-stopped,
1116
1121
  .wapp-badge-idle {
1122
+ --wapp-badge-fg: rgb(31 41 55);
1117
1123
  background: rgb(243 244 246);
1118
- color: rgb(31 41 55);
1124
+ color: var(--wapp-badge-fg);
1119
1125
  }
1120
1126
  .wapp-badge-planning {
1127
+ --wapp-badge-fg: rgb(21 94 117);
1121
1128
  background: rgb(207 250 254);
1122
- color: rgb(21 94 117);
1129
+ color: var(--wapp-badge-fg);
1123
1130
  }
1124
1131
  .wapp-badge-merged {
1132
+ --wapp-badge-fg: rgb(107 33 168);
1125
1133
  background: rgb(243 232 255);
1126
- color: rgb(107 33 168);
1134
+ color: var(--wapp-badge-fg);
1127
1135
  }
1128
1136
  .wapp-badge-pushed {
1137
+ --wapp-badge-fg: rgb(55 48 163);
1129
1138
  background: rgb(224 231 255);
1130
- color: rgb(55 48 163);
1139
+ color: var(--wapp-badge-fg);
1131
1140
  }
1132
1141
  .wapp-badge-deleted {
1142
+ --wapp-badge-fg: rgb(107 114 128);
1133
1143
  background: rgb(243 244 246);
1134
- color: rgb(107 114 128);
1144
+ color: var(--wapp-badge-fg);
1135
1145
  }
1136
1146
 
1137
1147
  :root.dark .wapp-badge-success,
1138
1148
  :root.dark .wapp-badge-completed {
1149
+ --wapp-badge-fg: rgb(134 239 172);
1139
1150
  background: rgb(20 83 45);
1140
- color: rgb(134 239 172);
1151
+ color: var(--wapp-badge-fg);
1141
1152
  }
1142
1153
  :root.dark .wapp-badge-warning,
1143
1154
  :root.dark .wapp-badge-plan_ready {
1155
+ --wapp-badge-fg: rgb(252 211 77);
1144
1156
  background: rgb(120 53 15);
1145
- color: rgb(252 211 77);
1157
+ color: var(--wapp-badge-fg);
1146
1158
  }
1147
1159
  :root.dark .wapp-badge-error,
1148
1160
  :root.dark .wapp-badge-failed {
1161
+ --wapp-badge-fg: rgb(252 165 165);
1149
1162
  background: rgb(127 29 29);
1150
- color: rgb(252 165 165);
1163
+ color: var(--wapp-badge-fg);
1151
1164
  }
1152
1165
  :root.dark .wapp-badge-info,
1153
1166
  :root.dark .wapp-badge-running,
@@ -1159,25 +1172,36 @@ textarea::placeholder {
1159
1172
  }
1160
1173
  :root.dark .wapp-badge-info,
1161
1174
  :root.dark .wapp-badge-running {
1162
- color: rgb(147 197 253);
1175
+ --wapp-badge-fg: rgb(147 197 253);
1176
+ color: var(--wapp-badge-fg);
1163
1177
  }
1164
1178
  :root.dark .wapp-badge-planning {
1165
- color: rgb(103 232 249);
1179
+ --wapp-badge-fg: rgb(103 232 249);
1180
+ color: var(--wapp-badge-fg);
1166
1181
  }
1167
1182
  :root.dark .wapp-badge-merged {
1168
- color: rgb(216 180 254);
1183
+ --wapp-badge-fg: rgb(216 180 254);
1184
+ color: var(--wapp-badge-fg);
1169
1185
  }
1170
1186
  :root.dark .wapp-badge-pushed {
1171
- color: rgb(165 180 252);
1187
+ --wapp-badge-fg: rgb(165 180 252);
1188
+ color: var(--wapp-badge-fg);
1172
1189
  }
1173
1190
  :root.dark .wapp-badge-deleted {
1174
- color: rgb(107 114 128);
1191
+ --wapp-badge-fg: rgb(107 114 128);
1192
+ color: var(--wapp-badge-fg);
1175
1193
  }
1176
1194
  :root.dark .wapp-badge-disabled,
1177
1195
  :root.dark .wapp-badge-stopped,
1178
1196
  :root.dark .wapp-badge-idle {
1197
+ --wapp-badge-fg: rgb(209 213 219);
1179
1198
  background: rgb(64 64 64);
1180
- color: rgb(209 213 219);
1199
+ color: var(--wapp-badge-fg);
1200
+ }
1201
+
1202
+ .wapp-badge.wapp-sidebar-badge,
1203
+ :root.dark .wapp-badge.wapp-sidebar-badge {
1204
+ background: var(--wapp-badge-fg);
1181
1205
  }
1182
1206
 
1183
1207
  .wapp-settings {