@pablozaiden/webapp 0.3.4 → 0.3.6
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/src/web/styles.css +43 -19
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>
|
package/src/web/styles.css
CHANGED
|
@@ -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-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1175
|
+
--wapp-badge-fg: rgb(147 197 253);
|
|
1176
|
+
color: var(--wapp-badge-fg);
|
|
1163
1177
|
}
|
|
1164
1178
|
:root.dark .wapp-badge-planning {
|
|
1165
|
-
|
|
1179
|
+
--wapp-badge-fg: rgb(103 232 249);
|
|
1180
|
+
color: var(--wapp-badge-fg);
|
|
1166
1181
|
}
|
|
1167
1182
|
:root.dark .wapp-badge-merged {
|
|
1168
|
-
|
|
1183
|
+
--wapp-badge-fg: rgb(216 180 254);
|
|
1184
|
+
color: var(--wapp-badge-fg);
|
|
1169
1185
|
}
|
|
1170
1186
|
:root.dark .wapp-badge-pushed {
|
|
1171
|
-
|
|
1187
|
+
--wapp-badge-fg: rgb(165 180 252);
|
|
1188
|
+
color: var(--wapp-badge-fg);
|
|
1172
1189
|
}
|
|
1173
1190
|
:root.dark .wapp-badge-deleted {
|
|
1174
|
-
|
|
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:
|
|
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 {
|