@pablozaiden/webapp 0.3.4 → 0.3.5
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 +1 -1
- package/src/web/WebAppRoot.tsx +47 -7
package/package.json
CHANGED
package/src/web/WebAppRoot.tsx
CHANGED
|
@@ -70,6 +70,8 @@ type StoredSidebarPin = {
|
|
|
70
70
|
route: WebAppRoute;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
type SidebarCollapsedState = Record<string, boolean>;
|
|
74
|
+
|
|
73
75
|
function isSidebarShortcutEditableTarget(target: EventTarget | null): boolean {
|
|
74
76
|
if (!(target instanceof HTMLElement)) {
|
|
75
77
|
return false;
|
|
@@ -251,6 +253,17 @@ function pinStorageKey(appName: string, explicitKey?: string): string {
|
|
|
251
253
|
return explicitKey ?? `webapp.${appName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.sidebar.pins`;
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
function sidebarCollapsedStorageKey(appName: string): string {
|
|
257
|
+
return `webapp.${appName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.sidebar.collapsed`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isSidebarCollapsedState(value: unknown): value is SidebarCollapsedState {
|
|
261
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return Object.values(value).every((entry) => typeof entry === "boolean");
|
|
265
|
+
}
|
|
266
|
+
|
|
254
267
|
function toStoredPin(node: SidebarNode): StoredSidebarPin | undefined {
|
|
255
268
|
if (!node.route) return undefined;
|
|
256
269
|
return {
|
|
@@ -291,6 +304,33 @@ function useSidebarPins(appName: string, storageKey?: string) {
|
|
|
291
304
|
return { pins, pinIds, pin, unpin };
|
|
292
305
|
}
|
|
293
306
|
|
|
307
|
+
function useSidebarCollapsedState(appName: string) {
|
|
308
|
+
const key = sidebarCollapsedStorageKey(appName);
|
|
309
|
+
const [collapsed, setCollapsed] = useState<SidebarCollapsedState>(() => {
|
|
310
|
+
try {
|
|
311
|
+
const raw = localStorage.getItem(key);
|
|
312
|
+
if (!raw) return {};
|
|
313
|
+
const parsed: unknown = JSON.parse(raw);
|
|
314
|
+
return isSidebarCollapsedState(parsed) ? parsed : {};
|
|
315
|
+
} catch {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
localStorage.setItem(key, JSON.stringify(collapsed));
|
|
322
|
+
}, [key, collapsed]);
|
|
323
|
+
|
|
324
|
+
const toggleCollapsed = useCallback((id: string, isCollapsed: boolean) => {
|
|
325
|
+
setCollapsed((current) => {
|
|
326
|
+
const currentIsCollapsed = current[id] ?? isCollapsed;
|
|
327
|
+
return { ...current, [id]: !currentIsCollapsed };
|
|
328
|
+
});
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
return { collapsed, toggleCollapsed };
|
|
332
|
+
}
|
|
333
|
+
|
|
294
334
|
function Icon({ name }: { name: "settings" | "sidebar" | "plus" | "home" | "search" | "bolt" | "chat" | "code" | "refresh" }) {
|
|
295
335
|
const common = { "aria-hidden": true, viewBox: "0 0 24 24", className: "wapp-svg" };
|
|
296
336
|
if (name === "settings") return <svg {...common}><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.65 1.65 0 0 0 15 19.4a1.65 1.65 0 0 0-1 .6 1.65 1.65 0 0 0-.33 1.82V22a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-.6-1 1.65 1.65 0 0 0-1.82-.33H2a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-.6 1.65 1.65 0 0 0 .33-1.82V2a2 2 0 1 1 4 0v.09A1.65 1.65 0 0 0 15 4.6a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.14.33.34.64.6 1 .26.36.61.6 1 .6H22a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-2.51.4Z" /></svg>;
|
|
@@ -500,8 +540,7 @@ function sidebarIndentStyle(level: number, parentKind: SidebarTreeParentKind): {
|
|
|
500
540
|
return { marginLeft: `${baseIndentRem + nestedSectionIndentRem}rem` };
|
|
501
541
|
}
|
|
502
542
|
|
|
503
|
-
function SidebarTree({ nodes, route, navigate, level = 0, parentKind = "root" }: { nodes: SidebarNode[]; route: WebAppRoute; navigate: (route: WebAppRoute) => void; level?: number; parentKind?: SidebarTreeParentKind }) {
|
|
504
|
-
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
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 }) {
|
|
505
544
|
const [contextMenu, setContextMenu] = useState<{ position: ContextMenuPosition; items: ActionMenuItem[]; title: string } | null>(null);
|
|
506
545
|
return (
|
|
507
546
|
<>
|
|
@@ -513,7 +552,7 @@ function SidebarTree({ nodes, route, navigate, level = 0, parentKind = "root" }:
|
|
|
513
552
|
<section className={`wapp-sidebar-section ${level === 0 ? "top" : "nested"}`} key={node.id}>
|
|
514
553
|
<div className="wapp-sidebar-section-title" style={sidebarIndentStyle(level, parentKind)}>
|
|
515
554
|
{hasChildren ? (
|
|
516
|
-
<button type="button" aria-expanded={!isCollapsed} aria-label={`${isCollapsed ? "Expand" : "Collapse"} ${node.title}`} onClick={() =>
|
|
555
|
+
<button type="button" aria-expanded={!isCollapsed} aria-label={`${isCollapsed ? "Expand" : "Collapse"} ${node.title}`} onClick={() => toggleCollapsed(node.id, isCollapsed)}>
|
|
517
556
|
<span>{isCollapsed ? "▶" : "▼"}</span>{node.title}
|
|
518
557
|
</button>
|
|
519
558
|
) : (
|
|
@@ -521,7 +560,7 @@ function SidebarTree({ nodes, route, navigate, level = 0, parentKind = "root" }:
|
|
|
521
560
|
)}
|
|
522
561
|
{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}
|
|
523
562
|
</div>
|
|
524
|
-
{!isCollapsed && hasChildren ? <SidebarTree nodes={node.children ?? []} route={route} navigate={navigate} level={level + 1} parentKind="section" /> : null}
|
|
563
|
+
{!isCollapsed && hasChildren ? <SidebarTree nodes={node.children ?? []} route={route} navigate={navigate} collapsed={collapsed} toggleCollapsed={toggleCollapsed} level={level + 1} parentKind="section" /> : null}
|
|
525
564
|
{!isCollapsed && !hasChildren && level === 0 ? <div className="wapp-sidebar-empty">No items.</div> : null}
|
|
526
565
|
</section>
|
|
527
566
|
);
|
|
@@ -529,7 +568,7 @@ function SidebarTree({ nodes, route, navigate, level = 0, parentKind = "root" }:
|
|
|
529
568
|
const active = node.route?.view === route.view && Object.entries(node.route).every(([key, value]) => key === "view" || route[key] === value);
|
|
530
569
|
return (
|
|
531
570
|
<div className={`wapp-sidebar-item-wrap ${hasChildren ? "has-toggle" : ""}`} key={node.id} style={sidebarIndentStyle(level, parentKind)}>
|
|
532
|
-
{hasChildren ? <button type="button" className="wapp-tree-toggle" aria-expanded={!isCollapsed} aria-label={`${isCollapsed ? "Expand" : "Collapse"} ${node.title}`} onClick={() =>
|
|
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}
|
|
533
572
|
<button
|
|
534
573
|
type="button"
|
|
535
574
|
className={`wapp-sidebar-item ${active ? "active" : ""}`}
|
|
@@ -546,7 +585,7 @@ function SidebarTree({ nodes, route, navigate, level = 0, parentKind = "root" }:
|
|
|
546
585
|
</span>
|
|
547
586
|
{node.badge ? <Badge variant={node.badgeVariant} className="wapp-sidebar-badge" title={node.badge} aria-label={node.badge}> </Badge> : null}
|
|
548
587
|
</button>
|
|
549
|
-
{!isCollapsed && node.children ? <div className="wapp-sidebar-children"><SidebarTree nodes={node.children} route={route} navigate={navigate} level={level + 1} parentKind="item" /></div> : null}
|
|
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}
|
|
550
589
|
</div>
|
|
551
590
|
);
|
|
552
591
|
})}
|
|
@@ -992,6 +1031,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
992
1031
|
const [search, setSearch] = useState("");
|
|
993
1032
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
994
1033
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
1034
|
+
const sidebarTreeState = useSidebarCollapsedState(appName);
|
|
995
1035
|
const toggleSidebarCollapsed = useCallback(() => {
|
|
996
1036
|
setSidebarCollapsed((current) => {
|
|
997
1037
|
const nextCollapsed = !current;
|
|
@@ -1153,7 +1193,7 @@ export function WebAppRoot({ appName, homeRoute, sidebar, routes, header, onRout
|
|
|
1153
1193
|
</div>
|
|
1154
1194
|
<div className="wapp-sidebar-scroll">
|
|
1155
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}
|
|
1156
|
-
<SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} />
|
|
1196
|
+
<SidebarTree nodes={nodes} route={route} navigate={(next) => { navigate(next); setSidebarOpen(false); }} collapsed={sidebarTreeState.collapsed} toggleCollapsed={sidebarTreeState.toggleCollapsed} />
|
|
1157
1197
|
<div className="wapp-sidebar-footer">v{effectiveVersion}<button type="button" aria-label="Reload" onClick={() => window.location.reload()}><Icon name="refresh" /></button></div>
|
|
1158
1198
|
</div>
|
|
1159
1199
|
</aside>
|