Skip to content

Commit 73b17db

Browse files
staredclaude
andcommitted
Fix mobile layout with split view panels and restore CSV functionality
- Implement split view for mobile with CODE and CHART panels - Both panels always visible with one collapsed showing glimpse - Fix content clipping (not rescaling) in collapsed panels - Remove animation issues when switching panel focus - Restore full CSV upload functionality with file and URL options - Optimize mobile toolbar to be more compact - Update responsive breakpoints for better mobile experience - Version bump to 0.2.9 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fe3a101 commit 73b17db

File tree

10 files changed

+323
-111
lines changed

10 files changed

+323
-111
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ Always reference the official WebR documentation when working with WebR features
5656
- DO use type guards and runtime validation when interfacing with untyped external systems
5757
- AVOID patterns like `export type WebRInstance = any` - these mask real type safety issues
5858
- AVOID creating type aliases that just re-export types - import directly instead
59-
- ALL functions must have explicit return types - this is enforced by ESLint rules
59+
- ALL functions must have explicit return types - this is enforced by ESLint rules
60+
- While it is mostly a website for desktop, it should be also usable on mobile.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "webr-ggplot2-demo",
33
"private": true,
4-
"version": "0.2.8",
4+
"version": "0.2.9",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/App.vue

Lines changed: 225 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed, onMounted } from 'vue'
2+
import { ref, computed, onMounted, onUnmounted } from 'vue'
33
import CodeEditor from './components/CodeEditor.vue'
44
import FileUpload from './components/FileUpload.vue'
55
import ExampleSelector from './components/ExampleSelector.vue'
@@ -19,6 +19,21 @@ const code = ref(examples[0].code)
1919
const lastExecutedCode = ref('')
2020
const hasChanges = computed(() => code.value !== lastExecutedCode.value)
2121
22+
// Mobile state
23+
const activePanel = ref<'code' | 'chart'>('code')
24+
const windowWidth = ref(window.innerWidth)
25+
const isMobile = computed(() => windowWidth.value < 768)
26+
27+
// Update width on resize
28+
const updateWidth = (): void => {
29+
windowWidth.value = window.innerWidth
30+
}
31+
32+
// Lifecycle hooks
33+
onUnmounted(() => {
34+
window.removeEventListener('resize', updateWidth)
35+
})
36+
2237
// Current CSV data state
2338
const currentCsvData = ref<CsvData | null>(null)
2439
@@ -133,6 +148,9 @@ const handleExampleSelect = async (example: RExample): Promise<void> => {
133148
}
134149
135150
onMounted(async () => {
151+
// Add resize listener
152+
window.addEventListener('resize', updateWidth)
153+
136154
// Initialize WebR first
137155
await initializeWebR('')
138156
@@ -148,7 +166,30 @@ onMounted(async () => {
148166
<AppHeader />
149167

150168
<main class="main">
151-
<div class="toolbar">
169+
<!-- Mobile toolbar -->
170+
<div
171+
v-if="isMobile"
172+
class="mobile-toolbar"
173+
>
174+
<FileUpload
175+
:uploaded-file="currentCsvData"
176+
@file-uploaded="handleFileUpload"
177+
@file-removed="handleFileRemoved"
178+
/>
179+
<ExampleSelector @example-selected="handleExampleSelect" />
180+
<LibrarySelector
181+
:installed-libraries="installedLibraries"
182+
:is-loading="isInitializing"
183+
:package-versions="packageVersions"
184+
@toggle-library="toggleLibrary"
185+
/>
186+
</div>
187+
188+
<!-- Desktop toolbar -->
189+
<div
190+
v-else
191+
class="toolbar"
192+
>
152193
<div class="toolbar-left">
153194
<FileUpload
154195
:uploaded-file="currentCsvData"
@@ -173,7 +214,57 @@ onMounted(async () => {
173214
</div>
174215
</div>
175216

176-
<div class="container">
217+
<!-- Mobile split view with glimpse -->
218+
<div
219+
v-if="isMobile"
220+
class="mobile-container"
221+
>
222+
<div
223+
class="panel-wrapper code-panel"
224+
:class="{ active: activePanel === 'code', collapsed: activePanel === 'chart' }"
225+
@click="activePanel === 'chart' ? activePanel = 'code' : null"
226+
>
227+
<div
228+
v-show="activePanel === 'chart'"
229+
class="panel-label"
230+
>
231+
CODE
232+
</div>
233+
<div class="panel-content-wrapper">
234+
<CodeEditor v-model="code" />
235+
</div>
236+
</div>
237+
238+
<div
239+
class="panel-wrapper chart-panel"
240+
:class="{ active: activePanel === 'chart', collapsed: activePanel === 'code' }"
241+
@click="activePanel === 'code' ? activePanel = 'chart' : null"
242+
>
243+
<div
244+
v-show="activePanel === 'code'"
245+
class="panel-label"
246+
>
247+
CHART
248+
</div>
249+
<div class="panel-content-wrapper">
250+
<OutputDisplay
251+
:messages="messages"
252+
:is-loading="isLoading"
253+
:is-executing="isExecuting"
254+
/>
255+
<ConsoleOutput
256+
ref="consoleRef"
257+
:messages="messages"
258+
/>
259+
</div>
260+
</div>
261+
</div>
262+
263+
<!-- Desktop view -->
264+
<div
265+
v-else
266+
class="container"
267+
>
177268
<div class="editor-section">
178269
<CodeEditor v-model="code" />
179270
</div>
@@ -233,33 +324,117 @@ onMounted(async () => {
233324
min-height: 0;
234325
}
235326
327+
/* Mobile toolbar */
328+
.mobile-toolbar {
329+
background: white;
330+
border-bottom: 1px solid #e5e7eb;
331+
padding: 0.25rem;
332+
display: flex;
333+
align-items: center;
334+
gap: 0.25rem;
335+
flex-shrink: 0;
336+
height: 32px;
337+
}
338+
339+
340+
/* Mobile split view */
341+
.mobile-container {
342+
flex: 1;
343+
display: flex;
344+
overflow: hidden;
345+
position: relative;
346+
width: 100%;
347+
}
348+
349+
.panel-wrapper {
350+
position: relative;
351+
height: 100%;
352+
overflow: hidden;
353+
}
354+
355+
.panel-wrapper.active {
356+
flex: 1;
357+
}
358+
359+
.panel-wrapper.collapsed {
360+
width: 80px;
361+
flex-shrink: 0;
362+
cursor: pointer;
363+
position: relative;
364+
overflow: hidden;
365+
}
366+
367+
.panel-wrapper.collapsed:first-child {
368+
border-right: 1px solid #e5e7eb;
369+
}
370+
371+
.panel-wrapper.collapsed:last-child {
372+
border-left: 1px solid #e5e7eb;
373+
}
374+
375+
.panel-label {
376+
position: absolute;
377+
top: 50%;
378+
left: 50%;
379+
transform: translate(-50%, -50%) rotate(-90deg);
380+
font-size: 0.75rem;
381+
font-weight: 700;
382+
color: #374151;
383+
text-transform: uppercase;
384+
letter-spacing: 0.1em;
385+
white-space: nowrap;
386+
z-index: 10;
387+
background: rgba(255, 255, 255, 0.95);
388+
padding: 0.375rem 0.75rem;
389+
border-radius: 4px;
390+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
391+
}
392+
393+
.panel-content-wrapper {
394+
position: absolute;
395+
top: 0;
396+
left: 0;
397+
height: 100%;
398+
width: calc(100vw - 80px);
399+
overflow: hidden;
400+
}
401+
402+
.panel-wrapper.active .panel-content-wrapper {
403+
opacity: 1;
404+
overflow: auto;
405+
}
406+
407+
.panel-wrapper.collapsed .panel-content-wrapper {
408+
opacity: 0.3;
409+
pointer-events: none;
410+
}
411+
412+
/* Desktop toolbar */
236413
.toolbar {
237414
background: white;
238415
border-bottom: 1px solid #e5e7eb;
239416
padding: 0.75rem 1rem;
240417
display: flex;
241418
justify-content: space-between;
242419
align-items: center;
243-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
244420
flex-shrink: 0;
245421
gap: 1rem;
246-
flex-wrap: wrap;
247422
}
248423
249424
.toolbar-left {
250425
display: flex;
251426
gap: 0.75rem;
252427
align-items: center;
253-
flex-wrap: wrap;
254428
}
255429
256430
.toolbar-right {
257431
display: flex;
258432
gap: 0.75rem;
259433
align-items: center;
260-
flex-wrap: wrap;
261434
}
262435
436+
437+
263438
.container {
264439
flex: 1;
265440
display: grid;
@@ -291,12 +466,12 @@ onMounted(async () => {
291466
.bottom-bar {
292467
background: white;
293468
border-top: 1px solid #e5e7eb;
294-
padding: 0.75rem 1rem;
469+
padding: 0.5rem 1rem;
295470
display: flex;
296471
justify-content: space-between;
297472
align-items: center;
298-
box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.1);
299-
min-height: 52px;
473+
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.05);
474+
min-height: 48px;
300475
flex-shrink: 0;
301476
gap: 1rem;
302477
}
@@ -324,36 +499,44 @@ onMounted(async () => {
324499
325500
/* Mobile styles */
326501
@media (max-width: 768px) {
327-
.toolbar {
328-
padding: 0.5rem;
329-
gap: 0.5rem;
502+
.mobile-toolbar {
503+
display: flex;
504+
}
505+
506+
.mobile-container {
507+
display: flex;
508+
flex: 1;
509+
min-height: 0;
510+
height: 100%;
330511
}
331512
332-
.toolbar-left,
333-
.toolbar-right {
334-
width: 100%;
335-
justify-content: space-between;
336-
gap: 0.5rem;
513+
.toolbar {
514+
display: none;
337515
}
338516
339517
.container {
340-
grid-template-columns: 1fr;
341-
grid-template-rows: 1fr 1fr;
518+
display: none;
342519
}
343520
344-
.editor-section {
345-
border-right: none;
346-
border-bottom: 1px solid #e5e7eb;
347-
padding: 0.5rem;
521+
.bottom-bar {
522+
padding: 0.25rem 0.5rem;
523+
min-height: 36px;
524+
box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.05);
348525
}
349526
350-
.output-section {
351-
min-height: 40vh;
527+
.panel-wrapper {
528+
min-height: 0;
529+
height: 100%;
352530
}
353531
354-
.bottom-bar {
355-
padding: 0.5rem;
356-
min-height: 48px;
532+
.panel-wrapper.active {
533+
width: calc(100% - 80px);
534+
flex: none;
535+
}
536+
537+
.code-panel .panel-content-wrapper,
538+
.chart-panel .panel-content-wrapper {
539+
height: 100%;
357540
}
358541
}
359542
@@ -363,15 +546,21 @@ onMounted(async () => {
363546
height: 100vh;
364547
}
365548
366-
.container {
367-
display: flex;
368-
flex-direction: column;
549+
.panel-wrapper.collapsed {
550+
width: 50px; /* Even smaller on very narrow screens */
369551
}
370552
371-
.editor-section,
372-
.output-section {
373-
flex: 1;
374-
min-height: 0;
553+
.panel-wrapper.active {
554+
width: calc(100% - 50px);
555+
}
556+
557+
.panel-content-wrapper {
558+
width: calc(100vw - 50px); /* Adjust for smaller collapsed width */
559+
}
560+
561+
.panel-label {
562+
font-size: 0.625rem;
563+
padding: 0.25rem 0.375rem;
375564
}
376565
}
377566
</style>

0 commit comments

Comments
 (0)