commit CLAUDE.md
This commit is contained in:
parent
e062dfe2b8
commit
1a5eee57cb
1 changed files with 260 additions and 1 deletions
261
CLAUDE.md
261
CLAUDE.md
|
|
@ -342,6 +342,42 @@ const onSubmit = form.handleSubmit(async (values) => {
|
|||
- ✅ **Checkbox arrays**: Use nested FormField pattern for multiple checkbox selection
|
||||
- ✅ **Type safety**: Zod schema provides full TypeScript type safety
|
||||
|
||||
**⚠️ CRITICAL: Checkbox Component Binding**
|
||||
|
||||
For Shadcn/ui Checkbox components, you MUST use the correct Vue.js binding pattern:
|
||||
|
||||
```vue
|
||||
<!-- ✅ CORRECT: Use model-value and @update:model-value for custom components -->
|
||||
<FormField v-slot="{ value, handleChange }" name="active">
|
||||
<FormItem>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
:model-value="value"
|
||||
@update:model-value="handleChange"
|
||||
:disabled="isCreating"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>Make product active</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- ❌ WRONG: Don't use :checked for custom components -->
|
||||
<Checkbox
|
||||
:checked="value"
|
||||
@update:checked="handleChange"
|
||||
/>
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- ✅ **Custom Components**: Use `:model-value` and `@update:model-value` for Shadcn/ui components
|
||||
- ✅ **Native HTML**: Use `:checked` and `@change` only for native `<input type="checkbox">` elements
|
||||
- ✅ **Force Re-render**: Use dynamic `:key` if checkbox doesn't reflect initial form values
|
||||
- ❌ **Don't Mix**: Never mix checked/model-value patterns - they have different behaviors
|
||||
|
||||
**Reference**: [Vue.js Forms Documentation](https://vuejs.org/guide/essentials/forms.html)
|
||||
|
||||
**❌ NEVER do this:**
|
||||
```vue
|
||||
<!-- Wrong: Manual form handling without vee-validate -->
|
||||
|
|
@ -703,4 +739,227 @@ export function useMyModule() {
|
|||
**Environment:**
|
||||
- Nostr relay configuration via `VITE_NOSTR_RELAYS` environment variable
|
||||
- PWA manifest configured for standalone app experience
|
||||
- Service worker with automatic updates every hour
|
||||
- Service worker with automatic updates every hour
|
||||
|
||||
## Mobile Browser File Input & Form Refresh Issues
|
||||
|
||||
### **Problem Overview**
|
||||
|
||||
Mobile browsers (especially Android) have well-documented issues with file inputs that can cause intermittent page refreshes during image upload operations. This is not a bug in our code, but rather a systemic issue with mobile browser memory management and activity lifecycle.
|
||||
|
||||
### **Root Causes**
|
||||
|
||||
1. **Memory-Induced Refreshes**: Android browsers may reload pages after file selection due to memory pressure when the camera or file chooser app is opened
|
||||
2. **Activity Lifecycle Kills**: Mobile operating systems can kill browser activities in the background during file selection, causing page reloads when the browser activity is restored
|
||||
3. **Visibility State Changes**: Screen lock/unlock and app switching can trigger visibility changes that affect authentication state evaluation, causing router guards to redirect
|
||||
4. **Android 14/15 Camera Issues**: Chrome on Android 14/15 has broken camera capture functionality, often triggering gallery first before camera
|
||||
|
||||
### **Defensive Programming Solutions**
|
||||
|
||||
**Implementation Files:**
|
||||
- `src/modules/base/components/ImageUpload.vue` - Multi-layer form submission prevention
|
||||
- `src/modules/market/components/CreateProductDialog.vue` - Visibility-based navigation protection
|
||||
|
||||
**1. Multi-Layer Form Submission Prevention:**
|
||||
```javascript
|
||||
// DEFENSIVE: Multiple layers of form submission prevention during upload
|
||||
const forms = document.querySelectorAll('form')
|
||||
forms.forEach(form => {
|
||||
// Layer 1: Override onsubmit handler directly
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// Layer 2: Add capturing event listener as backup
|
||||
form.addEventListener('submit', preventSubmit, true)
|
||||
})
|
||||
```
|
||||
|
||||
**2. Window-Level Submit Blocking:**
|
||||
```javascript
|
||||
// DEFENSIVE: Add temporary form submit blocker at window level
|
||||
const submitBlocker = (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
}
|
||||
window.addEventListener('submit', submitBlocker, true)
|
||||
|
||||
// Remove after operation completes
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('submit', submitBlocker, true)
|
||||
}, 500)
|
||||
```
|
||||
|
||||
**3. Visibility Change Protection:**
|
||||
```javascript
|
||||
// DEFENSIVE: Protect against screen wake/visibility triggered refreshes
|
||||
const blockVisibilityRefresh = (_event: Event) => {
|
||||
if (isDialogActive && props.isOpen) {
|
||||
console.warn('Visibility change detected while form is open', {
|
||||
visibilityState: document.visibilityState,
|
||||
isOpen: props.isOpen,
|
||||
hasData: !!form.values
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', blockVisibilityRefresh)
|
||||
```
|
||||
|
||||
**4. BeforeUnload User Confirmation:**
|
||||
```javascript
|
||||
// DEFENSIVE: Show user confirmation dialog for navigation attempts
|
||||
const blockNavigation = (event: BeforeUnloadEvent) => {
|
||||
if (isDialogActive && props.isOpen) {
|
||||
event.preventDefault()
|
||||
event.returnValue = 'You have unsaved changes. Are you sure you want to leave?'
|
||||
return event.returnValue
|
||||
}
|
||||
}
|
||||
window.addEventListener('beforeunload', blockNavigation)
|
||||
```
|
||||
|
||||
**5. Camera Input Separation:**
|
||||
```vue
|
||||
<!-- CRITICAL: Separate camera input without 'multiple' attribute -->
|
||||
<!-- Android browsers require separate inputs for proper camera/gallery handling -->
|
||||
<input
|
||||
ref="cameraInput"
|
||||
type="file"
|
||||
@change.stop.prevent="handleFileSelect"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
:disabled="disabled"
|
||||
tabindex="-1"
|
||||
hidden
|
||||
/>
|
||||
|
||||
<!-- Gallery input with multiple support -->
|
||||
<input
|
||||
ref="galleryInput"
|
||||
type="file"
|
||||
@change.stop.prevent="handleFileSelect"
|
||||
accept="image/*"
|
||||
:multiple="multiple"
|
||||
:disabled="disabled"
|
||||
tabindex="-1"
|
||||
hidden
|
||||
/>
|
||||
```
|
||||
|
||||
### **Vue.js Event Handling Best Practices**
|
||||
|
||||
**Use Vue Event Modifiers:**
|
||||
```vue
|
||||
<!-- ✅ CORRECT: Use Vue event modifiers -->
|
||||
<input @change.stop.prevent="handleFileSelect" />
|
||||
<button @click.stop="triggerCameraInput">Camera</button>
|
||||
|
||||
<!-- ❌ WRONG: Manual event handling in methods -->
|
||||
<input @change="handleFileSelect" />
|
||||
<!-- Then manually calling event.preventDefault() in method -->
|
||||
```
|
||||
|
||||
**Proper Form Submission Handling:**
|
||||
```vue
|
||||
<!-- ✅ CORRECT: Use form.handleSubmit (automatically prevents default) -->
|
||||
<form @submit="onSubmit">
|
||||
|
||||
<!-- ❌ WRONG: Manual preventDefault -->
|
||||
<form @submit.prevent="handleSubmit">
|
||||
```
|
||||
|
||||
### **Android 14/15 Camera Workarounds**
|
||||
|
||||
**Non-Standard MIME Type Workaround:**
|
||||
```html
|
||||
<!-- Add non-standard MIME type to force camera access -->
|
||||
<input type="file" accept="image/*,android/allowCamera" capture="environment" />
|
||||
```
|
||||
|
||||
**Plain File Input Fallback:**
|
||||
```html
|
||||
<!-- Fallback: Plain file input shows both camera and gallery options -->
|
||||
<input type="file" accept="image/*" />
|
||||
```
|
||||
|
||||
### **Industry-Standard Patterns**
|
||||
|
||||
**1. Page Visibility API (Primary Solution):**
|
||||
```javascript
|
||||
// Modern browsers: Use Page Visibility API instead of beforeunload
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// Resume critical operations, restore connections
|
||||
resumeOperations()
|
||||
} else {
|
||||
// Save state, pause operations for battery conservation
|
||||
saveStateAndPause()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**2. Conditional BeforeUnload Protection:**
|
||||
```javascript
|
||||
// Only add beforeunload listeners when user has unsaved changes
|
||||
const addFormProtection = (hasUnsavedChanges) => {
|
||||
if (hasUnsavedChanges) {
|
||||
window.addEventListener('beforeunload', preventUnload)
|
||||
} else {
|
||||
window.removeEventListener('beforeunload', preventUnload)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Session Recovery Pattern:**
|
||||
```javascript
|
||||
// Save form state on visibility change
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
localStorage.setItem('formDraft', JSON.stringify(formData))
|
||||
}
|
||||
})
|
||||
|
||||
// Restore on page load
|
||||
window.addEventListener('load', () => {
|
||||
const draft = localStorage.getItem('formDraft')
|
||||
if (draft) restoreFormData(JSON.parse(draft))
|
||||
})
|
||||
```
|
||||
|
||||
### **Testing & Debugging**
|
||||
|
||||
**Reproduction Steps:**
|
||||
1. Open form with file upload on mobile device
|
||||
2. Select camera input during image upload operations
|
||||
3. Turn screen off/on during upload process
|
||||
4. Switch between apps during file selection
|
||||
5. Low memory conditions during camera usage
|
||||
|
||||
**Success Indicators:**
|
||||
- User sees confirmation dialog instead of losing form data
|
||||
- Console warnings show visibility change detection working
|
||||
- Form state preservation during app switching
|
||||
- Camera input properly separates from gallery input
|
||||
|
||||
**Debug Console Messages:**
|
||||
```javascript
|
||||
// Look for these defensive programming console messages
|
||||
console.warn('Form submission blocked during file upload')
|
||||
console.warn('Visibility change detected while form is open')
|
||||
```
|
||||
|
||||
### **Key Takeaways**
|
||||
|
||||
1. **This is a systemic mobile browser issue**, not a bug in our application code
|
||||
2. **Multi-layer defensive programming** is the industry-standard solution
|
||||
3. **Page Visibility API** is more reliable than beforeunload events on mobile
|
||||
4. **User confirmation dialogs** provide the last line of defense against data loss
|
||||
5. **Separate camera/gallery inputs** are required for proper Android browser support
|
||||
6. **The defensive measures are working correctly** when users can choose to prevent navigation
|
||||
|
||||
**⚠️ IMPORTANT**: These issues are intermittent by nature. The defensive programming approach ensures that when they do occur, users have the opportunity to save their work instead of losing form data.
|
||||
Loading…
Add table
Add a link
Reference in a new issue