@pablozaiden/webapp 0.3.3 → 0.3.4

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.3",
3
+ "version": "0.3.4",
4
4
  "description": "Opinionated Bun + React webapp framework for single-server apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -70,6 +70,18 @@ type StoredSidebarPin = {
70
70
  route: WebAppRoute;
71
71
  };
72
72
 
73
+ function isSidebarShortcutEditableTarget(target: EventTarget | null): boolean {
74
+ if (!(target instanceof HTMLElement)) {
75
+ return false;
76
+ }
77
+ if (target.isContentEditable || target.closest("[contenteditable=''], [contenteditable='true']")) {
78
+ return true;
79
+ }
80
+ return target instanceof HTMLInputElement
81
+ || target instanceof HTMLTextAreaElement
82
+ || target instanceof HTMLSelectElement;
83
+ }
84
+
73
85
  function routeToHash(route: WebAppRoute): string {
74
86
  const params = new URLSearchParams();
75
87
  for (const [key, value] of Object.entries(route)) {
@@ -980,6 +992,13 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
980
992
  const [search, setSearch] = useState("");
981
993
  const [sidebarOpen, setSidebarOpen] = useState(false);
982
994
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
995
+ const toggleSidebarCollapsed = useCallback(() => {
996
+ setSidebarCollapsed((current) => {
997
+ const nextCollapsed = !current;
998
+ setSidebarOpen(!nextCollapsed);
999
+ return nextCollapsed;
1000
+ });
1001
+ }, []);
983
1002
  const sidebarSearchEnabled = sidebar.search !== false;
984
1003
  const pinningEnabled = sidebar.pinning !== false;
985
1004
  const sidebarPins = useSidebarPins(appName, sidebar.pinning ? sidebar.pinning.storageKey : undefined);
@@ -1041,6 +1060,28 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1041
1060
  .catch(() => undefined);
1042
1061
  }, [config?.currentUser?.id, setTheme]);
1043
1062
 
1063
+ useEffect(() => {
1064
+ function handleSidebarShortcut(event: KeyboardEvent) {
1065
+ if (
1066
+ event.key.toLowerCase() !== "b"
1067
+ || event.altKey
1068
+ || event.shiftKey
1069
+ || event.ctrlKey === event.metaKey
1070
+ || event.isComposing
1071
+ || event.repeat
1072
+ || isSidebarShortcutEditableTarget(event.target)
1073
+ ) {
1074
+ return;
1075
+ }
1076
+
1077
+ event.preventDefault();
1078
+ toggleSidebarCollapsed();
1079
+ }
1080
+
1081
+ document.addEventListener("keydown", handleSidebarShortcut);
1082
+ return () => document.removeEventListener("keydown", handleSidebarShortcut);
1083
+ }, [toggleSidebarCollapsed]);
1084
+
1044
1085
  useEffect(() => {
1045
1086
  onRouteChange?.(route);
1046
1087
  }, [onRouteChange, route]);
@@ -1084,6 +1125,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1084
1125
  const headerTitle = header?.renderTitle?.(headerContext) ?? defaultTitle;
1085
1126
  const primaryHeaderActions = header?.renderActions?.(headerContext);
1086
1127
  const headerActionLabel = typeof headerTitle === "string" ? headerTitle : defaultTitle;
1128
+ const sidebarToggleLabel = sidebarCollapsed ? "Show sidebar" : "Collapse sidebar";
1087
1129
  const navigateFromSidebarHeader = (nextRoute: WebAppRoute) => {
1088
1130
  navigate(nextRoute);
1089
1131
  setSidebarOpen(false);
@@ -1106,7 +1148,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1106
1148
  <div className="wapp-sidebar-actions">
1107
1149
  {topActions.map((action) => <IconButton key={action.id} className="wapp-sidebar-top-button" title={action.title} aria-label={action.title} onClick={() => runSidebarHeaderAction(action)}><ActionIcon icon={action.icon} /></IconButton>)}
1108
1150
  <IconButton className="wapp-sidebar-top-button" title="Settings" aria-label="Open settings" active={route.view === "settings"} onClick={() => navigateFromSidebarHeader({ view: "settings" })}><Icon name="settings" /></IconButton>
1109
- <IconButton className="wapp-sidebar-top-button" title="Collapse sidebar" aria-label="Collapse sidebar" onClick={() => { setSidebarCollapsed(true); setSidebarOpen(false); }}><Icon name="sidebar" /></IconButton>
1151
+ <IconButton className="wapp-sidebar-top-button" title={sidebarToggleLabel} aria-label={sidebarToggleLabel} onClick={toggleSidebarCollapsed}><Icon name="sidebar" /></IconButton>
1110
1152
  </div>
1111
1153
  </div>
1112
1154
  <div className="wapp-sidebar-scroll">
@@ -1118,7 +1160,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
1118
1160
  <section className="wapp-main">
1119
1161
  <header className="wapp-main-header">
1120
1162
  <div className="wapp-main-header-title">
1121
- {sidebarCollapsed ? <IconButton className="wapp-sidebar-top-button" aria-label="Show sidebar" title="Show sidebar" onClick={() => { setSidebarCollapsed(false); setSidebarOpen(true); }}><Icon name="sidebar" /></IconButton> : <IconButton className="wapp-mobile-only wapp-sidebar-top-button" aria-label="Show sidebar" title="Show sidebar" onClick={() => setSidebarOpen(true)}><Icon name="sidebar" /></IconButton>}
1163
+ {sidebarCollapsed ? <IconButton className="wapp-sidebar-top-button" aria-label={sidebarToggleLabel} title={sidebarToggleLabel} onClick={toggleSidebarCollapsed}><Icon name="sidebar" /></IconButton> : <IconButton className="wapp-mobile-only wapp-sidebar-top-button" aria-label="Show sidebar" title="Show sidebar" onClick={() => setSidebarOpen(true)}><Icon name="sidebar" /></IconButton>}
1122
1164
  <h1>{headerTitle}</h1>
1123
1165
  </div>
1124
1166
  {primaryHeaderActions || headerActions.length ? (
@@ -13,6 +13,8 @@
13
13
  --wapp-danger-bg: #991b1b;
14
14
  --wapp-primary: #111827;
15
15
  --wapp-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
16
+ --wapp-overlay-bg: rgb(0 0 0 / 0.5);
17
+ --wapp-overlay-blur: blur(4px);
16
18
  --wapp-safe-area-top: env(safe-area-inset-top, 0px);
17
19
  --wapp-safe-area-bottom: env(safe-area-inset-bottom, 0px);
18
20
  --wapp-safe-area-left: env(safe-area-inset-left, 0px);
@@ -1549,7 +1551,8 @@ textarea::placeholder {
1549
1551
  .wapp-mobile-backdrop {
1550
1552
  position: fixed;
1551
1553
  inset: 0;
1552
- background: rgb(15 23 42 / 0.36);
1554
+ background: var(--wapp-overlay-bg);
1555
+ backdrop-filter: var(--wapp-overlay-blur);
1553
1556
  z-index: 50;
1554
1557
  }
1555
1558
 
@@ -1650,8 +1653,8 @@ textarea::placeholder {
1650
1653
  .wapp-modal-overlay {
1651
1654
  position: absolute;
1652
1655
  inset: 0;
1653
- background: rgb(0 0 0 / 0.5);
1654
- backdrop-filter: blur(4px);
1656
+ background: var(--wapp-overlay-bg);
1657
+ backdrop-filter: var(--wapp-overlay-blur);
1655
1658
  }
1656
1659
 
1657
1660
  .wapp-modal {