diff --git a/src/apps/officer-web/frontend.tsx b/src/apps/officer-web/frontend.tsx index 3eb7190a..0b0c1254 100644 --- a/src/apps/officer-web/frontend.tsx +++ b/src/apps/officer-web/frontend.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; -import { AppRegistry } from 'officerdev'; +import { AppRegistry, WidgetRegistry } from 'officerdev'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ColorModeProvider } from '@/components/ui/ThemeProvider'; import { I18nBridge } from '@/lib/I18nBridge'; @@ -41,6 +41,7 @@ const app = ( + diff --git a/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx b/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx index 8e6fd3ce..0e754a74 100644 --- a/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx +++ b/src/workspaces/officerdev/src/AppRegistry/AppRegistry.tsx @@ -7,9 +7,10 @@ import { appRegistryMetas as workspaceMetas } from '../apps/Workspaces'; import { appRegistryMetas as projectMetas } from '../apps/Projects'; import { appRegistryMetas as chatHistoryMetas } from '../apps/ChatHistory'; import { appRegistryMetas as previewMetas } from '../apps/Preview'; +import { appRegistryMetas as widgetMetas } from '../apps/Widgets'; import { useAppRegistry } from './useAppRegistry'; -const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...workspaceMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas]; +const apps = [...fileBrowserMetas, ...terminalMetas, ...codeEditorMetas, ...chatMetas, ...fileViewerMetas, ...workspaceMetas, ...projectMetas, ...chatHistoryMetas, ...previewMetas, ...widgetMetas]; export const AppRegistry = () => { useAppRegistry(apps); diff --git a/src/workspaces/officerdev/src/WidgetRegistry/WidgetRegistry.tsx b/src/workspaces/officerdev/src/WidgetRegistry/WidgetRegistry.tsx new file mode 100644 index 00000000..7a550580 --- /dev/null +++ b/src/workspaces/officerdev/src/WidgetRegistry/WidgetRegistry.tsx @@ -0,0 +1,13 @@ +import { widgetRegistryMetas as clockMetas } from 'widgets/Clock'; +import { widgetRegistryMetas as weatherMetas } from 'widgets/Weather'; +import { widgetRegistryMetas as pomodoroMetas } from 'widgets/Pomodoro'; +import { widgetRegistryMetas as dailyGoalsMetas } from 'widgets/DailyGoals'; +import { widgetRegistryMetas as quickNotesMetas } from 'widgets/QuickNotes'; +import { useWidgetRegistry } from './useWidgetRegistry'; + +const widgets = [...clockMetas, ...weatherMetas, ...pomodoroMetas, ...dailyGoalsMetas, ...quickNotesMetas]; + +export const WidgetRegistry = () => { + useWidgetRegistry(widgets); + return null; +}; diff --git a/src/workspaces/officerdev/src/WidgetRegistry/index.ts b/src/workspaces/officerdev/src/WidgetRegistry/index.ts new file mode 100644 index 00000000..ce452cfc --- /dev/null +++ b/src/workspaces/officerdev/src/WidgetRegistry/index.ts @@ -0,0 +1,3 @@ +export * from './WidgetRegistry'; +export * from './useWidgetRegistry'; +export type { WidgetRegistryEntry } from './types'; diff --git a/src/workspaces/officerdev/src/WidgetRegistry/types.ts b/src/workspaces/officerdev/src/WidgetRegistry/types.ts new file mode 100644 index 00000000..ab7f87e6 --- /dev/null +++ b/src/workspaces/officerdev/src/WidgetRegistry/types.ts @@ -0,0 +1,10 @@ +import type { ComponentType } from 'react'; +import type { LucideIcon } from 'lucide-react'; + +export type WidgetRegistryEntry = { + name: string; + icon: LucideIcon; + component: ComponentType; +}; + +export type WidgetRegistry = Record; diff --git a/src/workspaces/officerdev/src/WidgetRegistry/useWidgetRegistry.ts b/src/workspaces/officerdev/src/WidgetRegistry/useWidgetRegistry.ts new file mode 100644 index 00000000..a7079d25 --- /dev/null +++ b/src/workspaces/officerdev/src/WidgetRegistry/useWidgetRegistry.ts @@ -0,0 +1,17 @@ +import type { WidgetRegistry, WidgetRegistryEntry } from './types'; +import { useGlobal } from 'hooks/useGlobal'; + +export type WidgetRegistryMeta = { key: string } & WidgetRegistryEntry; + +export function useWidgetRegistry(initialWidgets: WidgetRegistryMeta[] = []) { + const [registry, setRegistry] = useGlobal('WIDGET_REGISTRY', () => metasToRegistry(initialWidgets)); + + const registerWidget = (key: string, entry: WidgetRegistryEntry) => { + setRegistry((prev) => ({ ...prev, [key]: entry })); + }; + + return { registry, registerWidget }; +} + +const metasToRegistry = (metas: WidgetRegistryMeta[]): WidgetRegistry => + Object.fromEntries(metas.map(({ key, ...entry }) => [key, entry])); diff --git a/src/workspaces/widgets/WidgetPanel/index.tsx b/src/workspaces/officerdev/src/apps/Widgets/WidgetPanel.tsx similarity index 97% rename from src/workspaces/widgets/WidgetPanel/index.tsx rename to src/workspaces/officerdev/src/apps/Widgets/WidgetPanel.tsx index 82b20a7e..2766e642 100644 --- a/src/workspaces/widgets/WidgetPanel/index.tsx +++ b/src/workspaces/officerdev/src/apps/Widgets/WidgetPanel.tsx @@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; import { Plus, X } from 'lucide-react'; -import { widgetRegistry } from '../widget-registry'; +import { useWidgetRegistry } from '../../WidgetRegistry'; import { WidgetPicker } from './WidgetPicker'; type Position = { x: number; y: number }; // percentages (0–100) @@ -78,6 +78,7 @@ type DragState = { const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidgetProps) => { const containerRef = useRef(null); const dragRef = useRef(null); + const { registry } = useWidgetRegistry(); const onPointerDown = useCallback( (ev: ReactPointerEvent) => { @@ -136,7 +137,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg [endDrag], ); - const entry = widgetRegistry[instance.widgetType]; + const entry = registry[instance.widgetType]; if (!entry) return null; const WidgetComponent = entry.component; @@ -156,7 +157,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg > - + ); }; diff --git a/src/workspaces/widgets/WidgetPanel/WidgetPicker.tsx b/src/workspaces/officerdev/src/apps/Widgets/WidgetPicker.tsx similarity index 83% rename from src/workspaces/widgets/WidgetPanel/WidgetPicker.tsx rename to src/workspaces/officerdev/src/apps/Widgets/WidgetPicker.tsx index 086d8c22..9726bb71 100644 --- a/src/workspaces/widgets/WidgetPanel/WidgetPicker.tsx +++ b/src/workspaces/officerdev/src/apps/Widgets/WidgetPicker.tsx @@ -1,11 +1,12 @@ -import { widgetRegistry } from '../widget-registry'; +import { useWidgetRegistry } from '../../WidgetRegistry'; type WidgetPickerProps = { onSelect: (widgetType: string) => void; }; export const WidgetPicker = ({ onSelect }: WidgetPickerProps) => { - const entries = Object.entries(widgetRegistry); + const { registry } = useWidgetRegistry(); + const entries = Object.entries(registry); return (
diff --git a/src/workspaces/officerdev/src/apps/Widgets/index.ts b/src/workspaces/officerdev/src/apps/Widgets/index.ts new file mode 100644 index 00000000..0193d959 --- /dev/null +++ b/src/workspaces/officerdev/src/apps/Widgets/index.ts @@ -0,0 +1,13 @@ +import type { AppRegistryMeta } from '../../AppRegistry'; +import { LayoutGrid } from 'lucide-react'; +import { WidgetPanel } from './WidgetPanel'; + +export const appRegistryMetas: AppRegistryMeta[] = [ + { + key: 'officerdev/widgets', + name: 'Widgets', + icon: LayoutGrid, + component: WidgetPanel, + transparent: true, + }, +]; diff --git a/src/workspaces/officerdev/src/index.ts b/src/workspaces/officerdev/src/index.ts index 93d5e16f..3678567a 100644 --- a/src/workspaces/officerdev/src/index.ts +++ b/src/workspaces/officerdev/src/index.ts @@ -1,5 +1,6 @@ export * from './hooks'; export * from './AppRegistry'; +export * from './WidgetRegistry'; // Re-export app modules (excluding appRegistryMetas to avoid name collisions) export { MessageList, MessageBubble, StreamingBubble, ToolActivity, QuestionActivity, ModelSelector, InputArea, ChatLauncher, AttachmentList, AttachButton, WebpageDialog, EmbeddableChat, usePiChat, ChatList, useSlashCommands, useChatSessions, useChatSession, useAttachments, useAudioRecording } from './apps/Chat'; diff --git a/src/workspaces/widgets/Clock/index.tsx b/src/workspaces/widgets/Clock/index.tsx index 845ded8b..c79dc7da 100644 --- a/src/workspaces/widgets/Clock/index.tsx +++ b/src/workspaces/widgets/Clock/index.tsx @@ -1,4 +1,6 @@ +import type { WidgetRegistryMeta } from 'officerdev'; import { useState, useEffect } from 'react'; +import { Clock as ClockIcon } from 'lucide-react'; import { Widget } from '../Widget'; export const Clock = () => { @@ -21,3 +23,7 @@ export const Clock = () => { ); }; + +export const widgetRegistryMetas: WidgetRegistryMeta[] = [ + { key: 'clock', name: 'Clock', icon: ClockIcon, component: Clock }, +]; diff --git a/src/workspaces/widgets/DailyGoals/index.tsx b/src/workspaces/widgets/DailyGoals/index.tsx index b10d4755..747b96dd 100644 --- a/src/workspaces/widgets/DailyGoals/index.tsx +++ b/src/workspaces/widgets/DailyGoals/index.tsx @@ -1,8 +1,9 @@ +import type { WidgetRegistryMeta } from 'officerdev'; import { useCallback, useRef, useState } from 'react'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; -import { Plus, Trash2, Flame } from 'lucide-react'; +import { Plus, Target, Trash2, Flame } from 'lucide-react'; import { Widget } from '../Widget'; type Goal = { @@ -188,3 +189,7 @@ export const DailyGoals = () => { ); }; + +export const widgetRegistryMetas: WidgetRegistryMeta[] = [ + { key: 'daily-goals', name: 'Daily Goals', icon: Target, component: DailyGoals }, +]; diff --git a/src/workspaces/widgets/Pomodoro/index.tsx b/src/workspaces/widgets/Pomodoro/index.tsx index e7bcfefd..0966c9e8 100644 --- a/src/workspaces/widgets/Pomodoro/index.tsx +++ b/src/workspaces/widgets/Pomodoro/index.tsx @@ -1,8 +1,9 @@ +import type { WidgetRegistryMeta } from 'officerdev'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; -import { Pause, Play, RotateCcw, Settings, Check, Volume2, Square } from 'lucide-react'; +import { Pause, Play, RotateCcw, Settings, Check, Timer, Volume2, Square } from 'lucide-react'; import { allSounds, categories } from 'sounds'; import type { SoundAsset } from 'sounds'; import { Widget } from '../Widget'; @@ -412,3 +413,7 @@ export const Pomodoro = () => { ); }; + +export const widgetRegistryMetas: WidgetRegistryMeta[] = [ + { key: 'pomodoro', name: 'Pomodoro', icon: Timer, component: Pomodoro }, +]; diff --git a/src/workspaces/widgets/QuickNotes/index.tsx b/src/workspaces/widgets/QuickNotes/index.tsx index 8d5002b8..e47bcb72 100644 --- a/src/workspaces/widgets/QuickNotes/index.tsx +++ b/src/workspaces/widgets/QuickNotes/index.tsx @@ -1,7 +1,9 @@ +import type { WidgetRegistryMeta } from 'officerdev'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; +import { StickyNote } from 'lucide-react'; import { Widget } from '../Widget'; const USER_STATE_KEY = ['USER_STATE']; @@ -76,3 +78,7 @@ export const QuickNotes = () => { ); }; + +export const widgetRegistryMetas: WidgetRegistryMeta[] = [ + { key: 'quick-notes', name: 'Quick Notes', icon: StickyNote, component: QuickNotes }, +]; diff --git a/src/workspaces/widgets/Weather/index.tsx b/src/workspaces/widgets/Weather/index.tsx index bf3ad7f3..48b705d5 100644 --- a/src/workspaces/widgets/Weather/index.tsx +++ b/src/workspaces/widgets/Weather/index.tsx @@ -1,8 +1,9 @@ +import type { WidgetRegistryMeta } from 'officerdev'; import { useCallback, useRef, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; -import { MapPin, Settings } from 'lucide-react'; +import { CloudSun, MapPin, Settings } from 'lucide-react'; import { Widget } from '../Widget'; import { getWeatherInfo } from './weather-codes'; @@ -266,3 +267,7 @@ export const Weather = () => { ); }; + +export const widgetRegistryMetas: WidgetRegistryMeta[] = [ + { key: 'weather', name: 'Weather', icon: CloudSun, component: Weather }, +]; diff --git a/src/workspaces/widgets/package.json b/src/workspaces/widgets/package.json index 8920a5fc..7114b980 100644 --- a/src/workspaces/widgets/package.json +++ b/src/workspaces/widgets/package.json @@ -8,8 +8,6 @@ "./Pomodoro": "./Pomodoro/index.tsx", "./DailyGoals": "./DailyGoals/index.tsx", "./QuickNotes": "./QuickNotes/index.tsx", - "./widget-registry": "./widget-registry.tsx", - "./WidgetPanel": "./WidgetPanel/index.tsx", "./Workspaces": "./Workspaces/index.tsx" } } diff --git a/src/workspaces/widgets/widget-registry.tsx b/src/workspaces/widgets/widget-registry.tsx deleted file mode 100644 index 86c48d94..00000000 --- a/src/workspaces/widgets/widget-registry.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Clock as ClockIcon, CloudSun, Timer, Target, StickyNote } from 'lucide-react'; -import type { AppRegistryEntry } from 'officerdev'; -import { Clock } from './Clock/index'; -import { Weather } from './Weather/index'; -import { Pomodoro } from './Pomodoro/index'; -import { DailyGoals } from './DailyGoals/index'; -import { QuickNotes } from './QuickNotes/index'; - -export const widgetRegistry: Record = { - 'clock': { name: 'Clock', icon: ClockIcon, component: () => , availableOnPanel: false }, - 'weather': { name: 'Weather', icon: CloudSun, component: () => , availableOnPanel: false }, - 'pomodoro': { name: 'Pomodoro', icon: Timer, component: () => , availableOnPanel: false }, - 'daily-goals': { name: 'Daily Goals', icon: Target, component: () => , availableOnPanel: false }, - 'quick-notes': { name: 'Quick Notes', icon: StickyNote, component: () => , availableOnPanel: false }, -};