From 813ed911f1b12c000b6442b7345c1560f38d66bb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 09:47:23 +0000 Subject: [PATCH] Okay, I've made some changes to integrate the Valerie UI components into the Account, Group Detail, and List Detail pages. This is part of the ongoing effort to standardize the UI and make the code easier to maintain. Here's a breakdown of the changes: 1. **`AccountPage.vue`**: * I replaced the main heading with `VHeading`. * I updated the loading spinner to `VSpinner`. * I converted the error alert to `VAlert` with an action button. * I refactored the Profile, Password, and Notifications sections to use `VCard` for their structure. * The form elements within these cards (name, email, passwords) now use `VFormField` and `VInput`. * Action buttons like "Save Changes" and "Change Password" are now `VButton` with an integrated `VSpinner` for loading states. * The notification preferences list uses `VList` and `VListItem`, with each preference toggle converted to `VToggleSwitch`. 2. **`GroupDetailPage.vue`**: * I updated the page-level loading spinner, error alert, and main heading to `VSpinner`, `VAlert`, and `VHeading`. * I refactored the "Group Members", "Invite Members", "Chores", and "Expenses" sections from custom "neo-card" styling to use `VCard`. * Headers within these cards use `VHeading` and action buttons use `VButton` (I kept Material Icons where `VIcon` wasn't a direct replacement). * Lists of members, chores, and expenses now use `VList` and `VListItem`. * Buttons within list items (e.g., "Remove member") are `VButton` with `VSpinner`. * Role indicators and frequency/split type "chips" are now `VBadge` components, and I updated the helper functions to return VBadge-compatible variants. * The "Invite Members" form elements (input for code, copy button) use `VFormField`, `VInput`, and `VButton`. * I simplified empty states within card bodies using `VIcon` and text. 3. **`ListDetailPage.vue`**: This complex page required several steps to refactor: * **Page-Level & Header:** I updated the loading state to `VSpinner`, the error alert to `VAlert`, and the main title to `VHeading`. Header action buttons are `VButton` with icons, and the list status is `VBadge`. * **Modals:** I converted all five custom modals (OCR, Confirmation, Edit Item, Settle Share, Cost Summary shell) to use `VModal`. Internal forms and actions within these modals now use `VFormField`, `VInput`, `VButton`, `VSpinner`, `VList`, `VListItem`, and `VAlert` as appropriate. I removed the `onClickOutside` logic. * **Main Items List:** The loading state uses `VCard` with `VSpinner`, and the empty state uses `VCard variant="empty-state"`. The list itself is now a `VCard` containing a `VList`. Each item is a `VListItem` with internal content refactored to use `VCheckbox`, `VInput` (for price), and `VButton` with `VIcon` for actions. * **Add Item Form:** I re-structured this below the items list, using `VFormField`, `VInput`, and `VButton` with `VIcon`. * **Expenses Section:** The main card uses `VCard` with `VHeading` and `VButton` in the header. Loading/error/empty states use `VSpinner`, `VAlert`, `VIcon`. The expenses list is `VList`, with each expense item as a `VListItem`. Statuses are `VBadge`. This refactoring significantly increases the usage of the Valerie UI component library across these key application pages. This should help create a more consistent experience for you and make development smoother. Next, I'll focus on the Chores-related pages. --- fe/src/pages/AccountPage.vue | 168 +++---- fe/src/pages/GroupDetailPage.vue | 259 +++++------ fe/src/pages/ListDetailPage.vue | 773 +++++++++++++------------------ 3 files changed, 515 insertions(+), 685 deletions(-) diff --git a/fe/src/pages/AccountPage.vue b/fe/src/pages/AccountPage.vue index 2d2a9f3..dff3ff0 100644 --- a/fe/src/pages/AccountPage.vue +++ b/fe/src/pages/AccountPage.vue @@ -1,116 +1,84 @@ @@ -118,6 +86,16 @@ import { ref, onMounted } from 'vue'; import { apiClient, API_ENDPOINTS } from '@/config/api'; // Assuming path import { useNotificationStore } from '@/stores/notifications'; +import VHeading from '@/components/valerie/VHeading.vue'; +import VSpinner from '@/components/valerie/VSpinner.vue'; +import VAlert from '@/components/valerie/VAlert.vue'; +import VCard from '@/components/valerie/VCard.vue'; +import VFormField from '@/components/valerie/VFormField.vue'; +import VInput from '@/components/valerie/VInput.vue'; +import VButton from '@/components/valerie/VButton.vue'; +import VToggleSwitch from '@/components/valerie/VToggleSwitch.vue'; +import VList from '@/components/valerie/VList.vue'; +import VListItem from '@/components/valerie/VListItem.vue'; interface Profile { name: string; diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue index 934bdb2..1c9ca17 100644 --- a/fe/src/pages/GroupDetailPage.vue +++ b/fe/src/pages/GroupDetailPage.vue @@ -1,82 +1,57 @@ @@ -171,6 +129,17 @@ import { choreService } from '../services/choreService' import type { Chore, ChoreFrequency } from '../types/chore' import { format } from 'date-fns' import type { Expense } from '@/types/expense' +import VHeading from '@/components/valerie/VHeading.vue'; +import VSpinner from '@/components/valerie/VSpinner.vue'; +import VAlert from '@/components/valerie/VAlert.vue'; +import VCard from '@/components/valerie/VCard.vue'; +import VList from '@/components/valerie/VList.vue'; +import VListItem from '@/components/valerie/VListItem.vue'; +import VButton from '@/components/valerie/VButton.vue'; +import VBadge from '@/components/valerie/VBadge.vue'; +import VInput from '@/components/valerie/VInput.vue'; +import VFormField from '@/components/valerie/VFormField.vue'; +import VIcon from '@/components/valerie/VIcon.vue'; interface Group { id: string | number; @@ -355,16 +324,16 @@ const formatFrequency = (frequency: ChoreFrequency) => { return options[frequency] || frequency } -const getFrequencyColor = (frequency: ChoreFrequency) => { - const colors: Record = { - one_time: 'grey', - daily: 'blue', - weekly: 'green', - monthly: 'purple', - custom: 'orange' - } - return colors[frequency] -} +const getFrequencyBadgeVariant = (frequency: ChoreFrequency): string => { + const colorMap: Record = { + one_time: 'neutral', + daily: 'info', + weekly: 'success', + monthly: 'accent', // Using accent for purple as an example + custom: 'warning' + }; + return colorMap[frequency] || 'secondary'; +}; // Add new methods for expenses const loadRecentExpenses = async () => { @@ -387,16 +356,16 @@ const formatSplitType = (type: string) => { ).join(' ') } -const getSplitTypeColor = (type: string) => { - const colors: Record = { - equal: 'blue', - exact_amounts: 'green', - percentage: 'purple', - shares: 'orange', - item_based: 'teal' - } - return colors[type] || 'grey' -} +const getSplitTypeBadgeVariant = (type: string): string => { + const colorMap: Record = { + equal: 'info', + exact_amounts: 'success', + percentage: 'accent', // Using accent for purple + shares: 'warning', + item_based: 'secondary', // Using secondary for teal as an example + }; + return colorMap[type] || 'neutral'; +}; onMounted(() => { fetchGroupDetails(); diff --git a/fe/src/pages/ListDetailPage.vue b/fe/src/pages/ListDetailPage.vue index f50b1da..44c863d 100644 --- a/fe/src/pages/ListDetailPage.vue +++ b/fe/src/pages/ListDetailPage.vue @@ -1,106 +1,99 @@ @@ -365,7 +286,7 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'; import { useRoute } from 'vue-router'; import { apiClient, API_ENDPOINTS } from '@/config/api'; // Keep for item management -import { onClickOutside, useEventListener, useFileDialog, useNetwork } from '@vueuse/core'; +import { useEventListener, useFileDialog, useNetwork } from '@vueuse/core'; // onClickOutside removed import { useNotificationStore } from '@/stores/notifications'; import { useOfflineStore, type CreateListItemPayload } from '@/stores/offline'; import { useListDetailStore } from '@/stores/listDetailStore'; @@ -378,6 +299,19 @@ import type { SettlementActivityCreate } from '@/types/expense'; import SettleShareModal from '@/components/SettleShareModal.vue'; import CreateExpenseForm from '@/components/CreateExpenseForm.vue'; import type { Item } from '@/types/item'; +import VHeading from '@/components/valerie/VHeading.vue'; +import VSpinner from '@/components/valerie/VSpinner.vue'; +import VAlert from '@/components/valerie/VAlert.vue'; +import VButton from '@/components/valerie/VButton.vue'; +import VBadge from '@/components/valerie/VBadge.vue'; +import VIcon from '@/components/valerie/VIcon.vue'; +import VModal from '@/components/valerie/VModal.vue'; +import VFormField from '@/components/valerie/VFormField.vue'; +import VInput from '@/components/valerie/VInput.vue'; +import VList from '@/components/valerie/VList.vue'; +import VListItem from '@/components/valerie/VListItem.vue'; +import VCheckbox from '@/components/valerie/VCheckbox.vue'; +// VTextarea and VSelect are not used in this part of the refactor for ListDetailPage // UI-specific properties that we add to items @@ -430,16 +364,16 @@ const lastListUpdate = ref(null); const lastItemUpdate = ref(null); const newItem = ref<{ name: string; quantity?: number }>({ name: '' }); -const itemNameInputRef = ref(null); +const itemNameInputRef = ref | null>(null); // Changed type // OCR const showOcrDialogState = ref(false); -const ocrModalRef = ref(null); +// const ocrModalRef = ref(null); // Removed const ocrLoading = ref(false); const ocrItems = ref<{ name: string }[]>([]); // Items extracted from OCR const addingOcrItems = ref(false); const ocrError = ref(null); -const ocrFileInputRef = ref(null); +const ocrFileInputRef = ref | null>(null); // Changed to VInput ref type const { files: ocrFiles, reset: resetOcrFileDialog } = useFileDialog({ accept: 'image/*', multiple: false, @@ -448,13 +382,13 @@ const { files: ocrFiles, reset: resetOcrFileDialog } = useFileDialog({ // Confirmation Dialog const showConfirmDialogState = ref(false); -const confirmModalRef = ref(null); +// const confirmModalRef = ref(null); // Removed const confirmDialogMessage = ref(''); const pendingAction = ref<(() => Promise) | null>(null); // Cost Summary const showCostSummaryDialog = ref(false); -const costSummaryModalRef = ref(null); +// const costSummaryModalRef = ref(null); // Removed const listCostSummary = ref(null); const costSummaryLoading = ref(false); const costSummaryError = ref(null); @@ -462,7 +396,7 @@ const costSummaryError = ref(null); // Settle Share const authStore = useAuthStore(); const showSettleModal = ref(false); -const settleModalRef = ref(null); +// const settleModalRef = ref(null); // Removed const selectedSplitForSettlement = ref(null); const parentExpenseOfSelectedSplit = ref(null); const settleAmount = ref(''); @@ -474,14 +408,10 @@ const showCreateExpenseForm = ref(false); // Edit Item const showEditDialog = ref(false); -const editModalRef = ref(null); +// const editModalRef = ref(null); // Removed const editingItem = ref(null); -onClickOutside(ocrModalRef, () => { showOcrDialogState.value = false; }); -onClickOutside(costSummaryModalRef, () => { showCostSummaryDialog.value = false; }); -onClickOutside(confirmModalRef, () => { showConfirmDialogState.value = false; pendingAction.value = null; }); -onClickOutside(settleModalRef, () => { showSettleModal.value = false; }); -onClickOutside(editModalRef, () => { showEditDialog.value = false; }); +// onClickOutside for ocrModalRef, costSummaryModalRef, etc. are removed as VModal handles this. const formatCurrency = (value: string | number | undefined | null): string => { @@ -603,7 +533,7 @@ 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(); + itemNameInputRef.value?.focus?.(); // Updated focus call return; } addingItem.value = true; @@ -639,7 +569,7 @@ const onAddItem = async () => { }; list.value.items.push(optimisticItem); newItem.value = { name: '' }; - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call addingItem.value = false; return; } @@ -655,7 +585,7 @@ const onAddItem = async () => { const addedItem = response.data as Item; list.value.items.push(processListItems([addedItem])[0]); newItem.value = { name: '' }; - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call } catch (err) { notificationStore.addNotification({ message: (err instanceof Error ? err.message : String(err)) || 'Failed to add item.', type: 'error' }); } finally { @@ -795,8 +725,13 @@ const openOcrDialog = () => { resetOcrFileDialog(); showOcrDialogState.value = true; nextTick(() => { - if (ocrFileInputRef.value) { - ocrFileInputRef.value.value = ''; + // 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 = ''; + } else if (ocrFileInputRef.value) { // Fallback if ref is native input + (ocrFileInputRef.value as any).value = ''; } }); }; @@ -839,7 +774,12 @@ 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.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 = ''; + } } }; @@ -852,7 +792,7 @@ const addOcrItems = async () => { if (!item.name.trim()) continue; const response = await apiClient.post( API_ENDPOINTS.LISTS.ITEMS(String(list.value.id)), - { name: item.name, quantity: "1" } + { name: item.name, quantity: "1" } // Assuming default quantity 1 for OCR items ); const addedItem = response.data as Item; list.value.items.push(processListItems([addedItem])[0]); @@ -932,7 +872,7 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => { return; } event.preventDefault(); - itemNameInputRef.value?.focus(); + itemNameInputRef.value?.focus?.(); // Updated focus call } }); @@ -1400,16 +1340,17 @@ const handleExpenseCreated = (expense: any) => { flex-direction: column; } -.neo-item-name { +.item-name { /* Added for VListItem content */ font-size: 1.1rem; font-weight: 700; } -.neo-item-complete .neo-item-name { +.neo-item-complete .item-name { /* Adjusted for VListItem */ text-decoration: line-through; - opacity: 0.6; + /* opacity: 0.6; Combined with bg-gray-100 opacity-70 on VListItem */ } + .neo-item-quantity { font-size: 0.9rem; color: #555; @@ -1496,23 +1437,24 @@ const handleExpenseCreated = (expense: any) => { cursor: not-allowed; } -.neo-add-item-form { - display: flex; - gap: 0.5rem; - margin-top: 2rem; - border: 3px solid #111; - border-radius: 12px; - padding: 1rem; - background: #f9f9f9; - box-shadow: 4px 4px 0 #111; +.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) */ + /* padding: 1rem; (original was 1rem) */ + /* border: 3px solid #111; (original was 3px) */ + /* border-radius: 12px; (original was 12px) */ + /* background: #f9f9f9; (original was #f9f9f9) */ + /* box-shadow: 4px 4px 0 #111; (original was 4px) */ } -.neo-new-item-form { + +.neo-new-item-form { /* Kept for reference, but form tag itself is now styled */ width: 100%; gap: 10px; } -.neo-text-input { +.neo-text-input { /* Not directly used by VInput, but kept for reference */ flex-grow: 1; border: 2px solid #111; border-radius: 8px; @@ -1521,7 +1463,7 @@ const handleExpenseCreated = (expense: any) => { font-weight: 500; } -.neo-new-item-input { +.neo-new-item-input { /* Not directly used by VInput, but kept for reference */ background: transparent; border: none; outline: none; @@ -1533,13 +1475,13 @@ const handleExpenseCreated = (expense: any) => { flex-grow: 1; } -.neo-new-item-input::placeholder { +.neo-new-item-input::placeholder { /* VInput handles its own placeholder styling */ color: #999; font-weight: 500; } -.neo-quantity-input { - width: 80px; +.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; @@ -1547,7 +1489,7 @@ const handleExpenseCreated = (expense: any) => { font-weight: 500; } -.neo-number-input { +.neo-number-input { /* For price input, now VInput with class="w-24" */ border: 2px solid #111; border-radius: 6px; padding: 0.5rem; @@ -1555,7 +1497,7 @@ const handleExpenseCreated = (expense: any) => { width: 100px; } -.neo-add-button { +.neo-add-button { /* Replaced by VButton */ background: #111; color: white; border: none; @@ -1567,7 +1509,7 @@ const handleExpenseCreated = (expense: any) => { height: 2rem; } -.neo-button { +.neo-button { /* General button, mostly replaced by VButton */ background: #111; color: white; border: none; @@ -1578,7 +1520,7 @@ const handleExpenseCreated = (expense: any) => { cursor: pointer; } -.new-item-input { +.new-item-input { /* Styling for the old li wrapper of add item form, can be removed */ margin-top: 0.5rem; padding: 0.5rem; } @@ -1593,9 +1535,9 @@ const handleExpenseCreated = (expense: any) => { font-size: 1.8rem; } - .neo-item { + /* .neo-item { // VListItem might have its own padding padding: 1rem; - } + } */ } @media (max-width: 600px) { @@ -1619,7 +1561,7 @@ const handleExpenseCreated = (expense: any) => { gap: 0.5rem; } - .neo-action-button { + .neo-action-button { /* VButton has its own sizing */ padding: 0.8rem; font-size: 0.9rem; } @@ -1629,11 +1571,11 @@ const handleExpenseCreated = (expense: any) => { margin-bottom: 1.5rem; } - .neo-item { + /* .neo-item { // VListItem padding: 1rem; - } + } */ - .neo-item-name { + .item-name { /* Adjusted for VListItem */ font-size: 1rem; } @@ -1641,75 +1583,77 @@ const handleExpenseCreated = (expense: any) => { font-size: 0.85rem; } - .neo-checkbox-label input[type="checkbox"] { + /* .neo-checkbox-label input[type="checkbox"] { // VCheckbox has its own styling width: 1.4em; height: 1.4em; - } + } */ - .neo-icon-button { + .neo-icon-button { /* VButton icon-only replaces this */ padding: 0.6rem; } - .neo-new-item-form { + .add-item-form { /* Adjusted form class */ flex-wrap: wrap; gap: 0.5rem; } - .neo-new-item-input { + /* VInput placeholder styling is internal to VInput */ + /* .neo-new-item-input { width: 100%; font-size: 1rem; - } + } */ - .neo-quantity-input { + /* VInput type number styling is internal or via props */ + /* .neo-quantity-input { width: 80px; font-size: 0.9rem; - } + } */ - .neo-add-button { + /* VButton styling replaces this */ + /* .neo-add-button { width: 100%; margin-top: 0.5rem; padding: 0.8rem; - } + } */ /* Optimize modals for mobile */ - .modal-container { + .modal-container { /* VModal has its own responsive sizing via props/CSS */ width: 95%; max-height: 85vh; margin: 1rem; } - .modal-header { + .modal-header { /* VModal slot */ padding: 1rem; } - .modal-body { + .modal-body { /* VModal slot */ padding: 1rem; } - .modal-footer { + .modal-footer { /* VModal slot */ padding: 1rem; } - /* Improve touch targets */ - button, + /* Improve touch targets - general principle, components should handle this */ + /* button, input[type="checkbox"], .neo-checkbox-label { min-height: 44px; - /* Apple's recommended minimum touch target size */ - } + } */ /* Optimize loading states for mobile */ - .neo-loading-state { + .neo-loading-state { /* VSpinner used instead */ padding: 2rem 1rem; } - .spinner-dots span { + .spinner-dots span { /* VSpinner has its own dot styling */ width: 10px; height: 10px; } /* Improve scrolling performance */ - .neo-item-list { + .item-list-tight { /* Assuming VList with this class */ -webkit-overflow-scrolling: touch; } @@ -1727,8 +1671,8 @@ const handleExpenseCreated = (expense: any) => { } } -/* Add smooth transitions for all interactive elements */ -.neo-action-button, +/* Add smooth transitions for all interactive elements - VComponents have their own */ +/* .neo-action-button, .neo-icon-button, .neo-checkbox-label, .neo-add-button { @@ -1742,83 +1686,28 @@ const handleExpenseCreated = (expense: any) => { .neo-add-button:active { transform: scale(0.98); opacity: 0.9; -} +} */ /* Improve scrolling performance */ -.neo-item-list { +.item-list-tight { /* Assuming VList with this class */ will-change: transform; transform: translateZ(0); } -.modal-backdrop { - background-color: rgba(0, 0, 0, 0.5); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} +/* Modal styles are now handled by VModal component */ +/* .modal-backdrop { ... } */ +/* .modal-container { ... } */ +/* .modal-header { ... } */ +/* .modal-body { ... } */ +/* .modal-footer { ... } */ +/* .close-button { ... } */ -.modal-container { - background: white; - border-radius: 18px; - border: 3px solid #111; - box-shadow: 6px 6px 0 #111; - width: 90%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - padding: 0; -} -.modal-header { - padding: 1.5rem; - border-bottom: 1px solid #eee; - display: flex; - justify-content: space-between; - align-items: center; -} +/* Item badge styles are now handled by VBadge */ +/* .item-badge { ... } */ +/* .badge-settled { ... } */ +/* .badge-pending { ... } */ -.modal-body { - padding: 1.5rem; -} - -.modal-footer { - padding: 1.5rem; - border-top: 1px solid #eee; - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} - -.close-button { - background: none; - border: none; - cursor: pointer; - color: #666; -} - -.item-badge { - display: inline-block; - padding: 0.25rem 0.5rem; - border-radius: 16px; - font-weight: 700; - font-size: 0.9rem; -} - -.badge-settled { - background-color: #d4f7dd; - color: #2c784c; -} - -.badge-pending { - background-color: #ffe1d6; - color: #c64600; -} .text-right { text-align: right; @@ -1828,56 +1717,50 @@ const handleExpenseCreated = (expense: any) => { text-align: center; } -.spinner-dots { - display: flex; - align-items: center; - justify-content: center; - gap: 0.3rem; - margin: 0 auto; -} +/* Spinner styles are now handled by VSpinner */ +/* .spinner-dots { ... } */ +/* .spinner-dots span { ... } */ +/* .spinner-dots-sm { ... } */ +/* .spinner-dots-sm span { ... } */ +/* @keyframes dot-pulse { ... } */ -.spinner-dots span { - width: 8px; - height: 8px; - background-color: #555; - border-radius: 50%; - animation: dot-pulse 1.4s infinite ease-in-out both; -} +/* 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 */ -.spinner-dots-sm { - display: inline-flex; - align-items: center; - gap: 0.2rem; -} - -.spinner-dots-sm span { - width: 4px; - height: 4px; - background-color: white; - border-radius: 50%; - animation: dot-pulse 1.4s infinite ease-in-out both; -} - -.spinner-dots span:nth-child(1), -.spinner-dots-sm span:nth-child(1) { - animation-delay: -0.32s; -} - -.spinner-dots span:nth-child(2), -.spinner-dots-sm span:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes dot-pulse { - - 0%, - 80%, - 100% { - transform: scale(0); - } - - 40% { - transform: scale(1); - } +/* 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 */ + /* Default VListItem is display:flex, so this might not be needed or just for minor tweaks */ }