Hello from MCP server
<template>
<BaseLayout title="Technician - Service Call">
<!-- Toolbar at top of screen -->
<Toolbar :force-step1="true" @help-clicked="openHelpModal" @agent-clicked="openAgentModal" />
<div class="dashboard-container">
<div class="content-section">
<!-- Add More Jobs Button - shown when step5 is true and search is hidden -->
<div v-if="sessionStore.appProgress.step5 && !showSearch" class="add-jobs-button-container">
<ion-button expand="block" color="secondary" @click="showSearch = true">
Add More Jobs
</ion-button>
</div>
<!-- Search Component - shown by default or when button is clicked -->
<SearchBar
v-if="!sessionStore.appProgress.step5 || showSearch"
:search-terms="appliedSearchTerms"
:selected-tags="selectedTagObjects"
:available-tags="availableTagObjects"
:suggestions="searchSuggestions"
@navigate="handleNavigate"
@search-input="handleSearchInput"
/>
<div v-if="displayJobs.length > 0" class="divider-compact"></div>
<!-- Jobs Section Heading -->
<h2 v-if="displayJobs.length > 0 && sessionStore.appProgress.step5" class="jobs-section-heading">Copy All Job Details To Continue</h2>
<!-- Jobs Section -->
<div class="jobs-section">
<DashboardJob
v-for="job in displayJobs"
:key="job.id"
:title="job.title"
:hours="job.hours"
:blink-fix-hours="job.blinkFixHours"
:has-confirmed-offer="job.hasConfirmedOffer"
:job-data="job.jobData"
@delete="() => handleDeleteJob(job.id)"
@details="() => handleJobDetails(job.id)"
@fix-hours="() => handleFixHours(job.id)"
@edit="() => handleEdit(job.id)"
@copied="() => handleJobCopied(job.id)"
@card-click="() => handleJobCardClick(job.id)"
/>
</div>
<!-- Show Menus and Invoice Buttons - shown when all jobs have completed check-hours -->
<div v-if="allJobsHaveHours" class="show-menus-button-container">
<ion-button expand="block" color="primary" size="large" @click="handleShowMenus">
Show Menus
</ion-button>
<ion-button expand="block" color="success" size="large" @click="handleGoToInvoice">
Go to Invoice
</ion-button>
</div>
</div>
</div>
<!-- Help Modal -->
<HelpModal :is-open="isHelpModalOpen" title="Service Call" @close="closeHelpModal">
<template #documentation>
<h2>Service Call Dashboard</h2>
<p>This is your main screen for managing service calls. From here you can search for jobs, view active jobs, and track your progress.</p>
<h3>Adding Jobs</h3>
<ul>
<li>Use the search bar to find jobs by name or tags</li>
<li>Click on a job to start the confirmation process</li>
<li>Multiple jobs can be added to a single service call</li>
</ul>
<h3>Job Cards</h3>
<ul>
<li>Each card shows the job name and estimated hours</li>
<li>Click "Edit" to modify job details</li>
<li>Click "Delete" to remove a job from the service call</li>
<li>Once customer selects an option, use "Copy" to copy details</li>
</ul>
<h3>Workflow</h3>
<p>The progress dots at the top show your position in the workflow: Find Job → Confirm Job → Check Hours → Add Jobs → Show Menus</p>
</template>
</HelpModal>
<!-- Agent Modal -->
<AgentModal :is-open="isAgentModalOpen" @close="closeAgentModal" />
<!-- Bookmarks Modal -->
<ion-modal :is-open="isBookmarksModalOpen" @didDismiss="closeBookmarksModal">
<ion-header>
<ion-toolbar>
<ion-title>Bookmarks</ion-title>
<ion-buttons slot="end">
<ion-button @click="closeBookmarksModal">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="bookmarks-content">
<div class="bookmarks-list">
<div
v-for="job in bookmarkedJobs"
:key="job.id"
@click="selectBookmarkedJob(job)"
class="bookmark-item"
>
<ion-icon :icon="star" color="warning" class="bookmark-star"></ion-icon>
<div class="bookmark-details">
<h3 class="bookmark-title">{{ job.name }}</h3>
<p class="bookmark-description">{{ job.description }}</p>
</div>
</div>
</div>
</div>
</ion-content>
</ion-modal>
<!-- Delete Confirmation Alert -->
<ion-alert
:is-open="isDeleteAlertOpen"
header="Delete Job"
message="Are you sure you want to delete this job? This action cannot be undone."
:buttons="[
{
text: 'Cancel',
role: 'cancel',
handler: cancelDelete
},
{
text: 'Delete',
role: 'destructive',
handler: confirmDelete
}
]"
@didDismiss="cancelDelete"
></ion-alert>
<!-- Job Details Modal -->
<ion-modal :is-open="isJobDetailsModalOpen" @didDismiss="closeJobDetailsModal">
<ion-header>
<ion-toolbar>
<ion-title>Job Details{{ sessionStore.editMode ? ' (Edit Mode)' : '' }}</ion-title>
<ion-buttons slot="end">
<ion-button @click="closeJobDetailsModal">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div v-if="selectedJobForDetails" class="job-details-content">
<div class="detail-section">
<h3 class="detail-label">Job</h3>
<p class="detail-value">{{ selectedJobForDetails.problem?.name || selectedJobForDetails.title || 'Untitled Job' }}</p>
</div>
<div v-if="selectedJobForDetails.problem?.description" class="detail-section">
<h3 class="detail-label">Description</h3>
<p class="detail-value">{{ selectedJobForDetails.problem.description }}</p>
</div>
<div class="detail-section">
<h3 class="detail-label">
Steps
<ion-icon v-if="sessionStore.editMode && !editingSteps" :icon="pencil" class="edit-icon" @click="startEditingSteps" />
</h3>
<!-- Edit mode: textarea for multiline editing -->
<div v-if="editingSteps" class="steps-edit-container">
<ion-textarea
v-model="editStepsText"
:rows="8"
placeholder="Enter steps, one per line..."
class="steps-textarea"
/>
<div class="steps-edit-buttons">
<ion-button size="small" fill="outline" @click="cancelEditingSteps">Cancel</ion-button>
<ion-button size="small" @click="saveSteps">Save</ion-button>
</div>
</div>
<!-- View mode: ordered list -->
<ol v-else class="steps-list">
<li v-for="(step, index) in getJobSteps(selectedJobForDetails)" :key="index" class="step-item">
{{ step }}
</li>
</ol>
</div>
<div v-if="selectedJobForDetails.baseHours" class="detail-section">
<h3 class="detail-label">Estimated Time</h3>
<p class="detail-value">{{ ((selectedJobForDetails.baseHours || 0) + (selectedJobForDetails.extraTime || 0)).toFixed(1) }} hours</p>
</div>
<div v-if="selectedJobForDetails.selectedTierName" class="detail-section">
<h3 class="detail-label">Selected Option</h3>
<p class="detail-value">{{ selectedJobForDetails.selectedTierName }}</p>
</div>
<div v-if="selectedJobForDetails.selectedPrice" class="detail-section">
<h3 class="detail-label">Price</h3>
<p class="detail-value detail-price">${{ parseFloat(selectedJobForDetails.selectedPrice).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</p>
</div>
<div v-if="selectedJobForDetails.notes && selectedJobForDetails.notes.length > 0" class="detail-section">
<h3 class="detail-label">Notes</h3>
<div class="detail-notes">
<p v-for="(note, index) in selectedJobForDetails.notes" :key="index" class="detail-note">{{ note }}</p>
</div>
</div>
</div>
</ion-content>
</ion-modal>
</BaseLayout>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import BaseLayout from "@/components/BaseLayout.vue";
import Toolbar from "@/components/Toolbar.vue";
import SearchBar from "@/components/SearchBar.vue";
import DashboardJob from "@/components/DashboardJob.vue";
import HelpModal from "@/components/HelpModal.vue";
import AgentModal from "@/components/AgentModal.vue";
import type { ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types";
import { IonModal, IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonContent, IonIcon, IonAlert, IonTextarea } from "@ionic/vue";
import { star, pencil } from "ionicons/icons";
import { useRouter, useRoute } from "vue-router";
import { useSessionStore } from "@/stores/session";
import { useJobsAndTags } from "@/composables/useJobsAndTags";
const router = useRouter();
const route = useRoute();
const sessionStore = useSessionStore();
// Use composable for jobs and tags logic
const {
problems,
problemTags,
selectedTags,
appliedSearchQuery,
getProblemTagIds,
availableTagObjects,
selectedTagObjects,
appliedSearchTerms
} = useJobsAndTags();
// Clear search state and ensure tags are loaded whenever we navigate to service call (including back button)
watch(() => route.path, async (newPath) => {
if (newPath === '/service-call') {
await sessionStore.clearSearch();
// Ensure problems and tags are always loaded from database
await sessionStore.loadProblemsAndTags();
}
}, { immediate: true });
const isHelpModalOpen = ref(false);
const isAgentModalOpen = ref(false);
const isBookmarksModalOpen = ref(false);
const isDeleteAlertOpen = ref(false);
const jobToDelete = ref<string | null>(null);
const showSearch = ref(false);
const copiedJobIds = ref<Set<string>>(new Set());
const isJobDetailsModalOpen = ref(false);
const selectedJobForDetails = ref<any>(null);
// Edit mode state for job details modal
const editingSteps = ref(false);
const editStepsText = ref('');
const openHelpModal = () => {
isHelpModalOpen.value = true;
};
const closeHelpModal = () => {
isHelpModalOpen.value = false;
};
const openAgentModal = () => {
isAgentModalOpen.value = true;
};
const closeAgentModal = () => {
isAgentModalOpen.value = false;
};
const logActiveJobs = () => {
console.log('Active Jobs:', sessionStore.jobs);
console.log('Number of Jobs:', sessionStore.jobs.length);
console.log('App Progress:', sessionStore.appProgress);
};
const openBookmarksModal = () => {
isBookmarksModalOpen.value = true;
};
const closeBookmarksModal = () => {
isBookmarksModalOpen.value = false;
};
const handleDeleteJob = (jobId: string) => {
jobToDelete.value = jobId;
isDeleteAlertOpen.value = true;
};
const confirmDelete = async () => {
if (jobToDelete.value) {
await sessionStore.deleteJob(jobToDelete.value);
jobToDelete.value = null;
}
isDeleteAlertOpen.value = false;
};
const cancelDelete = () => {
jobToDelete.value = null;
isDeleteAlertOpen.value = false;
};
const handleJobDetails = (jobId: string) => {
// Find the job in the session to get its problem ID
const job = sessionStore.jobs.find(j => j.id === jobId);
if (job && job.problem?.id) {
router.push(`/view-job/${job.problem.id}`);
}
};
const handleFixHours = (jobId: string) => {
router.push(`/confirm-job/${jobId}`);
};
const handleEdit = (jobId: string) => {
router.push(`/editor/job/${jobId}`);
};
const handleJobCardClick = (jobId: string) => {
// Navigate to the job content screen
router.push(`/job/${jobId}`);
};
const closeJobDetailsModal = () => {
isJobDetailsModalOpen.value = false;
selectedJobForDetails.value = null;
editingSteps.value = false;
editStepsText.value = '';
};
// Start editing steps - populate textarea with current steps
const startEditingSteps = () => {
if (selectedJobForDetails.value) {
const steps = getJobSteps(selectedJobForDetails.value);
editStepsText.value = steps.join('\n');
editingSteps.value = true;
}
};
// Save edited steps - split lines into array and save to job
const saveSteps = async () => {
if (selectedJobForDetails.value) {
const newSteps = editStepsText.value
.split('\n')
.map(s => s.trim())
.filter(s => s !== '');
// Update the job's problem tech_handbook
const job = sessionStore.jobs.find(j => j.id === selectedJobForDetails.value.id);
if (job && job.problem) {
(job.problem as any).tech_handbook = newSteps;
await sessionStore.save();
}
editingSteps.value = false;
}
};
// Cancel editing steps
const cancelEditingSteps = () => {
editingSteps.value = false;
editStepsText.value = '';
};
// Default tech handbook steps
const DEFAULT_STEPS = [
"Do some work",
"Do some more work",
"And there are different steps.",
"Clean up the job site.",
"Tell the customer thank you."
];
// Get tech handbook steps from job's problem or use default
const getJobSteps = (job: any): string[] => {
if (!job || !job.problem) return DEFAULT_STEPS;
const techHandbook = (job.problem as any)?.tech_handbook || (job.problem as any)?.techHandbook;
if (techHandbook) {
// If tech handbook exists, parse it into steps
if (typeof techHandbook === 'string') {
return techHandbook.split('\n').filter((step: string) => step.trim() !== '');
} else if (Array.isArray(techHandbook)) {
return techHandbook;
}
}
// Return default steps
return DEFAULT_STEPS;
};
const handleJobCopied = async (jobId: string) => {
// Mark this job as copied
copiedJobIds.value.add(jobId);
// Check if all jobs have been copied
const allJobsCopied = sessionStore.jobs.every(job => copiedJobIds.value.has(job.id));
if (allJobsCopied && sessionStore.jobs.length > 0) {
// All jobs copied - review invoice step complete
console.log('[ServiceCall] All jobs copied - review invoice complete');
}
};
const handleShowMenus = async () => {
if (sessionStore.jobs.length > 0) {
// Set step 4 to true (Pass to customer)
sessionStore.appProgress.step4 = true;
await sessionStore.save();
router.push("/to-customer");
}
};
const handleGoToInvoice = () => {
router.push("/invoice");
};
const goToSearch = () => {
router.push("/find-job");
};
// Check if all jobs have completed check-hours (have baseHours set)
// extraTime can be 0 if no adjustments were made, so we only check baseHours
const allJobsHaveHours = computed(() => {
if (sessionStore.jobs.length === 0) return false;
return sessionStore.jobs.every(job => job.baseHours > 0);
});
// Search-related state
const searchInputQuery = ref("");
const searchSuggestions = computed(() => {
if (searchInputQuery.value.trim().length < 2) return [];
const query = searchInputQuery.value.toLowerCase();
const suggestions: Array<{ id: string; name: string; type: 'tag' | 'job' }> = [];
problemTags.value.forEach((tag) => {
if (tag.name?.toLowerCase().includes(query)) {
if (!selectedTags.value.includes(tag.id)) {
suggestions.push({
id: tag.id,
name: tag.name || '',
type: 'tag'
});
}
}
});
problems.value.forEach((problem) => {
if (problem.name?.toLowerCase().includes(query)) {
suggestions.push({
id: problem.id,
name: problem.name || '',
type: 'job'
});
}
});
return suggestions.slice(0, 10);
});
// Event handlers
const handleNavigate = (url: string) => {
router.push(url);
};
const handleSearchInput = (query: string) => {
searchInputQuery.value = query;
};
// Get 5 random jobs as bookmarks (mock data)
const bookmarkedJobs = computed(() => {
const problems = sessionStore.problems;
if (problems.length === 0) return [];
// Create a copy and shuffle
const shuffled = [...problems].sort(() => 0.5 - Math.random());
// Return first 5
return shuffled.slice(0, 5);
});
// Select a bookmarked job
const selectBookmarkedJob = (problem: ProblemsRecord) => {
closeBookmarksModal();
router.push(`/view-job/${problem.id}`);
};
// Jobs to display: mock jobs if no real jobs have ever been added, otherwise real jobs (or empty)
const displayJobs = computed(() => {
// If real jobs have been added, never show mock data again (even if jobs array is empty)
if (sessionStore.hasHadRealJobs) {
// Show real jobs (or empty array if all deleted)
return sessionStore.jobs.map(job => {
const totalHours = (job.baseHours || 0) + (job.extraTime || 0);
return {
id: job.id,
title: job.title || job.problem?.name || 'Untitled Job',
hours: totalHours > 0
? `${totalHours.toFixed(1)} h`
: '--- h',
blinkFixHours: !job.baseHours || job.baseHours === 0,
hasConfirmedOffer: !!(job.selectedTierName && job.selectedPrice),
jobData: job,
};
});
}
// Return empty array if no jobs
return [];
});
onMounted(async () => {
await sessionStore.load();
// Set test data for demo purposes
sessionStore.serviceCallsCount = 2;
sessionStore.menusShownCount = 4;
sessionStore.higherTierChosenCount = 1;
sessionStore.serviceCallsWithMenusShown = 2;
});
</script>
<style scoped>
.dashboard-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header-section {
margin-bottom: 32px;
}
.title-container {
position: relative;
margin-bottom: 24px;
}
.title-icons-left {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 8px;
align-items: center;
}
.title-icons-left ion-icon {
font-size: 48px;
}
.title-icons-right {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 8px;
align-items: center;
}
.title-icons-right ion-icon {
font-size: 48px;
}
.star-icon-wrapper {
position: relative;
display: inline-block;
}
.star-icon-shadow {
position: absolute;
top: 2px;
left: -2px;
font-size: 48px;
z-index: 0;
}
.star-icon-front {
position: relative;
z-index: 1;
}
.clickable-icon {
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease;
}
.clickable-icon:hover {
transform: scale(1.1);
color: var(--ion-color-primary-shade);
}
.clickable-icon:active {
transform: scale(0.95);
}
.page-title {
margin: 0;
color: var(--ion-text-color);
font-size: 48px;
font-weight: 700;
text-align: center;
font-variant: small-caps;
}
.content-section {
padding: 0 20px 20px 20px;
}
.progress-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1000px;
margin: 0 auto 24px auto;
}
.progress-column {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.progress-number {
font-size: 43px;
font-weight: 700;
color: var(--ion-color-primary);
font-variant: normal;
font-family: "Courier New", Courier, monospace;
letter-spacing: 4px;
text-align: center;
}
.progress-label {
font-size: 22px;
font-weight: 600;
color: var(--ion-color-medium);
font-variant: small-caps;
text-align: center;
}
.progress-wrapper {
position: relative;
width: 100%;
height: 43px;
border-radius: 12px;
background: var(--ion-color-medium);
overflow: hidden;
}
.progress-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to right,
var(--ion-color-secondary) 0%,
var(--ion-color-primary) 100%
);
border-radius: 12px;
}
.progress-cover {
position: absolute;
top: 0;
right: 0;
height: 100%;
background: var(--ion-color-medium);
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
transition: width 0.3s ease;
}
/* Mobile adjustments for progress bars */
@media (max-width: 767px) {
.progress-number {
font-size: 34px;
}
.progress-label {
font-size: 14px;
}
.progress-wrapper {
height: 34px;
}
}
.clocks-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1000px;
margin: 0 auto;
}
.divider {
width: 100%;
height: 1px;
background-color: #d3d3d3;
margin: 40px 0;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.divider-compact {
width: 100%;
height: 1px;
background-color: #d3d3d3;
margin: 16px 0;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
/* Dashboard Search Section */
.jobs-section-heading {
max-width: 1000px;
margin: 0 auto 16px auto;
color: var(--ion-text-color);
font-size: 24px;
font-weight: 600;
text-align: center;
font-variant: small-caps;
}
.jobs-section {
max-width: 1000px;
margin: 0 auto 16px auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.add-jobs-button-container {
max-width: 1000px;
margin: 0 auto 16px auto;
}
.add-jobs-button-container ion-button {
--padding-top: 16px;
--padding-bottom: 16px;
font-weight: 600;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.show-menus-button-container {
max-width: 1000px;
margin: 16px auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.show-menus-button-container ion-button {
--padding-top: 20px;
--padding-bottom: 20px;
font-weight: 700;
font-size: 20px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-content {
font-size: 16px;
line-height: 1.6;
color: var(--ion-text-color);
}
.info-content h2 {
margin-top: 24px;
margin-bottom: 12px;
font-size: 20px;
font-weight: 700;
color: var(--ion-color-primary);
}
.info-content h2:first-child {
margin-top: 0;
}
.info-content p {
margin-bottom: 16px;
color: var(--ion-text-color);
}
.info-content p:last-child {
margin-bottom: 0;
}
.debug-button {
padding: 12px 24px;
background-color: var(--ion-color-primary);
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: 16px;
}
.debug-button:hover {
background-color: var(--ion-color-primary-shade);
}
.debug-button:active {
transform: scale(0.98);
}
/* Mobile adjustments */
@media (max-width: 767px) {
.dashboard-container {
padding: 16px;
}
.page-title {
font-size: 36px;
}
.progress-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.clocks-grid {
grid-template-columns: 1fr;
gap: 16px;
}
}
/* Job Details Modal Styles */
.job-details-content {
max-width: 600px;
margin: 0 auto;
}
.detail-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.detail-section:last-child {
border-bottom: none;
}
.detail-label {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--ion-color-medium);
display: flex;
align-items: center;
gap: 8px;
}
.edit-icon {
font-size: 14px;
color: var(--ion-color-primary);
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.edit-icon:hover {
opacity: 1;
}
.detail-value {
margin: 0;
font-size: 18px;
line-height: 1.5;
color: var(--ion-text-color);
}
.detail-price {
font-size: 32px;
font-weight: 700;
color: var(--ion-color-success);
}
.detail-notes {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-note {
margin: 0;
padding: 12px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid var(--ion-color-medium);
border-radius: 8px;
font-size: 16px;
line-height: 1.5;
color: var(--ion-text-color);
}
.steps-list {
margin: 0;
padding-left: 24px;
color: var(--ion-text-color);
}
.step-item {
font-size: 16px;
line-height: 1.6;
margin-bottom: 8px;
color: var(--ion-text-color);
}
.steps-edit-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.steps-textarea {
--background: rgba(255, 255, 255, 0.1);
--color: var(--ion-text-color);
--padding-start: 12px;
--padding-end: 12px;
--padding-top: 12px;
--padding-bottom: 12px;
border: 1px solid var(--ion-color-medium);
border-radius: 8px;
font-size: 16px;
line-height: 1.6;
}
.steps-edit-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* Bookmarks Modal Styles */
.bookmarks-content {
font-size: 16px;
line-height: 1.6;
color: var(--ion-text-color);
}
.bookmarks-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.bookmark-item {
display: flex;
gap: 16px;
padding: 16px;
background-color: var(--ion-color-dark);
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
border-left: 3px solid var(--ion-color-warning);
}
.bookmark-item:hover {
background-color: rgba(118, 221, 84, 0.1);
}
.bookmark-star {
font-size: 24px;
flex-shrink: 0;
margin-top: 4px;
}
.bookmark-details {
flex: 1;
}
.bookmark-title {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: var(--ion-text-color);
}
.bookmark-description {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: var(--ion-color-medium);
max-width: 600px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>