mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
feat: tab names
This commit is contained in:
@@ -3,3 +3,4 @@ node_modules
|
||||
.claude
|
||||
*.md
|
||||
package-lock.json
|
||||
src/data/nostr-kinds-schema.yaml
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from "./ui/button";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LayoutControls } from "./LayoutControls";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function TabBar() {
|
||||
const {
|
||||
@@ -11,13 +11,50 @@ export function TabBar() {
|
||||
setActiveWorkspace,
|
||||
createWorkspace,
|
||||
createWorkspaceWithNumber,
|
||||
updateWorkspaceLabel,
|
||||
} = useGrimoire();
|
||||
const { workspaces, activeWorkspaceId } = state;
|
||||
|
||||
// State for inline label editing
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingLabel, setEditingLabel] = useState("");
|
||||
|
||||
const handleNewTab = () => {
|
||||
createWorkspace();
|
||||
};
|
||||
|
||||
// Start editing a workspace label
|
||||
const startEditing = (workspaceId: string, currentLabel?: string) => {
|
||||
setEditingId(workspaceId);
|
||||
setEditingLabel(currentLabel || "");
|
||||
};
|
||||
|
||||
// Save label changes
|
||||
const saveLabel = () => {
|
||||
if (editingId) {
|
||||
updateWorkspaceLabel(editingId, editingLabel);
|
||||
setEditingId(null);
|
||||
setEditingLabel("");
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel editing
|
||||
const cancelEditing = () => {
|
||||
setEditingId(null);
|
||||
setEditingLabel("");
|
||||
};
|
||||
|
||||
// Handle keyboard events in input
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveLabel();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
cancelEditing();
|
||||
}
|
||||
};
|
||||
|
||||
// Sort workspaces by number (for both rendering and keyboard shortcuts)
|
||||
const sortedWorkspaces = Object.values(workspaces).sort(
|
||||
(a, b) => a.number - b.number,
|
||||
@@ -62,22 +99,60 @@ export function TabBar() {
|
||||
<div className="h-8 border-t border-border bg-background flex items-center px-2 gap-1 overflow-x-auto">
|
||||
{/* Left side: Workspace tabs + new workspace button */}
|
||||
<div className="flex items-center gap-1 flex-nowrap">
|
||||
{sortedWorkspaces.map((ws) => (
|
||||
<button
|
||||
key={ws.id}
|
||||
onClick={() => setActiveWorkspace(ws.id)}
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs font-mono rounded transition-colors whitespace-nowrap flex-shrink-0",
|
||||
ws.id === activeWorkspaceId
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{ws.label && ws.label.trim()
|
||||
? `${ws.number} ${ws.label}`
|
||||
: ws.number}
|
||||
</button>
|
||||
))}
|
||||
{sortedWorkspaces.map((ws) => {
|
||||
const isEditing = editingId === ws.id;
|
||||
const isActive = ws.id === activeWorkspaceId;
|
||||
|
||||
if (isEditing) {
|
||||
// Render input field when editing
|
||||
return (
|
||||
<div
|
||||
key={ws.id}
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs font-mono rounded flex items-center gap-2 flex-shrink-0",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-foreground",
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>{ws.number}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={editingLabel}
|
||||
onChange={(e) => setEditingLabel(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={saveLabel}
|
||||
autoFocus
|
||||
style={{ width: `${Math.max(editingLabel.length, 1)}ch` }}
|
||||
className="bg-transparent border-0 outline-none focus:outline-none focus:ring-0 p-0 m-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render button when not editing
|
||||
return (
|
||||
<button
|
||||
key={ws.id}
|
||||
onClick={() => setActiveWorkspace(ws.id)}
|
||||
onDoubleClick={() => startEditing(ws.id, ws.label)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1 text-xs font-mono rounded transition-colors whitespace-nowrap flex-shrink-0",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
<span>{ws.number}</span>
|
||||
{ws.label && ws.label.trim() && (
|
||||
<span style={{ width: `${ws.label.trim().length || 0}ch` }}>
|
||||
{ws.label.trim()}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -399,3 +399,37 @@ export const applyPresetLayout = (
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the label of an existing workspace.
|
||||
* Labels are user-friendly names that appear alongside workspace numbers.
|
||||
*/
|
||||
export const updateWorkspaceLabel = (
|
||||
state: GrimoireState,
|
||||
workspaceId: string,
|
||||
label: string | undefined,
|
||||
): GrimoireState => {
|
||||
const workspace = state.workspaces[workspaceId];
|
||||
if (!workspace) {
|
||||
return state; // Workspace doesn't exist, return unchanged
|
||||
}
|
||||
|
||||
// Normalize label: trim and treat empty strings as undefined
|
||||
const normalizedLabel = label?.trim() || undefined;
|
||||
|
||||
// If label hasn't changed, return state unchanged (optimization)
|
||||
if (workspace.label === normalizedLabel) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
workspaces: {
|
||||
...state.workspaces,
|
||||
[workspaceId]: {
|
||||
...workspace,
|
||||
label: normalizedLabel,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -272,6 +272,12 @@ export const useGrimoire = () => {
|
||||
[setState],
|
||||
);
|
||||
|
||||
const updateWorkspaceLabel = useCallback(
|
||||
(workspaceId: string, label: string | undefined) =>
|
||||
setState((prev) => Logic.updateWorkspaceLabel(prev, workspaceId, label)),
|
||||
[setState],
|
||||
);
|
||||
|
||||
return {
|
||||
state,
|
||||
locale: state.locale || browserLocale,
|
||||
@@ -288,5 +294,6 @@ export const useGrimoire = () => {
|
||||
setActiveAccountRelays,
|
||||
updateLayoutConfig,
|
||||
applyPresetLayout,
|
||||
updateWorkspaceLabel,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -125,7 +125,9 @@
|
||||
|
||||
/* Smooth animations for window resizing and repositioning */
|
||||
/* Only animate during preset application, not manual resize/drag */
|
||||
body.animating-layout .mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-tile {
|
||||
body.animating-layout
|
||||
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
|
||||
.mosaic-tile {
|
||||
transition:
|
||||
width 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
height 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
|
||||
Reference in New Issue
Block a user