This commit is contained in:
Timo Knuth 2025-10-29 00:11:51 +01:00
parent 8dab9bfd25
commit 31dfe1dac5
7 changed files with 379 additions and 9 deletions

157
package-lock.json generated
View File

@ -31,6 +31,7 @@
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "^0.6.1" "react-native-worklets": "^0.6.1"
}, },
@ -4291,6 +4292,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -4922,6 +4929,56 @@
"hyphenate-style-name": "^1.0.3" "hyphenate-style-name": "^1.0.3"
} }
}, },
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -5057,6 +5114,61 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -5130,6 +5242,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -7895,6 +8019,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -8791,6 +8921,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -9722,6 +9864,21 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-svg": {
"version": "15.12.1",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
"integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.21.1", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz",

View File

@ -34,6 +34,7 @@
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "^0.6.1" "react-native-worklets": "^0.6.1"
}, },

View File

@ -0,0 +1,195 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
interface PotteryIconProps {
color: string;
finish?: 'glossy' | 'matte' | 'satin' | 'unknown';
size?: number;
}
export const PotteryIcon: React.FC<PotteryIconProps> = ({
color,
finish = 'unknown',
size = 60
}) => {
// Different pottery shapes based on finish type using View components
const renderShape = () => {
const shapeStyle = {
backgroundColor: color,
width: size,
height: size,
};
switch (finish) {
case 'glossy':
// Vase shape - wider at bottom, narrower at top
return (
<View style={{ alignItems: 'center' }}>
<View style={[styles.vaseTop, { width: size * 0.4, height: size * 0.15, backgroundColor: color }]} />
<View style={[styles.vaseNeck, { width: size * 0.5, height: size * 0.25, backgroundColor: color }]} />
<View style={[styles.vaseBody, { width: size * 0.8, height: size * 0.5, backgroundColor: color }]} />
<View style={[styles.vaseBase, { width: size * 0.6, height: size * 0.1, backgroundColor: color }]} />
{/* Glossy shine effect */}
<View style={[styles.shine, { top: size * 0.2, left: size * 0.2 }]} />
</View>
);
case 'matte':
// Bowl shape - smooth trapezoid with many layers
const numLayers = 20;
const startWidth = 0.9; // Top width (90% of size)
const endWidth = 0.4; // Bottom width (40% of size)
const layerHeight = size / numLayers;
return (
<View style={{ alignItems: 'center', justifyContent: 'center', height: size, width: size }}>
{Array.from({ length: numLayers }).map((_, index) => {
// Calculate width for this layer (gradually narrowing from top to bottom)
const progress = index / (numLayers - 1);
const layerWidth = startWidth - (startWidth - endWidth) * progress;
const isFirst = index === 0;
const isLast = index === numLayers - 1;
return (
<View
key={index}
style={[
isFirst ? styles.bowlTop : isLast ? styles.bowlBase : styles.bowlLayer,
{
width: size * layerWidth,
height: layerHeight,
backgroundColor: color,
borderTopLeftRadius: isFirst ? size * 0.15 : 0,
borderTopRightRadius: isFirst ? size * 0.15 : 0,
borderBottomLeftRadius: isLast ? size * 0.08 : 0,
borderBottomRightRadius: isLast ? size * 0.08 : 0,
}
]}
/>
);
})}
</View>
);
case 'satin':
// Mug shape - cylinder with handle
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ alignItems: 'center' }}>
<View style={[styles.mugRim, { width: size * 0.6, height: size * 0.1, backgroundColor: color }]} />
<View style={[styles.mugBody, { width: size * 0.6, height: size * 0.7, backgroundColor: color }]} />
<View style={[styles.mugBase, { width: size * 0.6, height: size * 0.2, backgroundColor: color }]} />
</View>
<View style={[styles.handle, { borderColor: color, marginLeft: -size * 0.1 }]} />
</View>
);
default:
// Simple circular swatch for unknown finish
return (
<View style={[styles.circle, shapeStyle]} />
);
}
};
return (
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
{renderShape()}
</View>
);
};
const styles = StyleSheet.create({
// Vase styles
vaseTop: {
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
borderWidth: 1,
borderColor: '#8B7355',
},
vaseNeck: {
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: '#8B7355',
},
vaseBody: {
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: '#8B7355',
borderRadius: 8,
},
vaseBase: {
borderBottomLeftRadius: 5,
borderBottomRightRadius: 5,
borderWidth: 1,
borderColor: '#8B7355',
},
shine: {
position: 'absolute',
width: 8,
height: 12,
backgroundColor: 'rgba(255, 255, 255, 0.4)',
borderRadius: 4,
},
// Bowl styles - trapezoid bowl shape
bowlTop: {
borderTopWidth: 2,
borderLeftWidth: 2,
borderRightWidth: 2,
borderColor: '#8B7355',
marginTop: -1, // Overlap to connect
},
bowlLayer: {
borderLeftWidth: 2,
borderRightWidth: 2,
borderColor: '#8B7355',
marginTop: -1, // Overlap to connect smoothly
},
bowlBase: {
borderBottomWidth: 2,
borderLeftWidth: 2,
borderRightWidth: 2,
borderColor: '#8B7355',
marginTop: -1, // Overlap to connect
},
// Mug styles
mugRim: {
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
borderTopWidth: 2,
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: '#8B7355',
},
mugBody: {
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: '#8B7355',
},
mugBase: {
borderBottomLeftRadius: 5,
borderBottomRightRadius: 5,
borderBottomWidth: 2,
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: '#8B7355',
},
handle: {
width: 15,
height: 25,
borderWidth: 2,
borderLeftWidth: 0,
borderRadius: 8,
backgroundColor: 'transparent',
},
// Default circle
circle: {
borderRadius: 100,
borderWidth: 2,
borderColor: '#8B7355',
},
});

View File

@ -1,3 +1,4 @@
export * from './Button'; export * from './Button';
export * from './Card'; export * from './Card';
export * from './Input'; export * from './Input';
export * from './PotteryIcon';

View File

@ -14,7 +14,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/types'; import { RootStackParamList } from '../navigation/types';
import { Glaze } from '../types'; import { Glaze } from '../types';
import { getAllGlazes, searchGlazes } from '../lib/db/repositories'; import { getAllGlazes, searchGlazes } from '../lib/db/repositories';
import { Button, Card } from '../components'; import { Button, Card, PotteryIcon } from '../components';
import { colors, spacing, typography, borderRadius } from '../lib/theme'; import { colors, spacing, typography, borderRadius } from '../lib/theme';
import { mixColors, generateMixName } from '../lib/utils'; import { mixColors, generateMixName } from '../lib/utils';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
@ -128,8 +128,14 @@ export const GlazeMixerScreen: React.FC = () => {
<TouchableOpacity onPress={() => toggleGlaze(item)}> <TouchableOpacity onPress={() => toggleGlaze(item)}>
<Card style={[styles.glazeCard, isSelected ? styles.glazeCardSelected : null]}> <Card style={[styles.glazeCard, isSelected ? styles.glazeCardSelected : null]}>
<View style={styles.glazeMainContent}> <View style={styles.glazeMainContent}>
{item.color && ( {item.color ? (
<View style={[styles.colorPreview, { backgroundColor: item.color }]} /> <PotteryIcon
color={item.color}
finish={item.finish}
size={60}
/>
) : (
<View style={styles.colorPreview} />
)} )}
<View style={styles.glazeInfo}> <View style={styles.glazeInfo}>
<View style={styles.glazeHeader}> <View style={styles.glazeHeader}>
@ -161,7 +167,11 @@ export const GlazeMixerScreen: React.FC = () => {
<Card style={styles.mixPreviewCard}> <Card style={styles.mixPreviewCard}>
<Text style={styles.mixPreviewTitle}>Mix Preview</Text> <Text style={styles.mixPreviewTitle}>Mix Preview</Text>
<View style={styles.mixPreviewContent}> <View style={styles.mixPreviewContent}>
<View style={[styles.mixedColorPreview, { backgroundColor: getMixedColor() }]} /> <PotteryIcon
color={getMixedColor()}
finish="glossy"
size={80}
/>
<View style={styles.mixInfo}> <View style={styles.mixInfo}>
<Text style={styles.mixedGlazesText}> <Text style={styles.mixedGlazesText}>
{selectedGlazes.map(g => g.name).join(' + ')} {selectedGlazes.map(g => g.name).join(' + ')}

View File

@ -13,7 +13,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/types'; import { RootStackParamList } from '../navigation/types';
import { Glaze } from '../types'; import { Glaze } from '../types';
import { getAllGlazes, searchGlazes } from '../lib/db/repositories'; import { getAllGlazes, searchGlazes } from '../lib/db/repositories';
import { Button, Card } from '../components'; import { Button, Card, PotteryIcon } from '../components';
import { colors, spacing, typography, borderRadius } from '../lib/theme'; import { colors, spacing, typography, borderRadius } from '../lib/theme';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
@ -100,8 +100,14 @@ export const GlazePickerScreen: React.FC = () => {
<TouchableOpacity onPress={() => toggleGlaze(item.id)}> <TouchableOpacity onPress={() => toggleGlaze(item.id)}>
<Card style={[styles.glazeCard, isSelected ? styles.glazeCardSelected : null]}> <Card style={[styles.glazeCard, isSelected ? styles.glazeCardSelected : null]}>
<View style={styles.glazeMainContent}> <View style={styles.glazeMainContent}>
{item.color && ( {item.color ? (
<View style={[styles.colorPreview, { backgroundColor: item.color }]} /> <PotteryIcon
color={item.color}
finish={item.finish}
size={60}
/>
) : (
<View style={styles.colorPreview} />
)} )}
<View style={styles.glazeInfo}> <View style={styles.glazeInfo}>
<View style={styles.glazeHeader}> <View style={styles.glazeHeader}>

View File

@ -281,7 +281,6 @@ const styles = StyleSheet.create({
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
header: { header: {
paddingHorizontal: spacing.lg,
paddingTop: 64, paddingTop: 64,
paddingBottom: spacing.lg, paddingBottom: spacing.lg,
backgroundColor: colors.background, backgroundColor: colors.background,
@ -294,6 +293,7 @@ const styles = StyleSheet.create({
color: colors.primary, color: colors.primary,
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',
paddingHorizontal: spacing.lg,
}, },
searchContainer: { searchContainer: {
flexDirection: 'row', flexDirection: 'row',
@ -359,10 +359,10 @@ const styles = StyleSheet.create({
color: colors.text, color: colors.text,
}, },
listContent: { listContent: {
paddingHorizontal: spacing.md,
paddingBottom: 100, // Extra space for floating tab bar paddingBottom: 100, // Extra space for floating tab bar
}, },
projectCard: { projectCard: {
marginHorizontal: spacing.md,
marginBottom: spacing.md, marginBottom: spacing.md,
}, },
imageContainer: { imageContainer: {