Merge pull request 'refactor: Update frontend components and Dockerfile for production' (#5) from ph4 into prod
Reviewed-on: #5
This commit is contained in:
commit
258798846d
@ -32,7 +32,7 @@ COPY . .
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
RUN npm run build-only
|
||||
|
||||
# Production stage
|
||||
FROM node:slim AS production
|
||||
|
@ -84,7 +84,7 @@ export default defineComponent({
|
||||
// Add other common styling for list item content area
|
||||
// e.g., text color, font size
|
||||
background-color: inherit; // Inherit from .list-item, can be overridden
|
||||
// Useful so that when it slides, it has the right bg
|
||||
// Useful so that when it slides, it has the right bg
|
||||
}
|
||||
|
||||
.swipe-actions {
|
||||
@ -108,7 +108,8 @@ export default defineComponent({
|
||||
transform: translateX(-100%); // Initially hidden to the left
|
||||
transition: transform 0.3s ease-out;
|
||||
|
||||
.list-item.is-swiped & { // When swiped to reveal left actions
|
||||
.list-item.is-swiped & {
|
||||
// When swiped to reveal left actions
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@ -161,9 +162,10 @@ export default defineComponent({
|
||||
// If wrapper translates right, it reveals the list-item's left-actions.
|
||||
|
||||
// Reveal right actions by translating the list-item-content-wrapper to the left
|
||||
.list-item.is-swiped & { // This assumes isSwiped means revealing RIGHT actions.
|
||||
// Need differentiation if both left/right can be revealed independently.
|
||||
// For now, isSwiped reveals right.
|
||||
.list-item.is-swiped & {
|
||||
// This assumes isSwiped means revealing RIGHT actions.
|
||||
// Need differentiation if both left/right can be revealed independently.
|
||||
// For now, isSwiped reveals right.
|
||||
// This class is on .list-item. The .swipe-actions-right is inside the wrapper.
|
||||
// So, the wrapper needs to translate.
|
||||
// No, this is fine. .list-item.is-swiped controls the transform of list-item-content-wrapper.
|
||||
@ -175,6 +177,7 @@ export default defineComponent({
|
||||
// This logic should be on .list-item-content-wrapper based on .is-swiped of parent.
|
||||
}
|
||||
}
|
||||
|
||||
// Adjusting transform on list-item-content-wrapper based on parent .is-swiped
|
||||
.list-item.is-swiped .list-item-content-wrapper {
|
||||
// This needs to be dynamic based on which actions are shown and their width.
|
||||
@ -231,15 +234,18 @@ export default defineComponent({
|
||||
// A true swipe needs JS to measure or fixed widths.
|
||||
// Let's go with a fixed transform for now for demo purposes.
|
||||
.list-item.is-swiped .list-item-content-wrapper {
|
||||
transform: translateX(-80px); // Assumes right actions are 80px.
|
||||
transform: translateX(-80px); // Assumes right actions are 80px.
|
||||
}
|
||||
// And left actions (if any)
|
||||
.list-item.is-left-swiped .list-item-content-wrapper { // Hypothetical class
|
||||
transform: translateX(80px); // Assumes left actions are 80px.
|
||||
|
||||
// And left actions (if any)
|
||||
.list-item.is-left-swiped .list-item-content-wrapper {
|
||||
// Hypothetical class
|
||||
transform: translateX(80px); // Assumes left actions are 80px.
|
||||
}
|
||||
|
||||
// Since `isSwiped` is boolean, it can only control one state.
|
||||
// Let's assume `isSwiped` means "the right actions are visible".
|
||||
|
||||
}
|
||||
|
||||
.list-item.completed {
|
||||
.list-item-content {
|
||||
@ -248,6 +254,7 @@ export default defineComponent({
|
||||
// text-decoration: line-through;
|
||||
background-color: #f0f8ff; // Light blue background for completed
|
||||
}
|
||||
|
||||
// You might want to disable swipe on completed items or style them differently
|
||||
&.swipable .list-item-content {
|
||||
// Specific style for swipable AND completed
|
||||
|
@ -5,17 +5,21 @@
|
||||
</div>
|
||||
|
||||
<VAlert v-else-if="error && !list" type="error" :message="error" class="mb-4">
|
||||
<template #actions> <VButton @click="fetchListDetails">Retry</VButton> </template>
|
||||
<template #actions>
|
||||
<VButton @click="fetchListDetails">Retry</VButton>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<template v-else-if="list">
|
||||
<!-- Header -->
|
||||
<div class="neo-list-header">
|
||||
<VHeading level="1" :text="list.name" class="mb-3 neo-title" /> {/* Kept neo-title for existing style */}
|
||||
<VHeading :level="1" :text="list.name" class="mb-3 neo-title" />
|
||||
<div class="neo-header-actions">
|
||||
<VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">Cost Summary</VButton>
|
||||
<VButton @click="showCostSummaryDialog = true" :disabled="!isOnline" icon-left="clipboard">Cost Summary
|
||||
</VButton>
|
||||
<VButton @click="openOcrDialog" :disabled="!isOnline" icon-left="plus">Add via OCR</VButton>
|
||||
<VBadge :text="list.group_id ? 'Group List' : 'Personal List'" :variant="list.group_id ? 'info' : 'success'" class="neo-status" /> {/* Kept neo-status for existing style */}
|
||||
<VBadge :text="list.group_id ? 'Group List' : 'Personal List'" :variant="list.group_id ? 'accent' : 'settled'"
|
||||
class="neo-status" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="list.description" class="neo-description">{{ list.description }}</p>
|
||||
@ -24,41 +28,34 @@
|
||||
<VCard v-if="itemsAreLoading" class="py-10 text-center mt-4">
|
||||
<VSpinner label="Loading items..." size="lg" />
|
||||
</VCard>
|
||||
<VCard v-else-if="!itemsAreLoading && list.items.length === 0" variant="empty-state" empty-icon="clipboard" empty-title="No Items Yet!" empty-message="Add some items using the form below." class="mt-4" />
|
||||
<VCard v-else-if="!itemsAreLoading && list.items.length === 0" variant="empty-state" empty-icon="clipboard"
|
||||
empty-title="No Items Yet!" empty-message="Add some items using the form below." class="mt-4" />
|
||||
<VCard v-else class="mt-4">
|
||||
<VList class="item-list-tight"> {/* Assuming item-list-tight might be needed or VList default is fine */}
|
||||
<VListItem v-for="item in list.items" :key="item.id" class="item-with-actions" :class="{ 'bg-gray-100 opacity-70': item.is_complete }">
|
||||
<VList class="item-list-tight">
|
||||
<VListItem v-for="item in list.items" :key="item.id" class="item-with-actions"
|
||||
:class="{ 'bg-gray-100 opacity-70': item.is_complete }">
|
||||
<template #default>
|
||||
<div class="flex items-center flex-grow gap-2">
|
||||
<VCheckbox
|
||||
:model-value="item.is_complete"
|
||||
@update:modelValue="confirmUpdateItem(item, $event)"
|
||||
:disabled="item.updating"
|
||||
:aria-label="item.name"
|
||||
/>
|
||||
<VCheckbox :model-value="item.is_complete" @update:modelValue="confirmUpdateItem(item, $event)"
|
||||
:disabled="item.updating" :aria-label="item.name" />
|
||||
<div class="flex-grow">
|
||||
<span class="item-name" :class="{'line-through': item.is_complete}">{{ item.name }}</span>
|
||||
<span class="item-name" :class="{ 'line-through': item.is_complete }">{{ item.name }}</span>
|
||||
<span v-if="item.quantity" class="text-sm text-gray-500 ml-1">× {{ item.quantity }}</span>
|
||||
<div v-if="item.is_complete" class="mt-1">
|
||||
<VInput
|
||||
type="number"
|
||||
:model-value="item.priceInput"
|
||||
@update:modelValue="item.priceInput = $event"
|
||||
placeholder="Price"
|
||||
size="sm"
|
||||
class="w-24"
|
||||
step="0.01"
|
||||
@blur="updateItemPrice(item)"
|
||||
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()"
|
||||
/>
|
||||
<VInput type="number" :model-value="item.priceInput || ''"
|
||||
@update:modelValue="item.priceInput = $event" placeholder="Price" size="sm" class="w-24"
|
||||
step="0.01" @blur="updateItemPrice(item)"
|
||||
@keydown.enter.prevent="($event.target as HTMLInputElement).blur()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 ml-2">
|
||||
<VButton icon-only="true" size="sm" variant="ghost" @click.stop="editItem(item)" aria-label="Edit item">
|
||||
<VButton :icon-only="true" size="sm" variant="neutral" @click.stop="editItem(item)"
|
||||
aria-label="Edit item">
|
||||
<VIcon name="edit" />
|
||||
</VButton>
|
||||
<VButton icon-only="true" size="sm" variant="ghost" color="danger" @click.stop="confirmDeleteItem(item)" :disabled="item.deleting" aria-label="Delete item">
|
||||
<VButton :icon-only="true" size="sm" variant="neutral" color="danger"
|
||||
@click.stop="confirmDeleteItem(item)" :disabled="item.deleting" aria-label="Delete item">
|
||||
<VIcon name="trash" />
|
||||
</VButton>
|
||||
</div>
|
||||
@ -69,24 +66,15 @@
|
||||
|
||||
<!-- Add New Item Form -->
|
||||
<form @submit.prevent="onAddItem" class="add-item-form mt-4 p-4 border rounded-lg shadow flex items-center gap-2">
|
||||
<VIcon name="plus-circle" class="text-gray-400 shrink-0" /> {/* Added shrink-0 */}
|
||||
<VIcon name="plus-circle" class="text-gray-400 shrink-0" />
|
||||
<VFormField class="flex-grow" label="New item name" :label-sr-only="true">
|
||||
<VInput
|
||||
v-model="newItem.name"
|
||||
placeholder="Add a new item"
|
||||
required
|
||||
ref="itemNameInputRef"
|
||||
/>
|
||||
<VInput v-model="newItem.name" placeholder="Add a new item" required ref="itemNameInputRef" />
|
||||
</VFormField>
|
||||
<VFormField label="Quantity" :label-sr-only="true" class="w-24 shrink-0"> {/* Added shrink-0 and changed w-20 to w-24 for better fit */}
|
||||
<VInput
|
||||
type="number"
|
||||
v-model="newItem.quantity"
|
||||
placeholder="Qty"
|
||||
min="1"
|
||||
/>
|
||||
<VFormField label="Quantity" :label-sr-only="true" class="w-24 shrink-0">
|
||||
<VInput type="number" :model-value="newItem.quantity || ''" @update:modelValue="newItem.quantity = $event"
|
||||
placeholder="Qty" min="1" />
|
||||
</VFormField>
|
||||
<VButton type="submit" :disabled="addingItem" class="shrink-0"> {/* Added shrink-0 */}
|
||||
<VButton type="submit" :disabled="addingItem" class="shrink-0">
|
||||
<VSpinner v-if="addingItem" size="sm" />
|
||||
<span v-else>Add</span>
|
||||
</VButton>
|
||||
@ -167,32 +155,38 @@
|
||||
<!-- OCR Dialog -->
|
||||
<VModal v-model="showOcrDialogState" title="Add Items via OCR" @update:modelValue="!$event && closeOcrDialog()">
|
||||
<template #default>
|
||||
<div v-if="ocrLoading" class="text-center"><VSpinner label="Processing image..." /></div>
|
||||
<div v-if="ocrLoading" class="text-center">
|
||||
<VSpinner label="Processing image..." />
|
||||
</div>
|
||||
<VList v-else-if="ocrItems.length > 0">
|
||||
<VListItem v-for="(ocrItem, index) in ocrItems" :key="index">
|
||||
<div class="flex items-center gap-2">
|
||||
<VInput type="text" v-model="ocrItem.name" class="flex-grow" required />
|
||||
<VButton variant="danger" size="sm" :icon-only="true" iconLeft="trash" @click="ocrItems.splice(index, 1)" />
|
||||
<VButton variant="danger" size="sm" :icon-only="true" iconLeft="trash"
|
||||
@click="ocrItems.splice(index, 1)" />
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VFormField v-else label="Upload Image" :error-message="ocrError">
|
||||
<VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef" />
|
||||
<VFormField v-else label="Upload Image" :error-message="ocrError || undefined">
|
||||
<VInput type="file" id="ocrFile" accept="image/*" @change="handleOcrFileUpload" ref="ocrFileInputRef"
|
||||
:model-value="''" />
|
||||
</VFormField>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeOcrDialog">Cancel</VButton>
|
||||
<VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="addOcrItems" :disabled="addingOcrItems">
|
||||
<VSpinner v-if="addingOcrItems" size="sm"/> Add Items
|
||||
<VButton v-if="ocrItems.length > 0" type="button" variant="primary" @click="addOcrItems"
|
||||
:disabled="addingOcrItems">
|
||||
<VSpinner v-if="addingOcrItems" size="sm" /> Add Items
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<VModal v-model="showConfirmDialogState" title="Confirmation" @update:modelValue="!$event && cancelConfirmation()" size="sm">
|
||||
<VModal v-model="showConfirmDialogState" title="Confirmation" @update:modelValue="!$event && cancelConfirmation()"
|
||||
size="sm">
|
||||
<template #default>
|
||||
<div class="text-center">
|
||||
<VIcon name="alert-triangle" size="lg" class="text-yellow-500 mb-2" /> {/* Ensure text-yellow-500 is defined or use VAlert type="warning" */}
|
||||
<VIcon name="alert-triangle" size="lg" class="text-yellow-500 mb-2" />
|
||||
<p>{{ confirmDialogMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -203,9 +197,12 @@
|
||||
</VModal>
|
||||
|
||||
<!-- Cost Summary Dialog -->
|
||||
<VModal v-model="showCostSummaryDialog" title="List Cost Summary" @update:modelValue="showCostSummaryDialog = false" size="lg">
|
||||
<VModal v-model="showCostSummaryDialog" title="List Cost Summary" @update:modelValue="showCostSummaryDialog = false"
|
||||
size="lg">
|
||||
<template #default>
|
||||
<div v-if="costSummaryLoading" class="text-center"><VSpinner label="Loading summary..." /></div>
|
||||
<div v-if="costSummaryLoading" class="text-center">
|
||||
<VSpinner label="Loading summary..." />
|
||||
</div>
|
||||
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
|
||||
<div v-else-if="listCostSummary">
|
||||
<div class="mb-3 cost-overview">
|
||||
@ -230,7 +227,8 @@
|
||||
<td class="text-right">{{ formatCurrency(userShare.items_added_value) }}</td>
|
||||
<td class="text-right">{{ formatCurrency(userShare.amount_due) }}</td>
|
||||
<td class="text-right">
|
||||
<VBadge :text="formatCurrency(userShare.balance)" :variant="parseFloat(String(userShare.balance)) >= 0 ? 'success' : 'pending'" />
|
||||
<VBadge :text="formatCurrency(userShare.balance)"
|
||||
:variant="parseFloat(String(userShare.balance)) >= 0 ? 'settled' : 'pending'" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -245,19 +243,23 @@
|
||||
</VModal>
|
||||
|
||||
<!-- Settle Share Modal -->
|
||||
<VModal v-model="showSettleModal" title="Settle Share" @update:modelValue="!$event && closeSettleShareModal()" size="md">
|
||||
<VModal v-model="showSettleModal" title="Settle Share" @update:modelValue="!$event && closeSettleShareModal()"
|
||||
size="md">
|
||||
<template #default>
|
||||
<div v-if="isSettlementLoading" class="text-center"><VSpinner label="Processing settlement..." /></div>
|
||||
<div v-if="isSettlementLoading" class="text-center">
|
||||
<VSpinner label="Processing settlement..." />
|
||||
</div>
|
||||
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
|
||||
<div v-else>
|
||||
<p>Settle amount for {{ selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}` }}:</p>
|
||||
<VFormField label="Amount" :error-message="settleAmountError"> {/* Error message was shown above, consider if needed here too */}
|
||||
<p>Settle amount for {{ selectedSplitForSettlement?.user?.name || selectedSplitForSettlement?.user?.email ||
|
||||
`User ID: ${selectedSplitForSettlement?.user_id}` }}:</p>
|
||||
<VFormField label="Amount" :error-message="settleAmountError || undefined">
|
||||
<VInput type="number" v-model="settleAmount" id="settleAmount" required />
|
||||
</VFormField>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeSettleShareModal">Cancel</VButton> {/* Added Cancel for consistency */}
|
||||
<VButton variant="neutral" @click="closeSettleShareModal">Cancel</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmSettle">Confirm</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
@ -265,20 +267,22 @@
|
||||
<!-- Edit Item Dialog -->
|
||||
<VModal v-model="showEditDialog" title="Edit Item" @update:modelValue="!$event && closeEditDialog()">
|
||||
<template #default>
|
||||
<VFormField v-if="editingItem" label="Item Name" class="mb-4"> {/* Added margin */}
|
||||
<VFormField v-if="editingItem" label="Item Name" class="mb-4">
|
||||
<VInput type="text" id="editItemName" v-model="editingItem.name" required />
|
||||
</VFormField>
|
||||
<VFormField v-if="editingItem" label="Quantity">
|
||||
<VInput type="number" id="editItemQuantity" v-model.number="editingItem.quantity" min="1" />
|
||||
<VInput type="number" id="editItemQuantity" :model-value="editingItem.quantity || ''"
|
||||
@update:modelValue="editingItem.quantity = $event" min="1" />
|
||||
</VFormField>
|
||||
</template>
|
||||
<template #footer>
|
||||
<VButton variant="neutral" @click="closeEditDialog">Cancel</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">Save Changes</VButton>
|
||||
<VButton variant="primary" @click="handleConfirmEdit" :disabled="!editingItem?.name.trim()">Save Changes
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
<VAlert v-else type="info" message="Group not found or an error occurred." />
|
||||
<VAlert v-if="!list && !pageInitialLoad" type="info" message="Group not found or an error occurred." />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -533,7 +537,9 @@ const isItemPendingSync = (item: Item) => {
|
||||
const onAddItem = async () => {
|
||||
if (!list.value || !newItem.value.name.trim()) {
|
||||
notificationStore.addNotification({ message: 'Please enter an item name.', type: 'warning' });
|
||||
itemNameInputRef.value?.focus?.(); // Updated focus call
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
addingItem.value = true;
|
||||
@ -569,7 +575,9 @@ const onAddItem = async () => {
|
||||
};
|
||||
list.value.items.push(optimisticItem);
|
||||
newItem.value = { name: '' };
|
||||
itemNameInputRef.value?.focus?.(); // Updated focus call
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
addingItem.value = false;
|
||||
return;
|
||||
}
|
||||
@ -585,7 +593,9 @@ const onAddItem = async () => {
|
||||
const addedItem = response.data as Item;
|
||||
list.value.items.push(processListItems([addedItem])[0]);
|
||||
newItem.value = { name: '' };
|
||||
itemNameInputRef.value?.focus?.(); // Updated focus call
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
} catch (err) {
|
||||
notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' });
|
||||
} finally {
|
||||
@ -728,10 +738,10 @@ const openOcrDialog = () => {
|
||||
// For VInput type file, direct .value = '' might not work or be needed.
|
||||
// VInput should handle its own reset if necessary, or this ref might target the native input inside.
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) { // Assuming VInput exposes $el
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if(inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if (inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
} else if (ocrFileInputRef.value) { // Fallback if ref is native input
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -774,11 +784,11 @@ const handleOcrUpload = async (file: File) => {
|
||||
ocrError.value = (err instanceof Error ? err.message : String(err)) || 'Failed to process image.';
|
||||
} finally {
|
||||
ocrLoading.value = false;
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) {
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if(inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
if (ocrFileInputRef.value && ocrFileInputRef.value.$el) {
|
||||
const inputElement = ocrFileInputRef.value.$el.querySelector('input[type="file"]') || ocrFileInputRef.value.$el;
|
||||
if (inputElement) (inputElement as HTMLInputElement).value = '';
|
||||
} else if (ocrFileInputRef.value) {
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
(ocrFileInputRef.value as any).value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -872,7 +882,9 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
itemNameInputRef.value?.focus?.(); // Updated focus call
|
||||
if (itemNameInputRef.value?.$el) {
|
||||
(itemNameInputRef.value.$el as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1340,12 +1352,14 @@ const handleExpenseCreated = (expense: any) => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-name { /* Added for VListItem content */
|
||||
.item-name {
|
||||
/* Added for VListItem content */
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.neo-item-complete .item-name { /* Adjusted for VListItem */
|
||||
.neo-item-complete .item-name {
|
||||
/* Adjusted for VListItem */
|
||||
text-decoration: line-through;
|
||||
/* opacity: 0.6; Combined with bg-gray-100 opacity-70 on VListItem */
|
||||
}
|
||||
@ -1409,17 +1423,14 @@ const handleExpenseCreated = (expense: any) => {
|
||||
}
|
||||
|
||||
.neo-action-button {
|
||||
background: #fff;
|
||||
border: 3px solid #111;
|
||||
/* General button, mostly replaced by VButton */
|
||||
background: #111;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
box-shadow: 3px 3px 0 #111;
|
||||
transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.neo-action-button:hover {
|
||||
@ -1437,7 +1448,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.add-item-form { /* Added for new form styling */
|
||||
.add-item-form {
|
||||
/* Added for new form styling */
|
||||
/* display: flex; (already on class) */
|
||||
/* gap: 0.5rem; (already on class) */
|
||||
/* margin-top: 1rem; (original was 2rem, now mt-4) */
|
||||
@ -1449,12 +1461,14 @@ const handleExpenseCreated = (expense: any) => {
|
||||
}
|
||||
|
||||
|
||||
.neo-new-item-form { /* Kept for reference, but form tag itself is now styled */
|
||||
.neo-new-item-form {
|
||||
/* Kept for reference, but form tag itself is now styled */
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.neo-text-input { /* Not directly used by VInput, but kept for reference */
|
||||
.neo-text-input {
|
||||
/* Not directly used by VInput, but kept for reference */
|
||||
flex-grow: 1;
|
||||
border: 2px solid #111;
|
||||
border-radius: 8px;
|
||||
@ -1463,7 +1477,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.neo-new-item-input { /* Not directly used by VInput, but kept for reference */
|
||||
.neo-new-item-input {
|
||||
/* Not directly used by VInput, but kept for reference */
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@ -1475,13 +1490,16 @@ const handleExpenseCreated = (expense: any) => {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.neo-new-item-input::placeholder { /* VInput handles its own placeholder styling */
|
||||
.neo-new-item-input::placeholder {
|
||||
/* VInput handles its own placeholder styling */
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.neo-quantity-input { /* Not directly used by VInput, but kept for reference */
|
||||
width: 80px; /* This specific width is now on VFormField for quantity */
|
||||
.neo-quantity-input {
|
||||
/* Not directly used by VInput, but kept for reference */
|
||||
width: 80px;
|
||||
/* This specific width is now on VFormField for quantity */
|
||||
border: 2px solid #111;
|
||||
border-radius: 8px;
|
||||
padding: 0.4rem;
|
||||
@ -1489,7 +1507,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.neo-number-input { /* For price input, now VInput with class="w-24" */
|
||||
.neo-number-input {
|
||||
/* For price input, now VInput with class="w-24" */
|
||||
border: 2px solid #111;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
@ -1497,7 +1516,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.neo-add-button { /* Replaced by VButton */
|
||||
.neo-add-button {
|
||||
/* Replaced by VButton */
|
||||
background: #111;
|
||||
color: white;
|
||||
border: none;
|
||||
@ -1509,7 +1529,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.neo-button { /* General button, mostly replaced by VButton */
|
||||
.neo-button {
|
||||
/* General button, mostly replaced by VButton */
|
||||
background: #111;
|
||||
color: white;
|
||||
border: none;
|
||||
@ -1520,7 +1541,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.new-item-input { /* Styling for the old li wrapper of add item form, can be removed */
|
||||
.new-item-input {
|
||||
/* Styling for the old li wrapper of add item form, can be removed */
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@ -1561,7 +1583,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.neo-action-button { /* VButton has its own sizing */
|
||||
.neo-action-button {
|
||||
/* VButton has its own sizing */
|
||||
padding: 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@ -1575,7 +1598,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
padding: 1rem;
|
||||
} */
|
||||
|
||||
.item-name { /* Adjusted for VListItem */
|
||||
.item-name {
|
||||
/* Adjusted for VListItem */
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@ -1588,11 +1612,13 @@ const handleExpenseCreated = (expense: any) => {
|
||||
height: 1.4em;
|
||||
} */
|
||||
|
||||
.neo-icon-button { /* VButton icon-only replaces this */
|
||||
.neo-icon-button {
|
||||
/* VButton icon-only replaces this */
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.add-item-form { /* Adjusted form class */
|
||||
.add-item-form {
|
||||
/* Adjusted form class */
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@ -1617,21 +1643,25 @@ const handleExpenseCreated = (expense: any) => {
|
||||
} */
|
||||
|
||||
/* Optimize modals for mobile */
|
||||
.modal-container { /* VModal has its own responsive sizing via props/CSS */
|
||||
.modal-container {
|
||||
/* VModal has its own responsive sizing via props/CSS */
|
||||
width: 95%;
|
||||
max-height: 85vh;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.modal-header { /* VModal slot */
|
||||
.modal-header {
|
||||
/* VModal slot */
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body { /* VModal slot */
|
||||
.modal-body {
|
||||
/* VModal slot */
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-footer { /* VModal slot */
|
||||
.modal-footer {
|
||||
/* VModal slot */
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@ -1643,17 +1673,20 @@ const handleExpenseCreated = (expense: any) => {
|
||||
} */
|
||||
|
||||
/* Optimize loading states for mobile */
|
||||
.neo-loading-state { /* VSpinner used instead */
|
||||
.neo-loading-state {
|
||||
/* VSpinner used instead */
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.spinner-dots span { /* VSpinner has its own dot styling */
|
||||
.spinner-dots span {
|
||||
/* VSpinner has its own dot styling */
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Improve scrolling performance */
|
||||
.item-list-tight { /* Assuming VList with this class */
|
||||
.item-list-tight {
|
||||
/* Assuming VList with this class */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@ -1689,7 +1722,8 @@ const handleExpenseCreated = (expense: any) => {
|
||||
} */
|
||||
|
||||
/* Improve scrolling performance */
|
||||
.item-list-tight { /* Assuming VList with this class */
|
||||
.item-list-tight {
|
||||
/* Assuming VList with this class */
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
@ -1725,42 +1759,152 @@ const handleExpenseCreated = (expense: any) => {
|
||||
/* @keyframes dot-pulse { ... } */
|
||||
|
||||
/* Utility classes that might still be used or can be replaced by Tailwind/global equivalents */
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-1 { gap: 0.25rem; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.ml-1 { margin-left: 0.25rem; }
|
||||
.ml-2 { margin-left: 0.5rem; }
|
||||
.mt-1 { margin-top: 0.25rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 1rem; } /* Adjusted from 1.5rem to match common spacing */
|
||||
.mb-4 { margin-bottom: 1.5rem; }
|
||||
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
|
||||
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.border { border-width: 1px; /* Assuming default border color from global styles or Tailwind */ }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.shadow { box-shadow: 0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px 0 rgba(0,0,0,0.06); /* Example shadow */}
|
||||
.flex-grow { flex-grow: 1; }
|
||||
.w-24 { width: 6rem; } /* Tailwind w-24 */
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-gray-500 { color: #6b7280; } /* Tailwind gray-500 */
|
||||
.text-gray-400 { color: #9ca3af; } /* Tailwind gray-400 */
|
||||
.text-green-600 { color: #16a34a; } /* Tailwind green-600 */
|
||||
.text-yellow-500 { color: #eab308; } /* Tailwind yellow-500 */
|
||||
.line-through { text-decoration: line-through; }
|
||||
.opacity-50 { opacity: 0.5; }
|
||||
.opacity-60 { opacity: 0.6; } /* Added for completed item name */
|
||||
.opacity-70 { opacity: 0.7; } /* Added for completed item background */
|
||||
.shrink-0 { flex-shrink: 0; }
|
||||
.bg-gray-100 { background-color: #f3f4f6; } /* Tailwind gray-100 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Adjusted from 1.5rem to match common spacing */
|
||||
.mb-4 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.py-10 {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
/* Assuming default border color from global styles or Tailwind */
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
/* Example shadow */
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
/* Tailwind w-24 */
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Tailwind gray-500 */
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Tailwind gray-400 */
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
/* Tailwind green-600 */
|
||||
.text-yellow-500 {
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
/* Tailwind yellow-500 */
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.opacity-60 {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Added for completed item name */
|
||||
.opacity-70 {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Added for completed item background */
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Tailwind gray-100 */
|
||||
|
||||
/* Styles for .neo-list-card, .neo-item-list, .neo-item might be replaced by VCard/VList/VListItem defaults or props */
|
||||
/* Keeping some specific styles for .neo-item-details, .item-name, etc. if they are distinct. */
|
||||
.item-with-actions { /* Custom class for VListItem if needed for specific layout */
|
||||
.item-with-actions {
|
||||
/* Custom class for VListItem if needed for specific layout */
|
||||
/* Default VListItem is display:flex, so this might not be needed or just for minor tweaks */
|
||||
}
|
||||
</style>
|
||||
|
@ -41,11 +41,7 @@ const initializeSW = async () => {
|
||||
|
||||
// Use with precache injection
|
||||
// vite-plugin-pwa will populate self.__WB_MANIFEST
|
||||
if (self.__WB_MANIFEST) {
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
} else {
|
||||
console.warn('No manifest found for precaching');
|
||||
}
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user