diff --git a/packages/vite-plugin/src/dev-host/App.tsx b/packages/vite-plugin/src/dev-host/App.tsx index 5dc1a4a1..87c01500 100644 --- a/packages/vite-plugin/src/dev-host/App.tsx +++ b/packages/vite-plugin/src/dev-host/App.tsx @@ -14,6 +14,7 @@ import { MIN_DETAILS_WIDTH, MIN_DEVTOOLS_HEIGHT, MIN_IFRAME_HEIGHT, + MIN_NARROW_IFRAME_HEIGHT, SPLITTER_SIZE, } from './constants.js'; import type { @@ -41,6 +42,7 @@ import { import { MessageLogPane } from './components/MessageLogPane.js'; import { PanelTabs } from './components/PanelTabs.js'; import { ResizeHandle } from './components/ResizeHandle.js'; +import { ToggleGroup } from './components/ui/ToggleGroup.js'; import { RozeniteLogo } from './components/icons.js'; type CSSVariables = CSSProperties & Record<`--${string}`, string>; @@ -50,6 +52,8 @@ type AppProps = DevHostState & { presets: DevHostPresetEntry[]; }; +type MobileDevtoolsTab = 'log' | 'actions'; + const getViewportMatch = () => { return window.matchMedia('(max-width: 960px)').matches; }; @@ -68,6 +72,7 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: const [detailsWidth, setDetailsWidth] = useState(DETAILS_PANEL_WIDTH); const [activeResizeHandle, setActiveResizeHandle] = useState(null); const [isNarrowViewport, setIsNarrowViewport] = useState(getViewportMatch); + const [activeMobileTab, setActiveMobileTab] = useState('log'); const [iframeLoadNonce, setIframeLoadNonce] = useState(0); const workspaceRef = useRef(null); const logWorkspaceRef = useRef(null); @@ -115,6 +120,8 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: })(); const canDispatch = hasCommandType && hasValidCommandPayload; const panelDescription = packageDescription.trim(); + const isDetailsVisible = isDetailsOpen && selectedMessage !== null; + const iframeMinHeight = isNarrowViewport ? MIN_NARROW_IFRAME_HEIGHT : MIN_IFRAME_HEIGHT; useEffect(() => { document.title = `${packageName} Dev Host`; @@ -153,6 +160,26 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: }; }, []); + useEffect(() => { + const workspace = workspaceRef.current; + if (!workspace) { + return; + } + + const bounds = workspace.getBoundingClientRect(); + const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - iframeMinHeight - 12); + + setDevToolsHeight((current) => clamp(current, MIN_DEVTOOLS_HEIGHT, maxHeight)); + }, [iframeMinHeight]); + + useEffect(() => { + if (!isNarrowViewport) { + return; + } + + setActiveMobileTab('log'); + }, [isNarrowViewport]); + const selectPanel = (value: string) => { const nextPanel = panels.find((panel) => panel.source === value); if (!nextPanel) { @@ -170,8 +197,6 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: registerMessage(nextEntry); setMessages((current) => [nextEntry, ...current]); - setSelectedMessageId(nextEntry.id); - setIsDetailsOpen(true); }; useEffect(() => { @@ -271,6 +296,7 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: setMessages([]); setSelectedMessageId(null); setIsDetailsOpen(false); + setActiveMobileTab('log'); }; const resizeDevtoolsHeight = (event: PointerEvent) => { @@ -281,7 +307,7 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: const bounds = workspace.getBoundingClientRect(); const nextHeight = bounds.bottom - event.clientY; - const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - MIN_IFRAME_HEIGHT - 12); + const maxHeight = Math.max(MIN_DEVTOOLS_HEIGHT, bounds.height - iframeMinHeight - 12); setDevToolsHeight(clamp(nextHeight, MIN_DEVTOOLS_HEIGHT, maxHeight)); }; @@ -293,9 +319,7 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: } const bounds = devtools.getBoundingClientRect(); - const nextWidth = isNarrowViewport - ? bounds.bottom - event.clientY - : bounds.right - event.clientX; + const nextWidth = bounds.right - event.clientX; const maxWidth = Math.max(MIN_COMMAND_WIDTH, bounds.width - 280); setCommandWidth(clamp(nextWidth, MIN_COMMAND_WIDTH, maxWidth)); @@ -303,19 +327,39 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: const resizeDetailsPane = (event: PointerEvent) => { const logWorkspace = logWorkspaceRef.current; - if (!logWorkspace || !isDetailsOpen || !selectedMessage) { + if (!logWorkspace || !isDetailsVisible || isNarrowViewport) { return; } const bounds = logWorkspace.getBoundingClientRect(); - const nextWidth = isNarrowViewport - ? bounds.bottom - event.clientY - : bounds.right - event.clientX; + const nextWidth = bounds.right - event.clientX; const maxWidth = Math.max(MIN_DETAILS_WIDTH, bounds.width - 280 - SPLITTER_SIZE); setDetailsWidth(clamp(nextWidth, MIN_DETAILS_WIDTH, maxWidth)); }; + const handleMessageSelect = (messageId: string) => { + setSelectedMessageId(messageId); + setIsDetailsOpen(true); + + if (isNarrowViewport) { + setActiveMobileTab('log'); + } + }; + + const handleDetailsClose = () => { + setIsDetailsOpen(false); + + if (isNarrowViewport) { + setActiveMobileTab('log'); + } + }; + + const handleMobileTabChange = (value: string) => { + const nextTab = value as MobileDevtoolsTab; + setActiveMobileTab(nextTab); + }; + const handleDispatch = (event: FormEvent) => { event.preventDefault(); @@ -386,78 +430,137 @@ export const App = ({ packageName, packageDescription, panels, flows, presets }: onPointerDown={(event) => startResize('devtools-height', event, resizeDevtoolsHeight)} /> -
-
+
+
+ +
+ +
+ {activeMobileTab === 'log' ? ( + isDetailsVisible ? ( + { + const nextValues = getDispatcherValuesFromMessage(message); + setCommandType(nextValues.commandType); + setCommandPayload(nextValues.commandPayload); + setIsDetailsOpen(false); + setActiveMobileTab('actions'); + }} + onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)} + /> + ) : ( + + ) + ) : ( + + )} +
+
+
+ ) : ( +
- { - setSelectedMessageId(messageId); - setIsDetailsOpen(true); - }} - onClearMessages={clearMessages} +
+ + + { + const nextValues = getDispatcherValuesFromMessage(message); + setCommandType(nextValues.commandType); + setCommandPayload(nextValues.commandPayload); + }} + onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)} + /> +
+ + startResize('command-width', event, resizeCommandPane)} /> - setIsDetailsOpen(false)} - onUseMessage={(message) => { - const nextValues = getDispatcherValuesFromMessage(message); - setCommandType(nextValues.commandType); - setCommandPayload(nextValues.commandPayload); - }} - onResizeStart={(event) => startResize('details-width', event, resizeDetailsPane)} + - - - startResize('command-width', event, resizeCommandPane)} - /> - - -
+ + )} ); diff --git a/packages/vite-plugin/src/dev-host/components/MessageDetailsPane.tsx b/packages/vite-plugin/src/dev-host/components/MessageDetailsPane.tsx index 0bddb853..2a3c226a 100644 --- a/packages/vite-plugin/src/dev-host/components/MessageDetailsPane.tsx +++ b/packages/vite-plugin/src/dev-host/components/MessageDetailsPane.tsx @@ -30,14 +30,16 @@ export const MessageDetailsPane = ({ return ( <> - + {!isNarrowViewport ? ( + + ) : null}
diff --git a/packages/vite-plugin/src/dev-host/constants.ts b/packages/vite-plugin/src/dev-host/constants.ts index c4b56e49..eb000b32 100644 --- a/packages/vite-plugin/src/dev-host/constants.ts +++ b/packages/vite-plugin/src/dev-host/constants.ts @@ -4,6 +4,7 @@ export const DEV_HOST_CONFIG_GLOBAL_KEY = '__rozenite-dev-config__'; export const DEFAULT_DEVTOOLS_HEIGHT = 288; export const MIN_DEVTOOLS_HEIGHT = 180; export const MIN_IFRAME_HEIGHT = 220; +export const MIN_NARROW_IFRAME_HEIGHT = 140; export const DETAILS_PANEL_WIDTH = 360; export const MIN_DETAILS_WIDTH = 280; export const DEFAULT_COMMAND_WIDTH = 320; diff --git a/packages/vite-plugin/src/dev-host/styles.css b/packages/vite-plugin/src/dev-host/styles.css index 6805e16f..112718c0 100644 --- a/packages/vite-plugin/src/dev-host/styles.css +++ b/packages/vite-plugin/src/dev-host/styles.css @@ -103,9 +103,18 @@ select { grid-template-columns: minmax(0, 1fr) var(--rz-command-splitter-width, 6px) minmax(260px, var(--rz-command-width, 320px)); } +.rz-devtools-mobile { + display: flex; + min-height: 0; + height: 100%; + width: 100%; + overflow: hidden; +} + .rz-log-workspace { display: grid; min-height: 0; + position: relative; grid-template-columns: minmax(0, 1fr) var(--rz-details-splitter-width, 0px) var(--rz-details-width, 0px); } @@ -199,6 +208,34 @@ select { display: none; } +.rz-devtools-mobile-tabs { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 8px; + min-height: 0; + height: 100%; + width: 100%; + padding-top: 8px; +} + +.rz-devtools-mobile-toggle { + display: flex; + align-items: flex-start; + padding: 0 8px; +} + +.rz-devtools-mobile-panel { + display: flex; + min-height: 0; + width: 100%; + overflow: hidden; +} + +.rz-devtools-mobile-panel > .rz-pane { + flex: 1; + width: 100%; +} + .rz-tabs-root { width: 100%; min-width: 0; @@ -884,19 +921,8 @@ select { grid-template-rows: minmax(0, 1fr) 12px minmax(180px, var(--rz-devtools-height, 272px)); } - .rz-devtools { - grid-template-columns: minmax(0, 1fr); - grid-template-rows: minmax(0, 1fr) var(--rz-command-splitter-width, 6px) minmax(240px, var(--rz-command-width, 320px)); - } - - .rz-log-workspace { - grid-template-columns: minmax(0, 1fr); - grid-template-rows: minmax(0, 1fr) var(--rz-details-splitter-width, 0px) var(--rz-details-width, 0px); - } - - .rz-pane + .rz-pane { - border-left: 0; - border-top: 1px solid rgba(255, 255, 255, 0.08); + .rz-devtools-mobile-tabs { + padding-top: 8px; } .rz-column-resize-handle { @@ -923,4 +949,21 @@ select { border-top-color: rgba(255, 255, 255, 0.08); border-bottom-color: rgba(255, 255, 255, 0.08); } + + .rz-action-tabs-header { + flex-wrap: wrap; + align-items: flex-start; + } +} + +@media (max-width: 640px) { + .rz-message-list-header, + .rz-message-row { + grid-template-columns: 36px 136px minmax(92px, 136px) minmax(0, 1fr); + } + + .rz-message-header-cell, + .rz-message-cell { + padding: 8px 10px; + } }