Skip to content

Commit b49bd12

Browse files
committed
fix(mobile): add sheet tabs at top and default zoom preference
- Move sheet tabs from bottom to top, connected to sheet headers - Style tabs with header background color and underline indicator - Add default sheet zoom preference in Settings (50%-200%) - Load saved zoom preference when viewing sheets - Add zoom reset on percentage tap in sheet viewer - Add 10 extra rows past content in sheets - Update loading messages with fun variations - Position zoom controls in bottom right corner
1 parent 5cf27f2 commit b49bd12

File tree

7 files changed

+315
-63
lines changed

7 files changed

+315
-63
lines changed

apps/mobile/v1/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@gorhom/bottom-sheet": "^5.2.6",
1818
"@react-native-async-storage/async-storage": "^2.2.0",
1919
"@react-native-community/netinfo": "11.4.1",
20+
"@react-native-community/slider": "^5.1.1",
2021
"@react-navigation/bottom-tabs": "^7.8.5",
2122
"@react-navigation/elements": "^2.6.3",
2223
"@react-navigation/native": "^7.1.8",

apps/mobile/v1/src/components/NativeSheetsViewer.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2+
import { getDefaultSheetZoomPreference } from '../lib/preferences';
23
import {
34
ActivityIndicator,
45
Animated,
@@ -14,6 +15,10 @@ import {
1415
export const GLASS_BG_DARK = 'rgba(255, 255, 255, 0.01)';
1516
export const GLASS_BG_LIGHT = 'rgba(0, 0, 0, 0.01)';
1617

18+
// Sheet header background colors - exported for parent to match tab bar
19+
export const SHEET_HEADER_BG_DARK = '#252629';
20+
export const SHEET_HEADER_BG_LIGHT = '#f5f5f5';
21+
1722
// Excel date serial number range (1900-2099)
1823
const MIN_DATE_SERIAL = 1;
1924
const MAX_DATE_SERIAL = 73050;
@@ -117,6 +122,7 @@ export interface SheetControlsData {
117122
tabBarHeight: number;
118123
onZoomIn: () => void;
119124
onZoomOut: () => void;
125+
onZoomReset: () => void;
120126
onSelectSheet: (index: number) => void;
121127
}
122128

@@ -134,7 +140,8 @@ interface NativeSheetsViewerProps {
134140
};
135141
onLoaded?: () => void;
136142
hideLoadingOverlay?: boolean;
137-
bottomInset?: number;
143+
/** Extra top inset for sheet tabs when present */
144+
topInset?: number;
138145
/** Callback to provide sheet controls data to parent for rendering outside ScrollView */
139146
onControlsReady?: (controls: SheetControlsData) => void;
140147
}
@@ -146,7 +153,7 @@ const BASE_ROW_HEADER_WIDTH = 36;
146153
const BASE_COLUMN_HEADER_HEIGHT = 20;
147154
const HEADER_FONT_SIZE = 10;
148155
const BUFFER = 30;
149-
const MIN_ZOOM = 0.5;
156+
const MIN_ZOOM = 0.50;
150157
const MAX_ZOOM = 3.0;
151158
const DEFAULT_ZOOM = 1.0;
152159

@@ -265,13 +272,29 @@ export function NativeSheetsViewer({
265272
theme,
266273
onLoaded,
267274
hideLoadingOverlay,
268-
bottomInset = 0,
275+
topInset = 0,
269276
onControlsReady,
270277
}: NativeSheetsViewerProps) {
271278
const [loading, setLoading] = useState(true);
272279
const [activeSheetIndex, setActiveSheetIndex] = useState(0);
273280
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
274281

282+
// Load saved zoom preference on mount
283+
useEffect(() => {
284+
const loadZoomPreference = async () => {
285+
try {
286+
const savedZoom = await getDefaultSheetZoomPreference();
287+
setZoom(savedZoom);
288+
} catch (error) {
289+
// Keep default zoom on error
290+
if (__DEV__) {
291+
console.error('Failed to load sheet zoom preference:', error);
292+
}
293+
}
294+
};
295+
loadZoomPreference();
296+
}, []);
297+
275298
// Track scroll for virtualization only (not for header sync)
276299
const scrollXRef = useRef(0);
277300
const scrollYRef = useRef(0);
@@ -340,8 +363,9 @@ export function NativeSheetsViewer({
340363
}
341364

342365
// Ensure minimum visible area for empty or small sheets
366+
// Add 10 extra rows beyond the data
343367
return {
344-
maxRow: Math.max(MIN_ROWS, maxRow + 3),
368+
maxRow: Math.max(MIN_ROWS, maxRow + 10),
345369
maxCol: Math.max(MIN_COLS, maxCol + 2)
346370
};
347371
}, [currentSheet?.cellData]);
@@ -449,6 +473,10 @@ export function NativeSheetsViewer({
449473
setZoom(z => Math.max(MIN_ZOOM, z - 0.25));
450474
}, []);
451475

476+
const handleZoomReset = useCallback(() => {
477+
setZoom(DEFAULT_ZOOM);
478+
}, []);
479+
452480
const handleSelectSheet = useCallback((index: number) => {
453481
setActiveSheetIndex(index);
454482
}, []);
@@ -464,7 +492,8 @@ export function NativeSheetsViewer({
464492
const borderColor = theme.isDark ? '#444' : '#ddd';
465493
const headerBg = theme.isDark ? '#252629' : '#f5f5f5';
466494
const cellBg = theme.colors.background;
467-
const tabBarHeight = hasMultipleSheets ? 48 + bottomInset : 0;
495+
// Tabs are now at top (rendered by parent), so no bottom space needed
496+
const tabBarHeight = 0;
468497

469498
// Notify parent about controls data so it can render glass controls outside ScrollView
470499
useEffect(() => {
@@ -479,10 +508,11 @@ export function NativeSheetsViewer({
479508
tabBarHeight,
480509
onZoomIn: handleZoomIn,
481510
onZoomOut: handleZoomOut,
511+
onZoomReset: handleZoomReset,
482512
onSelectSheet: handleSelectSheet,
483513
});
484514
}
485-
}, [currentSheet, onControlsReady, zoom, sheetInfo.names, activeSheetIndex, hasMultipleSheets, tabBarHeight, handleZoomIn, handleZoomOut, handleSelectSheet]);
515+
}, [currentSheet, onControlsReady, zoom, sheetInfo.names, activeSheetIndex, hasMultipleSheets, tabBarHeight, handleZoomIn, handleZoomOut, handleZoomReset, handleSelectSheet]);
486516

487517
if (!workbook || !currentSheet) {
488518
return (
@@ -779,6 +809,7 @@ export function NativeSheetsViewer({
779809
style={[
780810
styles.corner,
781811
{
812+
top: topInset,
782813
width: ROW_HEADER_WIDTH,
783814
height: COLUMN_HEADER_HEIGHT,
784815
backgroundColor: headerBg,
@@ -789,7 +820,7 @@ export function NativeSheetsViewer({
789820
/>
790821

791822
{/* Column headers - synced via Animated transform */}
792-
<View style={[styles.columnHeaderWrapper, { left: ROW_HEADER_WIDTH, height: COLUMN_HEADER_HEIGHT }]}>
823+
<View style={[styles.columnHeaderWrapper, { top: topInset, left: ROW_HEADER_WIDTH, height: COLUMN_HEADER_HEIGHT }]}>
793824
<Animated.View
794825
style={{
795826
flexDirection: 'row',
@@ -801,7 +832,7 @@ export function NativeSheetsViewer({
801832
</View>
802833

803834
{/* Row headers - synced via Animated transform */}
804-
<View style={[styles.rowHeaderWrapper, { top: COLUMN_HEADER_HEIGHT, bottom: tabBarHeight, width: ROW_HEADER_WIDTH }]}>
835+
<View style={[styles.rowHeaderWrapper, { top: topInset + COLUMN_HEADER_HEIGHT, bottom: tabBarHeight, width: ROW_HEADER_WIDTH }]}>
805836
<Animated.View
806837
style={{
807838
transform: [{ translateY: Animated.multiply(scrollYAnim, -1) }],
@@ -812,7 +843,7 @@ export function NativeSheetsViewer({
812843
</View>
813844

814845
{/* Main scrollable grid */}
815-
<View style={[styles.gridContainer, { top: COLUMN_HEADER_HEIGHT, left: ROW_HEADER_WIDTH, bottom: tabBarHeight }]}>
846+
<View style={[styles.gridContainer, { top: topInset + COLUMN_HEADER_HEIGHT, left: ROW_HEADER_WIDTH, bottom: tabBarHeight }]}>
816847
<Animated.ScrollView
817848
horizontal
818849
showsHorizontalScrollIndicator={false}
@@ -877,15 +908,13 @@ const styles = StyleSheet.create({
877908
},
878909
corner: {
879910
position: 'absolute',
880-
top: 0,
881911
left: 0,
882912
zIndex: 10,
883913
borderRightWidth: StyleSheet.hairlineWidth,
884914
borderBottomWidth: StyleSheet.hairlineWidth,
885915
},
886916
columnHeaderWrapper: {
887917
position: 'absolute',
888-
top: 0,
889918
right: 0,
890919
zIndex: 5,
891920
overflow: 'hidden',

apps/mobile/v1/src/lib/preferences.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
1212

1313
const PREFERENCE_KEYS = {
1414
CACHE_DECRYPTED_CONTENT: 'cache_decrypted_content',
15+
DEFAULT_SHEET_ZOOM: 'default_sheet_zoom',
1516
};
1617

1718
/**
@@ -78,3 +79,49 @@ export async function clearDecryptedCache(): Promise<void> {
7879
console.error('[Preferences] Failed to clear decrypted cache:', error);
7980
}
8081
}
82+
83+
/**
84+
* Get the default zoom level for sheets
85+
* @returns Zoom level as a decimal (e.g., 1.0 = 100%, 0.75 = 75%)
86+
*/
87+
export async function getDefaultSheetZoomPreference(): Promise<number> {
88+
try {
89+
const value = await AsyncStorage.getItem(PREFERENCE_KEYS.DEFAULT_SHEET_ZOOM);
90+
91+
// Default to 100% if not set
92+
if (value === null) {
93+
return 1.0;
94+
}
95+
96+
const zoom = parseFloat(value);
97+
// Validate zoom is within reasonable bounds (50% - 300%)
98+
if (isNaN(zoom) || zoom < 0.5 || zoom > 3.0) {
99+
return 1.0;
100+
}
101+
102+
return zoom;
103+
} catch (error) {
104+
console.error('[Preferences] Failed to get sheet zoom preference:', error);
105+
return 1.0; // Default to 100% on error
106+
}
107+
}
108+
109+
/**
110+
* Set the default zoom level for sheets
111+
* @param zoom Zoom level as a decimal (e.g., 1.0 = 100%, 0.75 = 75%)
112+
*/
113+
export async function setDefaultSheetZoomPreference(zoom: number): Promise<void> {
114+
try {
115+
// Clamp zoom to valid range
116+
const clampedZoom = Math.max(0.5, Math.min(3.0, zoom));
117+
118+
await AsyncStorage.setItem(
119+
PREFERENCE_KEYS.DEFAULT_SHEET_ZOOM,
120+
clampedZoom.toString()
121+
);
122+
123+
console.log(`[Preferences] Default sheet zoom: ${Math.round(clampedZoom * 100)}%`);
124+
} catch (error) {
125+
console.error('[Preferences] Failed to set sheet zoom preference:', error);
126+
}
127+
}

0 commit comments

Comments
 (0)