Peace 10 hours ago
parent 6181b5ce90
commit f703dc66ca
  1. 36
      emoji-diary/.gitignore
  2. 1
      emoji-diary/.npmrc
  3. 55
      emoji-diary/App.tsx
  4. 2
      emoji-diary/app-env.d.ts
  5. 24
      emoji-diary/app.json
  6. BIN
      emoji-diary/assets/splash-icon.png
  7. BIN
      emoji-diary/assets/splash.png
  8. 30
      emoji-diary/babel.config.js
  9. 31
      emoji-diary/cesconfig.jsonc
  10. 26
      emoji-diary/components/AdSlot.tsx
  11. 56
      emoji-diary/components/EmojiPicker.tsx
  12. 52
      emoji-diary/components/EntryCard.tsx
  13. 71
      emoji-diary/components/Stacks.tsx
  14. 19
      emoji-diary/components/ui/box/index.tsx
  15. 19
      emoji-diary/components/ui/box/index.web.tsx
  16. 10
      emoji-diary/components/ui/box/styles.tsx
  17. 439
      emoji-diary/components/ui/button/index.tsx
  18. 26
      emoji-diary/components/ui/card/index.tsx
  19. 23
      emoji-diary/components/ui/card/index.web.tsx
  20. 20
      emoji-diary/components/ui/card/styles.tsx
  21. 22
      emoji-diary/components/ui/center/index.tsx
  22. 20
      emoji-diary/components/ui/center/index.web.tsx
  23. 8
      emoji-diary/components/ui/center/styles.tsx
  24. 309
      emoji-diary/components/ui/gluestack-ui-provider/config.ts
  25. 87
      emoji-diary/components/ui/gluestack-ui-provider/index.next15.tsx
  26. 38
      emoji-diary/components/ui/gluestack-ui-provider/index.tsx
  27. 96
      emoji-diary/components/ui/gluestack-ui-provider/index.web.tsx
  28. 19
      emoji-diary/components/ui/gluestack-ui-provider/script.ts
  29. 27
      emoji-diary/components/ui/hstack/index.tsx
  30. 26
      emoji-diary/components/ui/hstack/index.web.tsx
  31. 25
      emoji-diary/components/ui/hstack/styles.tsx
  32. 1588
      emoji-diary/components/ui/icon/index.tsx
  33. 1573
      emoji-diary/components/ui/icon/index.web.tsx
  34. 48
      emoji-diary/components/ui/image/index.tsx
  35. 217
      emoji-diary/components/ui/input/index.tsx
  36. 39
      emoji-diary/components/ui/pressable/index.tsx
  37. 40
      emoji-diary/components/ui/spinner/index.tsx
  38. 48
      emoji-diary/components/ui/text/index.tsx
  39. 45
      emoji-diary/components/ui/text/index.web.tsx
  40. 47
      emoji-diary/components/ui/text/styles.tsx
  41. 94
      emoji-diary/components/ui/textarea/index.tsx
  42. 240
      emoji-diary/components/ui/toast/index.tsx
  43. 28
      emoji-diary/components/ui/vstack/index.tsx
  44. 27
      emoji-diary/components/ui/vstack/index.web.tsx
  45. 25
      emoji-diary/components/ui/vstack/styles.tsx
  46. 4
      emoji-diary/eslint.config.js
  47. 3
      emoji-diary/global.css
  48. 8
      emoji-diary/index.ts
  49. 7
      emoji-diary/metro.config.js
  50. 1
      emoji-diary/nativewind-env.d.ts
  51. 8992
      emoji-diary/package-lock.json
  52. 41
      emoji-diary/package.json
  53. 105
      emoji-diary/pages/EditorScreen.tsx
  54. 58
      emoji-diary/pages/ListScreen.tsx
  55. 51
      emoji-diary/pages/TestScreen.tsx
  56. 3
      emoji-diary/prettier.config.js
  57. 87
      emoji-diary/screens/IndexPage.tsx
  58. 54
      emoji-diary/screens/ListScreen.tsx
  59. 37
      emoji-diary/src/animation/useBounce.ts
  60. 68
      emoji-diary/src/theme.ts
  61. 17
      emoji-diary/storages/fileio.ts
  62. 18
      emoji-diary/storages/imageio.ts
  63. 28
      emoji-diary/stores/CounterStore.tsx
  64. 330
      emoji-diary/stores/diaryStore.tsx
  65. 206
      emoji-diary/tailwind.config.js
  66. 14
      emoji-diary/tsconfig.json
  67. 3
      emoji-diary/types/entry.ts

@ -1,23 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
npm-debug.*
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# Metro
.metro-health-check*
ios
android
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# generated native folders
/ios
/android

@ -1 +0,0 @@
legacy-peer-deps=true

@ -1,13 +1,12 @@
import { StatusBar } from 'expo-status-bar';
import { PaperProvider, Text } from 'react-native-paper';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import './global.css';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import { appTheme, spacing } from './src/theme';
import { DiaryProvider } from './stores/diaryStore';
import { useState } from 'react';
import { DiaryProvider } from './states/diaryStore';
import { Box } from './components/ui/box';
import ListScreen from './screens/ListScreen';
import { View } from 'react-native';
import ListScreen from './pages/ListScreen';
import EditorScreen from './pages/EditorScreen';
type Route = { name: 'list' } | { name: 'editor'; id?: string };
@ -15,24 +14,28 @@ export default function App() {
const [route, setRoute] = useState<Route>({ name: 'list' });
return (
<GluestackUIProvider mode="light">
<>
<SafeAreaProvider>
<SafeAreaView className="flex-1">
<DiaryProvider>
{route.name === 'list' ? (
<ListScreen
onOpenCreate={() => setRoute({ name: 'editor' })}
onOpenEdit={(id) => setRoute({ name: 'editor', id })}
/>
) : (
<Box className="flex-1 bg-neutral-50" />
)}
</DiaryProvider>
<StatusBar style="auto" />
</SafeAreaView>
</SafeAreaProvider>
</>
</GluestackUIProvider>
<PaperProvider theme={appTheme}>
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1, padding: spacing.md }}>
<StatusBar style="auto" />
<DiaryProvider>
{route.name === 'list' ? (
<ListScreen
onOpenCreate={() => setRoute({ name: 'editor' })}
onOpenEdit={(id) => setRoute({ name: 'editor', id })}
/>
) : (
<EditorScreen
editingId={route.id}
onDone={() => {
console.log('ONDONE');
setRoute({ name: 'list' });
}}
/>
)}
</DiaryProvider>
</SafeAreaView>
</SafeAreaProvider>
</PaperProvider>
);
}

@ -1,2 +0,0 @@
// @ts-ignore
/// <reference types="nativewind/types" />

@ -3,28 +3,15 @@
"name": "emoji-diary",
"slug": "emoji-diary",
"version": "1.0.0",
"web": {
"favicon": "./assets/favicon.png"
},
"experiments": {
"tsconfigPaths": true
},
"plugins": [],
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash.png",
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
@ -32,7 +19,12 @@
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

@ -1,30 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
[
'babel-preset-expo',
{
jsxImportSource: 'nativewind',
},
],
'nativewind/babel',
],
plugins: [
[
'module-resolver',
{
root: ['./'],
alias: {
'@': './',
'tailwind.config': './tailwind.config.js',
},
},
],
'react-native-worklets/plugin',
],
};
};

@ -1,31 +0,0 @@
// This is an optional configuration file used primarily for debugging purposes when reporting issues.
// It is safe to delete this file as it does not affect the functionality of your application.
{
"cesVersion": "2.18.10",
"projectName": "my-expo-app",
"packages": [
{
"name": "nativewind",
"type": "styling"
}
],
"flags": {
"noGit": false,
"noInstall": false,
"overwrite": false,
"importAlias": true,
"packageManager": "npm",
"eas": false,
"publish": false
},
"packageManager": {
"type": "npm",
"version": "11.6.0"
},
"os": {
"type": "Windows_NT",
"platform": "win32",
"arch": "x64",
"kernelVersion": "10.0.26100"
}
}

@ -1,5 +1,25 @@
import { Box } from './ui/box';
import { View } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
export function AdSlot() {
return <Box className="h-14 w-full bg-neutral-200/60" />;
type Props = {
height?: number;
};
export function AdSlot({ height = 56 }: Props) {
const theme = useTheme();
return (
<View
style={{
height: height,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: theme.colors.primary,
backgroundColor: theme.colors.surface,
}}>
<Text style={{ color: theme.colors.secondary }}>AS</Text>
</View>
);
}

@ -1,21 +1,47 @@
import { Mood } from '@/types/entry';
import { Box } from './ui/box';
import { Pressable } from './ui/pressable';
import { Text } from './ui/text';
import { View } from 'react-native';
import { Chip, useTheme } from 'react-native-paper';
import { radius, spacing } from '~/theme';
const MOODS: Mood[] = ['🤩', '😀', '🙂', '😐', '🙁', '😢', '😡', '🤒'];
const MOODS: Mood[] = ['😀', '😐', '🙁', '😢', '😡'];
type Props = {
value: Mood;
onChange: (m: Mood) => void;
compact?: boolean;
};
export function EmojiPicker({ value, onChange, compact = false }: Props) {
const theme = useTheme();
export function EmojiPicker({ value, onChange }: { value: Mood; onChange: (m: Mood) => void }) {
return (
<Box className="flex-row flex-wrap gap-2">
{MOODS.map((m) => (
<Pressable
className={`rounded-xl px-3 py-2 ${value === m ? 'bg-black/10' : 'bg-transparent'}`}
key={m}
onPress={() => onChange(m)}>
<Text className="text-2xl">{m}</Text>
</Pressable>
))}
</Box>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs }}>
{MOODS.map((m) => {
const selected = value === m;
return (
<Chip
key={m}
selected={selected}
showSelectedCheck={false}
onPress={() => onChange(m)}
compact={compact}
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: selected ? theme.colors.secondaryContainer : undefined,
}}
textStyle={{
fontSize: 15,
textAlign: 'center',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
{m}
</Chip>
);
})}
</View>
);
}

@ -1,29 +1,37 @@
import { DiaryEntry } from '@/types/entry';
import { Pressable } from './ui/pressable';
import { Image } from './ui/image';
import { Box } from './ui/box';
import { Text } from './ui/text';
import { formatDateHuman } from '@/utils/date';
import { formatSmart } from '@/utils/date';
import { Image, View } from 'react-native';
import { Card, Text, useTheme } from 'react-native-paper';
type Props = {
item: DiaryEntry;
onPress: () => void;
};
export function EntryCard({ item, onPress }: Props) {
const theme = useTheme();
const hasImage = !!item.imageUri;
export function EntryCard({ item, onPress }: { item: DiaryEntry; onPress: () => void }) {
return (
<Pressable className="flex-row items-center gap-3 rounded-2xl bg-white p-3" onPress={onPress}>
{item.imageUri ? (
<Image className="h-14 w-14 rounded-xl" source={{ uri: item.imageUri }} />
<Card mode="outlined" onPress={onPress} style={{ overflow: 'hidden' }}>
<Card.Title
title={item.text}
titleNumberOfLines={1}
subtitle={`${item.mood} ${formatSmart(item.dateTimeISO)}`}
/>
{hasImage ? (
<View style={{ width: '100%', height: 160, overflow: 'hidden' }}>
<Image
source={{ uri: item.imageUri! }}
style={{ width: '100%', height: '100%' }}
resizeMode="cover"
/>
</View>
) : (
<Box className="h-14 w-14 items-center justify-center rounded-xl bg-neutral-200">
<Text>🖼</Text>
</Box>
<View style={{ height: 80, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: theme.colors.onSurfaceVariant }}> </Text>
</View>
)}
<Box className="flex-1">
<Text className="flex-1">
{item.mood}{' '}
<Text className="text-sm text-neutral-500">{formatDateHuman(item.dateTimeISO)}</Text>
<Text className="text-neutral-700" numberOfLines={1}>
{item.text}
</Text>
</Text>
</Box>
</Pressable>
</Card>
);
}

@ -0,0 +1,71 @@
import { View, ViewStyle, StyleProp } from 'react-native';
type Align = 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
type Justify =
| 'flex-start'
| 'center'
| 'flex-end'
| 'space-between'
| 'space-around'
| 'space-evenly';
type StackProps = {
gap?: number;
align?: Align;
justify?: Justify;
wrap?: boolean;
style?: StyleProp<ViewStyle>;
children?: React.ReactNode;
};
export function HStack({
gap = 0,
align = 'center',
justify = 'flex-start',
wrap = false,
style,
children,
}: StackProps) {
return (
<View
style={[
{
flexDirection: 'row',
alignItems: align,
justifyContent: justify,
gap,
flexWrap: wrap ? 'wrap' : 'nowrap',
},
style,
]}>
{children}
</View>
);
}
export function VStack({
gap = 0,
align = 'stretch',
justify = 'flex-start',
wrap = false,
style,
children,
}: StackProps) {
return (
<View
style={[
{
flexDirection: 'column',
alignItems: align,
justifyContent: justify,
gap,
flexWrap: wrap ? 'wrap' : 'nowrap',
},
style,
]}>
{children}
</View>
);
}
export const Spacer = ({ flex = 1 }: { flex?: number }) => <View style={{ flex }} />;

@ -1,19 +0,0 @@
import React from 'react';
import { View, ViewProps } from 'react-native';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { boxStyle } from './styles';
type IBoxProps = ViewProps &
VariantProps<typeof boxStyle> & { className?: string };
const Box = React.forwardRef<React.ComponentRef<typeof View>, IBoxProps>(
function Box({ className, ...props }, ref) {
return (
<View ref={ref} {...props} className={boxStyle({ class: className })} />
);
}
);
Box.displayName = 'Box';
export { Box };

@ -1,19 +0,0 @@
import React from 'react';
import { boxStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type IBoxProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof boxStyle> & { className?: string };
const Box = React.forwardRef<HTMLDivElement, IBoxProps>(function Box(
{ className, ...props },
ref
) {
return (
<div ref={ref} className={boxStyle({ class: className })} {...props} />
);
});
Box.displayName = 'Box';
export { Box };

@ -1,10 +0,0 @@
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb
? 'flex flex-col relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none'
: '';
export const boxStyle = tva({
base: baseStyle,
});

@ -1,439 +0,0 @@
'use client';
import React from 'react';
import { createButton } from '@gluestack-ui/core/button/creator';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
import { ActivityIndicator, Pressable, Text, View } from 'react-native';
import type { VariantProps } from 'tailwind-variants';
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
const SCOPE = 'BUTTON';
const Root = withStyleContext(Pressable, SCOPE);
const UIButton = createButton({
Root: Root,
Text,
Group: View,
Spinner: ActivityIndicator,
Icon: UIIcon,
});
cssInterop(PrimitiveIcon, {
className: {
target: 'style',
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: 'classNameColor',
stroke: true,
},
},
});
const buttonStyle = tva({
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
variants: {
action: {
primary:
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
secondary:
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
positive:
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
negative:
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
default:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
variant: {
link: 'px-0',
outline:
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
solid: '',
},
size: {
xs: 'px-3.5 h-8',
sm: 'px-4 h-9',
md: 'px-5 h-10',
lg: 'px-6 h-11',
xl: 'px-7 h-12',
},
},
compoundVariants: [
{
action: 'primary',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'secondary',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'positive',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'negative',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'primary',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'secondary',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'positive',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'negative',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
],
});
const buttonTextStyle = tva({
base: 'text-typography-0 font-semibold web:select-none',
parentVariants: {
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
},
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
size: {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'secondary',
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
},
{
variant: 'solid',
action: 'positive',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'negative',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'outline',
action: 'primary',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
{
variant: 'outline',
action: 'secondary',
class:
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
},
{
variant: 'outline',
action: 'positive',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
{
variant: 'outline',
action: 'negative',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
],
});
const buttonIconStyle = tva({
base: 'fill-none',
parentVariants: {
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
size: {
xs: 'h-3.5 w-3.5',
sm: 'h-4 w-4',
md: 'h-[18px] w-[18px]',
lg: 'h-[18px] w-[18px]',
xl: 'h-5 w-5',
},
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'secondary',
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
},
{
variant: 'solid',
action: 'positive',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'negative',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
],
});
const buttonGroupStyle = tva({
base: '',
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
},
isAttached: {
true: 'gap-0',
},
flexDirection: {
'row': 'flex-row',
'column': 'flex-col',
'row-reverse': 'flex-row-reverse',
'column-reverse': 'flex-col-reverse',
},
},
});
type IButtonProps = Omit<
React.ComponentPropsWithoutRef<typeof UIButton>,
'context'
> &
VariantProps<typeof buttonStyle> & { className?: string };
const Button = React.forwardRef<
React.ElementRef<typeof UIButton>,
IButtonProps
>(
(
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
ref
) => {
return (
<UIButton
ref={ref}
{...props}
className={buttonStyle({ variant, size, action, class: className })}
context={{ variant, size, action }}
/>
);
}
);
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text> &
VariantProps<typeof buttonTextStyle> & { className?: string };
const ButtonText = React.forwardRef<
React.ElementRef<typeof UIButton.Text>,
IButtonTextProps
>(({ className, variant, size, action, ...props }, ref) => {
const {
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE);
return (
<UIButton.Text
ref={ref}
{...props}
className={buttonTextStyle({
parentVariants: {
variant: parentVariant,
size: parentSize,
action: parentAction,
},
variant: variant as 'link' | 'outline' | 'solid' | undefined,
size,
action: action as
| 'primary'
| 'secondary'
| 'positive'
| 'negative'
| undefined,
class: className,
})}
/>
);
});
const ButtonSpinner = UIButton.Spinner;
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon> &
VariantProps<typeof buttonIconStyle> & {
className?: string | undefined;
as?: React.ElementType;
height?: number;
width?: number;
};
const ButtonIcon = React.forwardRef<
React.ElementRef<typeof UIButton.Icon>,
IButtonIcon
>(({ className, size, ...props }, ref) => {
const {
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE);
if (typeof size === 'number') {
return (
<UIButton.Icon
ref={ref}
{...props}
className={buttonIconStyle({ class: className })}
size={size}
/>
);
} else if (
(props.height !== undefined || props.width !== undefined) &&
size === undefined
) {
return (
<UIButton.Icon
ref={ref}
{...props}
className={buttonIconStyle({ class: className })}
/>
);
}
return (
<UIButton.Icon
{...props}
className={buttonIconStyle({
parentVariants: {
size: parentSize,
variant: parentVariant,
action: parentAction,
},
size,
class: className,
})}
ref={ref}
/>
);
});
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group> &
VariantProps<typeof buttonGroupStyle>;
const ButtonGroup = React.forwardRef<
React.ElementRef<typeof UIButton.Group>,
IButtonGroupProps
>(
(
{
className,
space = 'md',
isAttached = false,
flexDirection = 'column',
...props
},
ref
) => {
return (
<UIButton.Group
className={buttonGroupStyle({
class: className,
space,
isAttached: isAttached as boolean,
flexDirection: flexDirection as any,
})}
{...props}
ref={ref}
/>
);
}
);
Button.displayName = 'Button';
ButtonText.displayName = 'ButtonText';
ButtonSpinner.displayName = 'ButtonSpinner';
ButtonIcon.displayName = 'ButtonIcon';
ButtonGroup.displayName = 'ButtonGroup';
export { Button, ButtonText, ButtonSpinner, ButtonIcon, ButtonGroup };

@ -1,26 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { View, ViewProps } from 'react-native';
import { cardStyle } from './styles';
type ICardProps = ViewProps &
VariantProps<typeof cardStyle> & { className?: string };
const Card = React.forwardRef<React.ComponentRef<typeof View>, ICardProps>(
function Card(
{ className, size = 'md', variant = 'elevated', ...props },
ref
) {
return (
<View
className={cardStyle({ size, variant, class: className })}
{...props}
ref={ref}
/>
);
}
);
Card.displayName = 'Card';
export { Card };

@ -1,23 +0,0 @@
import React from 'react';
import { cardStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type ICardProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof cardStyle>;
const Card = React.forwardRef<HTMLDivElement, ICardProps>(function Card(
{ className, size = 'md', variant = 'elevated', ...props },
ref
) {
return (
<div
className={cardStyle({ size, variant, class: className })}
{...props}
ref={ref}
/>
);
});
Card.displayName = 'Card';
export { Card };

@ -1,20 +0,0 @@
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb ? 'flex flex-col relative z-0' : '';
export const cardStyle = tva({
base: baseStyle,
variants: {
size: {
sm: 'p-3 rounded',
md: 'p-4 rounded-md',
lg: 'p-6 rounded-xl',
},
variant: {
elevated: 'bg-background-0',
outline: 'border border-outline-200 ',
ghost: 'rounded-none',
filled: 'bg-background-50',
},
},
});

@ -1,22 +0,0 @@
import { View, ViewProps } from 'react-native';
import React from 'react';
import { centerStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type ICenterProps = ViewProps & VariantProps<typeof centerStyle>;
const Center = React.forwardRef<React.ComponentRef<typeof View>, ICenterProps>(
function Center({ className, ...props }, ref) {
return (
<View
className={centerStyle({ class: className })}
{...props}
ref={ref}
/>
);
}
);
Center.displayName = 'Center';
export { Center };

@ -1,20 +0,0 @@
import React from 'react';
import { centerStyle } from './styles';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type ICenterProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof centerStyle>;
const Center = React.forwardRef<HTMLDivElement, ICenterProps>(function Center(
{ className, ...props },
ref
) {
return (
<div className={centerStyle({ class: className })} {...props} ref={ref} />
);
});
Center.displayName = 'Center';
export { Center };

@ -1,8 +0,0 @@
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb ? 'flex flex-col relative z-0' : '';
export const centerStyle = tva({
base: `justify-center items-center ${baseStyle}`,
});

@ -1,309 +0,0 @@
'use client';
import { vars } from 'nativewind';
export const config = {
light: vars({
'--color-primary-0': '179 179 179',
'--color-primary-50': '153 153 153',
'--color-primary-100': '128 128 128',
'--color-primary-200': '115 115 115',
'--color-primary-300': '102 102 102',
'--color-primary-400': '82 82 82',
'--color-primary-500': '51 51 51',
'--color-primary-600': '41 41 41',
'--color-primary-700': '31 31 31',
'--color-primary-800': '13 13 13',
'--color-primary-900': '10 10 10',
'--color-primary-950': '8 8 8',
/* Secondary */
'--color-secondary-0': '253 253 253',
'--color-secondary-50': '251 251 251',
'--color-secondary-100': '246 246 246',
'--color-secondary-200': '242 242 242',
'--color-secondary-300': '237 237 237',
'--color-secondary-400': '230 230 231',
'--color-secondary-500': '217 217 219',
'--color-secondary-600': '198 199 199',
'--color-secondary-700': '189 189 189',
'--color-secondary-800': '177 177 177',
'--color-secondary-900': '165 164 164',
'--color-secondary-950': '157 157 157',
/* Tertiary */
'--color-tertiary-0': '255 250 245',
'--color-tertiary-50': '255 242 229',
'--color-tertiary-100': '255 233 213',
'--color-tertiary-200': '254 209 170',
'--color-tertiary-300': '253 180 116',
'--color-tertiary-400': '251 157 75',
'--color-tertiary-500': '231 129 40',
'--color-tertiary-600': '215 117 31',
'--color-tertiary-700': '180 98 26',
'--color-tertiary-800': '130 73 23',
'--color-tertiary-900': '108 61 19',
'--color-tertiary-950': '84 49 18',
/* Error */
'--color-error-0': '254 233 233',
'--color-error-50': '254 226 226',
'--color-error-100': '254 202 202',
'--color-error-200': '252 165 165',
'--color-error-300': '248 113 113',
'--color-error-400': '239 68 68',
'--color-error-500': '230 53 53',
'--color-error-600': '220 38 38',
'--color-error-700': '185 28 28',
'--color-error-800': '153 27 27',
'--color-error-900': '127 29 29',
'--color-error-950': '83 19 19',
/* Success */
'--color-success-0': '228 255 244',
'--color-success-50': '202 255 232',
'--color-success-100': '162 241 192',
'--color-success-200': '132 211 162',
'--color-success-300': '102 181 132',
'--color-success-400': '72 151 102',
'--color-success-500': '52 131 82',
'--color-success-600': '42 121 72',
'--color-success-700': '32 111 62',
'--color-success-800': '22 101 52',
'--color-success-900': '20 83 45',
'--color-success-950': '27 50 36',
/* Warning */
'--color-warning-0': '255 249 245',
'--color-warning-50': '255 244 236',
'--color-warning-100': '255 231 213',
'--color-warning-200': '254 205 170',
'--color-warning-300': '253 173 116',
'--color-warning-400': '251 149 75',
'--color-warning-500': '231 120 40',
'--color-warning-600': '215 108 31',
'--color-warning-700': '180 90 26',
'--color-warning-800': '130 68 23',
'--color-warning-900': '108 56 19',
'--color-warning-950': '84 45 18',
/* Info */
'--color-info-0': '236 248 254',
'--color-info-50': '199 235 252',
'--color-info-100': '162 221 250',
'--color-info-200': '124 207 248',
'--color-info-300': '87 194 246',
'--color-info-400': '50 180 244',
'--color-info-500': '13 166 242',
'--color-info-600': '11 141 205',
'--color-info-700': '9 115 168',
'--color-info-800': '7 90 131',
'--color-info-900': '5 64 93',
'--color-info-950': '3 38 56',
/* Typography */
'--color-typography-0': '254 254 255',
'--color-typography-50': '245 245 245',
'--color-typography-100': '229 229 229',
'--color-typography-200': '219 219 220',
'--color-typography-300': '212 212 212',
'--color-typography-400': '163 163 163',
'--color-typography-500': '140 140 140',
'--color-typography-600': '115 115 115',
'--color-typography-700': '82 82 82',
'--color-typography-800': '64 64 64',
'--color-typography-900': '38 38 39',
'--color-typography-950': '23 23 23',
/* Outline */
'--color-outline-0': '253 254 254',
'--color-outline-50': '243 243 243',
'--color-outline-100': '230 230 230',
'--color-outline-200': '221 220 219',
'--color-outline-300': '211 211 211',
'--color-outline-400': '165 163 163',
'--color-outline-500': '140 141 141',
'--color-outline-600': '115 116 116',
'--color-outline-700': '83 82 82',
'--color-outline-800': '65 65 65',
'--color-outline-900': '39 38 36',
'--color-outline-950': '26 23 23',
/* Background */
'--color-background-0': '255 255 255',
'--color-background-50': '246 246 246',
'--color-background-100': '242 241 241',
'--color-background-200': '220 219 219',
'--color-background-300': '213 212 212',
'--color-background-400': '162 163 163',
'--color-background-500': '142 142 142',
'--color-background-600': '116 116 116',
'--color-background-700': '83 82 82',
'--color-background-800': '65 64 64',
'--color-background-900': '39 38 37',
'--color-background-950': '18 18 18',
/* Background Special */
'--color-background-error': '254 241 241',
'--color-background-warning': '255 243 234',
'--color-background-success': '237 252 242',
'--color-background-muted': '247 248 247',
'--color-background-info': '235 248 254',
/* Focus Ring Indicator */
'--color-indicator-primary': '55 55 55',
'--color-indicator-info': '83 153 236',
'--color-indicator-error': '185 28 28',
}),
dark: vars({
'--color-primary-0': '166 166 166',
'--color-primary-50': '175 175 175',
'--color-primary-100': '186 186 186',
'--color-primary-200': '197 197 197',
'--color-primary-300': '212 212 212',
'--color-primary-400': '221 221 221',
'--color-primary-500': '230 230 230',
'--color-primary-600': '240 240 240',
'--color-primary-700': '250 250 250',
'--color-primary-800': '253 253 253',
'--color-primary-900': '254 249 249',
'--color-primary-950': '253 252 252',
/* Secondary */
'--color-secondary-0': '20 20 20',
'--color-secondary-50': '23 23 23',
'--color-secondary-100': '31 31 31',
'--color-secondary-200': '39 39 39',
'--color-secondary-300': '44 44 44',
'--color-secondary-400': '56 57 57',
'--color-secondary-500': '63 64 64',
'--color-secondary-600': '86 86 86',
'--color-secondary-700': '110 110 110',
'--color-secondary-800': '135 135 135',
'--color-secondary-900': '150 150 150',
'--color-secondary-950': '164 164 164',
/* Tertiary */
'--color-tertiary-0': '84 49 18',
'--color-tertiary-50': '108 61 19',
'--color-tertiary-100': '130 73 23',
'--color-tertiary-200': '180 98 26',
'--color-tertiary-300': '215 117 31',
'--color-tertiary-400': '231 129 40',
'--color-tertiary-500': '251 157 75',
'--color-tertiary-600': '253 180 116',
'--color-tertiary-700': '254 209 170',
'--color-tertiary-800': '255 233 213',
'--color-tertiary-900': '255 242 229',
'--color-tertiary-950': '255 250 245',
/* Error */
'--color-error-0': '83 19 19',
'--color-error-50': '127 29 29',
'--color-error-100': '153 27 27',
'--color-error-200': '185 28 28',
'--color-error-300': '220 38 38',
'--color-error-400': '230 53 53',
'--color-error-500': '239 68 68',
'--color-error-600': '249 97 96',
'--color-error-700': '229 91 90',
'--color-error-800': '254 202 202',
'--color-error-900': '254 226 226',
'--color-error-950': '254 233 233',
/* Success */
'--color-success-0': '27 50 36',
'--color-success-50': '20 83 45',
'--color-success-100': '22 101 52',
'--color-success-200': '32 111 62',
'--color-success-300': '42 121 72',
'--color-success-400': '52 131 82',
'--color-success-500': '72 151 102',
'--color-success-600': '102 181 132',
'--color-success-700': '132 211 162',
'--color-success-800': '162 241 192',
'--color-success-900': '202 255 232',
'--color-success-950': '228 255 244',
/* Warning */
'--color-warning-0': '84 45 18',
'--color-warning-50': '108 56 19',
'--color-warning-100': '130 68 23',
'--color-warning-200': '180 90 26',
'--color-warning-300': '215 108 31',
'--color-warning-400': '231 120 40',
'--color-warning-500': '251 149 75',
'--color-warning-600': '253 173 116',
'--color-warning-700': '254 205 170',
'--color-warning-800': '255 231 213',
'--color-warning-900': '255 244 237',
'--color-warning-950': '255 249 245',
/* Info */
'--color-info-0': '3 38 56',
'--color-info-50': '5 64 93',
'--color-info-100': '7 90 131',
'--color-info-200': '9 115 168',
'--color-info-300': '11 141 205',
'--color-info-400': '13 166 242',
'--color-info-500': '50 180 244',
'--color-info-600': '87 194 246',
'--color-info-700': '124 207 248',
'--color-info-800': '162 221 250',
'--color-info-900': '199 235 252',
'--color-info-950': '236 248 254',
/* Typography */
'--color-typography-0': '23 23 23',
'--color-typography-50': '38 38 39',
'--color-typography-100': '64 64 64',
'--color-typography-200': '82 82 82',
'--color-typography-300': '115 115 115',
'--color-typography-400': '140 140 140',
'--color-typography-500': '163 163 163',
'--color-typography-600': '212 212 212',
'--color-typography-700': '219 219 220',
'--color-typography-800': '229 229 229',
'--color-typography-900': '245 245 245',
'--color-typography-950': '254 254 255',
/* Outline */
'--color-outline-0': '26 23 23',
'--color-outline-50': '39 38 36',
'--color-outline-100': '65 65 65',
'--color-outline-200': '83 82 82',
'--color-outline-300': '115 116 116',
'--color-outline-400': '140 141 141',
'--color-outline-500': '165 163 163',
'--color-outline-600': '211 211 211',
'--color-outline-700': '221 220 219',
'--color-outline-800': '230 230 230',
'--color-outline-900': '243 243 243',
'--color-outline-950': '253 254 254',
/* Background */
'--color-background-0': '18 18 18',
'--color-background-50': '39 38 37',
'--color-background-100': '65 64 64',
'--color-background-200': '83 82 82',
'--color-background-300': '116 116 116',
'--color-background-400': '142 142 142',
'--color-background-500': '162 163 163',
'--color-background-600': '213 212 212',
'--color-background-700': '229 228 228',
'--color-background-800': '242 241 241',
'--color-background-900': '246 246 246',
'--color-background-950': '255 255 255',
/* Background Special */
'--color-background-error': '66 43 43',
'--color-background-warning': '65 47 35',
'--color-background-success': '28 43 33',
'--color-background-muted': '51 51 51',
'--color-background-info': '26 40 46',
/* Focus Ring Indicator */
'--color-indicator-primary': '247 247 247',
'--color-indicator-info': '161 199 245',
'--color-indicator-error': '232 70 69',
}),
};

@ -1,87 +0,0 @@
// This is a Next.js 15 compatible version of the GluestackUIProvider
'use client';
import React, { useEffect, useLayoutEffect } from 'react';
import { config } from './config';
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
import { script } from './script';
const variableStyleTagId = 'nativewind-style';
const createStyle = (styleTagId: string) => {
const style = document.createElement('style');
style.id = styleTagId;
style.appendChild(document.createTextNode(''));
return style;
};
export const useSafeLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: 'light' | 'dark' | 'system';
children?: React.ReactNode;
}) {
let cssVariablesWithMode = ``;
Object.keys(config).forEach((configKey) => {
cssVariablesWithMode +=
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
const cssVariables = Object.keys(
config[configKey as keyof typeof config]
).reduce((acc: string, curr: string) => {
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
return acc;
}, '');
cssVariablesWithMode += `${cssVariables} \n}`;
});
setFlushStyles(cssVariablesWithMode);
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
script(e.matches ? 'dark' : 'light');
}, []);
useSafeLayoutEffect(() => {
if (mode !== 'system') {
const documentElement = document.documentElement;
if (documentElement) {
documentElement.classList.add(mode);
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
documentElement.style.colorScheme = mode;
}
}
}, [mode]);
useSafeLayoutEffect(() => {
if (mode !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
media.addListener(handleMediaQuery);
return () => media.removeListener(handleMediaQuery);
}, [handleMediaQuery]);
useSafeLayoutEffect(() => {
if (typeof window !== 'undefined') {
const documentElement = document.documentElement;
if (documentElement) {
const head = documentElement.querySelector('head');
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
if (!style) {
style = createStyle(variableStyleTagId);
style.innerHTML = cssVariablesWithMode;
if (head) head.appendChild(style);
}
}
}
}, []);
return (
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
);
}

@ -1,38 +0,0 @@
import React, { useEffect } from 'react';
import { config } from './config';
import { View, ViewProps } from 'react-native';
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
import { useColorScheme } from 'nativewind';
export type ModeType = 'light' | 'dark' | 'system';
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: ModeType;
children?: React.ReactNode;
style?: ViewProps['style'];
}) {
const { colorScheme, setColorScheme } = useColorScheme();
useEffect(() => {
setColorScheme(mode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode]);
return (
<View
style={[
config[colorScheme!],
{ flex: 1, height: '100%', width: '100%' },
props.style,
]}
>
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
</View>
);
}

@ -1,96 +0,0 @@
'use client';
import React, { useEffect, useLayoutEffect } from 'react';
import { config } from './config';
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
import { script } from './script';
export type ModeType = 'light' | 'dark' | 'system';
const variableStyleTagId = 'nativewind-style';
const createStyle = (styleTagId: string) => {
const style = document.createElement('style');
style.id = styleTagId;
style.appendChild(document.createTextNode(''));
return style;
};
export const useSafeLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: ModeType;
children?: React.ReactNode;
}) {
let cssVariablesWithMode = ``;
Object.keys(config).forEach((configKey) => {
cssVariablesWithMode +=
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
const cssVariables = Object.keys(
config[configKey as keyof typeof config]
).reduce((acc: string, curr: string) => {
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
return acc;
}, '');
cssVariablesWithMode += `${cssVariables} \n}`;
});
setFlushStyles(cssVariablesWithMode);
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
script(e.matches ? 'dark' : 'light');
}, []);
useSafeLayoutEffect(() => {
if (mode !== 'system') {
const documentElement = document.documentElement;
if (documentElement) {
documentElement.classList.add(mode);
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
documentElement.style.colorScheme = mode;
}
}
}, [mode]);
useSafeLayoutEffect(() => {
if (mode !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
media.addListener(handleMediaQuery);
return () => media.removeListener(handleMediaQuery);
}, [handleMediaQuery]);
useSafeLayoutEffect(() => {
if (typeof window !== 'undefined') {
const documentElement = document.documentElement;
if (documentElement) {
const head = documentElement.querySelector('head');
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
if (!style) {
style = createStyle(variableStyleTagId);
style.innerHTML = cssVariablesWithMode;
if (head) head.appendChild(style);
}
}
}
}, []);
return (
<>
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `(${script.toString()})('${mode}')`,
}}
/>
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
</>
);
}

@ -1,19 +0,0 @@
export const script = (mode: string) => {
const documentElement = document.documentElement;
function getSystemColorMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
try {
const isSystem = mode === 'system';
const theme = isSystem ? getSystemColorMode() : mode;
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
documentElement.classList.add(theme);
documentElement.style.colorScheme = theme;
} catch (e) {
console.error(e);
}
};

@ -1,27 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { View } from 'react-native';
import type { ViewProps } from 'react-native';
import { hstackStyle } from './styles';
type IHStackProps = ViewProps & VariantProps<typeof hstackStyle>;
const HStack = React.forwardRef<React.ComponentRef<typeof View>, IHStackProps>(
function HStack({ className, space, reversed, ...props }, ref) {
return (
<View
className={hstackStyle({
space,
reversed: reversed as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
HStack.displayName = 'HStack';
export { HStack };

@ -1,26 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { hstackStyle } from './styles';
type IHStackProps = React.ComponentPropsWithoutRef<'div'> &
VariantProps<typeof hstackStyle>;
const HStack = React.forwardRef<React.ComponentRef<'div'>, IHStackProps>(
function HStack({ className, space, reversed, ...props }, ref) {
return (
<div
className={hstackStyle({
space,
reversed: reversed as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
HStack.displayName = 'HStack';
export { HStack };

@ -1,25 +0,0 @@
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb
? 'flex relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none'
: '';
export const hstackStyle = tva({
base: `flex-row ${baseStyle}`,
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
},
reversed: {
true: 'flex-row-reverse',
},
},
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,48 +0,0 @@
import React from 'react';
import { createImage } from '@gluestack-ui/core/image/creator';
import { Platform, Image as RNImage } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
const imageStyle = tva({
base: 'max-w-full',
variants: {
size: {
'2xs': 'h-6 w-6',
'xs': 'h-10 w-10',
'sm': 'h-16 w-16',
'md': 'h-20 w-20',
'lg': 'h-24 w-24',
'xl': 'h-32 w-32',
'2xl': 'h-64 w-64',
'full': 'h-full w-full',
'none': '',
},
},
});
const UIImage = createImage({ Root: RNImage });
type ImageProps = VariantProps<typeof imageStyle> &
React.ComponentProps<typeof UIImage>;
const Image = React.forwardRef<
React.ComponentRef<typeof UIImage>,
ImageProps & { className?: string }
>(function Image({ size = 'md', className, ...props }, ref) {
return (
<UIImage
className={imageStyle({ size, class: className })}
{...props}
ref={ref}
// @ts-expect-error : web only
style={
Platform.OS === 'web'
? { height: 'revert-layer', width: 'revert-layer' }
: undefined
}
/>
);
});
Image.displayName = 'Image';
export { Image };

@ -1,217 +0,0 @@
'use client';
import React from 'react';
import { createInput } from '@gluestack-ui/core/input/creator';
import { View, Pressable, TextInput } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
const SCOPE = 'INPUT';
const UIInput = createInput({
Root: withStyleContext(View, SCOPE),
Icon: UIIcon,
Slot: Pressable,
Input: TextInput,
});
cssInterop(PrimitiveIcon, {
className: {
target: 'style',
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: 'classNameColor',
stroke: true,
},
},
});
const inputStyle = tva({
base: 'border-background-300 flex-row overflow-hidden content-center data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:hover:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:hover:border-background-300 items-center',
variants: {
size: {
xl: 'h-12',
lg: 'h-11',
md: 'h-10',
sm: 'h-9',
},
variant: {
underlined:
'rounded-none border-b data-[invalid=true]:border-b-2 data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700',
outline:
'rounded border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
rounded:
'rounded-full border data-[invalid=true]:border-error-700 data-[invalid=true]:hover:border-error-700 data-[invalid=true]:data-[focus=true]:border-error-700 data-[invalid=true]:data-[focus=true]:hover:border-error-700 data-[invalid=true]:data-[disabled=true]:hover:border-error-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[focus=true]:hover:web:ring-1 data-[invalid=true]:data-[focus=true]:hover:web:ring-inset data-[invalid=true]:data-[focus=true]:hover:web:ring-indicator-error data-[invalid=true]:data-[disabled=true]:hover:web:ring-1 data-[invalid=true]:data-[disabled=true]:hover:web:ring-inset data-[invalid=true]:data-[disabled=true]:hover:web:ring-indicator-error',
},
},
});
const inputIconStyle = tva({
base: 'justify-center items-center text-typography-400 fill-none',
parentVariants: {
size: {
'2xs': 'h-3 w-3',
'xs': 'h-3.5 w-3.5',
'sm': 'h-4 w-4',
'md': 'h-[18px] w-[18px]',
'lg': 'h-5 w-5',
'xl': 'h-6 w-6',
},
},
});
const inputSlotStyle = tva({
base: 'justify-center items-center web:disabled:cursor-not-allowed',
});
const inputFieldStyle = tva({
base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
parentVariants: {
variant: {
underlined: 'web:outline-0 web:outline-none px-0',
outline: 'web:outline-0 web:outline-none',
rounded: 'web:outline-0 web:outline-none px-4',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
},
});
type IInputProps = React.ComponentProps<typeof UIInput> &
VariantProps<typeof inputStyle> & { className?: string };
const Input = React.forwardRef<React.ComponentRef<typeof UIInput>, IInputProps>(
function Input(
{ className, variant = 'outline', size = 'md', ...props },
ref
) {
return (
<UIInput
ref={ref}
{...props}
className={inputStyle({ variant, size, class: className })}
context={{ variant, size }}
/>
);
}
);
type IInputIconProps = React.ComponentProps<typeof UIInput.Icon> &
VariantProps<typeof inputIconStyle> & {
className?: string;
height?: number;
width?: number;
};
const InputIcon = React.forwardRef<
React.ComponentRef<typeof UIInput.Icon>,
IInputIconProps
>(function InputIcon({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
if (typeof size === 'number') {
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({ class: className })}
size={size}
/>
);
} else if (
(props.height !== undefined || props.width !== undefined) &&
size === undefined
) {
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({ class: className })}
/>
);
}
return (
<UIInput.Icon
ref={ref}
{...props}
className={inputIconStyle({
parentVariants: {
size: parentSize,
},
class: className,
})}
/>
);
});
type IInputSlotProps = React.ComponentProps<typeof UIInput.Slot> &
VariantProps<typeof inputSlotStyle> & { className?: string };
const InputSlot = React.forwardRef<
React.ComponentRef<typeof UIInput.Slot>,
IInputSlotProps
>(function InputSlot({ className, ...props }, ref) {
return (
<UIInput.Slot
ref={ref}
{...props}
className={inputSlotStyle({
class: className,
})}
/>
);
});
type IInputFieldProps = React.ComponentProps<typeof UIInput.Input> &
VariantProps<typeof inputFieldStyle> & { className?: string };
const InputField = React.forwardRef<
React.ComponentRef<typeof UIInput.Input>,
IInputFieldProps
>(function InputField({ className, ...props }, ref) {
const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE);
return (
<UIInput.Input
ref={ref}
{...props}
className={inputFieldStyle({
parentVariants: {
variant: parentVariant,
size: parentSize,
},
class: className,
})}
/>
);
});
Input.displayName = 'Input';
InputIcon.displayName = 'InputIcon';
InputSlot.displayName = 'InputSlot';
InputField.displayName = 'InputField';
export { Input, InputField, InputIcon, InputSlot };

@ -1,39 +0,0 @@
'use client';
import React from 'react';
import { createPressable } from '@gluestack-ui/core/pressable/creator';
import { Pressable as RNPressable } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
const UIPressable = createPressable({
Root: withStyleContext(RNPressable),
});
const pressableStyle = tva({
base: 'data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-indicator-info data-[focus-visible=true]:ring-2 data-[disabled=true]:opacity-40',
});
type IPressableProps = Omit<
React.ComponentProps<typeof UIPressable>,
'context'
> &
VariantProps<typeof pressableStyle>;
const Pressable = React.forwardRef<
React.ComponentRef<typeof UIPressable>,
IPressableProps
>(function Pressable({ className, ...props }, ref) {
return (
<UIPressable
{...props}
ref={ref}
className={pressableStyle({
class: className,
})}
/>
);
});
Pressable.displayName = 'Pressable';
export { Pressable };

@ -1,40 +0,0 @@
'use client';
import { ActivityIndicator } from 'react-native';
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
cssInterop(ActivityIndicator, {
className: { target: 'style', nativeStyleToProp: { color: true } },
});
const spinnerStyle = tva({});
const Spinner = React.forwardRef<
React.ComponentRef<typeof ActivityIndicator>,
React.ComponentProps<typeof ActivityIndicator>
>(function Spinner(
{
className,
color,
focusable = false,
'aria-label': ariaLabel = 'loading',
...props
},
ref
) {
return (
<ActivityIndicator
ref={ref}
focusable={focusable}
aria-label={ariaLabel}
{...props}
color={color}
className={spinnerStyle({ class: className })}
/>
);
});
Spinner.displayName = 'Spinner';
export { Spinner };

@ -1,48 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { Text as RNText } from 'react-native';
import { textStyle } from './styles';
type ITextProps = React.ComponentProps<typeof RNText> &
VariantProps<typeof textStyle>;
const Text = React.forwardRef<React.ComponentRef<typeof RNText>, ITextProps>(
function Text(
{
className,
isTruncated,
bold,
underline,
strikeThrough,
size = 'md',
sub,
italic,
highlight,
...props
},
ref
) {
return (
<RNText
className={textStyle({
isTruncated: isTruncated as boolean,
bold: bold as boolean,
underline: underline as boolean,
strikeThrough: strikeThrough as boolean,
size,
sub: sub as boolean,
italic: italic as boolean,
highlight: highlight as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
Text.displayName = 'Text';
export { Text };

@ -1,45 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { textStyle } from './styles';
type ITextProps = React.ComponentProps<'span'> & VariantProps<typeof textStyle>;
const Text = React.forwardRef<React.ComponentRef<'span'>, ITextProps>(
function Text(
{
className,
isTruncated,
bold,
underline,
strikeThrough,
size = 'md',
sub,
italic,
highlight,
...props
}: { className?: string } & ITextProps,
ref
) {
return (
<span
className={textStyle({
isTruncated: isTruncated as boolean,
bold: bold as boolean,
underline: underline as boolean,
strikeThrough: strikeThrough as boolean,
size,
sub: sub as boolean,
italic: italic as boolean,
highlight: highlight as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
Text.displayName = 'Text';
export { Text };

@ -1,47 +0,0 @@
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb
? 'font-sans tracking-sm my-0 bg-transparent border-0 box-border display-inline list-none margin-0 padding-0 position-relative text-start no-underline whitespace-pre-wrap word-wrap-break-word'
: '';
export const textStyle = tva({
base: `text-typography-700 font-body ${baseStyle}`,
variants: {
isTruncated: {
true: 'web:truncate',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
sub: {
true: 'text-xs',
},
italic: {
true: 'italic',
},
highlight: {
true: 'bg-yellow-500',
},
},
});

@ -1,94 +0,0 @@
'use client';
import React from 'react';
import { createTextarea } from '@gluestack-ui/core/textarea/creator';
import { View, TextInput } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
const SCOPE = 'TEXTAREA';
const UITextarea = createTextarea({
Root: withStyleContext(View, SCOPE),
Input: TextInput,
});
const textareaStyle = tva({
base: 'w-full h-[100px] border border-background-300 rounded data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[focus=true]:data-[hover=true]:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:bg-background-50 data-[disabled=true]:data-[hover=true]:border-background-300',
variants: {
variant: {
default:
'data-[focus=true]:border-primary-700 data-[focus=true]:web:ring-1 data-[focus=true]:web:ring-inset data-[focus=true]:web:ring-indicator-primary data-[invalid=true]:border-error-700 data-[invalid=true]:web:ring-1 data-[invalid=true]:web:ring-inset data-[invalid=true]:web:ring-indicator-error data-[invalid=true]:data-[hover=true]:border-error-700 data-[invalid=true]:data-[focus=true]:data-[hover=true]:border-primary-700 data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-1 data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-inset data-[invalid=true]:data-[focus=true]:data-[hover=true]:web:ring-indicator-primary data-[invalid=true]:data-[disabled=true]:data-[hover=true]:border-error-700 data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-1 data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-inset data-[invalid=true]:data-[disabled=true]:data-[hover=true]:web:ring-indicator-error ',
},
size: {
sm: '',
md: '',
lg: '',
xl: '',
},
},
});
const textareaInputStyle = tva({
base: 'p-2 web:outline-0 web:outline-none flex-1 color-typography-900 placeholder:text-typography-500 web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
parentVariants: {
size: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
},
},
});
type ITextareaProps = React.ComponentProps<typeof UITextarea> &
VariantProps<typeof textareaStyle>;
const Textarea = React.forwardRef<
React.ComponentRef<typeof UITextarea>,
ITextareaProps
>(function Textarea(
{ className, variant = 'default', size = 'md', ...props },
ref
) {
return (
<UITextarea
ref={ref}
{...props}
className={textareaStyle({ variant, class: className })}
context={{ size }}
/>
);
});
type ITextareaInputProps = React.ComponentProps<typeof UITextarea.Input> &
VariantProps<typeof textareaInputStyle>;
const TextareaInput = React.forwardRef<
React.ComponentRef<typeof UITextarea.Input>,
ITextareaInputProps
>(function TextareaInput({ className, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UITextarea.Input
ref={ref}
{...props}
textAlignVertical="top"
className={textareaInputStyle({
parentVariants: {
size: parentSize,
},
class: className,
})}
/>
);
});
Textarea.displayName = 'Textarea';
TextareaInput.displayName = 'TextareaInput';
export { Textarea, TextareaInput };

@ -1,240 +0,0 @@
'use client';
import React from 'react';
import { createToastHook } from '@gluestack-ui/core/toast/creator';
import { AccessibilityInfo, Text, View, ViewStyle } from 'react-native';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { cssInterop } from 'nativewind';
import {
Motion,
AnimatePresence,
MotionComponentProps,
} from '@legendapp/motion';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/utils/nativewind-utils';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
type IMotionViewProps = React.ComponentProps<typeof View> &
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
const useToast = createToastHook(MotionView, AnimatePresence);
const SCOPE = 'TOAST';
cssInterop(MotionView, { className: 'style' });
const toastStyle = tva({
base: 'p-4 m-1 rounded-md gap-1 web:pointer-events-auto shadow-hard-5 border-outline-100',
variants: {
action: {
error: 'bg-error-800',
warning: 'bg-warning-700',
success: 'bg-success-700',
info: 'bg-info-700',
muted: 'bg-background-800',
},
variant: {
solid: '',
outline: 'border bg-background-0',
},
},
});
const toastTitleStyle = tva({
base: 'text-typography-0 font-medium font-body tracking-md text-left',
variants: {
isTruncated: {
true: '',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
},
parentVariants: {
variant: {
solid: '',
outline: '',
},
action: {
error: '',
warning: '',
success: '',
info: '',
muted: '',
},
},
parentCompoundVariants: [
{
variant: 'outline',
action: 'error',
class: 'text-error-800',
},
{
variant: 'outline',
action: 'warning',
class: 'text-warning-800',
},
{
variant: 'outline',
action: 'success',
class: 'text-success-800',
},
{
variant: 'outline',
action: 'info',
class: 'text-info-800',
},
{
variant: 'outline',
action: 'muted',
class: 'text-background-800',
},
],
});
const toastDescriptionStyle = tva({
base: 'font-normal font-body tracking-md text-left',
variants: {
isTruncated: {
true: '',
},
bold: {
true: 'font-bold',
},
underline: {
true: 'underline',
},
strikeThrough: {
true: 'line-through',
},
size: {
'2xs': 'text-2xs',
'xs': 'text-xs',
'sm': 'text-sm',
'md': 'text-base',
'lg': 'text-lg',
'xl': 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
'6xl': 'text-6xl',
},
},
parentVariants: {
variant: {
solid: 'text-typography-50',
outline: 'text-typography-900',
},
},
});
const Root = withStyleContext(View, SCOPE);
type IToastProps = React.ComponentProps<typeof Root> & {
className?: string;
} & VariantProps<typeof toastStyle>;
const Toast = React.forwardRef<React.ComponentRef<typeof Root>, IToastProps>(
function Toast(
{ className, variant = 'solid', action = 'muted', ...props },
ref
) {
return (
<Root
ref={ref}
className={toastStyle({ variant, action, class: className })}
context={{ variant, action }}
{...props}
/>
);
}
);
type IToastTitleProps = React.ComponentProps<typeof Text> & {
className?: string;
} & VariantProps<typeof toastTitleStyle>;
const ToastTitle = React.forwardRef<
React.ComponentRef<typeof Text>,
IToastTitleProps
>(function ToastTitle({ className, size = 'md', children, ...props }, ref) {
const { variant: parentVariant, action: parentAction } =
useStyleContext(SCOPE);
React.useEffect(() => {
// Issue from react-native side
// Hack for now, will fix this later
AccessibilityInfo.announceForAccessibility(children as string);
}, [children]);
return (
<Text
{...props}
ref={ref}
aria-live="assertive"
aria-atomic="true"
role="alert"
className={toastTitleStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
action: parentAction,
},
})}
>
{children}
</Text>
);
});
type IToastDescriptionProps = React.ComponentProps<typeof Text> & {
className?: string;
} & VariantProps<typeof toastDescriptionStyle>;
const ToastDescription = React.forwardRef<
React.ComponentRef<typeof Text>,
IToastDescriptionProps
>(function ToastDescription({ className, size = 'md', ...props }, ref) {
const { variant: parentVariant } = useStyleContext(SCOPE);
return (
<Text
ref={ref}
{...props}
className={toastDescriptionStyle({
size,
class: className,
parentVariants: {
variant: parentVariant,
},
})}
/>
);
});
Toast.displayName = 'Toast';
ToastTitle.displayName = 'ToastTitle';
ToastDescription.displayName = 'ToastDescription';
export { useToast, Toast, ToastTitle, ToastDescription };

@ -1,28 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { View } from 'react-native';
import { vstackStyle } from './styles';
type IVStackProps = React.ComponentProps<typeof View> &
VariantProps<typeof vstackStyle>;
const VStack = React.forwardRef<React.ComponentRef<typeof View>, IVStackProps>(
function VStack({ className, space, reversed, ...props }, ref) {
return (
<View
className={vstackStyle({
space,
reversed: reversed as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
VStack.displayName = 'VStack';
export { VStack };

@ -1,27 +0,0 @@
import React from 'react';
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
import { vstackStyle } from './styles';
type IVStackProps = React.ComponentProps<'div'> &
VariantProps<typeof vstackStyle>;
const VStack = React.forwardRef<React.ComponentRef<'div'>, IVStackProps>(
function VStack({ className, space, reversed, ...props }, ref) {
return (
<div
className={vstackStyle({
space,
reversed: reversed as boolean,
class: className,
})}
{...props}
ref={ref}
/>
);
}
);
VStack.displayName = 'VStack';
export { VStack };

@ -1,25 +0,0 @@
import { isWeb } from '@gluestack-ui/utils/nativewind-utils';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
const baseStyle = isWeb
? 'flex flex-col relative z-0 box-border border-0 list-none min-w-0 min-h-0 bg-transparent items-stretch m-0 p-0 text-decoration-none'
: '';
export const vstackStyle = tva({
base: `flex-col ${baseStyle}`,
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
},
reversed: {
true: 'flex-col-reverse',
},
},
});

@ -1,9 +1,10 @@
/* eslint-env node */
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
const prettierConfig = require('eslint-config-prettier');
module.exports = defineConfig([
expoConfig,
...expoConfig,
{
ignores: ['dist/*'],
},
@ -12,4 +13,5 @@ module.exports = defineConfig([
'react/display-name': 'off',
},
},
prettierConfig,
]);

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

@ -1,7 +0,0 @@
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
config.resolver.resolverMainFields = ['react-native', 'browser', 'main'];
module.exports = withNativeWind(config, { input: './global.css' });

@ -1 +0,0 @@
/// <reference types="nativewind/types" />

File diff suppressed because it is too large Load Diff

@ -1,51 +1,32 @@
{
"name": "emoji-diary",
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"android": "expo start --android --offline",
"ios": "expo start --ios --offline",
"start": "expo start --offline",
"prebuild": "expo prebuild",
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
"format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/html-elements": "^0.10.1",
"@gluestack-ui/core": "^3.0.10",
"@gluestack-ui/utils": "^3.0.7",
"@legendapp/motion": "^2.4.0",
"babel-plugin-module-resolver": "^5.0.2",
"dayjs": "^1.11.18",
"expo": "^54.0.8",
"expo": "~54.0.9",
"expo-file-system": "~19.0.14",
"expo-image-manipulator": "~14.0.7",
"expo-image-picker": "~17.0.8",
"expo-status-bar": "~3.0.8",
"nativewind": "^4.1.23",
"react": "19.1.0",
"react-aria": "^3.33.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-svg": "15.12.1",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"react-stately": "^3.39.0",
"tailwind-variants": "^0.1.20"
"react-native-safe-area-context": "~5.6.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "^19.1.13",
"eslint": "^9.25.1",
"eslint-config-expo": "~10.0.0",
"eslint-config-prettier": "^10.1.2",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.17",
"@types/react": "~19.1.0",
"eslint": "^9.35.0",
"eslint-config-expo": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"typescript": "~5.9.2"
},
"main": "node_modules/expo/AppEntry.js",
"private": true
}

@ -0,0 +1,105 @@
import { AdSlot } from '@/components/AdSlot';
import { EmojiPicker } from '@/components/EmojiPicker';
import { HStack, VStack } from '@/components/Stacks';
import { captureOne, pickOne } from '@/storages/imageio';
import { useDiary } from '@/stores/diaryStore';
import { Mood } from '@/types/entry';
import { useState } from 'react';
import { View } from 'react-native';
import { Button, Text, TextInput, useTheme } from 'react-native-paper';
import { spacing, styles } from '~/theme';
export default function EditorScreen({
editingId,
onDone,
}: {
editingId?: string;
onDone: () => void;
}) {
const theme = useTheme();
const { state, create, update, remove } = useDiary();
const editing = editingId ? state.byId[editingId] : undefined;
const [mood, setMood] = useState<Mood>(editing?.mood ?? '😐');
const [text, setText] = useState(editing?.text ?? '');
const [imageSrcUri, setImageSrcUri] = useState<string | null | undefined>(editing?.imageUri);
const onSave = async () => {
if (!editing) {
await create({ mood, text: text.slice(0, 140), imageSrcUri: imageSrcUri ?? undefined });
} else {
await update(editing.id, { mood, text: text.slice(0, 140), imageSrcUri });
}
onDone();
};
const onDelete = async () => {
if (editing) {
await remove(editing.id);
onDone();
}
};
return (
<VStack style={{ flex: 1 }} justify="space-between" gap={spacing.md}>
{/* 메인영역 */}
<View style={{ flex: 1, gap: spacing.md }}>
<View style={{ gap: 2 }}>
<Text variant="titleLarge"> </Text>
<EmojiPicker value={mood} onChange={setMood} />
</View>
<View style={{ gap: 2 }}>
<TextInput
mode="outlined"
label="오늘의 기분을 남겨보세요."
value={text}
onChangeText={setText}
multiline
numberOfLines={6}
maxLength={140}
style={{ fontSize: 13, height: 120 }}
/>
<Text
variant="bodySmall"
style={{ alignSelf: 'flex-end', color: theme.colors.secondary }}>
{text.length} / 140
</Text>
</View>
<HStack gap={spacing.md}>
<Button
mode="contained-tonal"
onPress={async () => {
const u = await pickOne();
if (u) setImageSrcUri(u);
}}>
</Button>
<Button
mode="contained-tonal"
onPress={async () => {
const u = await captureOne();
if (u) setImageSrcUri(u);
}}>
</Button>
{imageSrcUri && (
<Button mode="text" onPress={() => setImageSrcUri(null)}>
</Button>
)}
</HStack>
<Button mode="contained" onPress={onSave}>
</Button>
{editing && (
<Button mode="outlined" onPress={onDelete}>
</Button>
)}
</View>
<AdSlot height={80} />
</VStack>
);
}

@ -0,0 +1,58 @@
import { AdSlot } from '@/components/AdSlot';
import { EntryCard } from '@/components/EntryCard';
import { VStack } from '@/components/Stacks';
import { useDiary } from '@/stores/diaryStore';
import { FlatList, View } from 'react-native';
import { FAB, Text } from 'react-native-paper';
import { spacing, styles } from '~/theme';
export default function ListScreen({
onOpenCreate,
onOpenEdit,
}: {
onOpenCreate: () => void;
onOpenEdit: (id: string) => void;
}) {
const { state } = useDiary();
return (
<VStack style={{ flex: 1 }} justify="space-between">
{/* 메인영역 */}
<View style={styles.flex}>
<FlatList
style={styles.flex}
contentContainerStyle={{ padding: 16, gap: 12 }}
data={state.ids}
keyExtractor={(id) => id}
renderItem={({ item: id }) => {
const e = state.byId[id];
if (!e) return null;
return <EntryCard item={e} onPress={() => onOpenEdit(id)} />;
}}
ListEmptyComponent={
<View
style={{
height: 200,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text> . </Text>
<Text> + </Text>
<Text> .</Text>
</View>
}
/>
<FAB
mode="flat"
icon="plus"
style={{ position: 'absolute', right: 24, bottom: 24 }}
onPress={onOpenCreate}
/>
</View>
<AdSlot height={80} />
</VStack>
);
}

@ -0,0 +1,51 @@
import { View } from 'react-native';
import { useCounterDispatch, useCounterState } from '../stores/CounterStore';
import { styles } from '../src/theme';
import { Button, Card, Text } from 'react-native-paper';
import { HStack, VStack } from 'components/Stacks';
import { useBounce } from '../src/animation/useBounce';
import Animated from 'react-native-reanimated';
export default function TestPage() {
const state = useCounterState();
const dispatch = useCounterDispatch();
const { style: bounceStyle, trigger: bounce } = useBounce({ amplitude: 16, duration: 200 });
return (
<View style={styles.flexCenter}>
<Card mode="outlined" style={{ width: '80%' }}>
<Card.Title title={`Count: ${state.count}`} />
<Card.Content>
<VStack gap={12}>
<HStack justify="space-between">
<Text variant="bodyMedium" style={{ color: '#0000FF' }}>
Text1
</Text>
<Text variant="bodyMedium" style={{ color: '#FF0000' }}>
Text2
</Text>
</HStack>
<Animated.View style={bounceStyle}>
<Button
mode="outlined"
onPress={() => {
bounce();
}}>
🍞Action!
</Button>
</Animated.View>
<HStack justify="space-between">
<Button mode="contained" onPress={() => dispatch({ type: 'INCREMENT' })}>
+
</Button>
<Button mode="contained" onPress={() => dispatch({ type: 'DECREMENT' })}>
-
</Button>
</HStack>
</VStack>
</Card.Content>
</Card>
</View>
);
}

@ -7,7 +7,4 @@ module.exports = {
semi: true,
arrowParens: 'always',
endOfLine: 'auto',
plugins: [require.resolve('prettier-plugin-tailwindcss')],
tailwindAttributes: ['className', 'tva'],
};

@ -1,87 +0,0 @@
import { Box } from '@/components/ui/box';
import { Button, ButtonText } from '@/components/ui/button';
import { Center } from '@/components/ui/center';
import { HStack } from '@/components/ui/hstack';
import { Text } from '@/components/ui/text';
import { Toast, ToastDescription, ToastTitle, useToast } from '@/components/ui/toast';
import { useCounterDispatch, useCounterState } from '@/states/CounterProvider';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSequence,
withTiming,
} from 'react-native-reanimated';
export default function IndexPage() {
const state = useCounterState();
const dispatch = useCounterDispatch();
const toast = useToast();
const insets = useSafeAreaInsets();
const bounceY = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: bounceY.value }],
}));
const onPress = () => {
const totalDuration = 500;
bounceY.value = 0; // 항상 리셋
bounceY.value = withSequence(
withTiming(-16, { duration: totalDuration * 0.3 }), // 위로 빠르게
withTiming(4, { duration: totalDuration * 0.3 }), // 아래로 반동
withTiming(-8, { duration: totalDuration * 0.2 }), // 위로 살짝
withTiming(0, { duration: totalDuration * 0.2 }) // 원위치
);
toast.show({
placement: 'top',
duration: 2000,
render: () => {
return (
<Box style={{ marginTop: insets.top + 12, alignItems: 'center', width: '100%' }}>
<Toast
nativeID="text"
variant="solid"
className="py2 mx-5 max-w-[90%] flex-row items-center justify-center gap-2 rounded-full px-5 opacity-80">
<ToastTitle></ToastTitle>
<ToastDescription></ToastDescription>
</Toast>
</Box>
);
},
});
};
return (
<Box className="flex-1 items-center justify-center">
<Box className="h-64 w-64 rounded-md border border-secondary-600 p-3">
<Center>
<Text className="text-lg font-semibold">Count: {state.count}</Text>
</Center>
<HStack className="w-full flex-row justify-between gap-0 p-0">
<Text className="text-red-600">Text1</Text>
<Box className="flex-1" />
<Text className="text-green-600">Text2</Text>
</HStack>
<Box className="flex-1 items-center justify-center">
<Animated.View style={animatedStyle}>
<Button onPress={onPress}>
<ButtonText>🍞</ButtonText>
</Button>
</Animated.View>
</Box>
<HStack className="h-10 w-full flex-row justify-between gap-0 bg-red-500 p-0">
<Button onPress={() => dispatch({ type: 'INCREMENT' })}>
<ButtonText>+</ButtonText>
</Button>
<Box className="flex-1" />
<Button onPress={() => dispatch({ type: 'DECREMENT' })}>
<ButtonText>-</ButtonText>
</Button>
</HStack>
</Box>
</Box>
);
}

@ -1,54 +0,0 @@
import { AdSlot } from '@/components/AdSlot';
import { EntryCard } from '@/components/EntryCard';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { useDiary } from '@/states/diaryStore';
import { FlatList } from 'react-native';
export default function ListScreen({
onOpenCreate,
onOpenEdit,
}: {
onOpenCreate: () => void;
onOpenEdit: (id: string) => void;
}) {
const { state } = useDiary();
return (
<Box className="flex-1 bg-neutral-50">
<Text>Hello????</Text>
<FlatList
contentContainerStyle={{ padding: 16, gap: 12 }}
data={state.ids}
keyExtractor={(id) => id}
renderItem={({ item: id }) => {
const e = state.byId[id];
if (!e) return null;
return <EntryCard item={e} onPress={() => onOpenEdit(id)} />;
}}
ListFooterComponent={
<Box className="mt-4">
<AdSlot />
</Box>
}
ListEmptyComponent={
<Box className="flex-1 items-center justify-center">
<Text> .</Text>
</Box>
}
/>
<Box className="absolute bottom-5 right-5">
<Box className="rounded-full bg-black p-4" onTouchEnd={onOpenCreate}>
<Box className="h-6 w-6 items-center justify-center">
<Box className="h-1 w-6 rounded bg-white" />
<Box className="-mt-1 -rotate-90">
<Box className="h-1 w-6 rounded bg-white" />
</Box>
<Text>Hello?</Text>
</Box>
</Box>
</Box>
</Box>
);
}

@ -0,0 +1,37 @@
import {
useSharedValue,
useAnimatedStyle,
withSequence,
withTiming,
} from 'react-native-reanimated';
type UseBounceOptions = {
amplitude?: number; // 최대 위/아래 이동량(px)
duration?: number; // 전체 지속시간(ms)
};
export function useBounce({ amplitude = 16, duration = 200 }: UseBounceOptions = {}) {
const y = useSharedValue(0);
const style = useAnimatedStyle(
() => ({
transform: [{ translateY: y.value }],
}),
[]
);
const trigger = () => {
'worklet';
const d = duration;
const A = amplitude;
y.value = 0;
y.value = withSequence(
withTiming(-A, { duration: d * 0.3 }),
withTiming(A * 0.25, { duration: d * 0.3 }),
withTiming(-A * 0.5, { duration: d * 0.2 }),
withTiming(0, { duration: d * 0.2 })
);
};
return { style, trigger, y };
}

@ -0,0 +1,68 @@
import { configureFonts, MD3LightTheme, MD3Theme } from 'react-native-paper';
import { StyleSheet } from 'react-native';
export const spacing = { xs: 6, sm: 8, md: 12, lg: 16, xl: 20 } as const;
export const radius = { sm: 2, md: 6, lg: 12 } as const;
export const appTheme: MD3Theme = {
...MD3LightTheme,
roundness: radius.sm,
colors: {
primary: 'rgb(52, 92, 168)',
onPrimary: 'rgb(255, 255, 255)',
primaryContainer: 'rgb(217, 226, 255)',
onPrimaryContainer: 'rgb(0, 26, 67)',
secondary: 'rgb(87, 94, 113)',
onSecondary: 'rgb(255, 255, 255)',
secondaryContainer: 'rgb(219, 226, 249)',
onSecondaryContainer: 'rgb(20, 27, 44)',
tertiary: 'rgb(114, 85, 115)',
onTertiary: 'rgb(255, 255, 255)',
tertiaryContainer: 'rgb(252, 215, 251)',
onTertiaryContainer: 'rgb(42, 19, 45)',
error: 'rgb(186, 26, 26)',
onError: 'rgb(255, 255, 255)',
errorContainer: 'rgb(255, 218, 214)',
onErrorContainer: 'rgb(65, 0, 2)',
background: 'rgb(254, 251, 255)',
onBackground: 'rgb(27, 27, 31)',
surface: 'rgb(254, 251, 255)',
onSurface: 'rgb(27, 27, 31)',
surfaceVariant: 'rgb(225, 226, 236)',
onSurfaceVariant: 'rgb(68, 71, 79)',
outline: 'rgb(117, 119, 128)',
outlineVariant: 'rgb(197, 198, 208)',
shadow: 'rgb(0, 0, 0)',
scrim: 'rgb(0, 0, 0)',
inverseSurface: 'rgb(48, 48, 52)',
inverseOnSurface: 'rgb(242, 240, 244)',
inversePrimary: 'rgb(175, 198, 255)',
elevation: {
level0: 'transparent',
level1: 'rgb(244, 243, 251)',
level2: 'rgb(238, 238, 248)',
level3: 'rgb(232, 234, 245)',
level4: 'rgb(230, 232, 245)',
level5: 'rgb(226, 229, 243)',
},
surfaceDisabled: 'rgba(27, 27, 31, 0.12)',
onSurfaceDisabled: 'rgba(27, 27, 31, 0.38)',
backdrop: 'rgba(46, 48, 56, 0.4)',
},
};
export const styles = StyleSheet.create({
flex: {
flex: 1,
},
flexCenter: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
flexJustifyBetween: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
},
});

@ -1,5 +1,5 @@
import { DiaryEntry, EntryIndex } from '@/types/entry';
import { File, Directory, Paths } from 'expo-file-system';
import { DiaryEntry, EntryIndex } from '@/types/entry';
const JSON_MIMETYPE = 'application/json';
const JPG_MIMETYPE = 'image/jpeg';
@ -16,12 +16,21 @@ export function ensureDirs() {
}
export function writeTextAtomic(file: File, text: string) {
const tmp = new File(file.parentDirectory, `${file.name}.tmp`, file.type);
if (tmp.exists) tmp.delete();
try {
if (!file.parentDirectory.exists) file.parentDirectory.create();
} catch {}
const tmp = new File(file.parentDirectory, `${file.name}.tmp`);
try {
tmp.delete();
} catch {}
tmp.create();
tmp.write(text);
if (file.exists) file.delete();
try {
file.delete();
} catch {}
tmp.move(file);
}

@ -37,8 +37,14 @@ export async function saveOrReplaceImage(id: string, sourceUri: string): Promise
compress: 0.9,
});
const tmp = new File(target.parentDirectory, `${target.name}.tmp`, target.type);
if (tmp.exists) tmp.delete();
try {
if (!target.parentDirectory.exists) target.parentDirectory.create();
} catch {}
const tmp = new File(target.parentDirectory, `${target.name}.tmp`);
try {
tmp.delete();
} catch {}
tmp.create();
@ -46,7 +52,9 @@ export async function saveOrReplaceImage(id: string, sourceUri: string): Promise
const buf = await res.arrayBuffer();
tmp.write(new Uint8Array(buf));
if (target.exists) target.delete();
try {
target.delete();
} catch {}
tmp.move(target);
return target.uri;
@ -54,5 +62,7 @@ export async function saveOrReplaceImage(id: string, sourceUri: string): Promise
export function removeImageIfExists(id: string) {
const f = imageFile(id);
if (f.exists) f.delete();
try {
f.delete();
} catch {}
}

@ -1,29 +1,39 @@
import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react';
import {
createContext,
Dispatch,
ReactNode,
useContext,
useReducer,
} from "react";
type State = { count: number };
type Action = { type: 'INCREMENT' | 'DECREMENT' };
type Action = { type: "INCREMENT" | "DECREMENT" };
const initialState: State = { count: 0 };
function counterReducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
case "INCREMENT":
return { count: state.count + 1 };
case 'DECREMENT':
case "DECREMENT":
return { count: state.count - 1 };
default:
throw new Error('Unhandled action');
throw new Error("Unhandled action");
}
}
const CounterStateContext = createContext<State | undefined>(undefined);
const CounterDispatchContext = createContext<Dispatch<Action> | undefined>(undefined);
const CounterDispatchContext = createContext<Dispatch<Action> | undefined>(
undefined
);
export function CounterProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<CounterStateContext.Provider value={state}>
<CounterDispatchContext.Provider value={dispatch}>{children}</CounterDispatchContext.Provider>
<CounterDispatchContext.Provider value={dispatch}>
{children}
</CounterDispatchContext.Provider>
</CounterStateContext.Provider>
);
}
@ -31,7 +41,7 @@ export function CounterProvider({ children }: { children: ReactNode }) {
export function useCounterState() {
const context = useContext(CounterStateContext);
if (context === undefined)
throw new Error('useCounterState must be used within a CounterProvider');
throw new Error("useCounterState must be used within a CounterProvider");
return context;
}
@ -39,7 +49,7 @@ export function useCounterState() {
export function useCounterDispatch() {
const context = useContext(CounterDispatchContext);
if (context === undefined)
throw new Error('useCounterDispatch must be used within a CounterProvider');
throw new Error("useCounterDispatch must be used within a CounterProvider");
return context;
}

@ -1,165 +1,165 @@
import {
deleteEntryFile,
ensureDirs,
INDEX_FILE,
readEntry,
readJsonSafe,
writeEntryAtomic,
writeIndexAtomic,
} from '@/storages/fileio';
import { removeImageIfExists, saveOrReplaceImage } from '@/storages/imageio';
import { DiaryEntry, EntryIndex, Mood } from '@/types/entry';
import { toDateISOString } from '@/utils/date';
import { genId } from '@/utils/id';
import { createContext, useContext, useEffect, useMemo, useReducer } from 'react';
type State = { ids: string[]; byId: Record<string, DiaryEntry> };
type Action =
| { type: 'SET_ALL'; ids: string[]; byId: Record<string, DiaryEntry> }
| { type: 'UPSERT'; entry: DiaryEntry }
| { type: 'REMOVE'; id: string };
function reducer(state: State, a: Action): State {
switch (a.type) {
case 'SET_ALL':
return { ids: a.ids, byId: a.byId };
case 'UPSERT': {
const exists = state.ids.includes(a.entry.id);
const ids = exists ? state.ids : [a.entry.id, ...state.ids];
return { ids, byId: { ...state.byId, [a.entry.id]: a.entry } };
}
case 'REMOVE': {
const { [a.id]: _, ...rest } = state.byId;
return { ids: state.ids.filter((i) => i !== a.id), byId: rest };
}
}
}
const Ctx = createContext<{
state: State;
load: () => void;
create: (p: { mood: Mood; text: string; imageSrcUri?: string }) => Promise<string>;
update: (
id: string,
p: { mood?: Mood; text?: string; imageSrcUri?: string | null }
) => Promise<void>;
remove: (id: string) => Promise<void>;
} | null>(null);
export function DiaryProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { ids: [], byId: {} });
const load = () => {
ensureDirs();
const index = readJsonSafe<EntryIndex>(INDEX_FILE);
if (!index?.ids?.length) {
dispatch({ type: 'SET_ALL', ids: [], byId: {} });
return;
}
const byId: Record<string, DiaryEntry> = {};
for (const id of index.ids) {
const e = readEntry(id);
if (e) byId[id] = e;
}
dispatch({ type: 'SET_ALL', ids: index.ids, byId });
};
const create = async ({
mood,
text,
imageSrcUri,
}: {
mood: Mood;
text: string;
imageSrcUri?: string;
}) => {
const id = genId();
const now = Date.now();
let imageUri: string | undefined;
if (imageSrcUri) imageUri = await saveOrReplaceImage(id, imageSrcUri);
const entry: DiaryEntry = {
id,
dateTimeISO: toDateISOString(new Date()),
mood,
text: text.slice(0, 140),
imageUri,
createdAt: now,
updatedAt: now,
};
writeEntryAtomic(entry);
const prev = readJsonSafe<EntryIndex>(INDEX_FILE) ?? {
schemaVersion: 1 as const,
ids: [],
updatedAt: 0,
};
const next: EntryIndex = { schemaVersion: 1, ids: [id, ...prev.ids], updatedAt: now };
writeIndexAtomic(next);
dispatch({ type: 'UPSERT', entry });
return id;
};
const update = async (
id: string,
{ mood, text, imageSrcUri }: { mood?: Mood; text?: string; imageSrcUri?: string | null }
) => {
const cur = state.byId[id];
if (!cur) return;
let imageUri = cur.imageUri;
if (imageSrcUri === null) {
removeImageIfExists(id);
imageUri = undefined;
} else if (imageSrcUri) {
imageUri = await saveOrReplaceImage(id, imageSrcUri);
}
const next: DiaryEntry = {
...cur,
mood: mood ?? cur.mood,
text: (text ?? cur.text).slice(0, 140),
imageUri,
updatedAt: Date.now(),
};
writeEntryAtomic(next);
dispatch({ type: 'UPSERT', entry: next });
};
const remove = async (id: string) => {
deleteEntryFile(id);
const prev = readJsonSafe<EntryIndex>(INDEX_FILE) ?? {
schemaVersion: 1 as const,
ids: [],
updatedAt: 0,
};
const next: EntryIndex = {
schemaVersion: 1,
ids: prev.ids.filter((x) => x !== id),
updatedAt: Date.now(),
};
writeIndexAtomic(next);
dispatch({ type: 'REMOVE', id });
};
const value = useMemo(() => ({ state, load, create, update, remove }), [state]);
useEffect(() => {
load();
}, []);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export const useDiary = () => {
const value = useContext(Ctx);
if (!value) throw new Error('useDiary must be used within DiaryProvider');
return value;
};
import {
deleteEntryFile,
ensureDirs,
INDEX_FILE,
readEntry,
readJsonSafe,
writeEntryAtomic,
writeIndexAtomic,
} from '@/storages/fileio';
import { removeImageIfExists, saveOrReplaceImage } from '@/storages/imageio';
import { DiaryEntry, EntryIndex, Mood } from '@/types/entry';
import { toDateISOString } from '@/utils/date';
import { genId } from '@/utils/id';
import { createContext, useContext, useEffect, useMemo, useReducer } from 'react';
type State = { ids: string[]; byId: Record<string, DiaryEntry> };
type Action =
| { type: 'SET_ALL'; ids: string[]; byId: Record<string, DiaryEntry> }
| { type: 'UPSERT'; entry: DiaryEntry }
| { type: 'REMOVE'; id: string };
function reducer(state: State, a: Action): State {
switch (a.type) {
case 'SET_ALL':
return { ids: a.ids, byId: a.byId };
case 'UPSERT': {
const exists = state.ids.includes(a.entry.id);
const ids = exists ? state.ids : [a.entry.id, ...state.ids];
return { ids, byId: { ...state.byId, [a.entry.id]: a.entry } };
}
case 'REMOVE': {
const { [a.id]: _, ...rest } = state.byId;
return { ids: state.ids.filter((i) => i !== a.id), byId: rest };
}
}
}
const Ctx = createContext<{
state: State;
load: () => void;
create: (p: { mood: Mood; text: string; imageSrcUri?: string }) => Promise<string>;
update: (
id: string,
p: { mood?: Mood; text?: string; imageSrcUri?: string | null }
) => Promise<void>;
remove: (id: string) => Promise<void>;
} | null>(null);
export function DiaryProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { ids: [], byId: {} });
const load = () => {
ensureDirs();
const index = readJsonSafe<EntryIndex>(INDEX_FILE);
if (!index?.ids?.length) {
dispatch({ type: 'SET_ALL', ids: [], byId: {} });
return;
}
const byId: Record<string, DiaryEntry> = {};
for (const id of index.ids) {
const e = readEntry(id);
if (e) byId[id] = e;
}
dispatch({ type: 'SET_ALL', ids: index.ids, byId });
};
const create = async ({
mood,
text,
imageSrcUri,
}: {
mood: Mood;
text: string;
imageSrcUri?: string;
}) => {
const id = genId();
const now = Date.now();
let imageUri: string | undefined;
if (imageSrcUri) imageUri = await saveOrReplaceImage(id, imageSrcUri);
const entry: DiaryEntry = {
id,
dateTimeISO: toDateISOString(new Date()),
mood,
text: text.slice(0, 140),
imageUri,
createdAt: now,
updatedAt: now,
};
writeEntryAtomic(entry);
const prev = readJsonSafe<EntryIndex>(INDEX_FILE) ?? {
schemaVersion: 1 as const,
ids: [],
updatedAt: 0,
};
const next: EntryIndex = { schemaVersion: 1, ids: [id, ...prev.ids], updatedAt: now };
writeIndexAtomic(next);
dispatch({ type: 'UPSERT', entry });
return id;
};
const update = async (
id: string,
{ mood, text, imageSrcUri }: { mood?: Mood; text?: string; imageSrcUri?: string | null }
) => {
const cur = state.byId[id];
if (!cur) return;
let imageUri = cur.imageUri;
if (imageSrcUri === null) {
removeImageIfExists(id);
imageUri = undefined;
} else if (imageSrcUri) {
imageUri = await saveOrReplaceImage(id, imageSrcUri);
}
const next: DiaryEntry = {
...cur,
mood: mood ?? cur.mood,
text: (text ?? cur.text).slice(0, 140),
imageUri,
updatedAt: Date.now(),
};
writeEntryAtomic(next);
dispatch({ type: 'UPSERT', entry: next });
};
const remove = async (id: string) => {
deleteEntryFile(id);
const prev = readJsonSafe<EntryIndex>(INDEX_FILE) ?? {
schemaVersion: 1 as const,
ids: [],
updatedAt: 0,
};
const next: EntryIndex = {
schemaVersion: 1,
ids: prev.ids.filter((x) => x !== id),
updatedAt: Date.now(),
};
writeIndexAtomic(next);
dispatch({ type: 'REMOVE', id });
};
const value = useMemo(() => ({ state, load, create, update, remove }), [state]);
useEffect(() => {
load();
}, []);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export const useDiary = () => {
const value = useContext(Ctx);
if (!value) throw new Error('useDiary must be used within DiaryProvider');
return value;
};

@ -1,206 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class',
content: [
'./app/**/*.{html,js,jsx,ts,tsx,mdx}',
'./components/**/*.{html,js,jsx,ts,tsx,mdx}',
'./utils/**/*.{html,js,jsx,ts,tsx,mdx}',
'./*.{html,js,jsx,ts,tsx,mdx}',
'./src/**/*.{html,js,jsx,ts,tsx,mdx}',
],
presets: [require('nativewind/preset')],
important: 'html',
safelist: [
{
pattern:
/(bg|border|text|stroke|fill)-(primary|secondary|tertiary|error|success|warning|info|typography|outline|background|indicator)-(0|50|100|200|300|400|500|600|700|800|900|950|white|gray|black|error|warning|muted|success|info|light|dark|primary)/,
},
],
theme: {
extend: {
colors: {
primary: {
0: 'rgb(var(--color-primary-0)/<alpha-value>)',
50: 'rgb(var(--color-primary-50)/<alpha-value>)',
100: 'rgb(var(--color-primary-100)/<alpha-value>)',
200: 'rgb(var(--color-primary-200)/<alpha-value>)',
300: 'rgb(var(--color-primary-300)/<alpha-value>)',
400: 'rgb(var(--color-primary-400)/<alpha-value>)',
500: 'rgb(var(--color-primary-500)/<alpha-value>)',
600: 'rgb(var(--color-primary-600)/<alpha-value>)',
700: 'rgb(var(--color-primary-700)/<alpha-value>)',
800: 'rgb(var(--color-primary-800)/<alpha-value>)',
900: 'rgb(var(--color-primary-900)/<alpha-value>)',
950: 'rgb(var(--color-primary-950)/<alpha-value>)',
},
secondary: {
0: 'rgb(var(--color-secondary-0)/<alpha-value>)',
50: 'rgb(var(--color-secondary-50)/<alpha-value>)',
100: 'rgb(var(--color-secondary-100)/<alpha-value>)',
200: 'rgb(var(--color-secondary-200)/<alpha-value>)',
300: 'rgb(var(--color-secondary-300)/<alpha-value>)',
400: 'rgb(var(--color-secondary-400)/<alpha-value>)',
500: 'rgb(var(--color-secondary-500)/<alpha-value>)',
600: 'rgb(var(--color-secondary-600)/<alpha-value>)',
700: 'rgb(var(--color-secondary-700)/<alpha-value>)',
800: 'rgb(var(--color-secondary-800)/<alpha-value>)',
900: 'rgb(var(--color-secondary-900)/<alpha-value>)',
950: 'rgb(var(--color-secondary-950)/<alpha-value>)',
},
tertiary: {
50: 'rgb(var(--color-tertiary-50)/<alpha-value>)',
100: 'rgb(var(--color-tertiary-100)/<alpha-value>)',
200: 'rgb(var(--color-tertiary-200)/<alpha-value>)',
300: 'rgb(var(--color-tertiary-300)/<alpha-value>)',
400: 'rgb(var(--color-tertiary-400)/<alpha-value>)',
500: 'rgb(var(--color-tertiary-500)/<alpha-value>)',
600: 'rgb(var(--color-tertiary-600)/<alpha-value>)',
700: 'rgb(var(--color-tertiary-700)/<alpha-value>)',
800: 'rgb(var(--color-tertiary-800)/<alpha-value>)',
900: 'rgb(var(--color-tertiary-900)/<alpha-value>)',
950: 'rgb(var(--color-tertiary-950)/<alpha-value>)',
},
error: {
0: 'rgb(var(--color-error-0)/<alpha-value>)',
50: 'rgb(var(--color-error-50)/<alpha-value>)',
100: 'rgb(var(--color-error-100)/<alpha-value>)',
200: 'rgb(var(--color-error-200)/<alpha-value>)',
300: 'rgb(var(--color-error-300)/<alpha-value>)',
400: 'rgb(var(--color-error-400)/<alpha-value>)',
500: 'rgb(var(--color-error-500)/<alpha-value>)',
600: 'rgb(var(--color-error-600)/<alpha-value>)',
700: 'rgb(var(--color-error-700)/<alpha-value>)',
800: 'rgb(var(--color-error-800)/<alpha-value>)',
900: 'rgb(var(--color-error-900)/<alpha-value>)',
950: 'rgb(var(--color-error-950)/<alpha-value>)',
},
success: {
0: 'rgb(var(--color-success-0)/<alpha-value>)',
50: 'rgb(var(--color-success-50)/<alpha-value>)',
100: 'rgb(var(--color-success-100)/<alpha-value>)',
200: 'rgb(var(--color-success-200)/<alpha-value>)',
300: 'rgb(var(--color-success-300)/<alpha-value>)',
400: 'rgb(var(--color-success-400)/<alpha-value>)',
500: 'rgb(var(--color-success-500)/<alpha-value>)',
600: 'rgb(var(--color-success-600)/<alpha-value>)',
700: 'rgb(var(--color-success-700)/<alpha-value>)',
800: 'rgb(var(--color-success-800)/<alpha-value>)',
900: 'rgb(var(--color-success-900)/<alpha-value>)',
950: 'rgb(var(--color-success-950)/<alpha-value>)',
},
warning: {
0: 'rgb(var(--color-warning-0)/<alpha-value>)',
50: 'rgb(var(--color-warning-50)/<alpha-value>)',
100: 'rgb(var(--color-warning-100)/<alpha-value>)',
200: 'rgb(var(--color-warning-200)/<alpha-value>)',
300: 'rgb(var(--color-warning-300)/<alpha-value>)',
400: 'rgb(var(--color-warning-400)/<alpha-value>)',
500: 'rgb(var(--color-warning-500)/<alpha-value>)',
600: 'rgb(var(--color-warning-600)/<alpha-value>)',
700: 'rgb(var(--color-warning-700)/<alpha-value>)',
800: 'rgb(var(--color-warning-800)/<alpha-value>)',
900: 'rgb(var(--color-warning-900)/<alpha-value>)',
950: 'rgb(var(--color-warning-950)/<alpha-value>)',
},
info: {
0: 'rgb(var(--color-info-0)/<alpha-value>)',
50: 'rgb(var(--color-info-50)/<alpha-value>)',
100: 'rgb(var(--color-info-100)/<alpha-value>)',
200: 'rgb(var(--color-info-200)/<alpha-value>)',
300: 'rgb(var(--color-info-300)/<alpha-value>)',
400: 'rgb(var(--color-info-400)/<alpha-value>)',
500: 'rgb(var(--color-info-500)/<alpha-value>)',
600: 'rgb(var(--color-info-600)/<alpha-value>)',
700: 'rgb(var(--color-info-700)/<alpha-value>)',
800: 'rgb(var(--color-info-800)/<alpha-value>)',
900: 'rgb(var(--color-info-900)/<alpha-value>)',
950: 'rgb(var(--color-info-950)/<alpha-value>)',
},
typography: {
0: 'rgb(var(--color-typography-0)/<alpha-value>)',
50: 'rgb(var(--color-typography-50)/<alpha-value>)',
100: 'rgb(var(--color-typography-100)/<alpha-value>)',
200: 'rgb(var(--color-typography-200)/<alpha-value>)',
300: 'rgb(var(--color-typography-300)/<alpha-value>)',
400: 'rgb(var(--color-typography-400)/<alpha-value>)',
500: 'rgb(var(--color-typography-500)/<alpha-value>)',
600: 'rgb(var(--color-typography-600)/<alpha-value>)',
700: 'rgb(var(--color-typography-700)/<alpha-value>)',
800: 'rgb(var(--color-typography-800)/<alpha-value>)',
900: 'rgb(var(--color-typography-900)/<alpha-value>)',
950: 'rgb(var(--color-typography-950)/<alpha-value>)',
white: '#FFFFFF',
gray: '#D4D4D4',
black: '#181718',
},
outline: {
0: 'rgb(var(--color-outline-0)/<alpha-value>)',
50: 'rgb(var(--color-outline-50)/<alpha-value>)',
100: 'rgb(var(--color-outline-100)/<alpha-value>)',
200: 'rgb(var(--color-outline-200)/<alpha-value>)',
300: 'rgb(var(--color-outline-300)/<alpha-value>)',
400: 'rgb(var(--color-outline-400)/<alpha-value>)',
500: 'rgb(var(--color-outline-500)/<alpha-value>)',
600: 'rgb(var(--color-outline-600)/<alpha-value>)',
700: 'rgb(var(--color-outline-700)/<alpha-value>)',
800: 'rgb(var(--color-outline-800)/<alpha-value>)',
900: 'rgb(var(--color-outline-900)/<alpha-value>)',
950: 'rgb(var(--color-outline-950)/<alpha-value>)',
},
background: {
0: 'rgb(var(--color-background-0)/<alpha-value>)',
50: 'rgb(var(--color-background-50)/<alpha-value>)',
100: 'rgb(var(--color-background-100)/<alpha-value>)',
200: 'rgb(var(--color-background-200)/<alpha-value>)',
300: 'rgb(var(--color-background-300)/<alpha-value>)',
400: 'rgb(var(--color-background-400)/<alpha-value>)',
500: 'rgb(var(--color-background-500)/<alpha-value>)',
600: 'rgb(var(--color-background-600)/<alpha-value>)',
700: 'rgb(var(--color-background-700)/<alpha-value>)',
800: 'rgb(var(--color-background-800)/<alpha-value>)',
900: 'rgb(var(--color-background-900)/<alpha-value>)',
950: 'rgb(var(--color-background-950)/<alpha-value>)',
error: 'rgb(var(--color-background-error)/<alpha-value>)',
warning: 'rgb(var(--color-background-warning)/<alpha-value>)',
muted: 'rgb(var(--color-background-muted)/<alpha-value>)',
success: 'rgb(var(--color-background-success)/<alpha-value>)',
info: 'rgb(var(--color-background-info)/<alpha-value>)',
light: '#FBFBFB',
dark: '#181719',
},
indicator: {
primary: 'rgb(var(--color-indicator-primary)/<alpha-value>)',
info: 'rgb(var(--color-indicator-info)/<alpha-value>)',
error: 'rgb(var(--color-indicator-error)/<alpha-value>)',
},
},
fontFamily: {
heading: undefined,
body: undefined,
mono: undefined,
jakarta: ['var(--font-plus-jakarta-sans)'],
roboto: ['var(--font-roboto)'],
code: ['var(--font-source-code-pro)'],
inter: ['var(--font-inter)'],
'space-mono': ['var(--font-space-mono)'],
},
fontWeight: {
extrablack: '950',
},
fontSize: {
'2xs': '10px',
},
boxShadow: {
'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)',
'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)',
'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)',
'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)',
'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)',
'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)',
'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)',
},
},
},
};

@ -2,18 +2,10 @@
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"~/*": [
"src/*"
],
"@/*": [
"./*"
],
"tailwind.config": [
"./tailwind.config.js"
]
"~/*": ["src/*"],
"@/*": ["./*"]
}
}
}
}

@ -1,5 +1,4 @@
export type Mood = '🤩' | '😀' | '🙂' | '😐' | '🙁' | '😢' | '😡' | '🤒';
export type Mood = '😀' | '😐' | '🙁' | '😢' | '😡';
export interface DiaryEntry {
id: string;
dateTimeISO: string;

Loading…
Cancel
Save