From 111d4c86eb36f5c130a44b6f7bccb9fe9bbc5bba Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Thu, 19 Feb 2026 12:03:45 +0000 Subject: [PATCH] Hyprland --- .../Dashboard/Workspaces/WorkspaceListApp.tsx | 89 ++++++++------ .../Dashboard/Workspaces/WorkspaceScreen.tsx | 3 +- .../components/Workspace/AppPicker.tsx | 2 +- .../components/Workspace/PanelSlot.tsx | 116 +++++++++--------- .../components/Workspace/WorkspaceContext.ts | 2 - .../components/Workspace/WorkspaceLayout.tsx | 3 +- .../Workspace/WorkspaceRenderer.tsx | 20 +-- .../components/Workspace/WorkspaceView.tsx | 37 ++---- .../components/Workspace/layout-utils.ts | 53 +++----- src/workspaces/components/Workspace/types.ts | 1 + src/workspaces/widgets/Widget.tsx | 21 ++-- src/workspaces/widgets/widget-registry.tsx | 10 +- 12 files changed, 174 insertions(+), 183 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx index fe1b9531..6c128ea4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx @@ -1,3 +1,4 @@ +import { Link, useLocation, useNavigate } from 'react-router'; import { LayoutGrid, Plus, Pencil, Trash2 } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { useGlobal } from 'hooks/useGlobal'; @@ -14,6 +15,8 @@ import { } from './constants'; export const WorkspaceListApp = () => { + const location = useLocation(); + const navigate = useNavigate(); const client = useClient(); const queryClient = useQueryClient(); const [workspaces, setWorkspaces] = useUserState('workspaces', []); @@ -24,6 +27,8 @@ export const WorkspaceListApp = () => { const [, setDescription] = useGlobal(NEW_WS_DESC_KEY, ''); const [, setTemplateIdx] = useGlobal(NEW_WS_TEMPLATE_KEY, 0); + const isWorkspacesPage = location.pathname === '/workspaces'; + const handleEdit = (ev: React.MouseEvent, ws: WorkspaceDefinition) => { ev.stopPropagation(); setSelected(null); @@ -39,7 +44,6 @@ export const WorkspaceListApp = () => { setWorkspaces((prev) => prev.filter((w) => w.id !== ws.id)); if (selected === ws.id) setSelected(null); - // Clean up persisted layout const layoutKey = `ws-layout-${ws.id}`; const currentState = queryClient.getQueryData>(['USER_STATE']) ?? {}; const { [layoutKey]: _, ...rest } = currentState; @@ -47,62 +51,77 @@ export const WorkspaceListApp = () => { client.patch('/user/state', { [layoutKey]: null }).catch(() => {}); }; + const handleClick = (ws: WorkspaceDefinition) => { + if (isWorkspacesPage) { + setSelected(ws.id); + setCreating(false); + setEditing(null); + } else { + navigate(`/workspaces/${ws.id}`); + } + }; + return (
-
+ Workspaces -
+
{workspaces.map((ws) => (
{ - setSelected(ws.id); - setCreating(false); - setEditing(null); - }} + onClick={() => handleClick(ws)} > {ws.name} - - + {isWorkspacesPage && ( + <> + + + + )}
))} {workspaces.length === 0 && (

No workspaces yet

)} - + {isWorkspacesPage && ( + + )}
); diff --git a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceScreen.tsx index ca6466dd..00c97695 100644 --- a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceScreen.tsx @@ -6,9 +6,10 @@ import { appRegistry } from './app-registry'; export const WorkspaceScreen = () => { const { id } = useParams<{ id: string }>(); - const [workspaces] = useUserState('workspaces', []); + const [workspaces, , isLoaded] = useUserState('workspaces', []); const workspace = workspaces.find((ws) => ws.id === id); + if (!isLoaded) return null; if (!workspace) return ; return ; diff --git a/src/workspaces/components/Workspace/AppPicker.tsx b/src/workspaces/components/Workspace/AppPicker.tsx index 0624197d..a74f1a44 100644 --- a/src/workspaces/components/Workspace/AppPicker.tsx +++ b/src/workspaces/components/Workspace/AppPicker.tsx @@ -6,7 +6,7 @@ type AppPickerProps = { }; export const AppPicker = ({ registry, onSelect }: AppPickerProps) => { - const entries = Object.entries(registry); + const entries = Object.entries(registry).filter(([, entry]) => !entry.widget); return (
diff --git a/src/workspaces/components/Workspace/PanelSlot.tsx b/src/workspaces/components/Workspace/PanelSlot.tsx index d1bd46bb..a1f5bbf1 100644 --- a/src/workspaces/components/Workspace/PanelSlot.tsx +++ b/src/workspaces/components/Workspace/PanelSlot.tsx @@ -1,88 +1,92 @@ import type { LayoutPanel, AppRegistry, PanelComponents } from './types'; import { Card } from '../Card'; import { AppPicker } from './AppPicker'; -import { LayoutEditor } from './LayoutEditor'; +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, +} from '../ui/context-menu'; type PanelSlotProps = { panel: LayoutPanel; registry: AppRegistry; components?: PanelComponents; - editing: boolean; + interactive: boolean; isLastPanel: boolean; onSetApp: (panelId: string, appType: string | null) => void; onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void; onRemove: (panelId: string) => void; }; -export const PanelSlot = ({ panel, registry, components, editing, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => { +type PanelContextMenuProps = { + panelId: string; + hasApp: boolean; + isLastPanel: boolean; + onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void; + onRemove: (panelId: string) => void; + onClearApp: () => void; + children: React.ReactNode; +}; + +const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onClearApp, children }: PanelContextMenuProps) => ( + + {children} + + onSplit(panelId, 'horizontal')}>Split horizontal + onSplit(panelId, 'vertical')}>Split vertical + {hasApp && Clear app} + {!isLastPanel && ( + onRemove(panelId)}> + Remove panel + + )} + + +); + +export const PanelSlot = ({ panel, registry, components, interactive, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => { const PanelComponent = components?.[panel.id]; const entry = panel.appType ? registry[panel.appType] : null; const AppComponent = PanelComponent ?? entry?.component; - if (!editing && !AppComponent) { - return ( -
-
-
- ); - } + const contextMenu = interactive + ? (content: React.ReactNode) => ( + onSetApp(panel.id, null)}> + {content} + + ) + : (content: React.ReactNode) => <>{content}; if (!AppComponent) { - return ( + if (!interactive) { + return ( +
+
+
+ ); + } + + return contextMenu(
onSetApp(panel.id, type)} /> -
- - - {!isLastPanel && ( - - )} -
-
+
, ); } if (entry?.transparent) { - return ( + return contextMenu(
- {editing && ( - onSetApp(panel.id, null)} - /> - )}
-
+
, ); } - return ( + return contextMenu(
- {editing && ( - onSetApp(panel.id, null)} - /> - )}
-
+
, ); }; diff --git a/src/workspaces/components/Workspace/WorkspaceContext.ts b/src/workspaces/components/Workspace/WorkspaceContext.ts index c4d5e6be..9fc351c1 100644 --- a/src/workspaces/components/Workspace/WorkspaceContext.ts +++ b/src/workspaces/components/Workspace/WorkspaceContext.ts @@ -3,13 +3,11 @@ import { createContext, useContext } from 'react'; type WorkspaceContextValue = { workspaceId: string | null; cwd: string; - editing: boolean; }; const WorkspaceContext = createContext({ workspaceId: null, cwd: '~', - editing: false, }); export const WorkspaceProvider = WorkspaceContext.Provider; diff --git a/src/workspaces/components/Workspace/WorkspaceLayout.tsx b/src/workspaces/components/Workspace/WorkspaceLayout.tsx index b926d6b2..e23f1724 100644 --- a/src/workspaces/components/Workspace/WorkspaceLayout.tsx +++ b/src/workspaces/components/Workspace/WorkspaceLayout.tsx @@ -24,12 +24,11 @@ export const WorkspaceLayout = ({ layout, onLayoutChange, registry, components, ); return ( - + void; onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void; onRemove: (panelId: string) => void; @@ -19,7 +19,7 @@ export const WorkspaceRenderer = ({ layout, registry, components, - editing, + interactive = false, onSetApp, onSplit, onRemove, @@ -33,7 +33,7 @@ export const WorkspaceRenderer = ({ node={layout} registry={registry} components={components} - editing={editing} + interactive={interactive} totalPanels={totalPanels} onSetApp={onSetApp} onSplit={onSplit} @@ -48,7 +48,7 @@ type LayoutNodeRendererProps = { node: LayoutNode; registry: AppRegistry; components?: PanelComponents; - editing: boolean; + interactive: boolean; totalPanels: number; onSetApp: (panelId: string, appType: string | null) => void; onSplit: (panelId: string, direction: 'horizontal' | 'vertical') => void; @@ -65,7 +65,7 @@ const LayoutNodeRenderer = ({ node, registry, components, - editing, + interactive, totalPanels, onSetApp, onSplit, @@ -96,7 +96,7 @@ const LayoutNodeRenderer = ({ panel={node} registry={registry} components={components} - editing={editing} + interactive={interactive} isLastPanel={totalPanels <= 1} onSetApp={onSetApp} onSplit={onSplit} @@ -118,7 +118,7 @@ const LayoutNodeRenderer = ({ node={child.node} registry={registry} components={components} - editing={editing} + interactive={interactive} totalPanels={totalPanels} onSetApp={onSetApp} onSplit={onSplit} @@ -133,16 +133,16 @@ const LayoutNodeRenderer = ({ } return ( - + {node.children.map((child, i) => ( - +
void; registry: AppRegistry; }; -export const WorkspaceView = ({ workspace, name, layout, onLayoutChange, registry }: WorkspaceViewProps) => { - const [editing, setEditing] = useState(() => !hasAnyApp(layout)); - +export const WorkspaceView = ({ workspace, layout, onLayoutChange, registry }: WorkspaceViewProps) => { const handleSetApp = useCallback( (panelId: string, appType: string | null) => { onLayoutChange(setApp(layout, panelId, appType)); @@ -45,24 +41,17 @@ export const WorkspaceView = ({ workspace, name, layout, onLayoutChange, registr [layout, onLayoutChange], ); - const displayName = name ?? workspace?.name ?? 'Workspace'; - return ( - -
- setEditing((v) => !v)} /> -
- -
-
+ + ); }; diff --git a/src/workspaces/components/Workspace/layout-utils.ts b/src/workspaces/components/Workspace/layout-utils.ts index 52f5c1a2..d665b6ee 100644 --- a/src/workspaces/components/Workspace/layout-utils.ts +++ b/src/workspaces/components/Workspace/layout-utils.ts @@ -10,15 +10,14 @@ export const createDefaultLayout = (): LayoutPanel => ({ }); export function splitPanel(root: LayoutNode, panelId: string, direction: 'horizontal' | 'vertical'): LayoutNode { - return mapNode(root, (node, parent) => { - if (node.type !== 'panel' || node.id !== panelId) return node; + return splitInner(root, panelId, direction); +} + +function splitInner(node: LayoutNode, panelId: string, direction: 'horizontal' | 'vertical'): LayoutNode { + if (node.type === 'panel') { + if (node.id !== panelId) return node; const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType: null }; - - if (parent && parent.direction === direction) { - return null; - } - const group: LayoutGroup = { type: 'group', id: uid(), @@ -29,41 +28,29 @@ export function splitPanel(root: LayoutNode, panelId: string, direction: 'horizo ], }; return group; - }); -} + } -function mapNode( - node: LayoutNode, - fn: (node: LayoutNode, parent: LayoutGroup | null) => LayoutNode | null, - parent: LayoutGroup | null = null, -): LayoutNode { - const result = fn(node, parent); - - if (result === null && parent !== null && node.type === 'panel') { + // Check if the target panel is a direct child and the directions match — append as sibling + const childIdx = node.children.findIndex((c) => c.node.type === 'panel' && c.node.id === panelId); + if (childIdx !== -1 && node.direction === direction) { const newPanel: LayoutPanel = { type: 'panel', id: uid(), appType: null }; - const idx = parent.children.findIndex((c) => c.node.id === node.id); const newChildren = [ - ...parent.children.slice(0, idx + 1), + ...node.children.slice(0, childIdx + 1), { node: newPanel, size: 0 }, - ...parent.children.slice(idx + 1), + ...node.children.slice(childIdx + 1), ]; const size = 100 / newChildren.length; - parent.children = newChildren.map((c) => ({ ...c, size })); - return node; + return { ...node, children: newChildren.map((c) => ({ ...c, size })) }; } - if (result !== node) return result ?? node; - - if (node.type === 'group') { - const newChildren = node.children.map((child) => ({ - ...child, - node: mapNode(child.node, fn, node), - })); - if (newChildren.some((c, i) => c.node !== node.children[i]!.node)) { - return { ...node, children: newChildren }; - } + // Recurse into children + const newChildren = node.children.map((child) => ({ + ...child, + node: splitInner(child.node, panelId, direction), + })); + if (newChildren.some((c, i) => c.node !== node.children[i]!.node)) { + return { ...node, children: newChildren }; } - return node; } diff --git a/src/workspaces/components/Workspace/types.ts b/src/workspaces/components/Workspace/types.ts index 8b4153d6..51faa661 100644 --- a/src/workspaces/components/Workspace/types.ts +++ b/src/workspaces/components/Workspace/types.ts @@ -30,6 +30,7 @@ export type AppRegistryEntry = { component: ComponentType<{ panelId: string }>; transparent?: boolean; fixedHeight?: number; + widget?: boolean; }; export type AppRegistry = Record; diff --git a/src/workspaces/widgets/Widget.tsx b/src/workspaces/widgets/Widget.tsx index c6dda6be..ba44d147 100644 --- a/src/workspaces/widgets/Widget.tsx +++ b/src/workspaces/widgets/Widget.tsx @@ -11,13 +11,14 @@ type WidgetProps = ComponentPropsWithoutRef<'div'> & { title?: string; resizable?: boolean; collapsible?: boolean | { title: string; icon?: LucideIcon }; + minimizable?: boolean; moveable?: boolean; position?: Position; onPositionChange?: (pos: Position) => void; onClose?: () => void; }; -export const Widget = ({ title, className, style, resizable, collapsible, moveable, position: controlledPosition, onPositionChange, onClose, children, ...props }: WidgetProps) => { +export const Widget = ({ title, className, style, resizable, collapsible, minimizable = false, moveable, position: controlledPosition, onPositionChange, onClose, children, ...props }: WidgetProps) => { const [expanded, setExpanded] = useState(true); const [minimized, setMinimized] = useState(false); const [internalPosition, setInternalPosition] = useState({ x: 0, y: 0 }); @@ -69,7 +70,7 @@ export const Widget = ({ title, className, style, resizable, collapsible, moveab const isCardPadding = target === card; const isHeader = !isCardPadding && target.closest('[data-widget-header]') && !target.closest('button'); if (!isCardPadding && !isHeader) return; - if (isHeader && ev.detail === 2) { + if (minimizable && isHeader && ev.detail === 2) { toggleMinimized(); return; } @@ -132,13 +133,15 @@ export const Widget = ({ title, className, style, resizable, collapsible, moveab
{title && {title}}
- + {minimizable && ( + + )} {onClose && (