mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-25 20:12:53 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
159
apps-archived/reader/apps/mobile/components/ContextMenu.tsx
Normal file
159
apps-archived/reader/apps/mobile/components/ContextMenu.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useTheme } from '~/hooks/useTheme';
|
||||
|
||||
interface ContextMenuAction {
|
||||
title: string;
|
||||
systemIcon?: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
actions: ContextMenuAction[];
|
||||
onPress: (index: number) => void;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export function ContextMenu({ actions, onPress, children }: ContextMenuProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
const childRef = useRef<View>(null);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const handleLongPress = () => {
|
||||
childRef.current?.measure((x, y, width, height, pageX, pageY) => {
|
||||
const screenHeight = Dimensions.get('window').height;
|
||||
const menuHeight = actions.length * 50 + 20; // Approximate menu height
|
||||
|
||||
// Position menu above or below the pressed item based on available space
|
||||
const posY = pageY + height + menuHeight > screenHeight ? pageY - menuHeight : pageY + height;
|
||||
|
||||
setMenuPosition({ x: pageX, y: posY });
|
||||
setVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleActionPress = (index: number) => {
|
||||
setVisible(false);
|
||||
// Small delay to allow modal to close before action
|
||||
setTimeout(() => onPress(index), 100);
|
||||
};
|
||||
|
||||
const iconMap: Record<string, keyof typeof Ionicons.glyphMap> = {
|
||||
'doc.text': 'document-text-outline',
|
||||
'play.circle': 'play-circle-outline',
|
||||
'square.and.arrow.up': 'share-outline',
|
||||
tag: 'pricetag-outline',
|
||||
trash: 'trash-outline',
|
||||
};
|
||||
|
||||
const renderAction = ({ item, index }: { item: ContextMenuAction; index: number }) => {
|
||||
const iconName = item.icon || (item.systemIcon ? iconMap[item.systemIcon] : undefined);
|
||||
const isDisabled = item.disabled;
|
||||
const isDestructive = item.destructive;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => !isDisabled && handleActionPress(index)}
|
||||
disabled={isDisabled}
|
||||
className={`flex-row items-center px-4 py-3 ${
|
||||
index < actions.length - 1 ? `border-b ${colors.border}` : ''
|
||||
}`}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed && !isDisabled ? 'rgba(0, 0, 0, 0.05)' : 'transparent',
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
})}
|
||||
>
|
||||
{iconName && (
|
||||
<Ionicons
|
||||
name={iconName}
|
||||
size={20}
|
||||
color={
|
||||
isDestructive ? '#EF4444' : colors.text.includes('white') ? '#FFFFFF' : '#111827'
|
||||
}
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
)}
|
||||
<Text className={`text-base ${isDestructive ? 'text-red-500' : colors.text}`}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View ref={childRef} collapsable={false}>
|
||||
{React.cloneElement(children, {
|
||||
onLongPress: handleLongPress,
|
||||
delayLongPress: 500,
|
||||
} as any)}
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setVisible(false)}
|
||||
>
|
||||
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => setVisible(false)}>
|
||||
<View style={[styles.backdrop, { backgroundColor: 'rgba(0, 0, 0, 0.3)' }]} />
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.menu,
|
||||
{
|
||||
top: menuPosition.y,
|
||||
left: 20,
|
||||
right: 20,
|
||||
maxWidth: 300,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: colors.text.includes('white') ? '#1f2937' : '#ffffff',
|
||||
},
|
||||
]}
|
||||
className={`rounded-lg shadow-lg ${colors.surface}`}
|
||||
>
|
||||
<FlatList
|
||||
data={actions}
|
||||
renderItem={renderAction}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
menu: {
|
||||
position: 'absolute',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
// @ts-ignore - React Native Web supports boxShadow
|
||||
boxShadow: '0px 2px 10px rgba(0, 0, 0, 0.25)',
|
||||
},
|
||||
android: {
|
||||
elevation: 8,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue