Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/config/ui_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type UIFeaturesConfig struct {
Canvas *bool `toml:"canvas"`
Whiteboard *bool `toml:"whiteboard"`
Timeline *bool `toml:"timeline"`
Calendar *bool `toml:"calendar"`
Bases *bool `toml:"bases"`
DataSources *bool `toml:"data_sources"`
}
Expand All @@ -24,6 +25,7 @@ func (f UIFeaturesConfig) Resolved() map[string]bool {
"canvas": featureEnabled(f.Canvas),
"whiteboard": featureEnabled(f.Whiteboard),
"timeline": featureEnabled(f.Timeline),
"calendar": featureEnabled(f.Calendar),
"bases": featureEnabled(f.Bases),
"data_sources": featureEnabled(f.DataSources),
}
Expand Down
11 changes: 6 additions & 5 deletions internal/config/ui_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "testing"

func TestUIFeaturesConfigDefaults(t *testing.T) {
f := UIFeaturesConfig{}.Resolved()
for _, key := range []string{"graph", "kanban", "canvas", "whiteboard", "timeline", "bases", "data_sources"} {
for _, key := range []string{"graph", "kanban", "canvas", "whiteboard", "timeline", "calendar", "bases", "data_sources"} {
if !f[key] {
t.Fatalf("expected %s enabled by default", key)
}
Expand All @@ -14,11 +14,12 @@ func TestUIFeaturesConfigDefaults(t *testing.T) {
func TestUIFeaturesConfigExplicitFalse(t *testing.T) {
falseVal := false
f := UIFeaturesConfig{
Kanban: &falseVal,
Graph: &falseVal,
Kanban: &falseVal,
Graph: &falseVal,
Calendar: &falseVal,
}.Resolved()
if f["kanban"] || f["graph"] {
t.Fatal("expected kanban and graph disabled")
if f["kanban"] || f["graph"] || f["calendar"] {
t.Fatal("expected kanban, graph, and calendar disabled")
}
if !f["canvas"] || !f["bases"] {
t.Fatal("expected unset features to remain enabled")
Expand Down
2 changes: 2 additions & 0 deletions internal/keybindings/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var knownActions = map[string]struct{}{
"graph": {},
"toggle_bases": {},
"toggle_timeline": {},
"toggle_calendar": {},
"toggle_kanban": {},
"toggle_mode": {},
"shortcuts_help": {},
Expand All @@ -37,6 +38,7 @@ var DefaultBindings = map[string]string{
"graph": "Mod+G",
"toggle_bases": "Mod+Shift+B",
"toggle_timeline": "Mod+Shift+T",
"toggle_calendar": "Mod+Shift+C",
"toggle_kanban": "Mod+Shift+W",
"toggle_mode": "Mod+Shift+E",
"shortcuts_help": "Mod+/",
Expand Down
98 changes: 93 additions & 5 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
CalendarDays,
Clock4,
Columns3,
Database,
Expand Down Expand Up @@ -30,6 +31,7 @@ import { KiwiBases } from "./components/KiwiBases";
import { KiwiCanvasScreen } from "./components/KiwiCanvasScreen";
import { KiwiWhiteboardScreen } from "./components/KiwiWhiteboardScreen";
import { KiwiTimeline } from "./components/KiwiTimeline";
import { KiwiCalendar } from "./components/KiwiCalendar";
import { KiwiKanban } from "./components/KiwiKanban";
import { KiwiRecentStart } from "./components/KiwiRecentStart";
import { KanbanDragProvider } from "./components/kanban/KanbanDragProvider";
Expand All @@ -48,6 +50,11 @@ import { useKeybindings } from "./hooks/useKeybindings";
import { useUIConfig } from "./hooks/useUIConfig";
import { usePreferences } from "./hooks/usePreferences";
import { formatChordDisplay, matchBoundAction, type KeybindingAction } from "./lib/kiwiKeybindings";
import {
shouldOpenViewFromPathname,
shouldPreservePathnameForViewRoute,
type AppViewId,
} from "./lib/appViewRoutes";
import { resolveOverlayDismiss } from "./lib/overlayDismiss";
import { hasDeepLinkPath, resolveDashboardPath, resolveStartPage, shouldApplyStartPage } from "./lib/startPage";
import { formatDocumentTitle } from "./lib/pageTitle";
Expand Down Expand Up @@ -97,6 +104,7 @@ export default function App() {
const [whiteboardOpen, setWhiteboardOpen] = useState(false);
const [initialWhiteboardPath, setInitialWhiteboardPath] = useState<string | null>(null);
const [timelineOpen, setTimelineOpen] = useState(false);
const [calendarOpen, setCalendarOpen] = useState(false);
const [kanbanOpen, setKanbanOpen] = useState(false);
const [treeRevealRequest, setTreeRevealRequest] = useState<TreeRevealRequest | null>(null);
const treeRef = useRef<KiwiTreeHandle>(null);
Expand All @@ -119,12 +127,42 @@ export default function App() {
setCanvasOpen(false);
setWhiteboardOpen(false);
setTimelineOpen(false);
setCalendarOpen(false);
setKanbanOpen(false);
setDataOpen(false);
setGraphOpen(false);
setHistoryOpen(false);
}, []);

const openBuiltinView = useCallback((id: AppViewId) => {
switch (id) {
case "graph":
setGraphOpen(true);
break;
case "bases":
setBasesOpen(true);
break;
case "canvas":
setCanvasOpen(true);
break;
case "whiteboard":
setWhiteboardOpen(true);
break;
case "timeline":
setTimelineOpen(true);
break;
case "calendar":
setCalendarOpen(true);
break;
case "kanban":
setKanbanOpen(true);
break;
case "data":
setDataOpen(true);
break;
}
}, []);

const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.innerWidth < 768);
useEffect(() => {
const mq = window.matchMedia("(max-width: 767px)");
Expand Down Expand Up @@ -201,6 +239,7 @@ export default function App() {
canvasOpen,
whiteboardOpen,
timelineOpen,
calendarOpen,
kanbanOpen,
});
stateRef.current = {
Expand All @@ -216,6 +255,7 @@ export default function App() {
canvasOpen,
whiteboardOpen,
timelineOpen,
calendarOpen,
kanbanOpen,
};

Expand Down Expand Up @@ -327,8 +367,8 @@ export default function App() {
setNewOpen(true);
break;
case "toggle_editor": {
const { activePath, graphOpen, historyOpen, dataOpen } = state;
if (!activePath || graphOpen || historyOpen || dataOpen) return;
const { activePath, graphOpen, historyOpen, dataOpen, calendarOpen } = state;
if (!activePath || graphOpen || historyOpen || dataOpen || calendarOpen) return;
e.preventDefault();
setEditing((v) => !v);
break;
Expand Down Expand Up @@ -368,6 +408,13 @@ export default function App() {
setTimelineOpen(next);
break;
}
case "toggle_calendar": {
e.preventDefault();
const next = !state.calendarOpen;
closeAllViews();
setCalendarOpen(next);
break;
}
case "toggle_kanban": {
e.preventDefault();
const next = !state.kanbanOpen;
Expand Down Expand Up @@ -428,6 +475,9 @@ export default function App() {
case "timeline":
setTimelineOpen(false);
break;
case "calendar":
setCalendarOpen(false);
break;
case "kanban":
setKanbanOpen(false);
break;
Expand All @@ -449,6 +499,7 @@ const handleSpaceSwitch = useCallback(() => {
setBasesOpen(false);
setCanvasOpen(false);
setTimelineOpen(false);
setCalendarOpen(false);
setKanbanOpen(false);
setSpaceKey((k) => k + 1);
setRefreshKey((k) => k + 1);
Expand Down Expand Up @@ -527,6 +578,9 @@ const handleSpaceSwitch = useCallback(() => {
case "timeline":
setTimelineOpen(true);
break;
case "calendar":
setCalendarOpen(true);
break;
case "canvas":
setCanvasOpen(true);
break;
Expand All @@ -540,10 +594,22 @@ const handleSpaceSwitch = useCallback(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uiConfigLoaded]);

useEffect(() => {
if (!uiConfigLoaded || isDemoMode) return;
const viewId = shouldOpenViewFromPathname(window.location.pathname, features);
if (!viewId) return;
closeAllViews();
openBuiltinView(viewId);
}, [uiConfigLoaded, isDemoMode, features, closeAllViews, openBuiltinView]);

useEffect(() => {
if (isCloudMode || isDemoMode) return;
if (!activePath) {
if (window.location.pathname !== "/") {
const pathname = window.location.pathname;
if (
pathname !== "/"
&& !shouldPreservePathnameForViewRoute(pathname)
) {
window.history.pushState(null, "", "/");
}
return;
Expand Down Expand Up @@ -579,15 +645,26 @@ const handleSpaceSwitch = useCallback(() => {
setBasesOpen(false);
setCanvasOpen(false);
setTimelineOpen(false);
setCalendarOpen(false);
setKanbanOpen(false);
} else if (pathname === "/") {
fromPopState.current = true;
setActivePath(null);
closeAllViews();
} else {
const viewId = shouldOpenViewFromPathname(pathname, features);
if (viewId) {
fromPopState.current = true;
setActivePath(null);
setEditing(false);
closeAllViews();
openBuiltinView(viewId);
}
}
};
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
}, [isCloudMode, isDemoMode]);
}, [isCloudMode, isDemoMode, features, closeAllViews, openBuiltinView]);

function revealActivePageInTree() {
if (!activePath) return;
Expand Down Expand Up @@ -635,6 +712,7 @@ const handleSpaceSwitch = useCallback(() => {
setCanvasOpen(false);
setWhiteboardOpen(false);
setTimelineOpen(false);
setCalendarOpen(false);
setKanbanOpen(false);
recordVisit(path);
if (isMobile) setSidebarOpen(false);
Expand Down Expand Up @@ -703,6 +781,7 @@ const handleSpaceSwitch = useCallback(() => {
canvas: canvasOpen,
whiteboard: whiteboardOpen,
timeline: timelineOpen,
calendar: calendarOpen,
kanban: kanbanOpen,
data: dataOpen,
}[id];
Expand All @@ -723,6 +802,9 @@ const handleSpaceSwitch = useCallback(() => {
case "timeline":
setTimelineOpen(!wasOpen);
break;
case "calendar":
setCalendarOpen(!wasOpen);
break;
case "kanban":
setKanbanOpen(!wasOpen);
break;
Expand Down Expand Up @@ -810,7 +892,7 @@ const handleSpaceSwitch = useCallback(() => {
)}

{/* Main content area */}
<main className={`flex-1 relative ${basesOpen || canvasOpen || whiteboardOpen || timelineOpen || kanbanOpen || dataOpen || graphOpen ? "overflow-hidden" : "overflow-auto kiwi-scroll"}`}>
<main className={`flex-1 relative ${basesOpen || canvasOpen || whiteboardOpen || timelineOpen || calendarOpen || kanbanOpen || dataOpen || graphOpen ? "overflow-hidden" : "overflow-auto kiwi-scroll"}`}>
{basesOpen ? (
<KiwiBases
onClose={() => setBasesOpen(false)}
Expand All @@ -835,6 +917,11 @@ const handleSpaceSwitch = useCallback(() => {
onClose={() => setTimelineOpen(false)}
onNavigate={(p) => { setTimelineOpen(false); navigate(p); }}
/>
) : calendarOpen ? (
<KiwiCalendar
onClose={() => setCalendarOpen(false)}
onNavigate={(p) => { setCalendarOpen(false); navigate(p); }}
/>
) : kanbanOpen ? (
<KiwiKanban
onClose={() => setKanbanOpen(false)}
Expand Down Expand Up @@ -1042,6 +1129,7 @@ const BUILTIN_TOOLBAR_BUTTONS: Record<
canvas: { label: "Canvas", Icon: Presentation },
whiteboard: { label: "Whiteboard", Icon: PenTool },
timeline: { label: "Timeline", Icon: Clock4 },
calendar: { label: "Calendar", Icon: CalendarDays },
kanban: { label: "Kanban", Icon: Columns3 },
data: { label: "Data sources", Icon: Database },
};
Expand Down
Loading
Loading