refactor: Update frontend components and Dockerfile for production
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 38s

- Changed the build command in Dockerfile from `npm run build` to `npm run build-only` for optimized production builds.
- Simplified service worker initialization by removing conditional precaching logic.
- Enhanced styling and structure in `VListItem.vue` and `ListDetailPage.vue` for better readability and consistency with Valerie UI components.
- Improved focus handling in item addition and editing processes for better user experience.
- Cleaned up unused CSS classes and ensured consistent usage of Valerie UI components across the application.
This commit is contained in:
mohamad 2025-06-01 14:59:30 +02:00
parent ed76816a32
commit d05200b623
4 changed files with 301 additions and 154 deletions

View File

@ -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

View File

@ -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,7 +162,8 @@ 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.
.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.
@ -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.
@ -233,13 +236,16 @@ export default defineComponent({
.list-item.is-swiped .list-item-content-wrapper {
transform: translateX(-80px); // Assumes right actions are 80px.
}
// And left actions (if any)
.list-item.is-left-swiped .list-item-content-wrapper { // Hypothetical class
.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

View File

@ -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 {
@ -729,7 +739,7 @@ const openOcrDialog = () => {
// 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 = '';
if (inputElement) (inputElement as HTMLInputElement).value = '';
} else if (ocrFileInputRef.value) { // Fallback if ref is native input
(ocrFileInputRef.value as any).value = '';
}
@ -776,7 +786,7 @@ const handleOcrUpload = async (file: File) => {
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 (inputElement) (inputElement as HTMLInputElement).value = '';
} else if (ocrFileInputRef.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>

View File

@ -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();