Specjalne ostrzeżenia
Lactulosum Polfarmex

Produkt leczniczy Lactulosum Polfarmex wymaga szczególnej ostrożności podczas długotrwałego stosowania, zwłaszcza u pacjentów geriatrycznych, gdzie konieczne jest regularne monitorowanie stężenia elektrolitów, w tym potasu i chlorków, w osoczu. Preparat zawiera substancje pomocnicze takie jak etanol (0,00963 g na 15 ml syropu), fruktozę (0,075 g na 15 ml), laktozę (0,75 g na 15 ml) oraz galaktozę (1,125 g na 15 ml), które mogą mieć kliniczne znaczenie u wybranych grup pacjentów. Zawartość etanolu w dawkach terapeutycznych jest niska (np. 45 ml syropu zawiera 29,07 mg etanolu, co odpowiada mniej niż 1 ml piwa lub wina) i nie wywołuje istotnych efektów farmakologicznych.

  1. Specjalne ostrzeżenia i środki ostrożności dotyczące stosowania
    1. Monitorowanie elektrolitów podczas długotrwałej terapii
    2. Substancje pomocnicze o znanym działaniu
    3. Zawartość etanolu
    4. Zawartość fruktozy
    5. Zawartość laktozy
    6. Zawartość galaktozy
    7. Zawartość siarczyn# kevinkirbyuk/Opta-F24-Data-Parser # OptaParser.py import json import os import time import re import pandas as pd from time import sleep def preprocess_text_to_valid_json(text): „”” Ensure the text is valid JSON by handling common syntax errors. Parameters: – text (str): The JSON text to be preprocessed Returns: – str: Preprocessed JSON text that should be valid „”” # Common preprocessing steps # Remove trailing commas in arrays and objects text = re.sub(r’,s*([}]])’, r’1′, text) # Ensure properties are in double quotes text = re.sub(r'([{,])s*([a-zA-Z0-9_]+):’, r’1″2″:’, text) # Replace single quotes with double quotes for string values text = re.sub(r”'([^’]*)’”, r’”1″’, text) return text def read_opta_files(filenames): „”” Read multiple Opta F24 XML files and store them in a list. Parameters: – filenames (List[str]): List of Opta F24 filenames Returns: – List: List of parsed XML file contents „”” opta_files = [] for filename in filenames: # Skip if file doesn’t exist or is directory if not os.path.isfile(filename): print(f”Skipping {filename}: file not found.”) continue # Read the file with open(filename, 'r’, encoding=’utf-8′) as file: opta_files.append(file.read()) return opta_files def parse_opta_f24_to_json(xml_string): „”” Parse Opta F24 XML data into JSON format. Parameters: – xml_string (str): Opta F24 XML data as string Returns: – dict: Parsed data in JSON format „”” # Extract necessary components match_pattern = r’’ match_time = re.search(match_pattern, xml_string) timestamp = match_time.group(1) if match_time else „” game_pattern = r’’ match = re.search(game_pattern, xml_string) if not match: print(„Failed to extract game information.”) return None # Or handle the error in a way that makes sense for your application game_id = match.group(1) away_team_id = match.group(2) away_team_name = match.group(3) competition_id = match.group(4) competition_name = match.group(5) game_date = match.group(6) home_team_id = match.group(7) home_team_name = match.group(8) matchday = match.group(9) period_1_start = match.group(10) period_2_start = match.group(11) season_id = match.group(12) season_name = match.group(13) # Extract all event data pattern = r’)*’ r’.*?’ events = [] for match in re.finditer(pattern, xml_string): event_id = match.group(1) type_id = match.group(3) period_id = match.group(4) minute = match.group(5) second = match.group(6) team_id = match.group(7) outcome = match.group(8) x_pos = match.group(9) y_pos = match.group(10) event_timestamp = match.group(11) # Extract qualifiers for this event qualifier_pattern = r’’ qualifiers = [] for q_match in re.finditer(qualifier_pattern, match.group(0)): q_id = q_match.group(1) qualifier_id = q_match.group(2) value = q_match.group(3) qualifiers.append({ „id”: q_id, „qualifier_id”: qualifier_id, „value”: value }) # Create event object event = { „id”: event_id, „type_id”: type_id, „period_id”: period_id, „minute”: minute, „second”: second, „team_id”: team_id, „outcome”: outcome, „x”: x_pos, „y”: y_pos, „timestamp”: event_timestamp, „qualifiers”: qualifiers } events.append(event) # Create the final JSON structure json_data = { „OptaFeed”: { „timestamp”: timestamp, „Game”: { „id”: game_id, „away_team_id”: away_team_id, „away_team_name”: away_team_name, „competition_id”: competition_id, „competition_name”: competition_name, „game_date”: game_date, „home_team_id”: home_team_id, „home_team_name”: home_team_name, „matchday”: matchday, „period_1_start”: period_1_start, „period_2_start”: period_2_start, „season_id”: season_id, „season_name”: season_name, „Events”: events } } } return json_data def save_to_json(data, filename): „”” Save data as JSON to a file. Parameters: – data (dict): The data to save – filename (str): The output filename Returns: – bool: True if successful, False otherwise „”” try: with open(filename, 'w’, encoding=’utf-8′) as file: json.dump(data, file, indent=4) return True except Exception as e: print(f”Error saving JSON to {filename}: {e}”) return False def extract_match_info(json_data): „”” Extract match information from Opta F24 JSON data. Parameters: – json_data (dict): Parsed Opta F24 data Returns: – dict: Extracted match information „”” game = json_data[„OptaFeed”][„Game”] return { „match_id”: game[„id”], „home_team_id”: game[„home_team_id”], „home_team”: game[„home_team_name”], „away_team_id”: game[„away_team_id”], „away_team”: game[„away_team_name”], „competition”: game[„competition_name”], „season”: game[„season_name”], „date”: game[„game_date”] } def events_to_dataframe(json_data): „”” Convert Opta F24 events from JSON to a pandas DataFrame. Parameters: – json_data (dict): Parsed Opta F24 data Returns: – DataFrame: Events data in tabular format „”” events = json_data[„OptaFeed”][„Game”][„Events”] match_info = extract_match_info(json_data) # Initialize an empty list to store flattened events flattened_events = [] for event in events: # Start with basic event information flat_event = { „event_id”: event[„id”], „type_id”: event[„type_id”], „period_id”: event[„period_id”], „minute”: event[„minute”], „second”: event[„second”], „team_id”: event[„team_id”], „outcome”: event[„outcome”], „x”: event[„x”], „y”: event[„y”], „timestamp”: event[„timestamp”], „match_id”: match_info[„match_id”], „home_team”: match_info[„home_team”], „away_team”: match_info[„away_team”], „competition”: match_info[„competition”], „season”: match_info[„season”], „date”: match_info[„date”] } # Add team information if event[„team_id”] == match_info[„home_team_id”]: flat_event[„team”] = match_info[„home_team”] elif event[„team_id”] == match_info[„away_team_id”]: flat_event[„team”] = match_info[„away_team”] # Add qualifiers as separate columns for qualifier in event[„qualifiers”]: qualifier_key = f”q{qualifier[’qualifier_id’]}” flat_event[qualifier_key] = qualifier[„value”] flattened_events.append(flat_event) # Convert to DataFrame df = pd.DataFrame(flattened_events) return df def main(filenames, output_format=”json”): „”” Main function to process Opta F24 files. Parameters: – filenames (List[str]): List of Opta F24 filenames to process – output_format (str): Output format (json or csv) Returns: – None: Results are saved to files „”” # Read in all the specified files opta_files = read_opta_files(filenames) # Process each file for i, file_content in enumerate(opta_files): try: # Parse XML to JSON json_data = parse_opta_f24_to_json(file_content) if json_data: # Extract match info for filename match_info = extract_match_info(json_data) base_filename = f”{match_info[’home_team’]}_{match_info[’away_team’]}_{match_info[’date’]}” base_filename = base_filename.replace(” „, „_”).replace(„/”, „-„) # Save in requested format(s) if output_format.lower() == „json” or output_format.lower() == „both”: json_filename = f”{base_filename}.json” save_to_json(json_data, json_filename) print(f”Saved {json_filename}”) if output_format.lower() == „csv” or output_format.lower() == „both”: # Convert to DataFrame and save as CSV df = events_to_dataframe(json_data) csv_filename = f”{base_filename}.csv” df.to_csv(csv_filename, index=False) print(f”Saved {csv_filename}”) else: print(f”Failed to parse file {filenames[i]}”) except Exception as e: print(f”Error processing {filenames[i]}: {e}”) print(„Processing complete.”) if __name__ == „__main__”: # Example usage: filenames = [„f24-20-2019-2245250-eventdetails.xml”] main(filenames, output_format=”both”) # Save as both JSON and CSV # serbogdanov/project service = $service; $this->repository = $repository; $this->contactRepository = $contactRepository; $this->appointmentRepository = $appointmentRepository; } /** * @param Request $request * @return IlluminateContractsViewFactory|IlluminateViewView */ public function index(Request $request) { $sort_by = $request->get(’sort_by’, 'created_at’); $sort = $request->get(’sort’, 'desc’); $search = trim($request->get(’search’)); $user = Auth::user(); $per_page = config(’project.per_page’); $hasPermission = auth()->user()->hasPermission(’patient’); $patients = $this->repository->getUserWithContactByRole(’patient’, $sort_by, $sort, $search, $per_page, false); return view(’users::patient.index’, compact(’patients’, 'user’, 'hasPermission’)); } /** * @return IlluminateContractsViewFactory|IlluminateViewView */ public function create() { $isCreate = true; $doctors = $this->repository->getUsersByRole(’doctor’); $treatments = $this->service->getPreDefinedData(’treatment’); $bloodGroups = $this->service->getPreDefinedData(’blood_group’); $statuses = $this->service->getPreDefinedData(’status’); return view(’users::patient.create_edit’, compact(’doctors’, 'isCreate’, 'bloodGroups’, 'treatments’, 'statuses’)); } /** * @param Request $request * @return IlluminateHttpRedirectResponse */ public function store(CreateUserRequest $request) { try { $data = $request->all(); $this->service->createPatientContact($data); return redirect()->route(’patients.index’) ->with(’success’, trans(’users::messages.record_was_successfully_created’)); } catch (Exception $e) { return back() ->with(’error’, „Cannot save patient: „.$e->getMessage()); } } /** * @param $id * @return IlluminateContractsViewFactory|IlluminateViewView */ public function show($id) { $per_page = config(’project.per_page’); $user = $this->service->findPatientById($id); $doctors = $this->repository->getUsersByRole(’doctor’); return view(’users::patient.view’, compact(’user’, 'doctors’)); } /** * @param $id * @return IlluminateContractsViewFactory|IlluminateViewView */ public function edit($id) { $isCreate = false; $user = $this->service->findPatientById($id); $doctors = $this->repository->getUsersByRole(’doctor’); $bloodGroups = $this->service->getPreDefinedData(’blood_group’); $treatments = $this->service->getPreDefinedData(’treatment’); $statuses = $this->service->getPreDefinedData(’status’); return view(’users::patient.create_edit’, compact(’user’, 'doctors’, 'isCreate’, 'bloodGroups’, 'treatments’, 'statuses’)); } /** * @param Request $request * @param $id * @return IlluminateHttpRedirectResponse */ public function update(UpdateUserRequest $request, $id) { try { $data = $request->all(); $this->service->updatePatientContact($id, $data); return redirect()->route(’patients.index’) ->with(’success’, trans(’users::messages.record_was_successfully_updated’)); } catch (Exception $e) { return back() ->with(’error’, „Cannot update patient: „.$e->getMessage()); } } /** * @param $id * @return IlluminateHttpRedirectResponse */ public function destroy($id) { try { $this->repository->delete($id); return back() ->with(’success’, trans(’users::messages.record_was_successfully_deleted’)); } catch (Exception $e) { return back() ->with(’error’, trans(’users::messages.record_was_not_deleted_due_to_system_error’).$e->getMessage()); } } /** * @param Request $request * @param $id * @return string */ public function events(Request $request, $id) { $data = $this->appointmentRepository->getAppointmentByContactId($id); return json_encode($data); } /** * @param $id * @return IlluminateHttpJsonResponse */ public function getPatient($id) { try { $data = $this->contactRepository->findById($id, true); return response()->json([ 'status’ => 'success’, 'data’ => $data ]); } catch(Exception $e) { return response()->json([ 'status’ => 'error’, 'error’ => $e->getMessage() ]); } } /** * Copy patient data * * @param IlluminateHttpRequest $request * @return IlluminateHttpJsonResponse */ public function copyPatientData(Request $request) { try { $data = $request->only(’patient_ids’); $patientIds = !empty($data[’patient_ids’]) ? explode(’,’, $data[’patient_ids’]) : []; if (empty($patientIds)) { throw new Exception(’Please choose patient(s) for copy.’); } $patients = []; foreach ($patientIds as $patientId) { $patients[] = $this->service->findPatientById($patientId); } $data = [ 'datas’ => $patients ]; $view = view(’users::patient.includes.patient_data_mail_table’, $data)->render(); return response()->json([ 'status’ => 'success’, 'view’ => $view ]); } catch(Exception $e) { return response()->json([ 'status’ => 'error’, 'error’ => $e->getMessage() ]); } } public function patientStatus($id) { try { $this->service->updatePatientStatus($id); return back() ->with(’success’, trans(’users::messages.patient_status_successfully_updated’)); } catch (Exception $e) { return back() ->with(’error’, 'Cannot update patient: ’.$e->getMessage()); } } } End FileuserModel = $userModel; $this->roleUserModel = $roleUserModel; $this->roleModel = $roleModel; $this->contactModel = $contactModel; $this->fileEntityModel = $fileEntityModel; } /** * get All User Except Current User * @return IlluminateDatabaseEloquentCollection|static[] */ public function getAllUsers() { return $this->userModel->where(’id’, ’’, Auth::user()->id)->get(); } /** * get users by role * @param string $roleName * @return mixed */ public function getUsersByRole($roleName = 'user’) { $role = $this->roleModel->select(’id’)->where(’name’, $roleName)->first(); $userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray(); return $this->userModel->whereIn(’id’, $userIds)->get(); } /** * @param String $search * @return Collection */ public function searchUserName($search) { $users = DB::table(’users’) ->whereRaw(’CONCAT(first_name, ” „, last_name) like ?’, [„%{$search}%”]) ->orderBy(’first_name’) ->orderBy(’last_name’) ->get(); $role = $this->roleModel->select(’id’)->where(’name’, 'patient’)->first(); return $users; } /** * @param $userId * @param bool $isDoctorView * @return Contact|Contact[]|IlluminateDatabaseEloquentCollection|IlluminateDatabaseEloquentModel|null */ public function getUserContactProfileByUserId($userId, $isDoctorView = false) { $role = $this->roleModel->select(’id’)->where(’name’, 'patient’)->first(); $userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray(); if ($isDoctorView) { return $this->contactModel ->whereIn(’user_id’, $userIds) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’) ->where(’assigned_dr_id’, $userId) ->get(); } else { return $this->contactModel ->whereIn(’user_id’, $userIds) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’) ->get(); } } /** * @param $userId * @return mixed */ public function getUserById($userId) { return $this->userModel->where(’id’, $userId)->with(’profile’)->first(); } /** * @param $roleName * @param string $sort_by * @param string $sort * @param string $search * @param int $per_page * @param bool $isDoctorAssigned * @return IlluminateContractsPaginationLengthAwarePaginator */ public function getUserWithContactByRole($roleName, $sort_by = 'created_at’, $sort = 'desc’, $search = ”, $per_page = 10, $isDoctorAssigned = false) { $role = $this->roleModel->select(’id’)->where(’name’, $roleName)->first(); $userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray(); $query = $this->contactModel->whereIn(’user_id’, $userIds) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’); if ($isDoctorAssigned === true) { $query = $query->where(’assigned_dr_id’, Auth::user()->id); } if (!is_null($search) && !empty($search)) { $query = $query->where(function ($q) use ($search) { $q->where(’first_name’, 'like’, '%’.$search.’%’) ->orWhere(’last_name’, 'like’, '%’.$search.’%’) ->orWhere(’surname’, 'like’, '%’.$search.’%’) ->orWhere(’email’, 'like’, '%’.$search.’%’) ; }); } return $query->orderBy($sort_by, $sort)->paginate($per_page); } /** * @param $id * @return bool */ public function delete($id) { $user = $this->userModel->where(’id’, $id)->first(); return $user->delete(); } /** * @param $userId * @param $entityId * @param $category * @return bool */ public function saveUserAvatar($userId, $entityId, $category) { $object = $this->fileEntityModel ->where(’entity_id’, $userId) ->where(’category’, $category) ->first(); if (is_null($object)) { $this->fileEntityModel->entity_id = $userId; $this->fileEntityModel->file_id = $entityId; $this->fileEntityModel->category = $category; return $this->fileEntityModel->save(); } else { return $object ->where(’entity_id’, $userId) ->where(’category’, $category) ->update([’file_id’ => $entityId]); } } /** * @param $userId * @param $category * @return FileEntity */ public function getUserAvatar($userId, $category) { return $this->fileEntityModel ->where(’entity_id’, $userId) ->where(’category’, $category) ->first(); } /** * @param $roleId * @return IlluminateDatabaseEloquentCollection|static[] */ public function findAllUsersByRoleId($roleId) { return $this->userModel ->join(’role_user’, 'role_user.user_id’, '=’, 'users.id’) ->select(’users.*’, 'role_user.created_at as user_role_created_at’) ->where(’role_user.role_id’, $roleId) ->get(); } /** * @return IlluminateDatabaseEloquentCollection|static[] */ public function getAdminUser() { return $this->userModel ->join(’role_user’, 'role_user.user_id’, '=’, 'users.id’) ->join(’roles’, 'roles.id’, '=’, 'role_user.role_id’) ->where(’role_user.role_id’, env(’ROLE_ADMIN_USER_ID’)) ->select(’users.*’) ->get(); } /** * @param $userId * @return Contact[]|IlluminateDatabaseEloquentCollection|IlluminateDatabaseEloquentModel|mixed|null */ public function getContactData($userId) { $contact = $this->contactModel ->where(’assigned_dr_id’, $userId) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’) ->get(); return $contact; } /** * @param $doctor_ids * @return Contact[]|IlluminateDatabaseEloquentCollection */ public function getReferredDoctorPatients($doctor_ids) { return $this->contactModel ->whereIn(’assigned_dr_id’, $doctor_ids) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’) ->get(); } /** * @param $user_ids * @return mixed */ public function getContactsByUserIds($user_ids) { return $this->contactModel ->whereIn(’user_id’, $user_ids) ->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’) ->get(); } }End FileuserRepository = $userRepository; $this->roleRepository = $roleRepository; $this->contactService = $contactService; $this->preDefinedService = $preDefinedService; } /** * @param $data * @param $is_creating * @return User */ public function createOrUpdate($data, $is_creating) { $user = new User(); if (!$is_creating) { $user = User::find($data[’id’]); } if ($is_creating) { $user->email = $data[’email’]; $user->first_name = $data[’first_name’]; $user->last_name = $data[’last_name’]; $user->password = Hash::make($data[’password’]); $user->status = true; } else { if (isset($data[’password’]) && !empty($data[’password’])) { $user->password = Hash::make($data[’password’]); } } $user->save(); return $user; } /** * @param $user * @param $data * @param $entityId * @param $type * @param bool $is_creating * @return mixed */ public function saveRole($user, $data, $entityId, $type, $is_creating = true) { if (!empty($data[’roles’])) { $roles = []; foreach ($data[’roles’] as $roleId => $value) { $roles[] = $roleId; } $user->syncRoles($roles); } return $user; } /** * @param array $data * @return mixed */ public function createUserProfile(array $data) { try { DB::beginTransaction(); $data[’password’] = str_random(8); $data[’password_confirmation’] = $data[’password’]; $data[’email’] = (!empty($data[’email’])) ? $data[’email’] : 'john.’.$data[’last_name’].’@example.com’; $user = $this->createOrUpdate($data, true); $data[’roles’][$data[’role’]] = 1; $this->saveRole($user, $data, $user->id, 'user’); $contact = $this->contactService->setOwner($user->id); $this->contactService->storeContact($data); DB::commit(); return $contact; } catch (Exception $e) { DB::rollBack(); return $e->getMessage(); } } /** * @param array $data * @return mixed */ public function createPatientContact(array $data) { try { DB::beginTransaction(); $data[’password’] = str_random(8); $data[’password_confirmation’] = $data[’password’]; $user = $this->createOrUpdate($data, true); $data[’roles’][config(’project.roles.patient’)] = 1; $this->saveRole($user, $data, $user->id, 'user’); $contact = $this->contactService->setOwner($user->id); $this->contactService->storeContact($data); $this->updateContactAssignedDoctor($contact->id, isset($data[’doctor’]) ? $data[’doctor’] : ”); DB::commit(); return true; } catch (Exception $e) { DB::rollBack(); throw $e; } } /** * find patient information and details by id * @param $id * @return mixed */ public function findPatientById($id) { $user = $this->contactService->findContactByUserId($id); return $user; } /** * update patient contact by id * @param $id * @param $data * @return bool * @throws Exception */ public function updatePatientContact($id, $data) { try { DB::beginTransaction(); $contact = $this->contactService->findById($id, true); if (!empty($data[’password’])) { $user = User::find($contact->user_id); $user->password = Hash::make($data[’password’]); $user->email = $data[’email’]; $user->save(); } $this->contactService->updateContact($contact, $data); $this->updateContactAssignedDoctor($id, isset($data[’doctor’]) ? $data[’doctor’] : ”); DB::commit(); return true; } catch (Exception $e) { DB::rollBack(); throw $e; } } /** * @param $category * @return mixed */ public function getPreDefinedData($category) { return $this->preDefinedService->getDataByCategory($category); } /** * @param $contactId * @param $doctorID * @return mixed */ public function updateContactAssignedDoctor($contactId, $doctorID = ”) { return $this->contactService->updateContactAssignedDoctor($contactId, $doctorID); } /** * @param $userId * @return mixed */ public function updatePatientStatus($userId) { $contact = $this->contactService->findContactByUserId($userId); return $this->contactService->updatePatientStatus($contact->id); } } End File# serbogdanov/project # app/Modules/Appointments/Repositories/AppointmentRepository.php appointmentModel = $appointmentModel; $this->userRepository = $userRepository; } /** * @param array $data * @return Appointment|IlluminateDatabaseEloquentModel */ public function create(array $data) { $start = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’start’]); $end = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’end’]); $query = $this->appointmentModel->create([ 'contact_id’ => $data[’contact_id’], 'title’ => $data[’title’], 'dentist_id’ => $data[’dentist_id’], 'type_id’ => $data[’type_id’], 'duration’ => $data[’duration’], 'date’ => $data[’date’], 'treatment_id’ => $data[’treatment_id’], 'start_time’ => $data[’start’], 'end_time’ => $data[’end’], 'all_day’ => 0, 'notes’ => isset($data[’notes’]) ? $data[’notes’] : ”, 'color’ => isset($data[’color’]) ? $data[’color’] : ”, 'is_block’ => isset($data[’is_block’]) ? $data[’is_block’] : 0, 'is_initial’ => isset($data[’is_initial’]) ? $data[’is_initial’] : 0, 'status_id’ => $data[’status_id’], 'start’ => $start, 'end’ => $end, ]); return $query; } /** * Update appointment * @param int $id * @param array $data * @return Appointment * @throws Exception */ public function update($id, array $data) { $appointment = $this->findById($id); if (empty($appointment)) { throw new Exception(’No appointment found.’); } $start = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’start’]); $end = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’end’]); if (array_key_exists(’contact_id’, $data)) { $appointment->contact_id = $data[’contact_id’]; } if (array_key_exists(’title’, $data)) { $appointment->title = $data[’title’]; } if (array_key_exists(’dentist_id’, $data)) { $appointment->dentist_id = $data[’dentist_id’]; } if (array_key_exists(’type_id’, $data)) { $appointment->type_id = $data[’type_id’]; } if (array_key_exists(’duration’, $data)) { $appointment->duration = $data[’duration’]; } if (array_key_exists(’date’, $data)) { $appointment->date = $data[’date’]; } if (array_key_exists(’treatment_id’, $data)) { $appointment->treatment_id = $data[’treatment_id’]; } if (array_key_exists(’start’, $data)) { $appointment->start_time = $data[’start’]; } if (array_key_exists(’end’, $data)) { $appointment->end_time = $data[’end’]; } if (array_key_exists(’all_day’, $data)) { $appointment->all_day = $data[’all_day’]; } if (array_key_exists(’notes’, $data)) { $appointment->notes = $data[’notes’]; } if (array_key_exists(’color’, $data)) { $appointment->color = $data[’color’]; } if (array_key_exists(’is_block’, $data)) { $appointment->is_block = $data[’is_block’]; } if (array_key_exists(’is_initial’, $data)) { $appointment->is_initial = $data[’is_initial’]; } if (array_key_exists(’status_id’, $data)) { $appointment->status_id = $data[’status_id’]; } $appointment->start = $start; $appointment->end = $end; $appointment->save(); return $appointment; } /** * Delete appointment * @param $id * @return bool * @throws Exception */ public function delete($id) { $appointment = $this->findById($id); if (empty($appointment)) { throw new Exception(’No appointment found.’); } return $appointment->delete(); } /** * @param $id * @return Appointment|IlluminateDatabaseEloquentModel|null|object */ public function findById($id) { return $this->appointmentModel->where(’id’, $id)->with([’contact’])->first(); } /** * @param $id * @return Appointment|IlluminateDatabaseEloquentModel|null|object */ public function findDetailById($id) { return $this->appointmentModel->where(’id’, $id) ->with([’contact’,’dentist’,’type’, 'status’, 'treatment’]) ->first(); } /** * @param null $doctorId * @param null $startStr * @param null $endStr * @return mixed */ public function getDentistAppointment($doctorId = null, $startStr = null, $endStr = null) { $query = $this->appointmentModel->where(’dentist_id’, $doctorId); if (!is_null($startStr) && !is_null($endStr)) { $start = Carbon::parse($startStr)->format(’Y-m-d’); $end = Carbon::parse($endStr)->format(’Y-m-d’); // Use date based conditions directly on date column for more accurate search $query = $query->whereRaw(’date(date) >= ?’, [$start]) ->whereRaw(’date(date) with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]); return $query->get(); } /** * @param $contactId * @return mixed */ public function getAppointmentByContactId($contactId) { $query = $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’contact_id’, $contactId); return $query->get(); } /** * @param $startDate * @param $endDate * @return mixed */ public function getAppointmentByRange($startDate, $endDate) { $doctorIds = []; $contactIds = []; $user = Auth::user(); if ($user->hasRole(’doctor’)) { $doctorIds = [$user->id]; } else { $result = $this->userRepository->getUsersByRole(’doctor’); foreach($result as $doctor) { $doctorIds[] = $doctor->id; } } if ($user->hasRole(’doctor’)) { $contacts = $this->userRepository->getContactData($user->id); foreach($contacts as $contact) { $contactIds[] = $contact->id; } } else { $query = $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’date’, ’>=’, $startDate) ->where(’date’, ’whereIn(’dentist_id’, $doctorIds); } return $query->get(); } /** * @param string $sort_by * @param string $sort * @param string $search * @param int $per_page * @return IlluminateContractsPaginationLengthAwarePaginator */ public function getAllAppointments($sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10) { $query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]); if (!is_null($search) && !empty($search)) { $query = $query->where(function ($q) use ($search) { $q->where(’title’, 'like’, '%’.$search.’%’) ->orWhere(’notes’, 'like’, '%’.$search.’%’) ->orWhereHas(’contact’, function ($query) use ($search) { $query->where(’first_name’, 'like’, '%’.$search.’%’) ->orWhere(’last_name’, 'like’, '%’.$search.’%’); }) ->orWhereHas(’dentist’, function ($query) use ($search) { $query->where(’first_name’, 'like’, '%’.$search.’%’) ->orWhere(’last_name’, 'like’, '%’.$search.’%’); }); }); } return $query->orderBy($sort_by, $sort)->paginate($per_page); } /** * @param $user_id * @param string $sort_by * @param string $sort * @param string $search * @param int $per_page * @return IlluminateContractsPaginationLengthAwarePaginator */ public function getAppointmentsByDentist($user_id, $sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10) { $query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’dentist_id’, $user_id); if (!is_null($search) && !empty($search)) { $query = $query->where(function ($q) use ($search) { $q->where(’title’, 'like’, '%’.$search.’%’) ->orWhere(’notes’, 'like’, '%’.$search.’%’) ->orWhereHas(’contact’, function ($query) use ($search) { $query->where(’first_name’, 'like’, '%’.$search.’%’) ->orWhere(’last_name’, 'like’, '%’.$search.’%’); }); }); } return $query->orderBy($sort_by, $sort)->paginate($per_page); } /** * @param $contact_id * @param string $sort_by * @param string $sort * @param string $search * @param int $per_page * @return IlluminateContractsPaginationLengthAwarePaginator */ public function getAppointmentsByContact($contact_id, $sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10) { $query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’contact_id’, $contact_id); if (!is_null($search) && !empty($search)) { $query = $query->where(function ($q) use ($search) { $q->where(’title’, 'like’, '%’.$search.’%’) ->orWhere(’notes’, 'like’, '%’.$search.’%’) ->orWhereHas(’dentist’, function ($query) use ($search) { $query->where(’first_name’, 'like’, '%’.$search.’%’) ->orWhere(’last_name’, 'like’, '%’.$search.’%’); }); }); } return $query->orderBy($sort_by, $sort)->paginate($per_page); } /** * @param $startDate * @param $endDate * @return mixed */ public function getWeeklyAppointments($startDate, $endDate) { $query = $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’date’, ’>=’, $startDate) ->where(’date’, ’get(); } /** * @param string $status * @return mixed */ public function getAppointmentsByStatus($status = 'booked’) { $query = $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->whereHas(’status’, function ($query) use ($status) { $query->where(’value’, $status); }) ->where(’date’, ’>=’, Carbon::now()->format(’Y-m-d’)); return $query->orderBy(’date’, 'asc’)->orderBy(’start_time’, 'asc’)->take(5)->get(); } /** * Get upcoming appointments count * @return int */ public function getUpcomingAppointmentsCount() { return $this->appointmentModel ->where(’date’, ’>=’, Carbon::now()->format(’Y-m-d’)) ->count(); } /** * Get today’s appointments count * @return int */ public function getTodayAppointmentsCount() { return $this->appointmentModel ->where(’date’, Carbon::now()->format(’Y-m-d’)) ->count(); } /** * Get weekly appointments statistics * @return array */ public function getWeeklyAppointmentStats() { $startOfWeek = Carbon::now()->startOfWeek()->format(’Y-m-d’); $endOfWeek = Carbon::now()->endOfWeek()->format(’Y-m-d’); $results = DB::table(’appointments’) ->select(DB::raw(’DATE(date) as day, COUNT(*) as count’)) ->whereBetween(’date’, [$startOfWeek, $endOfWeek]) ->groupBy(’day’) ->orderBy(’day’) ->get(); // Initialize stats array with zeros for each day $stats = []; $current = Carbon::parse($startOfWeek); while ($current->format(’Y-m-d’) format(’Y-m-d’)] = 0; $current->addDay(); } // Fill in actual counts foreach ($results as $result) { $stats[$result->day] = $result->count; } return $stats; } /** * Get dentist specific weekly appointment stats * @param int $dentistId * @return array */ public function getDentistWeeklyAppointmentStats($dentistId) { $startOfWeek = Carbon::now()->startOfWeek()->format(’Y-m-d’); $endOfWeek = Carbon::now()->endOfWeek()->format(’Y-m-d’); $results = DB::table(’appointments’) ->select(DB::raw(’DATE(date) as day, COUNT(*) as count’)) ->where(’dentist_id’, $dentistId) ->whereBetween(’date’, [$startOfWeek, $endOfWeek]) ->groupBy(’day’) ->orderBy(’day’) ->get(); // Initialize stats array with zeros for each day $stats = []; $current = Carbon::parse($startOfWeek); while ($current->format(’Y-m-d’) format(’Y-m-d’)] = 0; $current->addDay(); } // Fill in actual counts foreach ($results as $result) { $stats[$result->day] = $result->count; } return $stats; } /** * Get appointment statistics by treatment type * @return array */ public function getAppointmentStatsByTreatment() { $results = DB::table(’appointments’) ->join(’pre_defined_data’, 'appointments.treatment_id’, '=’, 'pre_defined_data.id’) ->select(’pre_defined_data.value as treatment’, DB::raw(’COUNT(*) as count’)) ->where(’pre_defined_data.category’, 'treatment’) ->groupBy(’pre_defined_data.value’) ->orderBy(’count’, 'desc’) ->take(5) ->get(); $stats = []; foreach ($results as $result) { $stats[$result->treatment] = $result->count; } return $stats; } /** * Get upcoming appointments * @param int $limit * @return mixed */ public function getUpcomingAppointments($limit = 5) { return $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’date’, ’>=’, Carbon::now()->format(’Y-m-d’)) ->orderBy(’date’, 'asc’) ->orderBy(’start_time’, 'asc’) ->take($limit) ->get(); } /** * Get today’s appointments * @return mixed */ public function getTodayAppointments() { return $this->appointmentModel ->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]) ->where(’date’, Carbon::now()->format(’Y-m-d’)) ->orderBy(’start_time’, 'asc’) ->get(); } } End File# serbogdanov/project # app/Modules/Appointments/Controllers/AppointmentController.php repository = $repository; $this->service = $service; $this->preDefinedService = $preDefinedService; $this->userRepository = $userRepository; } /** * @param Request $request * @return IlluminateContractsViewFactory|IlluminateViewView */ public function index(Request $request) { $sort_by = $request->get(’sort_by’, 'date’); $sort = $request->get(’sort’, 'desc’); $search = trim($request->get(’search’)); $per_page = config(’project.per_page’); $user = Auth::user(); if ($user->hasRole(’doctor’)) { $appointments = $this->repository->getAppointmentsByDentist($user->id, $sort_by, $sort, $search, $per_page); } else { $appointments = $this->repository->getAllAppointments($sort_by, $sort, $search, $per_page); } return view(’appointments::index’, compact(’appointments’, 'user’)); } /** * @return IlluminateContractsViewFactory|IlluminateViewView */ public function create() { $doctors = $this->userRepository->getUsersByRole(’doctor’); $patients = $this->userRepository->getUsersByRole(’patient’); $status = $this->preDefinedService->getDataByCategory(’status’); $treatments = $this->preDefinedService->getDataByCategory(’treatment’); $types = $this->preDefinedService->getDataByCategory(’appointment_type’); return view(’appointments::create’, compact(’doctors’, 'patients’, 'status’, 'treatments’, 'types’)); } /** * @param CreateAppointmentRequest $request * @return IlluminateHttpRedirectResponse */ public function store(CreateAppointmentRequest $request) { try { $data = $request->all(); $this->service->createAppointment($data); if ($request->has(’save_and_new’)) { return redirect()->route(’appointments.create’) ->with(’success’, trans(’appointments::messages.record_was_successfully_created’)); } return redirect()->route(’appointments.index’) ->with(’success’, trans(’appointments::messages.record_was_successfully_created’)); } catch (Exception $e) { return back() ->with(’error’, „Cannot save appointment: ” . $e->getMessage()); } } /** * @param $id * @return IlluminateContractsViewFactory|IlluminateViewView */ public function show($id) { $appointment = $this->repository->findDetailById($id); if (empty($appointment)) { return redirect()->route(’appointments.index’) ->with(’error’, trans(’appointments::messages.record_not_found’)); } return view(’appointments::show’, compact(’appointment’)); } /** * @param $id * @return IlluminateContractsViewFactory|IlluminateViewView */ public function edit($id) { $appointment = $this->repository->findDetailById($id); if (empty($appointment)) { return redirect()->route(’appointments.index’) ->with(’error’, trans(’appointments::messages.record_not_found’)); } $doctors = $this->userRepository->getUsersByRole(’doctor’); $patients = $this->userRepository->getUsersByRole(’patient’); $status = $this->preDefinedService->getDataByCategory(’status’); $treatments = $this->preDefinedService->getDataByCategory(’treatment’); $types = $this->preDefinedService->getDataByCategory(’appointment_type’); return view(’appointments::edit’, compact(’appointment’, 'doctors’, 'patients’, 'status’, 'treatments’, 'types’)); } /** * @param UpdateAppointmentRequest $request * @param $id * @return IlluminateHttpRedirectResponse */ public function update(UpdateAppointmentRequest $request, $id) { try { $data = $request->all(); $this->service->updateAppointment($id, $data); return redirect()->route(’appointments.index’) ->with(’success’, trans(’appointments::messages.record_was_successfully_updated’)); } catch (Exception $e) { return back() ->with(’error’, „Cannot update appointment: ” . $e->getMessage()); } } /** * @param $id * @return IlluminateHttpRedirectResponse */ public function destroy($id) { try { $this->repository->delete($id); return back() ->with(’success’, trans(’appointments::messages.record_was_successfully_deleted’)); } catch (Exception $e) { return back() ->with(’error’, trans(’appointments::messages.record_was_not_deleted_due_to_system_error’)); } } /** * @param Request $request * @return IlluminateHttpJsonResponse */ public function events(Request $request) { try { $start = $request->get(’start’); $end = $request->get(’end’); $doctorId = $request->get(’doctor_id’); $data = $this->repository->getDentistAppointment($doctorId, $start, $end); $formattedData = $this->service->formatAppointmentsForCalendar($data); return response()->json([ 'status’ => 'success’, 'data’ => $formattedData ]); } catch (Exception $e) { return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @param Request $request * @return IlluminateHttpJsonResponse */ public function quickCreate(Request $request) { try { DB::beginTransaction(); $data = $request->all(); $appointment = $this->service->createAppointment($data); DB::commit(); return response()->json([ 'status’ => 'success’, 'message’ => 'Appointment created successfully’, 'data’ => $appointment ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @param Request $request * @param $id * @return IlluminateHttpJsonResponse */ public function updateEvent(Request $request, $id) { try { DB::beginTransaction(); $data = $request->all(); $appointment = $this->service->updateEventTiming($id, $data); DB::commit(); return response()->json([ 'status’ => 'success’, 'message’ => 'Appointment updated successfully’, 'data’ => $appointment ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @param Request $request * @param $id * @return IlluminateHttpJsonResponse */ public function quickUpdate(Request $request, $id) { try { DB::beginTransaction(); $data = $request->all(); $appointment = $this->service->updateAppointment($id, $data); DB::commit(); return response()->json([ 'status’ => 'success’, 'message’ => 'Appointment updated successfully’, 'data’ => $appointment ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @param $id * @return IlluminateHttpJsonResponse */ public function getAppointment($id) { try { $appointment = $this->repository->findDetailById($id); if (empty($appointment)) { throw new Exception(’Appointment not found’); } return response()->json([ 'status’ => 'success’, 'data’ => $appointment ]); } catch (Exception $e) { return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @return IlluminateContractsViewFactory|IlluminateViewView */ public function calendar() { $doctors = $this->userRepository->getUsersByRole(’doctor’); $patients = $this->userRepository->getUsersByRole(’patient’); $status = $this->preDefinedService->getDataByCategory(’status’); $treatments = $this->preDefinedService->getDataByCategory(’treatment’); $types = $this->preDefinedService->getDataByCategory(’appointment_type’); // Get current user $user = Auth::user(); $currentDoctorId = $user->hasRole(’doctor’) ? $user->id : null; return view(’appointments::calendar’, compact( 'doctors’, 'patients’, 'status’, 'treatments’, 'types’, 'currentDoctorId’ )); } /** * @param Request $request * @return IlluminateHttpJsonResponse */ public function weeklyView(Request $request) { try { $startDate = $request->get(’start_date’, Carbon::now()->startOfWeek()->format(’Y-m-d’)); $endDate = $request->get(’end_date’, Carbon::now()->endOfWeek()->format(’Y-m-d’)); $appointments = $this->repository->getWeeklyAppointments($startDate, $endDate); $formattedData = $this->service->formatAppointmentsForWeeklyView($appointments); return response()->json([ 'status’ => 'success’, 'data’ => $formattedData, 'period’ => [ 'start’ => $startDate, 'end’ => $endDate ] ]); } catch (Exception $e) { return response()->json([ 'status’ => 'error’, 'message’ => $e->getMessage() ]); } } /** * @return IlluminateContractsViewFactory|IlluminateViewView */ public function weeklySchedule() { $doctors = $this->userRepository->getUsersByRole(’doctor’); $currentWeek = [ 'start’ => Carbon::now()->startOfWeek()->format(’Y-m-d’), 'end’ => Carbon::now()->endOfWeek()->format(’Y-m-d’) ]; return view(’appointments::weekly’, compact(’doctors’, 'currentWeek’)); } /** * @param Request $request * @return IlluminateHttpResponse|SymfonyComponentHttpFoundationBinaryFileResponse */ public function exportPdf(Request $request) { $data = $request->all(); $format = isset($data[’format’]) ? $data[’format’] : 'weekly’; if ($format === 'weekly’) { $startDate = $request->get(’start_date’, Carbon::now()->startOfWeek()->format(’Y-m-d’)); $endDate = $request->get(’end_date’, Carbon::now()->endOfWeek()->format(’Y-m-d’)); $appointments = $this->repository->getWeeklyAppointments($startDate, $endDate); $formattedData = $this->service->formatAppointmentsForWeeklyView($appointments); $pdf = PDF::loadView(’appointments::exports.weekly_pdf’, [ 'appointments’ => $formattedData, 'startDate’ => Carbon::parse($startDate)->format(’d M Y’), 'endDate’ => Carbon::parse($endDate)->format(’d M Y’) ]); return $pdf->download(’weekly-schedule-’.Carbon::now()->format(’Y-m-d’).’.pdf’); } else { // Handle other formats if needed return response(’Unsupported format’, 400); } } } End File# app/Modules/Appointments/Services/AppointmentService.php repository = $repository; } /** * @param array $data * @return mixed * @throws Exception */ public function createAppointment(array $data) { // Validate appointment overlap if needed if (isset($data[’check_overlap’]) && $data[’check_overlap’]) { $this->validateAppointmentOverlap($data); } // Format data for creation $appointmentData = $this->prepareAppointmentData($data); // Create appointment return $this->repository->create($appointmentData); } /** * @param int $id * @param array $data * @return mixed * @throws Exception */ public function updateAppointment($id, array $data) { // Validate appointment overlap if needed if (isset($data[’check_overlap’]) && $data[’check_overlap’]) { $this->validateAppointmentOverlap($data, $id); } // Format data for update $appointmentData = $this->prepareAppointmentData($data); // Update appointment return $this->repository->update($id, $appointmentData); } /** * @param $id * @param array $data * @return mixed * @throws Exception */ public function updateEventTiming($id, array $data) { $appointment = $this->repository->findById($id); if (empty($appointment)) { throw new Exception(’Appointment not found.’); } // Extract date and time from start and end $startDt = Carbon::parse($data[’start’]); $endDt = Carbon::parse($data[’end’]); $updateData = [ 'date’ => $startDt->format(’Y-m-d’), 'start’ => $startDt->format(’H:i’), 'end’ => $endDt->format(’H:i’), 'duration’ => $endDt->diffInMinutes($startDt) ]; // Check for all_day flag if (isset($data[’allDay’])) { $updateData[’all_day’] = $data[’allDay’] ? 1 : 0; } return $this->repository->update($id, $updateData); } /** * @param array $data * @param int|null $excludeId * @throws Exception */ protected function validateAppointmentOverlap(array $data, $excludeId = null) { // Implementation of overlap validation logic // This should check if the new appointment time conflicts with existing ones // for the same dentist // Example implementation left for you to complete based on your requirements } /** * @param array $data * @return array */ protected function prepareAppointmentData(array $data) { // Clean and format the data for appointment creation/update $appointmentData = []; // Map fields from request to appointment model fields $fieldMappings = [ 'contact_id’, 'title’, 'dentist_id’, 'type_id’, 'duration’, 'date’, 'treatment_id’, 'start’, 'end’, 'all_day’, 'notes’, 'color’, 'is_block’, 'is_initial’, 'status_id’ ]; foreach ($fieldMappings as $field) { if (array_key_exists($field, $data)) { $appointmentData[$field] = $data[$field]; } } // Handle special field transformations if needed return $appointmentData; } /** * Format appointments for calendar view * @param $appointments * @return array */ public function formatAppointmentsForCalendar($appointments) { $formattedAppointments = []; foreach ($appointments as $appointment) { $event = [ 'id’ => $appointment->id, 'title’ => $appointment->title, 'start’ => Carbon::parse($appointment->date.’ ’.$appointment->start_time)->toIso8601String(), 'end’ => Carbon::parse($appointment->date.’ ’.$appointment->end_time)->toIso8601String(), 'allDay’ => (bool)$appointment->all_day, 'backgroundColor’ => !empty($appointment->color) ? $appointment->color : '#3788d8′, 'borderColor’ => !empty($appointment->color) ? $appointment->color : '#3788d8′, 'extendedProps’ => [ 'contact_id’ => $appointment->contact_id, 'dentist_id’ => $appointment->dentist_id, 'type_id’ => $appointment->type_id, 'treatment_id’ => $appointment->treatment_id, 'status_id’ => $appointment->status_id, 'notes’ => $appointment->notes, 'is_block’ => $appointment->is_block, 'is_initial’ => $appointment->is_initial, 'duration’ => $appointment->duration, ] ]; // Add patient information if available if ($appointment->contact) { $event[’extendedProps’][’patient’] = [ 'id’ => $appointment->contact->id, 'name’ => $appointment->contact->first_name . ’ ’ . $appointment->contact->last_name, 'phone’ => $appointment->contact->phone ]; } // Add dentist information if available if ($appointment->dentist) { $event[’extendedProps’][’dentist’] = [ 'id’ => $appointment->dentist->id, 'name’ => $appointment->dentist->first_name . ’ ’ . $appointment->dentist->last_name ]; } $formattedAppointments[] = $event; } return $formattedAppointments; } /** * Format appointments for weekly view * @param $appointments * @return array */ public function formatAppointmentsForWeeklyView($appointments) { $formattedData = []; // Group appointments by date $appointmentsByDate = []; foreach ($appointments as $appointment) { $date = $appointment->date; if (!isset($appointmentsByDate[$date])) { $appointmentsByDate[$date] = []; } $appointmentsByDate[$date][] = $appointment; } // Sort appointments by time for each date foreach ($appointmentsByDate as $date => $dateAppointments) { usort($dateAppointments, function($a, $b) { return strtotime($a->start_time) – strtotime($b->start_time); }); $formattedData[$date] = []; foreach ($dateAppointments as $appointment) { $formattedData[$date][] = [ 'id’ => $appointment->id, 'title’ => $appointment->title, 'start_time’ => $appointment->start_time, 'end_time’ => $appointment->end_time, 'patient_name’ => $appointment->contact ? ($appointment->contact->first_name . ’ ’ . $appointment->contact->last_name) : 'N/A’, 'dentist_name’ => $appointment->dentist ? ($appointment->dentist->first_name . ’ ’ . $appointment->dentist->last_name) : 'N/A’, 'treatment’ => $appointment->treatment ? $appointment->treatment->value : 'N/A’, 'status’ => $appointment->status ? $appointment->status->value : 'N/A’, 'color’ => !empty($appointment->color) ? $appointment->color : '#3788d8′, ]; } } return $formattedData; } } End FilegetClientOriginalExtension()); if (!in_array($fileExtension, $allowedTypes)) { throw new Exception(„File type not allowed. Allowed types: ” . implode(’, ’, $allowedTypes)); } } // Validate the file size if maxSize is specified if ($maxSize > 0 && $file->getSize() > $maxSize * 1024) { throw new Exception(„File size exceeds maximum allowed size of {$maxSize}KB”); } // Generate a unique filename $filename = Str::random(20) . ’.’ . $file->getClientOriginalExtension(); // Set storage path $path = $folder ? $folder . '/’ . $filename : $filename; // Store the file Storage::disk($disk)->put($path, file_get_contents($file)); // Return the file path return $path; } /** * Upload and resize image * * @param IlluminateHttpUploadedFile $file * @param string $folder * @param bool $isPublic * @param int $width * @param int $height * @param bool $maintainAspectRatio * @return string|null */ public function uploadImage($file, $folder = null, $isPublic = true, $width = null, $height = null, $maintainAspectRatio = true) { $allowedTypes = [’jpg’, 'jpeg’, 'png’, 'gif’, 'webp’]; // Check if file is valid image $fileExtension = strtolower($file->getClientOriginalExtension()); if (!in_array($fileExtension, $allowedTypes)) { throw new Exception(„File type not allowed. Allowed types: ” . implode(’, ’, $allowedTypes)); } // Generate a unique filename $filename = Str::random(20) . ’.’ . $fileExtension; // Set storage path $path = $folder ? $folder . '/’ . $filename : $filename; // Create image instance $image = Image::make($file); // Resize if dimensions provided if ($width || $height) { if ($maintainAspectRatio) { $image->resize($width, $height, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); } else { $image->resize($width, $height); } } // Define the disk to use $disk = $isPublic ? 'public’ : 'local’; // Save the image Storage::disk($disk)->put($path, (string) $image->encode()); // Return the file path return $path; } /** * Delete file from storage * * @param string $path * @param bool $isPublic * @return bool */ public function deleteFile($path, $isPublic = true) { if (!$path) { return false; } $disk = $isPublic ? 'public’ : 'local’; if (Storage::disk($disk)->exists($path)) { Storage::disk($disk)->delete($path); return true; } return false; } /** * Get public URL for a file * * @param string $path * @return string|null */ public function getFileUrl($path) { if (!$path) { return null; } return Storage::url($path); } } x: number, y: number, width: number, height: number, offset: number, borderRadius: number) { // 3D的框子 context.save(); // 边框设置 context.strokeStyle = 'rgba(66, 77, 116, 0.7)’; context.lineWidth = 2; // 绘制立体框的内壁 const innerTop = y + offset; const innerLeft = x + offset; const innerWidth = width – offset * 2; const innerHeight = height – offset * 2; // 绘制底部矩形(带圆角) this.drawRoundedRect(context, x, y, width, height, borderRadius); context.fillStyle = 'rgba(23, 30, 46, 0.7)’; context.fill(); context.stroke(); // 绘制顶部矩形(带圆角) this.drawRoundedRect(context, innerLeft, innerTop, innerWidth, innerHeight, borderRadius – offset); context.fillStyle = 'rgba(47, 57, 86, 0.7)’; context.fill(); context.stroke(); // 绘制内壁连接线 context.beginPath(); context.moveTo(x + borderRadius, y); context.lineTo(innerLeft + borderRadius – offset, innerTop); context.stroke(); context.beginPath(); context.moveTo(x + width – borderRadius, y); context.lineTo(innerLeft + innerWidth – borderRadius + offset, innerTop); context.stroke(); context.beginPath(); context.moveTo(x + width, y + borderRadius); context.lineTo(innerLeft + innerWidth, innerTop + borderRadius – offset); context.stroke(); context.beginPath(); context.moveTo(x + width, y + height – borderRadius); context.lineTo(innerLeft + innerWidth, innerTop + innerHeight – borderRadius + offset); context.stroke(); context.beginPath(); context.moveTo(x + width – borderRadius, y + height); context.lineTo(innerLeft + innerWidth – borderRadius + offset, innerTop + innerHeight); context.stroke(); context.beginPath(); context.moveTo(x + borderRadius, y + height); context.lineTo(innerLeft + borderRadius – offset, innerTop + innerHeight); context.stroke(); context.beginPath(); context.moveTo(x, y + height – borderRadius); context.lineTo(innerLeft, innerTop + innerHeight – borderRadius + offset); context.stroke(); context.beginPath(); context.moveTo(x, y + borderRadius); context.lineTo(innerLeft, innerTop + borderRadius – offset); context.stroke(); context.restore(); } private drawRoundedRect(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) { context.beginPath(); context.moveTo(x + radius, y); context.lineTo(x + width – radius, y); context.quadraticCurveTo(x + width, y, x + width, y + radius); context.lineTo(x + width, y + height – radius); context.quadraticCurveTo(x + width, y + height, x + width – radius, y + height); context.lineTo(x + radius, y + height); context.quadraticCurveTo(x, y + height, x, y + height – radius); context.lineTo(x, y + radius); context.quadraticCurveTo(x, y, x + radius, y); context.closePath(); } //绘制装饰性箭头 private drawDecorativeArrow(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { const centerX = x + width / 2; const centerY = y + height / 2; const arrowSize = Math.min(width, height) * 0.3; context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.6)’; context.lineWidth = 2; context.lineCap = ’round’; context.lineJoin = ’round’; // 绘制四个方向的箭头 // 上箭头 context.beginPath(); context.moveTo(centerX, centerY – arrowSize); context.lineTo(centerX, centerY – arrowSize / 3); context.moveTo(centerX – arrowSize / 4, centerY – arrowSize * 0.6); context.lineTo(centerX, centerY – arrowSize); context.lineTo(centerX + arrowSize / 4, centerY – arrowSize * 0.6); context.stroke(); // 右箭头 context.beginPath(); context.moveTo(centerX + arrowSize, centerY); context.lineTo(centerX + arrowSize / 3, centerY); context.moveTo(centerX + arrowSize * 0.6, centerY – arrowSize / 4); context.lineTo(centerX + arrowSize, centerY); context.lineTo(centerX + arrowSize * 0.6, centerY + arrowSize / 4); context.stroke(); // 下箭头 context.beginPath(); context.moveTo(centerX, centerY + arrowSize); context.lineTo(centerX, centerY + arrowSize / 3); context.moveTo(centerX – arrowSize / 4, centerY + arrowSize * 0.6); context.lineTo(centerX, centerY + arrowSize); context.lineTo(centerX + arrowSize / 4, centerY + arrowSize * 0.6); context.stroke(); // 左箭头 context.beginPath(); context.moveTo(centerX – arrowSize, centerY); context.lineTo(centerX – arrowSize / 3, centerY); context.moveTo(centerX – arrowSize * 0.6, centerY – arrowSize / 4); context.lineTo(centerX – arrowSize, centerY); context.lineTo(centerX – arrowSize * 0.6, centerY + arrowSize / 4); context.stroke(); context.restore(); } //绘制内部装饰线 private drawDecorativeLines(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.3)’; context.lineWidth = 1; // 绘制网格状的装饰线 const gridSize = Math.min(width, height) / 8; // 水平线 for (let i = 1; i < height / gridSize; i++) { context.beginPath(); context.moveTo(x, y + i * gridSize); context.lineTo(x + width, y + i * gridSize); context.stroke(); } // 垂直线 for (let i = 1; i < width / gridSize; i++) { context.beginPath(); context.moveTo(x + i * gridSize, y); context.lineTo(x + i * gridSize, y + height); context.stroke(); } context.restore(); } //绘制高科技圆形装饰 private drawTechCircle(context: CanvasRenderingContext2D, x: number, y: number, size: number) { context.save(); const centerX = x + size / 2; const centerY = y + size / 2; const radius = size / 2; // 绘制外圆环 context.beginPath(); context.arc(centerX, centerY, radius, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.4)'; context.lineWidth = 2; context.stroke(); // 绘制内圆环 context.beginPath(); context.arc(centerX, centerY, radius * 0.8, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.3)'; context.lineWidth = 1; context.stroke(); // 绘制十字标记 context.beginPath(); context.moveTo(centerX – radius * 0.6, centerY); context.lineTo(centerX + radius * 0.6, centerY); context.moveTo(centerX, centerY – radius * 0.6); context.lineTo(centerX, centerY + radius * 0.6); context.strokeStyle = 'rgba(66, 133, 244, 0.5)'; context.lineWidth = 1; context.stroke(); // 绘制四个点 const pointRadius = radius * 0.05; const pointDistance = radius * 0.7; context.fillStyle = 'rgba(66, 133, 244, 0.7)'; context.beginPath(); context.arc(centerX + pointDistance, centerY, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX – pointDistance, centerY, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX, centerY + pointDistance, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX, centerY – pointDistance, pointRadius, 0, Math.PI * 2); context.fill(); // 绘制弧线装饰 for (let i = 0; i < 4; i++) { const startAngle = i * Math.PI / 2 + Math.PI / 8; const endAngle = startAngle + Math.PI / 4; context.beginPath(); context.arc(centerX, centerY, radius * 0.9, startAngle, endAngle); context.strokeStyle = 'rgba(66, 133, 244, 0.6)'; context.lineWidth = 1.5; context.stroke(); } context.restore(); } /** * 绘制旋转中的装饰性圆 */ private drawRotatingCircles(context: CanvasRenderingContext2D, x: number, y: number, size: number, time: number) { const centerX = x + size / 2; const centerY = y + size / 2; context.save(); // 计算旋转角度 const rotation1 = (time / 2000) % (Math.PI * 2); const rotation2 = (time / 3000) % (Math.PI * 2); // 绘制外圆 context.beginPath(); context.arc(centerX, centerY, size * 0.45, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.2)'; context.lineWidth = 1; context.stroke(); // 绘制旋转的分割线 – 外圈 context.translate(centerX, centerY); context.rotate(rotation1); context.translate(-centerX, -centerY); for (let i = 0; i < 12; i++) { context.save(); context.translate(centerX, centerY); context.rotate(i * Math.PI / 6); context.beginPath(); context.moveTo(size * 0.35, 0); context.lineTo(size * 0.45, 0); context.strokeStyle = 'rgba(66, 133, 244, 0.6)'; context.lineWidth = 1; context.stroke(); context.restore(); } // 绘制内圆 context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); context.arc(centerX, centerY, size * 0.3, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.3)'; context.lineWidth = 1; context.stroke(); // 绘制旋转的分割线 – 内圈 context.translate(centerX, centerY); context.rotate(rotation2); context.translate(-centerX, -centerY); for (let i = 0; i < 8; i++) { context.save(); context.translate(centerX, centerY); context.rotate(i * Math.PI / 4); context.beginPath(); context.moveTo(size * 0.2, 0); context.lineTo(size * 0.3, 0); context.strokeStyle = 'rgba(66, 133, 244, 0.5)'; context.lineWidth = 1; context.stroke(); context.restore(); } // 绘制中心小圆 context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); context.arc(centerX, centerY, size * 0.05, 0, Math.PI * 2); context.fillStyle = 'rgba(66, 133, 244, 0.6)'; context.fill(); context.restore(); } /** * 绘制左下角的指示标记 */ private drawCornerMarker(context: CanvasRenderingContext2D, x: number, y: number, size: number) { const cornerX = x; const cornerY = y + size; context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.fillStyle = 'rgba(66, 133, 244, 0.1)'; context.lineWidth = 1; // 绘制L形标记 context.beginPath(); context.moveTo(cornerX, cornerY – size * 0.3); context.lineTo(cornerX, cornerY); context.lineTo(cornerX + size * 0.3, cornerY); context.stroke(); // 绘制小方块 context.beginPath(); context.rect(cornerX + size * 0.15 – size * 0.05, cornerY – size * 0.15 – size * 0.05, size * 0.1, size * 0.1); context.fill(); context.stroke(); context.restore(); } /** * 绘制右上角的指示标记 */ private drawTopRightMarker(context: CanvasRenderingContext2D, x: number, y: number, width: number, size: number) { const cornerX = x + width; const cornerY = y; context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.fillStyle = 'rgba(66, 133, 244, 0.1)'; context.lineWidth = 1; // 绘制反L形标记 context.beginPath(); context.moveTo(cornerX, cornerY + size * 0.3); context.lineTo(cornerX, cornerY); context.lineTo(cornerX – size * 0.3, cornerY); context.stroke(); // 绘制小方块 context.beginPath(); context.rect(cornerX – size * 0.15 – size * 0.05, cornerY + size * 0.15 – size * 0.05, size * 0.1, size * 0.1); context.fill(); context.stroke(); context.restore(); } /** * 绘制数据流动的动画效果 */ private drawDataFlow(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, time: number) { context.save(); // 数据流的参数 const flowSpeed = 1000; // 流动速度的基准值(毫秒) const flowCount = 3; // 数据流的数量 const flowLength = Math.min(width, height) * 0.15; // 数据流的长度 const flowWidth = 2; // 数据流的宽度 // 设置数据流样式 context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.lineWidth = flowWidth; context.lineCap = 'round'; // 绘制右边的数据流 for (let i = 0; i < flowCount; i++) { const offset = (time + i * (flowSpeed / flowCount)) % flowSpeed / flowSpeed; const startY = y + height * offset; if (startY y + height) { visibleLength = y + height – startY; } context.beginPath(); context.moveTo(x + width, startY); context.lineTo(x + width, startY + visibleLength); context.stroke(); } } // 绘制下边的数据流 for (let i = 0; i < flowCount; i++) { const offset = (time + i * (flowSpeed / flowCount) + flowSpeed / 2) % flowSpeed / flowSpeed; const startX = x + width * offset; if (startX x + width) { visibleLength = x + width – startX; } context.beginPath(); context.moveTo(startX, y + height); context.lineTo(startX + visibleLength, y + height); context.stroke(); } } context.restore(); } /** * 绘制高科技左上角标记 */ private drawTopLeftMarker(context: CanvasRenderingContext2D, x: number, y: number, size: number) { context.save(); // 设置样式 context.strokeStyle = 'rgba(66, 133, 244, 0.7)’; context.lineWidth = 1; // 绘制角标记 context.beginPath(); context.moveTo(x, y + size * 0.3); context.lineTo(x, y); context.lineTo(x + size * 0.3, y); context.stroke(); // 绘制装饰点 context.fillStyle = 'rgba(66, 133, 244, 0.8)’; context.beginPath(); context.arc(x + size * 0.15, y + size * 0.15, size * 0.03, 0, Math.PI * 2); context.fill(); context.restore(); } /** * 绘制高科技右下角标记 */ private drawBottomRightMarker(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, size: number) { const cornerX = x + width; const cornerY = y + height; context.save(); // 设置样式 context.strokeStyle = 'rgba(66, 133, 244, 0.7)’; context.lineWidth = 1; // 绘制角标记 context.beginPath(); context.moveTo(cornerX, cornerY – size * 0.3); context.lineTo(cornerX, cornerY); context.lineTo(cornerX – size * 0.3, cornerY); context.stroke(); // 绘制装饰点 context.fillStyle = 'rgba(66, 133, 244, 0.8)’; context.beginPath(); context.arc(cornerX – size * 0.15, cornerY – size * 0.15, size * 0.03, 0, Math.PI * 2); context.fill(); context.restore(); } /** * 绘制高科技装饰性刻度 */ private drawTechScale(context: CanvasRenderingContext2D, x: number, y: number, width: number, time: number) { context.save(); // 刻度参数 const scaleHeight = 5; const scaleCount = 20; const scaleSpacing = width / scaleCount; const animOffset = (time / 1000) % 1 * scaleSpacing; // 设置样式 context.strokeStyle = 'rgba(66, 133, 244, 0.5)’; context.lineWidth = 1; // 绘制刻度 for (let i = 0; i void, debounceTimeMs: number = 300) { // 取消之前的订阅 if (this.searchSubscription) { this.searchSubscription.unsubscribe(); } // 创建新的订阅,处理搜索操作 this.searchSubscription = this.searchSubject.pipe( debounceTime(debounceTimeMs), distinctUntilChanged(), tap(() => { this.isLoading = true; // 搜索开始时设置loading状态 }), switchMap(searchText => { return timer(500).pipe( map(() => searchText) ); }), ).subscribe(searchText => { searchHandler(searchText); this.isLoading = false; // 搜索完成后关闭loading状态 }); } /** * 设置刷新处理器 * @param refreshHandler 刷新处理函数 */ setupRefresh(refreshHandler: () => void) { // 取消之前的订阅 if (this.refreshSubscription) { this.refreshSubscription.unsubscribe(); } // 创建新的订阅,处理刷新操作 this.refreshSubscription = this.refreshSubject.pipe( tap(() => { this.isLoading = true; // 刷新开始时设置loading状态 }), switchMap(() => { return timer(500).pipe( map(() => {}) ); }), ).subscribe(() => { refreshHandler(); this.isLoading = false; // 刷新完成后关闭loading状态 }); } /** * 触发搜索 * @param searchText 搜索文本 */ search(searchText: string) { this.searchSubject.next(searchText); } /** * 触发刷新 */ refresh() { this.refreshSubject.next(); } /** * 清理订阅,避免内存泄漏 */ destroy() { if (this.searchSubscription) { this.searchSubscription.unsubscribe(); this.searchSubscription = null; } if (this.refreshSubscription) { this.refreshSubscription.unsubscribe(); this.refreshSubscription = null; } } } End Fileimport { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core’; import { CommonModule } from '@angular/common’; import { SciFiHUDRenderer } from ’./sci-fi-hud-renderer’; // 定义HUD模式枚举 export enum HUDMode { NORMAL = 'normal’, // 正常模式 ALERT = 'alert’, // 警告模式 SUCCESS = 'success’, // 成功模式 INACTIVE = 'inactive’ // 不活跃模式 } // HUD显示数据接口 export interface HUDData { title?: string; // 标题 data?: any; // 数据 targetValue?: number; // 目标值 currentValue?: number; // 当前值 percentage?: number; // 百分比 isLocked?: boolean; // 是否锁定 status?: string; // 状态文本 mode?: HUDMode; // HUD模式 subtitle?: string; // 副标题 } @Component({ selector: 'app-sci-fi-hud’, standalone: true, imports: [CommonModule], templateUrl: ’./sci-fi-hud.component.html’, styleUrl: ’./sci-fi-hud.component.scss’ }) export class SciFiHudComponent implements OnInit { @ViewChild(’hudCanvas’, { static: true }) hudCanvas!: ElementRef; @Input() width: number = 300; @Input() height: number = 200; @Input() data: HUDData = {}; private renderer!: SciFiHUDRenderer; private animationId: number = 0; constructor() {} ngOnInit(): void { this.initializeCanvas(); this.startRendering(); } ngOnChanges(): void { if (this.renderer) { this.renderer.setData(this.data); } } ngOnDestroy(): void { if (this.animationId) { cancelAnimationFrame(this.animationId); } } private initializeCanvas(): void { const canvas = this.hudCanvas.nativeElement; canvas.width = this.width; canvas.height = this.height; const ctx = canvas.getContext(’2d’); if (!ctx) { console.error(’Cannot get 2D context from canvas’); return; } // 初始化渲染器 this.renderer = new SciFiHUDRenderer(ctx, this.width, this.height); this.renderer.setData(this.data); } private startRendering(): void { const renderFrame = () => { if (!this.renderer) return; this.renderer.render(); this.animationId = requestAnimationFrame(renderFrame); }; renderFrame(); } /** * 更新数据并刷新HUD显示 * @param data 要更新的HUD数据 */ public updateData(data: Partial): void { this.data = { …this.data, …data }; if (this.renderer) { this.renderer.setData(this.data); } } /** * 设置HUD模式 * @param mode HUD模式 */ public setMode(mode: HUDMode): void { this.updateData({ mode }); } /** * 设置进度值 * @param currentValue 当前值 * @param targetValue 目标值(可选) */ public setProgress(currentValue: number, targetValue?: number): void { const updateData: Partial = { currentValue }; if (targetValue !== undefined) { updateData.targetValue = targetValue; } if (updateData.targetValue) { updateData.percentage = Math.min(100, Math.max(0, (updateData.currentValue || 0) / updateData.targetValue * 100)); } this.updateData(updateData); } /** * 设置锁定状态 * @param isLocked 是否锁定 */ public setLocked(isLocked: boolean): void { this.updateData({ isLocked }); } /** * 设置状态文本 * @param status 状态文本 */ public setStatus(status: string): void { this.updateData({ status }); } } End Fileimport { Component, EventEmitter, Input, OnInit, Output } from '@angular/core’; import { CommonModule } from '@angular/common’; import { FormsModule } from '@angular/forms’; import { ListLoadingController } from ’../list-loading-controller.service’; /** * 表格分页事件接口 */ export interface PageEvent { pageIndex: number; // 当前页码索引(从0开始) pageSize: number; // 每页显示条数 length: number; // 总条数 } /** * 表格列配置接口 */ export interface TableColumn { field: string; // 字段名 header: string; // 显示的表头 sortable?: boolean; // 是否可排序 class?: string; // 列样式类 type?: 'text’ | 'number’ | 'date’ | 'boolean’ | 'custom’; // 列数据类型 format?: (value: any) => any; // 自定义格式化函数 width?: string; // 列宽度 align?: 'left’ | 'center’ | 'right’; // 对齐方式 visible?: boolean; // 是否可见,默认为true } /** * 加载中表格组件 * 一个现代化、高性能的数据表格组件,支持排序、分页和加载状态 */ @Component({ selector: 'app-list-loading-table’, standalone: true, imports: [CommonModule, FormsModule], templateUrl: ’./list-loading-table.component.html’, styleUrl: ’./list-loading-table.component.scss’ }) export class ListLoadingTableComponent implements OnInit { @Input() data: any[] = []; // 表格数据 @Input() columns: TableColumn[] = []; // 列配置 @Input() enablePagination: boolean = true; // 是否启用分页 @Input() pageSize: number = 10; // 每页数据量 @Input() pageSizeOptions: number[] = [5, 10, 25, 50]; // 可选每页数量 @Input() totalRecords: number = 0; // 总记录数 @Input() currentPageIndex: number = 0; // 当前页码(从0开始) @Input() showSearch: boolean = true; // 是否显示搜索 @Input() searchPlaceholder: string = '搜索…’; // 搜索框提示文本 @Input() noDataMessage: string = '没有数据’; // 无数据时的提示信息 @Input() loading: boolean = false; // 加载状态 @Input() showRefresh: boolean = true; // 是否显示刷新按钮 @Input() stripped: boolean = true; // 是否使用条纹背景 @Input() bordered: boolean = false; // 是否显示边框 @Input() hoverable: boolean = true; // 行是否有悬停效果 @Input() emptyStateImage?: string; // 空状态图片 @Input() expandableRows: boolean = false; // 是否允许行展开 @Input() selectable: boolean = false; // 是否可选择行 @Output() pageChange = new EventEmitter(); // 分页变更事件 @Output() sortChange = new EventEmitter(); // 排序变更事件 @Output() searchChange = new EventEmitter(); // 搜索变更事件 @Output() refreshRequest = new EventEmitter(); // 刷新请求事件 @Output() rowClick = new EventEmitter(); // 行点击事件 @Output() rowSelect = new EventEmitter(); // 行选择事件 searchText: string = ”; // 搜索文本 selectedRows: Set = new Set(); // 已选择的行 expandedRows: Set = new Set(); // 已展开的行 sortField: string | null = null; // 当前排序字段 sortDirection: 'asc’ | 'desc’ = 'asc’; // 当前排序方向 get visibleColumns(): TableColumn[] { return this.columns.filter(col => col.visible !== false); } constructor(public loadingController: ListLoadingController) {} ngOnInit() { // 设置搜索处理 this.loadingController.setupSearch((searchText) => { this.searchChange.emit(searchText); }); // 设置刷新处理 this.loadingController.setupRefresh(() => { this.refreshRequest.emit(); }); } /** * 处理搜索变更 */ onSearchChange() { this.loadingController.search(this.searchText); } /** * 处理刷新请求 */ onRefreshClick() { this.loadingController.refresh(); } /** * 处理排序变更 * @param field 排序字段 */ onSort(field: string) { // 查找列配置,检查是否可排序 const column = this.columns.find(col => col.field === field); if (!column || column.sortable === false) return; // 如果点击的是当前排序列,则切换排序方向 if (this.sortField === field) { this.sortDirection = this.sortDirection === 'asc’ ? 'desc’ : 'asc’; } else { // 否则使用新的排序列,并设置默认排序方向为升序 this.sortField = field; this.sortDirection = 'asc’; } this.sortChange.emit({ field, direction: this.sortDirection }); } /** * 处理页大小变更 * @param size 新的页大小 */ onPageSizeChange(size: number) { this.pageSize = size; this.currentPageIndex = 0; // 重置页码 this.emitPageEvent(); } /** * 处理页面变更 * @param offset 页码偏移量(1 表示下一页,-1 表示上一页) */ onPageChange(offset: number) { const newPageIndex = this.currentPageIndex + offset; if (newPageIndex >= 0 && newPageIndex * this.pageSize this.selectedRows.add(row)); } else { this.selectedRows.clear(); } this.rowSelect.emit(Array.from(this.selectedRows)); } /** * 检查行是否被选择 * @param row 行数据 */ isSelected(row: any): boolean { return this.selectedRows.has(row); } /** * 获取全选状态 * 如果所有行都被选中,返回true; * 如果部分行被选中,返回null(indeterminate状态); * 如果没有行被选中,返回false。 */ get selectAllState(): boolean | null { if (this.data.length === 0) return false; if (this.selectedRows.size === 0) return false; if (this.selectedRows.size === this.data.length) return true; return null; // indeterminate state } /** * 检查行是否展开 * @param row 行数据 */ isExpanded(row: any): boolean { return this.expandedRows.has(row); } /** * 格式化单元格值 * @param column 列配置 * @param row 行数据 */ getCellValue(column: TableColumn, row: any): any { const value = row[column.field]; // 使用列格式化函数(如果有) if (column.format) { return column.format(value); } // 根据列类型进行格式化 switch (column.type) { case 'date’: return value ? new Date(value).toLocaleDateString() : ”; case 'boolean’: return value ? '是’ : '否’; default: return value; } } /** * 获取当前分页信息文本 */ get paginationInfo(): string { const start = this.currentPageIndex * this.pageSize + 1; const end = Math.min(start + this.pageSize – 1, this.totalRecords); return `${start}-${end} / ${this.totalRecords}`; } /** * 获取列对齐样式类 * @param column 列配置 */ getColumnAlignClass(column: TableColumn): string { return column.align ? `text-${column.align}` : ”; } /** * 获取当前页数据 */ get pageData(): any[] { if (!this.enablePagination) return this.data; const start = this.currentPageIndex * this.pageSize; return this.data.slice(start, start + this.pageSize); } /** * 重置所有筛选和分页状态 */ reset() { this.searchText = ”; this.currentPageIndex = 0; this.sortField = null; this.sortDirection = 'asc’; this.selectedRows.clear(); this.expandedRows.clear(); this.searchChange.emit(”); this.emitPageEvent(); } } End File# serbogdanov/project /** * 科幻风格HUD (Heads-Up Display)渲染器 * 负责绘制各种科幻元素和动画效果 */ export class SciFiHUDRenderer { private context: CanvasRenderingContext2D; private width: number; private height: number; private startTime: number; private data: any = {}; private animationValues: any = { scale: 0, opacity: 0, progress: 0, rotation: 0 }; // 颜色方案 private colorSchemes = { normal: { primary: 'rgba(0, 149, 255, 0.8)’, secondary: 'rgba(0, 149, 255, 0.4)’, background: 'rgba(10, 20, 40, 0.3)’, text: 'rgba(150, 210, 255, 0.9)’, highlight: 'rgba(0, 200, 255, 1)’ }, alert: { primary: 'rgba(255, 60, 50, 0.8)’, secondary: 'rgba(255, 60, 50, 0.4)’, background: 'rgba(40, 15, 15, 0.3)’, text: 'rgba(255, 180, 170, 0.9)’, highlight: 'rgba(255, 100, 70, 1)’ }, success: { primary: 'rgba(50, 200, 100, 0.8)’, secondary: 'rgba(50, 200, 100, 0.4)’, background: 'rgba(15, 30, 20, 0.3)’, text: 'rgba(170, 255, 200, 0.9)’, highlight: 'rgba(80, 240, 120, 1)’ }, inactive: { primary: 'rgba(150, 150, 150, 0.5)’, secondary: 'rgba(100, 100, 100, 0.3)’, background: 'rgba(20, 20, 20, 0.3)’, text: 'rgba(200, 200, 200, 0.7)’, highlight: 'rgba(180, 180, 180, 0.8)’ } }; // 当前颜色方案 private currentColorScheme: any; constructor(context: CanvasRenderingContext2D, width: number, height: number) { this.context = context; this.width = width; this.height = height; this.startTime = Date.now(); this.currentColorScheme = this.colorSchemes.normal; } /** * 设置HUD数据 * @param data HUD数据对象 */ setData(data: any): void { this.data = data; // 根据模式更新颜色方案 if (data.mode) { this.currentColorScheme = this.colorSchemes[data.mode] || this.colorSchemes.normal; } } /** * 渲染HUD帧 */ render(): void { const ctx = this.context; const currentTime = Date.now(); const elapsed = currentTime – this.startTime; // 清除画布 ctx.clearRect(0, 0, this.width, this.height); // 更新动画值 this.updateAnimationValues(elapsed); // 绘制背景 this.drawBackground(); // 绘制边框和装饰元素 this.drawBorder(); this.drawDecorativeElements(elapsed); // 绘制内容 this.drawContent(elapsed); } /** * 更新动画值 * @param elapsed 经过的时间(毫秒) */ private updateAnimationValues(elapsed: number): void { // 平滑的进入动画 this.animationValues.scale = Math.min(1, elapsed / 500); this.animationValues.opacity = Math.min(1, elapsed / 700); // 进度动画(如果有目标值和当前值) if (this.data.percentage !== undefined) { const targetProgress = this.data.percentage / 100; this.animationValues.progress += (targetProgress – this.animationValues.progress) * 0.1; } // 旋转动画 this.animationValues.rotation = (elapsed / 3000) % (Math.PI * 2); } /** * 绘制HUD背景 */ private drawBackground(): void { const ctx = this.context; const { width, height } = this; // 半透明背景 ctx.fillStyle = this.currentColorScheme.background; ctx.fillRect(0, 0, width, height); // 网格效果 ctx.strokeStyle = this.currentColorScheme.secondary; ctx.lineWidth = 0.5; ctx.globalAlpha = 0.3; const gridSize = 20; for (let x = 0; x <= width; x += gridSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for (let y = 0; y 0) { ctx.save(); ctx.globalAlpha = 0.4; ctx.fillStyle = this.currentColorScheme.highlight; const stripeWidth = 10; const stripeAngle = Math.PI / 4; const stripeSpacing = 15; ctx.beginPath(); for (let i = -width; i { const prettierRC = JSON.parse(fs.readFileSync(’.prettierrc’, 'utf8′)); const eslint = new ESLint({ fix: true, useEslintrc: true, }); // Format with Prettier const formattedJS = prettier.format(jsStr, { …prettierRC, parser: 'babel’, }); // Apply ESLint fixes const [result] = await eslint.lintText(formattedJS); return result.output || formattedJS; }; // Utility to format SCSS code const formatSCSS = (scssStr) => { const prettierRC = JSON.parse(fs.readFileSync(’.prettierrc’, 'utf8′)); return prettier.format(scssStr, { …prettierRC, parser: 'scss’, }); }; // Format number with 0 decimal places. const formatNumber = (num) => parseFloat(num.toFixed(0)); // Create and detect dark/light colors const generateColorInfo = (color) => { const chromaColor = chroma(color); const luminance = chromaColor.luminance(); const isDark = luminance contrast.black ? '#FFFFFF’ : '#1A1A1A’; return { hex: color.toUpperCase(), rgb: chromaColor.rgb().map(formatNumber), isDark, textColor, }; }; // Generate JS output const generateJS = async () => { // Generate JavaScript version of the color swatch let jsOutput = `/** * Auto-generated color swatch. * DO NOT EDIT DIRECTLY. */ const swatch = ${JSON.stringify(swatch, null, 2)};n export default swatch;`; // Format and write the JS file const formattedJS = await formatJS(jsOutput); fs.writeFileSync(JS_PATH, formattedJS); console.log(`✅ Generated JavaScript color swatch at ${JS_PATH}`); }; // Generate SCSS output const generateSCSS = () => { let scssOutput = '// Auto-generated color swatch variables.n// DO NOT EDIT DIRECTLY.nn’; // Process each palette Object.entries(swatch).forEach(([paletteName, palette]) => { scssOutput += `// ${paletteName} paletten`; // Process each color in the palette Object.entries(palette).forEach(([colorName, color]) => { if (typeof color === 'string’) { // For base colors (strings) const varName = `$color-${paletteName}-${colorName}`; scssOutput += `${varName}: ${color.toUpperCase()};n`; } else { // For color objects with shades Object.entries(color).forEach(([shadeName, shadeValue]) => { const varName = `$color-${paletteName}-${colorName}-${shadeName}`; scssOutput += `${varName}: ${shadeValue.toUpperCase()};n`; }); } }); scssOutput += 'n’; }); // Add CSS custom properties (variables) scssOutput += '// CSS custom propertiesn’; scssOutput += ’:root {n’; // Process each palette for CSS variables Object.entries(swatch).forEach(([paletteName, palette]) => { scssOutput += ` // ${paletteName} paletten`; // Process each color in the palette Object.entries(palette).forEach(([colorName, color]) => { if (typeof color === 'string’) { // For base colors (strings) const varName = `–color-${paletteName}-${colorName}`; scssOutput += ` ${varName}: ${color.toUpperCase()};n`; } else { // For color objects with shades Object.entries(color).forEach(([shadeName, shadeValue]) => { const varName = `–color-${paletteName}-${colorName}-${shadeName}`; scssOutput += ` ${varName}: ${shadeValue.toUpperCase()};n`; }); } }); }); scssOutput += ’}n’; // Format and write the SCSS file const formattedSCSS = formatSCSS(scssOutput); fs.writeFileSync(SCSS_PATH, formattedSCSS); console.log(`✅ Generated SCSS color variables at ${SCSS_PATH}`); }; // Generate documentation const generateDocs = () => { let mdOutput = '# Color SwatchnnAuto-generated color documentation.nn’; // Process each palette Object.entries(swatch).forEach(([paletteName, palette]) => { mdOutput += `## ${paletteName} Palettenn`; // Process each color in the palette Object.entries(palette).forEach(([colorName, color]) => { if (typeof color === 'string’) { // For base colors (strings) const colorInfo = generateColorInfo(color); mdOutput += `### ${colorName}nn`; mdOutput += ` n`; mdOutput += ` ${colorName}n`; mdOutput += ` HEX: ${colorInfo.hex}n`; mdOutput += ` RGB: ${colorInfo.rgb.join(’, ’)}n`; mdOutput += ` nn`; mdOutput += `CSS Variable: `–color-${paletteName}-${colorName}`nn`; mdOutput += `SCSS Variable: `$color-${paletteName}-${colorName}`nn`; } else { // For color objects with shades mdOutput += `### ${colorName}nn`; mdOutput += ’ n’; Object.entries(color).forEach(([shadeName, shadeValue]) => { const colorInfo = generateColorInfo(shadeValue); mdOutput += ` n`; mdOutput += ` ${colorName}-${shadeName}n`; mdOutput += ` HEX: ${colorInfo.hex}n`; mdOutput += ` RGB: ${colorInfo.rgb.join(’, ’)}n`; mdOutput += ` n`; }); mdOutput += ’ nn’; // Add variables reference mdOutput += 'CSS Variables:nn’; Object.keys(color).forEach((shadeName) => { mdOutput += `- `–color-${paletteName}-${colorName}-${shadeName}`n`; }); mdOutput += 'n’; mdOutput += 'SCSS Variables:nn’; Object.keys(color).forEach((shadeName) => { mdOutput += `- `$color-${paletteName}-${colorName}-${shadeName}`n`; }); mdOutput += 'n’; } }); }); fs.writeFileSync(DOCS_PATH, mdOutput); console.log(`✅ Generated color documentation at ${DOCS_PATH}`); }; // Main function to run all generators const main = async () => { try { // Make sure the directories exist if (!fs.existsSync(path.dirname(JS_PATH))) { fs.mkdirSync(path.dirname(JS_PATH), { recursive: true }); } if (!fs.existsSync(path.dirname(SCSS_PATH))) { fs.mkdirSync(path.dirname(SCSS_PATH), { recursive: true }); } if (!fs.existsSync(path.dirname(DOCS_PATH))) { fs.mkdirSync(path.dirname(DOCS_PATH), { recursive: true }); } // Generate files await generateJS(); generateSCSS(); generateDocs(); console.log(’✨ All color files generated successfully!’); } catch (error) { console.error(’Error generating color files:’, error); process.exit(1); } }; main(); End File# packages/react/src/hooks/useMediaQuery.ts import { useEffect, useState } from 'react’; /** * Media query breakpoints */ export const breakpoints = { xs: '(max-width: 575px)’, sm: '(min-width: 576px)’, md: '(min-width: 768px)’, lg: '(min-width: 992px)’, xl: '(min-width: 1200px)’, xxl: '(min-width: 1400px)’, }; export type Breakpoint = keyof typeof breakpoints; interface UseMediaQueryOptions { /* If true, will return false on the server regardless of actual matches */ noSSR?: boolean; /* Default value to return before the media query is evaluated */ defaultValue?: boolean; } /** * React hook for using media queries in components * * @param query The media query string or a breakpoint key * @param options Configuration options * @returns Boolean indicating if the media query matches * * @example * // Basic usage with breakpoint * const isMobile = useMediaQuery(’sm’); * * // Custom media query * const isPrint = useMediaQuery(’print’); * const isLandscape = useMediaQuery('(orientation: landscape)’); * * // With options * const isDesktop = useMediaQuery(’lg’, { noSSR: true, defaultValue: false }); */ export function useMediaQuery( query: string | Breakpoint, options: UseMediaQueryOptions = {}, ): boolean { const { noSSR = false, defaultValue = false } = options; const [matches, setMatches] = useState(() => { // Handle SSR case if (typeof window === 'undefined’) { return noSSR ? false : defaultValue; } // Resolve the query string const mediaQuery = breakpoints[query as Breakpoint] || query; // Initial match return window.matchMedia(mediaQuery).matches; }); useEffect(() => { // Resolve the query string const mediaQuery = breakpoints[query as Breakpoint] || query; // Get the media query list const mediaQueryList = window.matchMedia(mediaQuery); // Set initial value setMatches(mediaQueryList.matches); // Define handler for media query changes const handler = (event: MediaQueryListEvent) => setMatches(event.matches); // Add event listener mediaQueryList.addEventListener(’change’, handler); // Cleanup return () => { mediaQueryList.removeEventListener(’change’, handler); }; }, [query]); return matches; } End File# myrta-ds/myrta import { useState, useEffect, RefObject } from 'react’; /** * Options for the click outside hook */ interface UseClickOutsideOptions { /** * Whether the hook is active * @default true */ active?: boolean; /** * Callback to execute when click outside occurs */ onClickOutside: () => void; } /** * React hook to detect clicks outside of the specified element * * @param ref Reference to the element to detect clicks outside of * @param options Configuration options * * @example * „`jsx * const MyComponent = () => { * const ref = useRef(null); * const [isOpen, setIsOpen] = useState(false); * * useClickOutside(ref, { * active: isOpen, * onClickOutside: () => setIsOpen(false), * }); * * return ( * * setIsOpen(true)}>Open * {isOpen && ( * * This will close when clicking outside * * )} * * ); * }; * „` */ export function useClickOutside( ref: RefObject, options: UseClickOutsideOptions, ): void { const { active = true, onClickOutside } = options; const [initialized, setInitialized] = useState(false); useEffect(() => { setInitialized(true); }, []); useEffect(() => { // Skip if not active or not initialized (for SSR compatibility) if (!active || !initialized) { return undefined; } const handleClickOutside = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { onClickOutside(); } }; // Add event listener with capture to ensure it runs before other click handlers document.addEventListener(’mousedown’, handleClickOutside, true); // Cleanup function return () => { document.removeEventListener(’mousedown’, handleClickOutside, true); }; }, [ref, active, initialized, onClickOutside]); } End Fileimport { useRef, useCallback, Dispatch, SetStateAction, useState } from 'react’; /** * Options for useDisclosure hook */ interface UseDisclosureOptions { /** * The default state * @default false */ defaultIsOpen?: boolean; /** * Function called when state changes */ onStateChange?: (isOpen: boolean) => void; } /** * The return value of useDisclosure */ interface UseDisclosureReturn { /** Current state */ isOpen: boolean; /** Function to open */ open: () => void; /** Function to close */ close: () => void; /** Function to toggle */ toggle: () => void; /** Function to set state directly */ setOpen: Dispatch>; } /** * Hook for managing disclosure state (open/close) * Useful for modals, dropdowns, etc. * * @param options Configuration options * @returns Object with state and handlers * * @example * „`jsx * function Modal() { * const { isOpen, open, close } = useDisclosure(); * * return ( * * Open Modal * {isOpen && ( * * Modal content * Close * * )} * * ); * } * „` */ export function useDisclosure( options: UseDisclosureOptions = {} ): UseDisclosureReturn { const { defaultIsOpen = false, onStateChange } = options; const [isOpen, setIsOpen] = useState(defaultIsOpen); const onStateChangeRef = useRef(onStateChange); // Keep the onStateChange ref up to date onStateChangeRef.current = onStateChange; const setOpen = useCallback((value: boolean | ((prevState: boolean) => boolean)) => { setIsOpen((prev) => { const next = typeof value === 'function’ ? value(prev) : value; if (prev !== next) { onStateChangeRef.current?.(next); } return next; }); }, []); const open = useCallback(() => setOpen(true), [setOpen]); const close = useCallback(() => setOpen(false), [setOpen]); const toggle = useCallback(() => setOpen((prev) => !prev), [setOpen]); return { isOpen, open, close, toggle, setOpen, }; } End Fileimport { useEffect, useRef, useState } from 'react’; /** * Options for useCopyToClipboard hook */ interface UseCopyToClipboardOptions { /** * Reset the copied state after this many milliseconds * @default null (no reset) */ resetAfter?: number | null; /** * Callback called after copy success */ onSuccess?: (text: string) => void; /** * Callback called after copy failure */ onError?: (error: Error) => void; } /** * The return value of useCopyToClipboard */ interface UseCopyToClipboardReturn { /** The currently copied text or null if nothing copied */ copied: string | null; /** Is the copy operation in progress */ copying: boolean; /** Whether the last copy operation succeeded */ success: boolean | null; /** Function to copy text to clipboard */ copy: (text: string) => Promise; /** Function to clear the copied state */ reset: () => void; } /** * Hook to copy text to clipboard * * @param options Configuration options * @returns Object with copied state and copy function * * @example * „`jsx * function CopyButton() { * const { copied, copying, success, copy } = useCopyToClipboard({ * resetAfter: 2000, * onSuccess: () => toast.success(’Copied to clipboard!’), * }); * * return ( * copy(’Text to copy’)} * disabled={copying} * > * {success ? 'Copied!’ : 'Copy’} * * ); * } * „` */ export function useCopyToClipboard( options: UseCopyToClipboardOptions = {} ): UseCopyToClipboardReturn { const { resetAfter = null, onSuccess, onError } = options; const [copied, setCopied] = useState(null); const [copying, setCopying] = useState(false); const [success, setSuccess] = useState(null); const resetTimeoutRef = useRef(null); // Clear timeout on unmount useEffect(() => { return () => { if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); } }; }, []); const reset = () => { setCopied(null); setSuccess(null); if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); resetTimeoutRef.current = null; } }; const copy = async (text: string): Promise => { if (!navigator?.clipboard) { const error = new Error(’Clipboard API not supported’); onError?.(error); return false; } // Clear any existing reset timeout if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); resetTimeoutRef.current = null; } setCopying(true); setSuccess(null); try { await navigator.clipboard.writeText(text); setCopied(text); setSuccess(true); onSuccess?.(text); // Set up reset timeout if specified if (resetAfter !== null) { resetTimeoutRef.current = window.setTimeout(reset, resetAfter); } return true; } catch (error) { setCopied(null); setSuccess(false); onError?.(error as Error); return false; } finally { setCopying(false); } }; return { copied, copying, success, copy, reset, }; } End File# packages/react/src/hooks/useCountdown.ts import { useState, useEffect, useRef, useCallback } from 'react’; /** * Options for useCountdown hook */ interface UseCountdownOptions { /** * Time in milliseconds */ milliseconds: number; /** * Interval for countdown updates in milliseconds * @default 1000 */ interval?: number; /** * Whether to start the countdown automatically * @default true */ autoStart?: boolean; /** * Callback when countdown reaches zero */ onComplete?: () => void; /** * Callback on each countdown tick */ onTick?: (millisecondsLeft: number) => void; } /** * The return value of useCountdown */ interface UseCountdownReturn { /** Whether the countdown is currently running */ isRunning: boolean; /** Whether the countdown is complete */ isComplete: boolean; /** Time left in milliseconds */ millisecondsLeft: number; /** Function to start/resume the countdown */ start: () => void; /** Function to pause the countdown */ pause: () => void; /** Function to stop and reset the countdown */ reset: () => void; /** Percentage of time elapsed (0-100) */ progress: number; /** Formatted time left as mm:ss */ formatted: string; } /** * Hook to create a countdown timer * * @param options Configuration options * @returns Object with countdown state and controls * * @example * „`jsx * function Timer() { * const { * isRunning, * formatted, * progress, * start, * pause, * reset * } = useCountdown({ * milliseconds: 60000, // 1 minute * onComplete: () => console.log(’Countdown finished!’), * }); * * return ( * * Time left: {formatted} * * * {isRunning ? 'Pause’ : 'Start’} * * Reset * * ); * } * „` */ export function useCountdown( options: UseCountdownOptions ): UseCountdownReturn { const { milliseconds, interval = 1000, autoStart = true, onComplete, onTick, } = options; const [millisecondsLeft, setMillisecondsLeft] = useState(milliseconds); const [isRunning, setIsRunning] = useState(autoStart); const [isComplete, setIsComplete] = useState(false); const intervalRef = useRef(null); const startTimeRef = useRef(0); const timeRemainingRef = useRef(milliseconds); // Format time as mm:ss const formatTime = (ms: number): string => { const totalSeconds = Math.ceil(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, '0′)}:${seconds.toString().padStart(2, '0′)}`; }; // Calculate progress percentage const calculateProgress = (ms: number): number => { return ((milliseconds – ms) / milliseconds) * 100; }; const stop = useCallback(() => { if (intervalRef.current !== null) { window.clearInterval(intervalRef.current); intervalRef.current = null; } setIsRunning(false); }, []); const reset = useCallback(() => { stop(); setMillisecondsLeft(milliseconds); timeRemainingRef.current = milliseconds; setIsComplete(false); }, [milliseconds, stop]); const tick = useCallback(() => { const currentTime = Date.now(); const elapsedTime = currentTime – startTimeRef.current; startTimeRef.current = currentTime; const newTimeRemaining = Math.max(0, timeRemainingRef.current – elapsedTime); timeRemainingRef.current = newTimeRemaining; setMillisecondsLeft(newTimeRemaining); onTick?.(newTimeRemaining); if (newTimeRemaining { if (isRunning || isComplete) return; setIsRunning(true); startTimeRef.current = Date.now(); // Clear any existing interval to be safe if (intervalRef.current !== null) { window.clearInterval(intervalRef.current); } // Start the interval intervalRef.current = window.setInterval(tick, interval); }, [interval, isComplete, isRunning, tick]); const pause = useCallback(() => { stop(); }, [stop]); // Set up interval when running useEffect(() => { if (isRunning) { startTimeRef.current = Date.now(); intervalRef.current = window.setInterval(tick, interval); } return () => { if (intervalRef.current !== null) { window.clearInterval(intervalRef.current); } }; }, [interval, isRunning, tick]); return { isRunning, isComplete, millisecondsLeft, start, pause, reset, progress: calculateProgress(millisecondsLeft), formatted: formatTime(millisecondsLeft), }; } End Fileimport React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import { Box, BoxProps } from ’../Box/Box’; import ’./Tooltip.scss’; export interface TooltipProps extends BoxProps { /** * Content to display inside the tooltip */ content: React.ReactNode; /** * Tooltip position relative to the trigger * @default 'top’ */ position?: 'top’ | 'right’ | 'bottom’ | 'left’; /** * Trigger element to hook the tooltip to */ children: React.ReactElement; /** * Tooltip width in pixels * @default 200 */ width?: number; /** * Delay before showing the tooltip in milliseconds * @default 200 */ showDelay?: number; /** * Delay before hiding the tooltip in milliseconds * @default 100 */ hideDelay?: number; /** * Whether the tooltip should show on click instead of hover * @default false */ clickTrigger?: boolean; /** * Whether the tooltip should be visible initially * @default false */ defaultVisible?: boolean; /** * Whether the tooltip is disabled * @default false */ disabled?: boolean; } export const Tooltip: React.FC = ({ content, position = 'top’, children, width = 200, showDelay = 200, hideDelay = 100, clickTrigger = false, defaultVisible = false, disabled = false, className, …props }) => { const [visible, setVisible] = useState(defaultVisible); const [tooltipStyle, setTooltipStyle] = useState({}); const triggerRef = useRef(null); const tooltipRef = useRef(null); const timeoutRef = useRef(null); const tooltipId = useId(); const calculatePosition = () => { if (!triggerRef.current || !tooltipRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollLeft = window.scrollX || document.documentElement.scrollLeft; let top, left; switch (position) { case 'top’: top = triggerRect.top + scrollTop – tooltipRect.height – 8; left = triggerRect.left + scrollLeft + (triggerRect.width / 2) – (tooltipRect.width / 2); break; case 'right’: top = triggerRect.top + scrollTop + (triggerRect.height / 2) – (tooltipRect.height / 2); left = triggerRect.right + scrollLeft + 8; break; case 'bottom’: top = triggerRect.bottom + scrollTop + 8; left = triggerRect.left + scrollLeft + (triggerRect.width / 2) – (tooltipRect.width / 2); break; case 'left’: top = triggerRect.top + scrollTop + (triggerRect.height / 2) – (tooltipRect.height / 2); left = triggerRect.left + scrollLeft – tooltipRect.width – 8; break; } // Ensure tooltip stays within viewport const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (left viewportWidth) { left = viewportWidth – tooltipRect.width; } if (top viewportHeight + scrollTop) { top = viewportHeight + scrollTop – tooltipRect.height; } setTooltipStyle({ top: `${top}px`, left: `${left}px`, width: `${width}px`, opacity: 1, visibility: 'visible’ }); }; const handleShowTooltip = () => { if (disabled) return; if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = window.setTimeout(() => { setVisible(true); }, showDelay); }; const handleHideTooltip = () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = window.setTimeout(() => { setVisible(false); }, hideDelay); }; const handleToggleTooltip = () => { if (disabled) return; setVisible(prevVisible => !prevVisible); }; useEffect(() => { if (visible) { calculatePosition(); // Recalculate on window resize window.addEventListener(’resize’, calculatePosition); // Recalculate on scroll window.addEventListener(’scroll’, calculatePosition); } return () => { window.removeEventListener(’resize’, calculatePosition); window.removeEventListener(’scroll’, calculatePosition); if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, [visible]); // Clone the trigger element to add event handlers const triggerElement = React.cloneElement(children, { ref: (node: HTMLElement | null) => { triggerRef.current = node; // Forward ref if the child has one const { ref } = children as any; if (typeof ref === 'function’) ref(node); else if (ref) ref.current = node; }, …(clickTrigger ? { onClick: (e: React.MouseEvent) => { handleToggleTooltip(); // Call the original onClick if it exists if (children.props.onClick) children.props.onClick(e); }, } : { onMouseEnter: (e: React.MouseEvent) => { handleShowTooltip(); // Call the original onMouseEnter if it exists if (children.props.onMouseEnter) children.props.onMouseEnter(e); }, onMouseLeave: (e: React.MouseEvent) => { handleHideTooltip(); // Call the original onMouseLeave if it exists if (children.props.onMouseLeave) children.props.onMouseLeave(e); }, onFocus: (e: React.FocusEvent) => { handleShowTooltip(); // Call the original onFocus if it exists if (children.props.onFocus) children.props.onFocus(e); }, onBlur: (e: React.FocusEvent) => { handleHideTooltip(); // Call the original onBlur if it exists if (children.props.onBlur) children.props.onBlur(e); }, }), 'aria-describedby’: visible ? tooltipId : undefined, }); return ( {triggerElement} {visible && ( {content} )} ); }; End Fileimport React, { useEffect, useRef, useState } from 'react’; import clsx from 'clsx’; import { Icon } from ’../Icon/Icon’; import { useId } from ’../../hooks/useId’; import ’./Accordion.scss’; export interface AccordionProps { /** * Unique identifier for the accordion */ id?: string; /** * Accordion item title */ title: React.ReactNode; /** * Whether the accordion is expanded by default * @default false */ defaultExpanded?: boolean; /** * Whether the accordion is expanded (controlled component) */ expanded?: boolean; /** * Function called when the expanded state changes */ onExpandedChange?: (expanded: boolean) => void; /** * Content of the accordion */ children: React.ReactNode; /** * Additional CSS class for the accordion */ className?: string; /** * Whether to show the divider between accordion items * @default true */ showDivider?: boolean; /** * Custom icon to be displayed instead of the default */ icon?: React.ReactNode; /** * Additional CSS class for the title button */ titleClassName?: string; /** * Additional CSS class for the content panel */ contentClassName?: string; /** * Callback when accordion is focused */ onFocus?: React.FocusEventHandler; /** * Callback when accordion loses focus */ onBlur?: React.FocusEventHandler; /** * Whether the accordion is disabled * @default false */ disabled?: boolean; } export const Accordion = React.forwardRef( ( { id, title, defaultExpanded = false, expanded: expandedProp, onExpandedChange, children, className, showDivider = true, icon, titleClassName, contentClassName, onFocus, onBlur, disabled = false, …rest }, ref, ) => { const [isExpanded, setIsExpanded] = useState(defaultExpanded); const generatedId = useId(); const accordionId = id || `accordion-${generatedId}`; const headingId = `${accordionId}-heading`; const contentId = `${accordionId}-content`; const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState( defaultExpanded ? undefined : 0, ); // Handle controlled component const expanded = expandedProp !== undefined ? expandedProp : isExpanded; const handleToggle = () => { if (disabled) return; const newExpanded = !expanded; setIsExpanded(newExpanded); onExpandedChange?.(newExpanded); }; // Update height when expanded state changes useEffect(() => { if (!contentRef.current) return; if (expanded) { const height = contentRef.current.scrollHeight; setContentHeight(height); // Remove fixed height after transition to allow for content changes const timer = setTimeout(() => { setContentHeight(undefined); }, 300); // Should match transition duration return () => clearTimeout(timer); } else { // Set current height before collapsing for smooth animation setContentHeight(contentRef.current.scrollHeight); // Force a reflow void contentRef.current.offsetHeight; // Set to 0 for animation const timer = setTimeout(() => { setContentHeight(0); }, 10); return () => clearTimeout(timer); } }, [expanded]); return ( {title} {icon || } {children} ); }, ); Accordion.displayName = 'Accordion’; End File# packages/react/src/components/Button/Button.tsx import React from 'react’; import clsx from 'clsx’; import ’./Button.scss’; import { Spinner } from ’../Spinner/Spinner’; export type ButtonVariant = 'primary’ | 'secondary’ | 'tertiary’ | 'danger’; export type ButtonSize = 'small’ | 'medium’ | 'large’; export interface ButtonProps extends React.ButtonHTMLAttributes { /** * Button variant * @default 'primary’ */ variant?: ButtonVariant; /** * Button size * @default 'medium’ */ size?: ButtonSize; /** * Whether the button extends to fill its container * @default false */ fullWidth?: boolean; /** * Icon to display at the start of the button */ startIcon?: React.ReactNode; /** * Icon to display at the end of the button */ endIcon?: React.ReactNode; /** * Whether the button is in a loading state * @default false */ loading?: boolean; /** * Text to display when in loading state */ loadingText?: string; /** * CSS class for the loading spinner */ spinnerClassName?: string; /** * Whether the button should be rendered as a link */ href?: string; /** * Target attribute for link buttons */ target?: string; /** * Rel attribute for link buttons */ rel?: string; } const ButtonRoot = React.forwardRef( ( { as: Component, className, variant = 'primary’, size = 'medium’, disabled, fullWidth = false, startIcon, endIcon, loading = false, loadingText, spinnerClassName, children, …rest }, ref ) => { const isDisabled = disabled || loading; // Use proper DOM props based on the component type const domProps = Component === 'button’ ? { disabled: isDisabled } : { 'aria-disabled’: isDisabled }; return ( {loading && ( )} {startIcon && {startIcon}} {loading && loadingText ? loadingText : children} {endIcon && {endIcon}} ); } ); export const Button = React.forwardRef( (props, ref) => { if (props.href && !props.disabled) { return ; } return ; } ); Button.displayName = 'Button’; End File# myrta-ds/myrta # packages/react/src/components/Toggle/Toggle.tsx import React, { useState, useEffect } from 'react’; import clsx from 'clsx’; import ’./Toggle.scss’; import { useId } from ’../../hooks/useId’; export interface ToggleProps { /** * Whether the toggle is checked */ checked?: boolean; /** * Default checked state when uncontrolled * @default false */ defaultChecked?: boolean; /** * Whether the toggle is disabled * @default false */ disabled?: boolean; /** * Function called when the toggle state changes */ onChange?: (checked: boolean, event: React.ChangeEvent) => void; /** * Size of the toggle * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * An accessible label for the toggle */ label?: string; /** * ID for the toggle input */ id?: string; /** * Additional CSS class for the toggle */ className?: string; /** * Additional CSS class for the toggle track */ trackClassName?: string; /** * Additional CSS class for the toggle thumb */ thumbClassName?: string; /** * Name attribute for the hidden input */ name?: string; /** * ARIA label for the toggle */ 'aria-label’?: string; /** * ARIA labelledby for the toggle */ 'aria-labelledby’?: string; /** * Loading state * @default false */ loading?: boolean; /** * Required state * @default false */ required?: boolean; } export const Toggle = React.forwardRef( ( { checked, defaultChecked = false, disabled = false, onChange, size = 'medium’, label, id, className, trackClassName, thumbClassName, name, 'aria-label’: ariaLabel, 'aria-labelledby’: ariaLabelledBy, loading = false, required = false, …rest }, ref ) => { const [isChecked, setIsChecked] = useState(defaultChecked); const generatedId = useId(); const toggleId = id || `toggle-${generatedId}`; // Handle the controlled component case useEffect(() => { if (checked !== undefined) { setIsChecked(checked); } }, [checked]); const handleChange = (event: React.ChangeEvent) => { if (disabled || loading) return; const newChecked = event.target.checked; // Only update internal state for uncontrolled component if (checked === undefined) { setIsChecked(newChecked); } onChange?.(newChecked, event); }; return ( {loading && } {label && {label}} ); } ); Toggle.displayName = 'Toggle’; End Fileimport React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Select.scss’; import { Icon } from ’../Icon/Icon’; export type SelectOption = { value: string; label: React.ReactNode; disabled?: boolean; }; export type SelectSize = 'small’ | 'medium’ | 'large’; export interface SelectProps extends Omit, 'size’> { /** * Options for the select dropdown */ options: SelectOption[]; /** * Label for the select field */ label?: string; /** * Helper text to be displayed below the select */ helperText?: string; /** * Error message to be displayed below the select */ error?: string; /** * Size of the select * @default 'medium’ */ size?: SelectSize; /** * Placeholder text displayed when no option is selected * @default 'Select an option’ */ placeholder?: string; /** * Called when the selected option changes */ onValueChange?: (value: string) => void; /** * Whether the select should take the full width of its container * @default false */ fullWidth?: boolean; /** * Additional CSS class for the select container */ className?: string; /** * Additional CSS class for the select element */ selectClassName?: string; /** * Additional CSS class for the label */ labelClassName?: string; /** * Whether the select is loading * @default false */ loading?: boolean; /** * Loading text to display when loading * @default 'Loading…’ */ loadingText?: string; /** * Required field indicator */ required?: boolean; } export const Select = React.forwardRef( ( { options, label, helperText, error, size = 'medium’, placeholder = 'Select an option’, value, defaultValue, onChange, onValueChange, fullWidth = false, className, selectClassName, labelClassName, disabled = false, loading = false, loadingText = 'Loading…’, required = false, id, name, …props }, ref ) => { const [selectedValue, setSelectedValue] = useState( value !== undefined ? String(value) : defaultValue !== undefined ? String(defaultValue) : undefined ); const selectRef = useRef(null); const generatedId = useId(); const selectId = id || `select-${generatedId}`; const helperTextId = `helper-text-${generatedId}`; const errorId = `error-${generatedId}`; // Update internal state when controlled value changes useEffect(() => { if (value !== undefined) { setSelectedValue(String(value)); } }, [value]); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; // Only update internal state if it’s an uncontrolled component if (value === undefined) { setSelectedValue(newValue); } // Call the provided onChange handler if (onChange) { onChange(e); } // Call the custom onValueChange handler if (onValueChange) { onValueChange(newValue); } }; return ( {label && ( {label} {required && *} )} { // Handle both the ref prop and the ref object if (ref) { if (typeof ref === 'function’) { ref(node); } else { ref.current = node; } } selectRef.current = node; }} id={selectId} name={name} value={selectedValue} onChange={handleChange} disabled={disabled || loading} className={clsx(’mds-select__control’, selectClassName)} aria-invalid={Boolean(error)} aria-describedby={ error ? errorId : helperText ? helperTextId : undefined } required={required} {…props} > {placeholder && ( {loading ? loadingText : placeholder} )} {options.map((option) => ( {option.label} ))} {error ? ( {error} ) : helperText ? ( {helperText} ) : null} ); } ); Select.displayName = 'Select’; End File# myrta-ds/myrta # packages/react/src/components/Table/Table.tsx import React from 'react’; import clsx from 'clsx’; import ’./Table.scss’; // Table component export interface TableProps extends React.TableHTMLAttributes { /** * Whether the table has zebra-striped rows * @default false */ striped?: boolean; /** * Whether the table has borders * @default false */ bordered?: boolean; /** * Whether the table’s cells are compact * @default false */ compact?: boolean; /** * Whether the table has hover state on rows * @default false */ hoverable?: boolean; /** * Whether the table is in a loading state * @default false */ loading?: boolean; /** * Width of the table * @default '100%’ */ width?: string | number; /** * Custom CSS class for table container */ wrapperClassName?: string; } export const Table = React.forwardRef( ( { children, className, striped = false, bordered = false, compact = false, hoverable = false, loading = false, width = '100%’, wrapperClassName, …rest }, ref ) => ( {children} Przewiń tabelę w poziomie ←→ {loading && ( )} ) ); Table.displayName = 'Table’; // Table Head component export interface TableHeadProps extends React.HTMLAttributes { /** * Whether the header is sticky * @default false */ sticky?: boolean; } export const TableHead = React.forwardRef( ({ children, className, sticky = false, …rest }, ref) => ( {children} ) ); TableHead.displayName = 'TableHead’; // Table Body component export const TableBody = React.forwardRef(({ children, className, …rest }, ref) => ( {children} )); TableBody.displayName = 'TableBody’; // Table Row component export interface TableRowProps extends React.HTMLAttributes { /** * Whether the row is selected * @default false */ selected?: boolean; /** * Whether the row is clickable * @default false */ clickable?: boolean; /** * Whether the row is disabled * @default false */ disabled?: boolean; } export const TableRow = React.forwardRef( ({ children, className, selected = false, clickable = false, disabled = false, …rest }, ref) => ( {children} ) ); TableRow.displayName = 'TableRow’; // Table Cell component export interface TableCellProps extends React.TdHTMLAttributes { /** * Text alignment within the cell */ align?: 'left’ | 'center’ | 'right’; /** * Truncate text with ellipsis if it’s too long * @default false */ truncate?: boolean; /** * Maximum width of the cell */ maxWidth?: string | number; } export const TableCell = React.forwardRef( ({ children, className, align, truncate = false, maxWidth, …rest }, ref) => ( {children} ) ); TableCell.displayName = 'TableCell’; // Table Header Cell component export interface TableHeaderCellProps extends React.ThHTMLAttributes { /** * Text alignment within the header cell */ align?: 'left’ | 'center’ | 'right’; /** * Whether the column is sortable * @default false */ sortable?: boolean; /** * Current sort direction for this column */ sortDirection?: 'asc’ | 'desc’ | null; /** * Width of the column */ width?: string | number; } export const TableHeaderCell = React.forwardRef( ( { children, className, align, sortable = false, sortDirection, width, …rest }, ref ) => ( {children} {sortable && ( {sortDirection === 'asc’ ? '▲’ : sortDirection === 'desc’ ? '▼’ : '⇅’} )} ) ); TableHeaderCell.displayName = 'TableHeaderCell’; // Table Footer component export const TableFooter = React.forwardRef(({ children, className, …rest }, ref) => ( {children} )); TableFooter.displayName = 'TableFooter’; // Table Pagination component export interface TablePaginationProps extends React.HTMLAttributes { /** * Total number of items */ count: number; /** * Number of items per page * @default 10 */ rowsPerPage?: number; /** * Available options for rows per page * @default [5, 10, 25] */ rowsPerPageOptions?: number[]; /** * Current page (0-based) * @default 0 */ page: number; /** * Callback fired when the page changes */ onPageChange: (page: number) => void; /** * Callback fired when rows per page changes */ onRowsPerPageChange?: (rowsPerPage: number) => void; /** * Label for the rows per page selector * @default 'Rows per page:’ */ labelRowsPerPage?: string; } export const TablePagination: React.FC = ({ count, rowsPerPage = 10, rowsPerPageOptions = [5, 10, 25], page, onPageChange, onRowsPerPageChange, labelRowsPerPage = 'Rows per page:’, className, …rest }) => { const totalPages = Math.ceil(count / rowsPerPage); const from = page * rowsPerPage + 1; const to = Math.min(count, (page + 1) * rowsPerPage); const handlePrevPage = () => { if (page > 0) { onPageChange(page – 1); } }; const handleNextPage = () => { if (page { onRowsPerPageChange?.(Number(e.target.value)); }; return ( {labelRowsPerPage} {rowsPerPageOptions.map((option) => ( {option} ))} {count > 0 ? `${from}-${to} of ${count}` : '0-0 of 0′} ◀ = totalPages – 1} aria-label=”Next page” > ▶ ); }; TablePagination.displayName = 'TablePagination’; End File# packages/react/src/components/Dialog/Dialog.tsx import React, { useEffect, useRef } from 'react’; import { createPortal } from 'react-dom’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Dialog.scss’; export interface DialogProps { /** * Whether the dialog is open */ open: boolean; /** * Title of the dialog */ title?: React.ReactNode; /** * Content of the dialog */ children: React.ReactNode; /** * Function called when the dialog should close */ onClose: () => void; /** * Additional content for the dialog’s footer */ footer?: React.ReactNode; /** * CSS class for the dialog container */ className?: string; /** * CSS class for the dialog content */ contentClassName?: string; /** * CSS class for the dialog backdrop */ backdropClassName?: string; /** * Whether to show a close button in the top-right corner * @default true */ showCloseButton?: boolean; /** * The size of the dialog * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’ | 'fullscreen’; /** * Whether to close the dialog when the backdrop is clicked * @default true */ closeOnBackdropClick?: boolean; /** * Whether to close the dialog when Escape key is pressed * @default true */ closeOnEsc?: boolean; /** * The maximum height of the dialog content */ maxContentHeight?: string; /** * The ID of the dialog */ id?: string; /** * Whether the dialog is in a loading state * @default false */ loading?: boolean; /** * Whether to prevent scrolling of the body when the dialog is open * @default true */ preventBodyScroll?: boolean; } export const Dialog: React.FC = ({ open, title, children, onClose, footer, className, contentClassName, backdropClassName, showCloseButton = true, size = 'medium’, closeOnBackdropClick = true, closeOnEsc = true, maxContentHeight, id, loading = false, preventBodyScroll = true, }) => { const dialogRef = useRef(null); const generatedId = useId(); const dialogId = id || `dialog-${generatedId}`; const dialogTitleId = `${dialogId}-title`; useEffect(() => { // Focus the dialog when it opens if (open && dialogRef.current) { // Set focus after a small delay to ensure the dialog is fully rendered const timeoutId = setTimeout(() => { if (dialogRef.current) { dialogRef.current.focus(); } }, 50); return () => clearTimeout(timeoutId); } }, [open]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape’ && open && closeOnEsc) { onClose(); } }; document.addEventListener(’keydown’, handleKeyDown); return () => { document.removeEventListener(’keydown’, handleKeyDown); }; }, [open, closeOnEsc, onClose]); useEffect(() => { if (preventBodyScroll) { if (open) { // Store the original overflow style const originalOverflow = document.body.style.overflow; // Prevent scrolling on the body document.body.style.overflow = 'hidden’; return () => { // Restore original overflow style document.body.style.overflow = originalOverflow; }; } } }, [open, preventBodyScroll]); const handleBackdropClick = (event: React.MouseEvent) => { if ( closeOnBackdropClick && event.target === event.currentTarget ) { onClose(); } }; if (!open) return null; const dialogContent = ( {title && ( {title} {showCloseButton && ( × )} )} {children} {footer && {footer} } ); // Use createPortal to render the dialog at the end of the document body return createPortal(dialogContent, document.body); }; End File# myrta-ds/myrta import React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./TextField.scss’; export interface TextFieldProps extends Omit, 'size’> { /** * Label for the text field */ label?: string; /** * Helper text to be displayed below the text field */ helperText?: string; /** * Error message to be displayed below the text field */ error?: string; /** * Size of the text field * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Whether the text field should take the full width of its container * @default false */ fullWidth?: boolean; /** * Icon to display at the start of the text field */ startIcon?: React.ReactNode; /** * Icon to display at the end of the text field */ endIcon?: React.ReactNode; /** * Additional CSS class for the text field container */ className?: string; /** * Additional CSS class for the input element */ inputClassName?: string; /** * Additional CSS class for the label */ labelClassName?: string; /** * Whether the text field is loading * @default false */ loading?: boolean; /** * Type of the input * @default 'text’ */ type?: string; /** * Maximum length of text input */ maxLength?: number; /** * Whether to show character count * @default false */ showCharacterCount?: boolean; } export const TextField = React.forwardRef( ( { label, helperText, error, size = 'medium’, fullWidth = false, startIcon, endIcon, className, inputClassName, labelClassName, disabled = false, loading = false, type = 'text’, maxLength, showCharacterCount = false, id, value, defaultValue, onChange, required, …props }, ref ) => { const [inputValue, setInputValue] = useState( value !== undefined ? String(value) : defaultValue !== undefined ? String(defaultValue) : ” ); const inputRef = useRef(null); const generatedId = useId(); const inputId = id || `text-field-${generatedId}`; const helperTextId = `helper-text-${generatedId}`; const errorId = `error-${generatedId}`; // Update internal state when controlled value changes useEffect(() => { if (value !== undefined) { setInputValue(String(value)); } }, [value]); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; // Only update internal state if it’s an uncontrolled component if (value === undefined) { setInputValue(newValue); } if (onChange) { onChange(e); } }; return ( 0, }, className )} > {label && ( {label} {required && *} )} {startIcon && ( {startIcon} )} { // Handle both the ref prop and the ref object if (ref) { if (typeof ref === 'function’) { ref(node); } else { ref.current = node; } } inputRef.current = node; }} id={inputId} type={type} value={value !== undefined ? value : inputValue} onChange={handleChange} disabled={disabled || loading} className={clsx(’mds-text-field__input’, inputClassName)} aria-invalid={Boolean(error)} aria-describedby={ error ? errorId : helperText ? helperTextId : undefined } maxLength={maxLength} required={required} {…props} /> {endIcon && ( {endIcon} )} {loading && ( )} {error ? ( {error} ) : helperText ? ( {helperText} ) : null} {(maxLength && showCharacterCount) && ( {inputValue.length}/{maxLength} )} ); } ); TextField.displayName = 'TextField’; End File# myrta-ds/myrta import React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Tabs.scss’; export interface TabProps extends React.ButtonHTMLAttributes { /** * Value of the tab, used for selection */ value: string; /** * Whether the tab is disabled * @default false */ disabled?: boolean; /** * Icon to display before the label */ icon?: React.ReactNode; /** * Badge or notification indicator */ badge?: React.ReactNode; } export const Tab = React.forwardRef( ( { children, className, value, disabled = false, icon, badge, …rest }, ref ) => { // This component is not rendered directly // It serves as a configuration component for TabsRoot return null; } ); Tab.displayName = 'Tab’; export interface TabPanelProps extends React.HTMLAttributes { /** * Value of the tab this panel is associated with */ value: string; /** * Additional CSS class */ className?: string; } export const TabPanel = React.forwardRef( ({ children, className, value, …rest }, ref) => { // This component is not rendered directly // It serves as a configuration component for TabsRoot return null; } ); TabPanel.displayName = 'TabPanel’; export interface TabsProps extends React.HTMLAttributes { /** * Current active tab value */ value: string; /** * Callback when tab changes */ onChange: (value: string) => void; /** * Orientation of the tabs * @default 'horizontal’ */ orientation?: 'horizontal’ | 'vertical’; /** * Variant of the tabs * @default 'default’ */ variant?: 'default’ | 'enclosed’ | 'pills’; /** * Size of the tabs * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Whether tabs should fill the container width * @default false */ fullWidth?: boolean; /** * Whether to center the tabs * @default false */ centered?: boolean; /** * Custom CSS class for the tabs container */ className?: string; /** * Custom CSS class for the tab list */ tabListClassName?: string; /** * Custom CSS class for the tab panels container */ tabPanelsClassName?: string; /** * Children should be Tab and TabPanel components */ children: React.ReactNode; } /** * A component that displays a set of tabs */ export const Tabs = React.forwardRef( ( { children, value, onChange, orientation = 'horizontal’, variant = 'default’, size = 'medium’, fullWidth = false, centered = false, className, tabListClassName, tabPanelsClassName, …rest }, ref ) => { const [tabs, setPanels] = useState([]); const [panels, setTabs] = useState([]); const [indicatorStyle, setIndicatorStyle] = useState({}); const tabRefs = useRef>(new Map()); const tabListRef = useRef(null); const generatedId = useId(); // Separate Tab and TabPanel components useEffect(() => { const tabsArray: React.ReactElement[] = []; const panelsArray: React.ReactElement[] = []; React.Children.forEach(children, (child) => { if (!React.isValidElement(child)) return; if (child.type === Tab) { tabsArray.push(child); } else if (child.type === TabPanel) { panelsArray.push(child); } }); setPanels(tabsArray); setTabs(panelsArray); }, [children]); // Update indicator position when value changes useEffect(() => { const updateIndicator = () => { const activeTab = tabRefs.current.get(value); if (!activeTab || !tabListRef.current) return; const tabRect = activeTab.getBoundingClientRect(); const tabListRect = tabListRef.current.getBoundingClientRect(); if (orientation === 'horizontal’) { setIndicatorStyle({ left: `${activeTab.offsetLeft}px`, width: `${tabRect.width}px`, bottom: '0′, }); } else { setIndicatorStyle({ top: `${activeTab.offsetTop}px`, height: `${tabRect.height}px`, right: '0′, }); } }; updateIndicator(); window.addEventListener(’resize’, updateIndicator); return () => { window.removeEventListener(’resize’, updateIndicator); }; }, [value, orientation, tabs]); return ( {tabs.map((tab, index) => { const { value: tabValue, disabled, icon, badge, children: tabChildren, className: tabClassName, …tabProps } = tab.props; const isSelected = value === tabValue; const tabId = `tab-${generatedId}-${tabValue}`; const panelId = `panel-${generatedId}-${tabValue}`; return ( { if (node) { tabRefs.current.set(tabValue, node); } else { tabRefs.current.delete(tabValue); } }} role=”tab” id={tabId} aria-controls={panelId} aria-selected={isSelected} tabIndex={isSelected ? 0 : -1} className={clsx( 'mds-tabs__tab’, { 'mds-tabs__tab–selected’: isSelected, 'mds-tabs__tab–disabled’: disabled, 'mds-tabs__tab–with-icon’: Boolean(icon), }, tabClassName )} onClick={() => !disabled && onChange(tabValue)} disabled={disabled} {…tabProps} > {icon && {icon}} {tabChildren} {badge && {badge}} ); })} {panels.map((panel) => { const { value: panelValue, children: panelChildren, className: panelClassName, …panelProps } = panel.props; const isSelected = value === panelValue; const tabId = `tab-${generatedId}-${panelValue}`; const panelId = `panel-${generatedId}-${panelValue}`; if (!isSelected) return null; return ( {panelChildren} ); })} ); } ); Tabs.displayName = 'Tabs’; End File# myrta-ds/myrta # packages/react/src/components/Slider/Slider.tsx import React, { useRef, useState, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Slider.scss’; export interface SliderProps { /** * The current value of the slider */ value?: number; /** * Default value for uncontrolled component * @default 0 */ defaultValue?: number; /** * Minimum allowed value * @default 0 */ min?: number; /** * Maximum allowed value * @default 100 */ max?: number; /** * Step size * @default 1 */ step?: number; /** * Callback when value changes */ onChange?: (value: number) => void; /** * Whether the slider is disabled * @default false */ disabled?: boolean; /** * Size of the slider * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Label for the slider */ label?: string; /** * Whether to show the current value * @default false */ showValue?: boolean; /** * Format function for the displayed value */ formatValue?: (value: number) => string; /** * Mark specific values on the track */ marks?: { value: number; label?: string }[]; /** * Custom CSS class */ className?: string; /** * Custom CSS class for the track */ trackClassName?: string; /** * Custom CSS class for the thumb */ thumbClassName?: string; /** * ID attribute for the slider */ id?: string; /** * Callback when dragging starts */ onDragStart?: () => void; /** * Callback when dragging ends */ onDragEnd?: () => void; } export const Slider = React.forwardRef( ( { value: valueProp, defaultValue = 0, min = 0, max = 100, step = 1, onChange, disabled = false, size = 'medium’, label, showValue = false, formatValue = (value) => value.toString(), marks, className, trackClassName, thumbClassName, id, onDragStart, onDragEnd, …props }, ref ) => { const [value, setValue] = useState( valueProp !== undefined ? valueProp : defaultValue ); const [isDragging, setIsDragging] = useState(false); const trackRef = useRef(null); const thumbRef = useRef(null); const generatedId = useId(); const sliderId = id || `slider-${generatedId}`; const labelId = `${sliderId}-label`; const valueId = `${sliderId}-value`; // Handle controlled component updates useEffect(() => { if (valueProp !== undefined) { setValue(valueProp); } }, [valueProp]); // Calculate percentage for positioning const percentage = ((value – min) / (max – min)) * 100; const handleTrackClick = (e: React.MouseEvent) => { if (disabled || !trackRef.current) return; const rect = trackRef.current.getBoundingClientRect(); const clickPosition = e.clientX – rect.left; const percentageClicked = (clickPosition / rect.width) * 100; const rawValue = min + (percentageClicked / 100) * (max – min); const snappedValue = Math.round(rawValue / step) * step; const clampedValue = Math.max(min, Math.min(max, snappedValue)); setValue(clampedValue); onChange?.(clampedValue); }; const startDragging = (e: React.MouseEvent | React.TouchEvent) => { if (disabled) return; setIsDragging(true); onDragStart?.(); // Capture initial position const clientX = 'touches’ in e ? e.touches[0].clientX : e.clientX; updateValueFromClientX(clientX); // Prevent text selection during drag document.body.style.userSelect = 'none’; }; const stopDragging = () => { if (isDragging) { setIsDragging(false); onDragEnd?.(); document.body.style.userSelect = ”; } }; const updateValueFromClientX = (clientX: number) => { if (!trackRef.current || !isDragging) return; const rect = trackRef.current.getBoundingClientRect(); const clickPosition = Math.max(0, Math.min(rect.width, clientX – rect.left)); const percentageClicked = (clickPosition / rect.width) * 100; const rawValue = min + (percentageClicked / 100) * (max – min); const snappedValue = Math.round(rawValue / step) * step; const clampedValue = Math.max(min, Math.min(max, snappedValue)); setValue(clampedValue); onChange?.(clampedValue); }; const handleMouseMove = (e: MouseEvent) => { if (isDragging) { updateValueFromClientX(e.clientX); } }; const handleTouchMove = (e: TouchEvent) => { if (isDragging && e.touches.length > 0) { updateValueFromClientX(e.touches[0].clientX); } }; // Set up event listeners for drag useEffect(() => { if (isDragging) { document.addEventListener(’mousemove’, handleMouseMove); document.addEventListener(’touchmove’, handleTouchMove); document.addEventListener(’mouseup’, stopDragging); document.addEventListener(’touchend’, stopDragging); } return () => { document.removeEventListener(’mousemove’, handleMouseMove); document.removeEventListener(’touchmove’, handleTouchMove); document.removeEventListener(’mouseup’, stopDragging); document.removeEventListener(’touchend’, stopDragging); }; }, [isDragging]); // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (disabled) return; let newValue = value; switch (e.key) { case 'ArrowRight’: case 'ArrowUp’: newValue = Math.min(max, value + step); break; case 'ArrowLeft’: case 'ArrowDown’: newValue = Math.max(min, value – step); break; case 'Home’: newValue = min; break; case 'End’: newValue = max; break; default: return; } if (newValue !== value) { e.preventDefault(); setValue(newValue); onChange?.(newValue); } }; return ( {(label || showValue) && ( {label && ( {label} )} {showValue && ( {formatValue(value)} )} )} {/* Render marks if provided */} {marks && marks.length > 0 && ( {marks.map((mark) => { const markPercentage = ((mark.value – min) / (max – min)) * 100; return ( = mark.value, })} style={{ left: `${markPercentage}%` }} > {mark.label && ( {mark.label} )} ); })} )} ); } ); Slider.displayName = 'Slider’; End Fileimport React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Checkbox.scss’; export interface CheckboxProps extends Omit, 'size’> { /** * Label for the checkbox */ label?: React.ReactNode; /** * Additional helper text */ helperText?: React.ReactNode; /** * Error message */ error?: React.ReactNode; /** * Whether the checkbox is checked */ checked?: boolean; /** * Default checked state for uncontrolled component * @default false */ defaultChecked?: boolean; /** * Whether the checkbox is in an indeterminate state * @default false */ indeterminate?: boolean; /** * Whether the checkbox is disabled * @default false */ disabled?: boolean; /** * Callback when the checked state changes */ onChange?: (checked: boolean, event: React.ChangeEvent) => void; /** * Size of the checkbox * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Additional CSS class for the checkbox container */ className?: string; /** * Additional CSS class for the checkbox input element */ inputClassName?: string; /** * Additional CSS class for the checkbox label */ labelClassName?: string; /** * Name attribute for the input element */ name?: string; /** * ID attribute for the input element */ id?: string; /** * Whether the checkbox is in a loading state * @default false */ loading?: boolean; } export const Checkbox = React.forwardRef( ( { label, helperText, error, checked, defaultChecked = false, indeterminate = false, disabled = false, onChange, size = 'medium’, className, inputClassName, labelClassName, name, id, loading = false, …props }, ref ) => { const [isChecked, setIsChecked] = useState( checked !== undefined ? checked : defaultChecked ); const inputRef = useRef(null); const generatedId = useId(); const checkboxId = id || `checkbox-${generatedId}`; const helperTextId = `helper-text-${generatedId}`; const errorId = `error-${generatedId}`; // Update internal state when controlled value changes useEffect(() => { if (checked !== undefined) { setIsChecked(checked); } }, [checked]); // Set the indeterminate prop useEffect(() => { if (inputRef.current) { inputRef.current.indeterminate = indeterminate; } }, [indeterminate]); const handleChange = (event: React.ChangeEvent) => { if (disabled || loading) return; const newChecked = event.target.checked; // Only update internal state for uncontrolled component if (checked === undefined) { setIsChecked(newChecked); } if (onChange) { onChange(newChecked, event); } }; return ( { // Handle both the ref prop and the ref object if (ref) { if (typeof ref === 'function’) { ref(node); } else { ref.current = node; } } inputRef.current = node; }} type=”checkbox” id={checkboxId} name={name} checked={isChecked} onChange={handleChange} disabled={disabled || loading} className={clsx(’mds-checkbox__input’, inputClassName)} aria-invalid={Boolean(error)} aria-describedby={ error ? errorId : helperText ? helperTextId : undefined } {…props} /> {loading && } {label && ( {label} )} {error ? ( {error} ) : helperText ? ( {helperText} ) : null} ); } ); Checkbox.displayName = 'Checkbox’; End File# myrta-ds/myrta # packages/react/src/components/Menu/Menu.tsx import React, { useState, useRef, useEffect } from 'react’; import { createPortal } from 'react-dom’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import { useClickOutside } from ’../../hooks/useClickOutside’; import ’./Menu.scss’; export interface MenuProps { /** * The trigger element */ trigger: React.ReactElement; /** * The content of the menu */ children: React.ReactNode; /** * Whether the menu is open */ open?: boolean; /** * Callback when the menu should open or close */ onOpenChange?: (open: boolean) => void; /** * Position of the menu relative to the trigger * @default 'bottom-start’ */ placement?: 'top’ | 'top-start’ | 'top-end’ | 'bottom’ | 'bottom-start’ | 'bottom-end’ | 'left’ | 'right’; /** * Custom CSS class for the menu */ className?: string; /** * Width of the menu */ width?: number | string; /** * Maximum height of the menu before scrolling */ maxHeight?: number | string; /** * Whether to close the menu when an item is clicked * @default true */ closeOnItemClick?: boolean; /** * CSS z-index for the menu * @default 1000 */ zIndex?: number; /** * Whether to match the width of the menu with the trigger * @default false */ matchWidth?: boolean; /** * Distance between the menu and the trigger * @default 4 */ offset?: number; } export interface MenuItemProps extends React.HTMLAttributes { /** * Whether the item is disabled * @default false */ disabled?: boolean; /** * Icon to display before item text */ icon?: React.ReactNode; /** * Custom CSS class for the menu item */ className?: string; } export const MenuItem: React.FC = ({ children, disabled = false, icon, className, onClick, …rest }) => { const menuContext = useMenuContext(); const handleClick = (e: React.MouseEvent) => { if (disabled) { e.preventDefault(); return; } // Call the original onClick handler onClick?.(e); // Close the menu if closeOnItemClick is true if (menuContext?.closeOnItemClick) { menuContext.onClose(); } }; return ( {icon && {icon}} {children} ); }; MenuItem.displayName = 'MenuItem’; export interface MenuDividerProps extends React.HTMLAttributes { /** * Custom CSS class for the divider */ className?: string; } export const MenuDivider: React.FC = ({ className, …rest }) => ( ); MenuDivider.displayName = 'MenuDivider’; export interface MenuGroupProps extends React.HTMLAttributes { /** * Group title */ title?: React.ReactNode; /** * Custom CSS class for the group */ className?: string; } export const MenuGroup: React.FC = ({ title, children, className, …rest }) => ( {title && {title} } {children} ); MenuGroup.displayName = 'MenuGroup’; // Create a context for the menu interface MenuContextValue { closeOnItemClick: boolean; onClose: () => void; } const MenuContext = React.createContext(null); const useMenuContext = () => React.useContext(MenuContext); export const Menu: React.FC = ({ trigger, children, open: openProp, onOpenChange, placement = 'bottom-start’, className, width, maxHeight, closeOnItemClick = true, zIndex = 1000, matchWidth = false, offset = 4, }) => { const [isOpen, setIsOpen] = useState(Boolean(openProp)); const [menuStyle, setMenuStyle] = useState({}); const triggerRef = useRef(null); const menuRef = useRef(null); const generatedId = useId(); const menuId = `menu-${generatedId}`; // Set up click outside handling useClickOutside(menuRef, { active: isOpen, onClickOutside: () => { setIsOpen(false); onOpenChange?.(false); }, }); // Handle controlled component useEffect(() => { if (openProp !== undefined) { setIsOpen(openProp); } }, [openProp]); // Calculate and set menu position const updateMenuPosition = () => { if (!triggerRef.current || !menuRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); const menuRect = menuRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let top, left; // Calculate position based on placement switch (placement) { case 'top’: top = triggerRect.top – menuRect.height – offset; left = triggerRect.left + triggerRect.width / 2 – menuRect.width / 2; break; case 'top-start’: top = triggerRect.top – menuRect.height – offset; left = triggerRect.left; break; case 'top-end’: top = triggerRect.top – menuRect.height – offset; left = triggerRect.right – menuRect.width; break; case 'bottom’: top = triggerRect.bottom + offset; left = triggerRect.left + triggerRect.width / 2 – menuRect.width / 2; break; case 'bottom-start’: top = triggerRect.bottom + offset; left = triggerRect.left; break; case 'bottom-end’: top = triggerRect.bottom + offset; left = triggerRect.right – menuRect.width; break; case 'left’: top = triggerRect.top + triggerRect.height / 2 – menuRect.height / 2; left = triggerRect.left – menuRect.width – offset; break; case 'right’: top = triggerRect.top + triggerRect.height / 2 – menuRect.height / 2; left = triggerRect.right + offset; break; default: top = triggerRect.bottom + offset; left = triggerRect.left; } // Adjust for viewport boundaries if (left viewportWidth) left = viewportWidth – menuRect.width; if (top viewportHeight) top = viewportHeight – menuRect.height; const style: React.CSSProperties = { position: 'fixed’, top: `${top}px`, left: `${left}px`, zIndex, }; if (width) { style.width = typeof width === 'number’ ? `${width}px` : width; } else if (matchWidth) { style.width = `${triggerRect.width}px`; } if (maxHeight) { style.maxHeight = typeof maxHeight === 'number’ ? `${maxHeight}px` : maxHeight; } setMenuStyle(style); }; // Update position when menu opens useEffect(() => { if (isOpen) { updateMenuPosition(); // Update position on resize/scroll window.addEventListener(’resize’, updateMenuPosition); window.addEventListener(’scroll’, updateMenuPosition); } return () => { window.removeEventListener(’resize’, updateMenuPosition); window.removeEventListener(’scroll’, updateMenuPosition); }; }, [isOpen]); // Toggle menu const toggleMenu = () => { const newState = !isOpen; setIsOpen(newState); onOpenChange?.(newState); }; // Close menu const closeMenu = () => { setIsOpen(false); onOpenChange?.(false); }; // Clone trigger with ref and event handlers const triggerElement = React.cloneElement(trigger, { ref: (node: HTMLElement | null) => { triggerRef.current = node; // Forward ref if the child has one const { ref } = trigger as any; if (typeof ref === 'function’) ref(node); else if (ref) ref.current = node; }, onClick: (e: React.MouseEvent) => { toggleMenu(); // Call the original onClick if it exists if (trigger.props.onClick) trigger.props.onClick(e); }, 'aria-haspopup’: 'menu’, 'aria-expanded’: isOpen, 'aria-controls’: isOpen ? menuId : undefined, }); return ( {triggerElement} {isOpen && createPortal( {children} , document.body )} ); }; Menu.displayName = 'Menu’; End File# packages/react/src/components/Avatar/Avatar.tsx import React, { useState } from 'react’; import clsx from 'clsx’; import ’./Avatar.scss’; // Status types for avatar type AvatarStatus = 'online’ | 'offline’ | 'away’ | 'busy’ | 'invisible’; // Size variants for avatar type AvatarSize = 'xs’ | 'small’ | 'medium’ | 'large’ | 'xl’; // Shape variants for avatar type AvatarShape = 'circle’ | 'square’ | ’rounded’; export interface AvatarProps extends React.HTMLAttributes { /** * Image source for the avatar */ src?: string; /** * Alt text for the avatar image */ alt?: string; /** * Text to display when no image is available */ name?: string; /** * Size of the avatar * @default 'medium’ */ size?: AvatarSize; /** * Shape of the avatar * @default 'circle’ */ shape?: AvatarShape; /** * Status indicator for the avatar */ status?: AvatarStatus; /** * Whether to show a border around the avatar * @default false */ bordered?: boolean; /** * Background color for the avatar when displaying initials */ backgroundColor?: string; /** * Text color for the avatar when displaying initials */ textColor?: string; /** * Custom CSS class */ className?: string; /** * Optional icon to display instead of initials when no image is available */ icon?: React.ReactNode; /** * Callback when avatar fails to load */ onError?: (e: React.SyntheticEvent) => void; /** * Callback when avatar is clicked */ onClick?: (e: React.MouseEvent) => void; } export const Avatar = React.forwardRef( ( { src, alt, name, size = 'medium’, shape = 'circle’, status, bordered = false, backgroundColor, textColor, className, icon, onError, onClick, style, …rest }, ref ) => { const [imgError, setImgError] = useState(false); // Generate initials from name const getInitials = () => { if (!name) return ”; return name .split(’ ’) .map(n => n[0]) .join(”) .toUpperCase() .substring(0, 2); }; // Handle image load error const handleError = (e: React.SyntheticEvent) => { setImgError(true); onError?.(e); }; // Combine styles const avatarStyle = { …style, …(backgroundColor && !src && { backgroundColor }), …(textColor && !src && { color: textColor }), }; // Determine if we should render the image const shouldRenderImage = src && !imgError; return ( {shouldRenderImage ? ( ) : icon ? ( {icon} ) : ( {getInitials()} )} {status && ( )} ); } ); Avatar.displayName = 'Avatar’; End File# packages/react/src/components/Card/Card.tsx import React from 'react’; import clsx from 'clsx’; import ’./Card.scss’; export interface CardProps extends React.HTMLAttributes { /** * Variant of the card * @default 'outlined’ */ variant?: 'outlined’ | 'elevated’ | 'filled’; /** * Whether the card has a hover effect * @default false */ hoverable?: boolean; /** * Whether the card is clickable * @default false */ clickable?: boolean; /** * Whether the card is disabled * @default false */ disabled?: boolean; /** * Additional CSS class for the card */ className?: string; } export const Card = React.forwardRef( ( { children, variant = 'outlined’, hoverable = false, clickable = false, disabled = false, className, …rest }, ref ) => ( {children} ) ); Card.displayName = 'Card’; export interface CardHeaderProps extends React.HTMLAttributes { /** * Title of the card */ title?: React.ReactNode; /** * Subtitle of the card */ subtitle?: React.ReactNode; /** * Action element to display in the header */ action?: React.ReactNode; /** * Avatar or icon to display in the header */ avatar?: React.ReactNode; /** * Additional CSS class for the header */ className?: string; } export const CardHeader = React.forwardRef( ( { title, subtitle, action, avatar, className, children, …rest }, ref ) => ( {avatar && {avatar} } {title && {title} } {subtitle && {subtitle} } {children} {action && {action} } ) ); CardHeader.displayName = 'CardHeader’; export interface CardContentProps extends React.HTMLAttributes { /** * Additional CSS class for the content */ className?: string; } export const CardContent = React.forwardRef( ({ className, children, …rest }, ref) => ( {children} ) ); CardContent.displayName = 'CardContent’; export interface CardFooterProps extends React.HTMLAttributes { /** * Additional CSS class for the footer */ className?: string; /** * Align the footer content * @default 'left’ */ align?: 'left’ | 'center’ | 'right’ | 'space-between’; } export const CardFooter = React.forwardRef( ( { className, align = 'left’, children, …rest }, ref ) => ( {children} ) ); CardFooter.displayName = 'CardFooter’; export interface CardMediaProps extends React.HTMLAttributes { /** * Image source */ image?: string; /** * Alt text for the image */ alt?: string; /** * Height of the media area */ height?: number | string; /** * Position of the background image * @default 'center’ */ position?: string; /** * Title overlay on the media */ title?: React.ReactNode; /** * Additional CSS class for the media */ className?: string; } export const CardMedia = React.forwardRef( ( { image, alt, height = 200, position = 'center’, title, className, children, style, …rest }, ref ) => { const mediaStyle = { …style, height: typeof height === 'number’ ? `${height}px` : height, backgroundImage: image ? `url(${image})` : undefined, backgroundPosition: position, }; return ( {children} {title && {title} } ); } ); CardMedia.displayName = 'CardMedia’; export interface CardDividerProps extends React.HTMLAttributes { /** * Whether the divider is vertical * @default false */ vertical?: boolean; /** * Additional CSS class for the divider */ className?: string; } export const CardDivider = React.forwardRef( ({ vertical = false, className, …rest }, ref) => ( ) ); CardDivider.displayName = 'CardDivider’; End Fileimport { Component, ElementType, HTMLAttributes, MouseEvent, ReactNode } from 'react’; import clsx from 'clsx’; interface SpinnerProps extends HTMLAttributes { /** * Custom spinner size */ size?: 'small’ | 'medium’ | 'large’; /** * Change the color of the spinner */ color?: string; /** * Display a text message along the spinner */ message?: string; /** * Custom classes to apply to the spinner */ className?: string; /** * Display the spinner as an inline element */ inline?: boolean; /** * Whether to center the spinner */ centered?: boolean; /** * Spinner overlay mode with backdrop */ overlay?: boolean; /** * Custom icon for the spinner */ customIcon?: ReactNode; /** * Icon to display instead of spinner */ icon?: string; /** * Whether the spinner should take up the entire height of the parent */ fullHeight?: boolean; /** * Aria label for accessibility */ 'aria-label’?: string; /** * Component to render as */ as?: ElementType; /** * Whether the spinner should spin faster */ speedUp?: boolean; /** * Called when spinner is clicked */ onClick?: (event: MouseEvent) => void; } class Spinner extends Component { render() { const { size = 'medium’, color, message, className, inline = false, centered = false, overlay = false, customIcon, icon, fullHeight = false, 'aria-label’: ariaLabel = 'Loading’, as: Component = 'div’, speedUp = false, onClick, …rest } = this.props; const spinnerClasses = clsx( 'mds-spinner’, `mds-spinner–${size}`, { 'mds-spinner–inline’: inline, 'mds-spinner–centered’: centered, 'mds-spinner–overlay’: overlay, 'mds-spinner–with-message’: !!message, 'mds-spinner–full-height’: fullHeight, 'mds-spinner–speed-up’: speedUp, }, className ); const spinnerStyle = color ? { ’–spinner-color’: color } as React.CSSProperties : undefined; return ( {customIcon ? ( {customIcon} ) : icon ? ( {icon} ) : ( )} {message && {message} } {ariaLabel} ); } } export { Spinner, SpinnerProps }; End File# myrta-ds/myrta # packages/react/src/components/Radio/Radio.tsx import React, { useState, useRef, useEffect } from 'react’; import clsx from 'clsx’; import { useId } from ’../../hooks/useId’; import ’./Radio.scss’; export interface RadioProps extends Omit, 'size’> { /** * Label for the radio */ label?: React.ReactNode; /** * Helper text to display */ helperText?: React.ReactNode; /** * Error message to display */ error?: React.ReactNode; /** * Whether the radio is checked */ checked?: boolean; /** * Default checked state for uncontrolled component * @default false */ defaultChecked?: boolean; /** * Whether the radio is disabled * @default false */ disabled?: boolean; /** * Callback when checked state changes */ onChange?: (checked: boolean, event: React.ChangeEvent) => void; /** * Size of the radio * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * CSS class for the radio container */ className?: string; /** * CSS class for the radio input */ inputClassName?: string; /** * CSS class for the radio label */ labelClassName?: string; /** * Name for the radio input */ name?: string; /** * ID for the radio input */ id?: string; /** * Whether the radio is in a loading state * @default false */ loading?: boolean; /** * Value of the radio */ value?: string | number; } export const Radio = React.forwardRef( ( { label, helperText, error, checked, defaultChecked = false, disabled = false, onChange, size = 'medium’, className, inputClassName, labelClassName, name, id, loading = false, value, …props }, ref ) => { const [isChecked, setIsChecked] = useState( checked !== undefined ? checked : defaultChecked ); const inputRef = useRef(null); const generatedId = useId(); const radioId = id || `radio-${generatedId}`; const helperTextId = `helper-text-${generatedId}`; const errorId = `error-${generatedId}`; // Update internal state when controlled value changes useEffect(() => { if (checked !== undefined) { setIsChecked(checked); } }, [checked]); const handleChange = (event: React.ChangeEvent) => { if (disabled || loading) return; const newChecked = event.target.checked; // Only update internal state for uncontrolled component if (checked === undefined) { setIsChecked(newChecked); } if (onChange) { onChange(newChecked, event); } }; return ( { // Handle both the ref prop and the ref object if (ref) { if (typeof ref === 'function’) { ref(node); } else { ref.current = node; } } inputRef.current = node; }} type=”radio” id={radioId} name={name} checked={isChecked} onChange={handleChange} disabled={disabled || loading} className={clsx(’mds-radio__input’, inputClassName)} aria-invalid={Boolean(error)} aria-describedby={ error ? errorId : helperText ? helperTextId : undefined } value={value} {…props} /> {loading && } {label && ( {label} )} {error ? ( {error} ) : helperText ? ( {helperText} ) : null} ); } ); Radio.displayName = 'Radio’; export interface RadioGroupProps extends Omit, 'onChange’> { /** * Name attribute to be applied to all radios */ name: string; /** * Value of the selected radio */ value?: string | number; /** * Default value for uncontrolled component */ defaultValue?: string | number; /** * Called when a radio is selected */ onChange?: (value: string | number, event: React.ChangeEvent) => void; /** * CSS class for the radio group */ className?: string; /** * Orientation of the radio group * @default 'vertical’ */ orientation?: 'horizontal’ | 'vertical’; /** * Whether the radio group is disabled * @default false */ disabled?: boolean; /** * Error message to display */ error?: React.ReactNode; /** * Label for the radio group */ label?: React.ReactNode; /** * CSS class for the label */ labelClassName?: string; /** * Size for all radios in the group * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Whether the radio group is required * @default false */ required?: boolean; } export const RadioGroup = React.forwardRef( ( { children, name, value, defaultValue, onChange, className, orientation = 'vertical’, disabled = false, error, label, labelClassName, size = 'medium’, required = false, …rest }, ref ) => { const [selectedValue, setSelectedValue] = useState( value !== undefined ? value : defaultValue ); const generatedId = useId(); const groupId = `radio-group-${generatedId}`; const errorId = `radio-group-error-${generatedId}`; // Update internal state when controlled value changes useEffect(() => { if (value !== undefined) { setSelectedValue(value); } }, [value]); const handleChange = (checked: boolean, event: React.ChangeEvent) => { if (checked) { const newValue = event.target.value; // Only update internal state for uncontrolled component if (value === undefined) { setSelectedValue(newValue); } if (onChange) { onChange(newValue, event); } } }; // Clone children to pass props const radioButtons = React.Children.map(children, (child) => { if (!React.isValidElement(child)) return child; // Only modify Radio components if (child.type === Radio) { return React.cloneElement(child, { name, size, disabled: disabled || child.props.disabled, checked: child.props.value === selectedValue, onChange: handleChange, }); } return child; }); return ( {label && ( {label} {required && *} )} {radioButtons} {error && ( {error} )} ); } ); RadioGroup.displayName = 'RadioGroup’; End Fileimport React from 'react’; import clsx from 'clsx’; import ’./Badge.scss’; export interface BadgeProps extends React.HTMLAttributes { /** * Content to be displayed inside the badge */ content?: React.ReactNode; /** * Maximum count to show (e.g., 99+) * @default 99 */ max?: number; /** * Badge color variant * @default 'primary’ */ variant?: 'primary’ | 'secondary’ | 'success’ | 'warning’ | 'error’ | 'info’; /** * Badge size * @default 'medium’ */ size?: 'small’ | 'medium’ | 'large’; /** * Whether the badge is a dot style (without content) * @default false */ dot?: boolean; /** * Position of the badge relative to its children * @default 'top-right’ */ position?: 'top-right’ | 'top-left’ | 'bottom-right’ | 'bottom-left’; /** * Whether to show the badge even when content is zero * @default false */ showZero?: boolean; /** * Whether the badge should be visible * @default true */ visible?: boolean; /** * Horizontal offset of the badge * @default 0 */ offsetX?: number; /** * Vertical offset of the badge * @default 0 */ offsetY?: number; /** * Whether the badge is standalone (not positioned relative to children) * @default false */ standalone?: boolean; /** * Elements that the badge wraps */ children?: React.ReactNode; } export const Badge = React.forwardRef( ( { content, max = 99, variant = 'primary’, size = 'medium’, dot = false, position = 'top-right’, showZero = false, visible = true, offsetX = 0, offsetY = 0, standalone = false, children, className, style, …rest }, ref ) => { // Determine if the badge should be shown const shouldShowBadge = () => { if (!visible) return false; if (dot) return true; if (content === 0 || content === '0′) return showZero; return Boolean(content); }; // Format the content for the badge const formattedContent = () => { if (dot) return null; if (typeof content === 'number’ && content > max) { return `${max}+`; } return content; }; // Create style with offsets const badgeStyle = { …style, …(offsetX && { ’–badge-offset-x’: `${offsetX}px` }), …(offsetY && { ’–badge-offset-y’: `${offsetY}px` }), }; // For standalone badge, just render the badge itself if (standalone) { return ( {formattedContent()} ); } // For badge with children, render the badge positioned relative to children return ( {children} {shouldShowBadge() && ( {formattedContent()} )} ); } ); Badge.displayName = 'Badge’; End Fileimport React, { useId } from 'react’; import clsx from 'clsx’; // Icon type definition export type IconName = | 'alert-circle’ | 'alert-triangle’ | 'arrow-down’ | 'arrow-left’ | 'arrow-right’ | 'arrow-up’ | 'bell’ | 'calendar’ | 'check’ | 'check-circle’ | 'chevron-down’ | 'chevron-left’ | 'chevron-right’ | 'chevron-up’ | 'clock’ | 'close’ | 'download’ | 'edit’ | 'eye’ | 'eye-off’ | 'file’ | 'folder’ | 'globe’ | 'heart’ | 'home’ | 'info’ | 'link’ | 'lock’ | 'mail’ | 'menu’ | 'message’ | 'more-horizontal’ | 'more-vertical’ | 'phone’ | 'plus’ | 'search’ | 'settings’ | 'share’ | 'star’ | 'trash’ | 'unlock’ | 'upload’ | 'user’ | 'users’ | 'x’; // Icon size definition export type IconSize = 'small’ | 'medium’ | 'large’ | number; // Icon props interface export interface IconProps { /** * Name of the icon */ name: IconName; /** * Size of the icon * @default 'medium’ */ size?: IconSize; /** * Color of the icon * @default 'currentColor’ */ color?: string; /** * Additional CSS class */ className?: string; /** * ARIA label for accessibility */ 'aria-label’?: string; /** * ARIA hidden attribute * @default false when aria-label is provided, otherwise true */ 'aria-hidden’?: boolean; /** * Viewbox for the SVG * @default '0 0 24 24′ */ viewBox?: string; /** * Custom stroke width * @default 2 */ strokeWidth?: number; /** * HTML title for the icon */ title?: string; /** * Whether the icon should spin * @default false */ spin?: boolean; /** * Whether the icon should pulse * @default false */ pulse?: boolean; /** * Custom CSS style */ style?: React.CSSProperties; /** * Custom SVG path */ customPath?: string; /** * Custom SVG content */ customSvg?: React.ReactNode; /** * onClick event handler */ onClick?: (event: React.MouseEvent) => void; } // Icon component export const Icon: React.FC = ({ name, size = 'medium’, color = 'currentColor’, className, 'aria-label’: ariaLabel, 'aria-hidden’: ariaHiddenProp, viewBox = '0 0 24 24′, strokeWidth = 2, title, spin = false, pulse = false, style, customPath, customSvg, onClick, …props }) => { // Generate unique IDs for accessibility const titleId = useId(); // Determine if the icon should be hidden from screen readers const ariaHidden = ariaHiddenProp ?? (ariaLabel ? false : true); // Convert size to pixel value if needed const getSizeInPixels = () => { if (typeof size === 'number’) return size; switch (size) { case 'small’: return 16; case 'large’: return 32; case 'medium’: default: return 24; } }; // Get the icon path based on name const getIconPath = (): string => { if (customPath) return customPath; // Return path data based on icon name switch (name) { case 'alert-circle’: return 'M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v7h-2V7zm0 8h2v2h-2v-2z’; case 'alert-triangle’: return 'M12.866 3l9.526 16.5a1 1 0 0 1-.866 1.5H2.474a1 1 0 0 1-.866-1.5L11.134 3a1 1 0 0 1 1.732 0zM11 16v2h2v-2h-2zm0-7v5h2V9h-2z’; case 'arrow-down’: return 'M12 4v12.25L17.25 11l1.42 1.41L12 19.08l-6.67-6.67L6.75 11 12 16.25V4h2z’; case 'arrow-left’: return 'M7.75 12L13 6.75 11.59 5.34 4.92 12l6.67 6.66L13 17.25 7.75 12z’; case 'arrow-right’: return 'M16.25 12L11 17.25l1.41 1.41L19.08 12l-6.67-6.66L11 6.75 16.25 12z’; case 'arrow-up’: return 'M12 20V7.75L6.75 13 5.34 11.59 12 4.92l6.66 6.67L17.25 13 12 7.75V20h-2z’; case 'bell’: return 'M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z’; case 'calendar’: return 'M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2zM7 12h5v5H7v-5z’; case 'check’: return 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z’; case 'check-circle’: return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z’; case 'chevron-down’: return 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z’; case 'chevron-left’: return 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z’; case 'chevron-right’: return 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z’; case 'chevron-up’: return 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z’; case 'clock’: return 'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z’; case 'close’: return 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z’; case 'download’: return 'M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z’; case 'edit’: return 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z’; case 'eye’: return 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z’; case 'eye-off’: return 'M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z’; case 'file’: return 'M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z’; case 'folder’: return 'M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z’; case 'globe’: return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z’; case 'heart’: return 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z’; case 'home’: return 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z’; case 'info’: return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z’; case 'link’: return 'M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z’; case 'lock’: return 'M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z’; case 'mail’: return 'M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z’; case 'menu’: return 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z’; case 'message’: return 'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z’; case 'more-horizontal’: return 'M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z’; case 'more-vertical’: return 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z’; case 'phone’: return 'M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z’; case 'plus’: return 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z’; case 'search’: return 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z’; case 'settings’: return 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z’; case 'share’: return 'M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z’; case 'star’: return 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z’; case 'trash’: return 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z’; case 'unlock’: return 'M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z’; case 'upload’: return 'M5 20h14v-2H5v2zm0-10h4v7h6v-7h4l-7-7-7 7z’; case 'user’: return 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z’; case 'users’: return 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z’; case 'x’: return 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z’; default: return ”; } }; // Style object with size and animation properties const sizeInPixels = getSizeInPixels(); const iconStyle: React.CSSProperties = { …style, width: sizeInPixels, height: sizeInPixels, color, }; // Default SVG attributes const svgProps = { xmlns: 'http://www.w3.org/2000/svg’, viewBox, width: sizeInPixels, height: sizeInPixels, fill: 'currentColor’, stroke: 'none’, className: clsx( 'mds-icon’, { 'mds-icon–spin’: spin, 'mds-icon–pulse’: pulse, }, className ), style: iconStyle, 'aria-hidden’: ariaHidden, 'aria-labelledby’: title ? titleId : undefined, onClick, …props, }; // Render the icon return ( {title && {title}} {customSvg || } ); }; End File# packages/react/rollup.config.mjs import resolve from '@rollup/plugin-node-resolve’; import commonjs from '@rollup/plugin-commonjs’; import typescript from '@rollup/plugin-typescript’; import terser from '@rollup/plugin-terser’; import external from 'rollup-plugin-peer-deps-external’; import dts from 'rollup-plugin-dts’; import sass from 'rollup-plugin-sass’; import autoprefixer from 'autoprefixer’; import postcss from 'postcss’; import url from '@rollup/plugin-url’; import { readFileSync } from 'fs’; // Parse package.json const packageJson = JSON.parse(readFileSync(’./package.json’, 'utf8′)); // Define output directory const outputDir = 'dist’; const createPath = path => `${outputDir}/${path}`; // Configure SASS processing const sassOptions = { // Process and insert CSS directly insert: true, // Process SASS with PostCSS for autoprefixing processor: css => postcss([autoprefixer]) .process(css, { from: undefined }) .then(result => result.css) }; // Main Rollup configuration export default [ // ESM and CJS bundles (code) { input: 'src/index.ts’, output: [ { file: createPath(packageJson.module), format: 'esm’, sourcemap: true }, { file: createPath(packageJson.main), format: 'cjs’, sourcemap: true } ], plugins: [ // Handle external dependencies external(), // Resolve node_modules resolve(), // Convert CommonJS modules to ES6 commonjs(), // Process TypeScript typescript({ tsconfig: ’./tsconfig.json’, exclude: [’**/*.test.tsx’, '**/*.test.ts’, '**/*.stories.tsx’] }), // Process SASS files sass(sassOptions), // Handle assets (limit: 8KB, otherwise use file) url({ limit: 8 * 1024, // 8KB include: [’**/*.svg’, '**/*.png’, '**/*.jpg’, '**/*.gif’], fileName: '[dirname][name][extname]’ }), // Minify for production terser() ], // Explicitly mark React as external external: [’react’, 'react-dom’] }, // TypeScript declaration files { input: 'src/index.ts’, output: [ { file: createPath(packageJson.types), format: 'esm’ } ], plugins: [ dts({ tsconfig: ’./tsconfig.json’, compilerOptions: { emitDeclarationOnly: true, } }) ], external: [/.scss$/] } ]; End File# myrta-ds/myrta „use client”; import { Card, CardContent, Button, CardFooter, CardHeader, CardTitle, } from „@/components/ui/card”; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from „@/components/ui/table”; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from „@/components/ui/select”; import { Tabs, TabsList, TabsTrigger, TabsContent } from „@/components/ui/tabs”; import { useCallback, useEffect, useState } from „react”; import { Input } from „@/components/ui/input”; import { fetchLogData, filterLogData } from „@/lib/data”; import { LogData } from „@/lib/definitions”; import { CopyIcon, LoaderIcon, SearchIcon } from „lucide-react”; import { Label } from „@/components/ui/label”; import { Pagination, PaginationContent, PaginationItem, PaginationPrev, PaginationLink, PaginationEllipsis, PaginationNext, } from „@/components/ui/pagination”; import { useDebounce } from „@/lib/hooks”; import { truncateFirstLine, formatLogDate, getSeverityClass, formatStackTrace, } from „@/lib/utils”; import { Badge } from „@/components/ui/badge”; import copyToClipboard from „@/lib/clipboard”; import { ScrollArea } from „@/components/ui/scroll-area”; import LogMessageCard from „@/components/logs/log-message-card”; const DEBOUNCE_MS = 300; const LOGS_PER_PAGE = 10; export default function LogsPage() { // State for logs data and filters const [logs, setLogs] = useState([]); const [filteredLogs, setFilteredLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedLog, setSelectedLog] = useState(null); const [tableView, setTableView] = useState(true); // Filters state const [searchTerm, setSearchTerm] = useState(„”); const [severity, setSeverity] = useState(„all”); const [timeRange, setTimeRange] = useState(„24h”); const [source, setSource] = useState(„all”); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); // Debounced search term const debouncedSearchTerm = useDebounce(searchTerm, DEBOUNCE_MS); // Load logs data const loadLogs = useCallback(async () => { setIsLoading(true); try { const data = await fetchLogData(); setLogs(data); setIsLoading(false); } catch (error) { console.error(„Failed to fetch log data:”, error); setIsLoading(false); } }, []); // Initial data load useEffect(() => { loadLogs(); }, [loadLogs]); // Apply filters whenever filter criteria change useEffect(() => { const applyFilters = () => { const filtered = filterLogData( logs, debouncedSearchTerm, severity, timeRange, source ); setFilteredLogs(filtered); setTotalPages(Math.ceil(filtered.length / LOGS_PER_PAGE)); setCurrentPage(1); // Reset to first page when filters change // Clear selected log when filters change setSelectedLog(null); }; applyFilters(); }, [logs, debouncedSearchTerm, severity, timeRange, source]); // Handle pagination const getPaginatedLogs = () => { const startIndex = (currentPage – 1) * LOGS_PER_PAGE; const endIndex = startIndex + LOGS_PER_PAGE; return filteredLogs.slice(startIndex, endIndex); }; const handleCopySelected = () => { if (selectedLog) { copyToClipboard( `${selectedLog.timestamp} [${selectedLog.severity}] ${selectedLog.message}n${selectedLog.stackTrace}` ); } }; // Render loading state if (isLoading) { return ( Loading logs… ); } const renderPagination = () => { if (totalPages { const pages = []; const showEllipsisStart = currentPage > 3; const showEllipsisEnd = currentPage 3) { pages.push( setCurrentPage(1)}>1 ); } // Show ellipsis if needed if (showEllipsisStart) { pages.push( ); } // Show current page and adjacent pages for ( let i = Math.max(2, currentPage – 1); i {i} ); } // Show ellipsis if needed if (showEllipsisEnd) { pages.push( ); } // Always show last page if (currentPage 3) { pages.push( setCurrentPage(totalPages)}> {totalPages} ); } return pages; }; return ( setCurrentPage((prev) => Math.max(prev – 1, 1))} isDisabled={currentPage === 1} /> {renderPageNumbers()} setCurrentPage((prev) => Math.min(prev + 1, totalPages)) } isDisabled={currentPage === totalPages} /> ); }; const paginatedLogs = getPaginatedLogs(); const renderTableView = () => ( Timestamp Severity Source Message {paginatedLogs.length === 0 ? ( No logs found matching the current filters. ) : ( paginatedLogs.map((log) => ( setSelectedLog(log)} style={{ cursor: 'pointer’ }} > {formatLogDate(log.timestamp)} {log.severity} {log.source} {truncateFirstLine(log.message)} )) )} Przewiń tabelę w poziomie ←→ {renderPagination()} ); const renderCardView = () => ( {paginatedLogs.length === 0 ? ( No logs found matching the current filters. ) : ( paginatedLogs.map((log) => ( setSelectedLog(log)} /> )) )} {renderPagination()} ); return ( System Logs {/* Filters panel */} setSearchTerm(e.target.value)} className=”pl-10″ /> All Severities Error Warning Info Debug Last hour Last 24 hours Last 7 days Last 30 days All time All Sources API Frontend Backend Database { setSearchTerm(„”); setSeverity(„all”); setTimeRange(„24h”); setSource(„all”); }} > Reset Filters {/* Main content area */} Log Events setTableView(v === „table”)} > Table Cards {tableView ? renderTableView() : renderCardView()} Showing {Math.min(filteredLogs.length, LOGS_PER_PAGE)} of{” „} {filteredLogs.length} logs {/* Log details panel */} Log Details {selectedLog && ( Copy )} {selectedLog ? ( Timestamp {formatLogDate(selectedLog.timestamp, true)} Severity {selectedLog.severity} Source {selectedLog.source} Message {selectedLog.message} {selectedLog.stackTrace && ( Stack Trace {formatStackTrace(selectedLog.stackTrace)} )} {selectedLog.metadata && ( Metadata {JSON.stringify(selectedLog.metadata, null, 2)} )} ) : ( Select a log entry to view details )} ); } End File# apps/dashboard-demo/app/analytics/performance/page.tsx „use client”; import { Card, CardContent, CardHeader, CardTitle } from „@/components/ui/card”; import { ArrowDownIcon, ArrowUpIcon, LoaderIcon, MoreHorizontalIcon, } from „lucide-react”; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from „@/components/ui/select”; import { useState, useEffect } from „react”; import { Button } from „@/components/ui/button”; import { Tabs, TabsList, TabsTrigger } from „@/components/ui/tabs”; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from „@/components/ui/table”; import { Progress } from „@/components/ui/progress”; import { TimeSeriesChart } from „@/components/charts/time-series-chart”; import { BarChart } from „@/components/charts/bar-chart”; import { DonutChart } from „@/components/charts/donut-chart”; import { fetchPerformanceData, fetchPerformanceRouteData, fetchPerformanceResources, } from „@/lib/data”; import { PerformanceData, PerformanceRouteData, PerformanceResource, } from „@/lib/definitions”; import { formatBytes, formatNumber } from „@/lib/utils”; import ResourcesTable from „@/components/performance/resources-table”; // Page component export default function PerformancePage() { const [timeRange, setTimeRange] = useState(„7d”); const [isLoading, setIsLoading] = useState(true); const [performanceData, setPerformanceData] = useState( null ); const [routeData, setRouteData] = useState([]); const [resources, setResources] = useState([]); useEffect(() => { async function loadData() { setIsLoading(true); try { const perfData = await fetchPerformanceData(timeRange); const routesData = await fetchPerformanceRouteData(timeRange); const resourcesData = await fetchPerformanceResources(); setPerformanceData(perfData); setRouteData(routesData); setResources(resourcesData); setIsLoading(false); } catch (error) { console.error(„Error loading performance data:”, error); setIsLoading(false); } } loadData(); }, [timeRange]); if (isLoading) { return ( Loading performance data… ); } if (!performanceData) { return ( No performance data available. ); } return ( Performance Dashboard Last 24 hours Last 7 days Last 30 days Last 90 days Export {/* Key Metrics Cards */} 0 ? „slower” : „faster” } isGoodWhenNegative={true} /> 0 ? „slower” : „faster” } isGoodWhenNegative={true} /> 0 ? „slower” : „faster” } isGoodWhenNegative={true} /> 0 ? „slower” : „faster” } isGoodWhenNegative={true} /> {/* Time Series Charts */} Page Load Time (seconds) First Contentful Paint (seconds) {/* Routes Performance */} Routes Performance Slowest All Routes Route Avg. Load Time Min Max Page Views Performance {routeData.map((route) => ( {route.route} {route.avgLoadTime.toFixed(2)}s {route.minLoadTime.toFixed(2)}s {route.maxLoadTime.toFixed(2)}s {formatNumber(route.pageViews)} 0.7 ? „bg-green-500” : route.performanceScore > 0.4 ? „bg-amber-500” : „bg-red-500” } /> 0.7 ? „text-green-500” : route.performanceScore > 0.4 ? „text-amber-500” : „text-red-500” } > {(route.performanceScore * 100).toFixed(0)}% ))} Przewiń tabelę w poziomie ←→ {/* Charts and Resources */} Page Sizes by Type ({ name: item.type, value: item.size, }))} height={300} valueFormatter={(value) => formatBytes(value)} /> Loading Times by Device Type {/* Resources Table */} Resource Performance ); } // Metric Card Component function MetricCard({ title, value, change, changeLabel, isGoodWhenNegative = false, }: { title: string; value: string; change: number; changeLabel: string; isGoodWhenNegative?: boolean; }) { // Determine if the change is positive or negative from a business perspective const isPositiveChange = isGoodWhenNegative ? change 0; return ( {title} {value} {change >= 0 ? ( ) : ( )} {Math.abs(change).toFixed(1)}% {changeLabel} vs. previous period ); } End File# apps/dashboard-demo/app/analytics/engagement/page.tsx „use client”; import { Card, CardContent, CardHeader, CardTitle } from „@/components/ui/card”; import { ArrowDownIcon, ArrowUpIcon, LoaderIcon, MoreHorizontalIcon, } from „lucide-react”; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from „@/components/ui/select”; import { useState, useEffect } from „react”; import { Button } from „@/components/ui/button”; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from „@/components/ui/table”; import { TimeSeriesChart } from „@/components/charts/time-series-chart”; import { BarChart } from „@/components/charts/bar-chart”; import { DonutChart } from „@/components/charts/donut-chart”; import { fetchEngagementData, fetchReferrers, fetchPopularContent, } from „@/lib/data”; import { EngagementData, Referrer, ContentItem } from „@/lib/definitions”; import { formatNumber, formatTime } from „@/lib/utils”; // Page component export default function EngagementPage() { const [timeRange, setTimeRange] = useState(„30d”); const [isLoading, setIsLoading] = useState(true); const [engagementData, setEngagementData] = useState( null ); const [referrers, setReferrers] = useState([]); const [popularContent, setPopularContent] = useState([]); useEffect(() => { async function loadData() { setIsLoading(true); try { const data = await fetchEngagementData(timeRange); const referrerData = await fetchReferrers(timeRange); const contentData = await fetchPopularContent(timeRange); setEngagementData(data); setReferrers(referrerData); setPopularContent(contentData); setIsLoading(false); } catch (error) { console.error(„Error loading engagement data:”, error); setIsLoading(false); } } loadData(); }, [timeRange]); if (isLoading) { return ( Loading engagement data… ); } if (!engagementData) { return ( No engagement data available. ); } return ( User Engagement Last 7 days Last 30 days Last 90 days Last year Export {/* Key Metrics Cards */} {/* Time Series Charts */} Average Session Duration formatTime(value * 60)} /> Bounce Rate Over Time {/* Charts and Referrers */} Traffic by Device `${value}%`} /> {engagementData.trafficByDevice.map((item) => ( {item.name} {item.value}% ))} User Engagement by Age Group formatTime(value * 60)} /> {/* Popular Content & Referrers */} Most Popular Content Title Views Avg. Time Engagement {popularContent.map((item) => ( {item.title} {formatNumber(item.views)} {formatTime(item.avgTimeOnPage)} 75 ? „text-green-500 font-medium” : item.engagementScore > 50 ? „text-amber-500 font-medium” : „text-muted-foreground font-medium” } > {item.engagementScore}% ))} Przewiń tabelę w poziomie ←→ Top Referrers Source Sessions Bounce Rate Conversion {referrers.map((referrer) => ( {referrer.source} {formatNumber(referrer.sessions)} 70 ? „text-red-500 font-medium” : referrer.bounceRate > 50 ? „text-amber-500 font-medium” : „text-green-500 font-medium” } > {referrer.bounceRate.toFixed(1)}% {referrer.conversionRate.toFixed(1)}% ))} Przewiń tabelę w poziomie ←→ ); } // Metric Card Component function MetricCard({ title, value, change, isGoodWhenPositive = true, }: { title: string; value: string; change: number; isGoodWhenPositive?: boolean; }) { // Determine if the change is positive or negative from a business perspective const isPositiveChange = isGoodWhenPositive ? change > 0 : change = 0 ? ( ) : ( )} {Math.abs(change).toFixed(1)}% vs. previous period ); } End File# apps/dashboard-demo/lib/data.ts import { addDays, addHours, format, subDays, subHours, subMonths } from „date-fns”; import { LogData, PerformanceData, PerformanceRouteData, PerformanceResource, EngagementData, Referrer, ContentItem } from „./definitions”; // Mock log data export async function fetchLogData(): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 800)); const now = new Date(); const logs: LogData[] = []; // Generate random logs for (let i = 0; i 0.7 ? { userId: `user_${Math.floor(Math.random() * 10000)}`, requestId: `req_${Math.random().toString(36).substring(2, 12)}`, path: `/api/${[’users’, 'products’, 'orders’][Math.floor(Math.random() * 3)]}/${Math.floor(Math.random() * 1000)}`, browser: [’Chrome’, 'Firefox’, 'Safari’, 'Edge’][Math.floor(Math.random() * 4)], os: [’Windows’, 'MacOS’, 'Linux’, 'iOS’, 'Android’][Math.floor(Math.random() * 5)] } : undefined; logs.push({ id: `log_${i}`, timestamp, severity, source, message, stackTrace, metadata }); } // Sort by most recent first return logs.sort((a, b) => new Date(b.timestamp).getTime() – new Date(a.timestamp).getTime()); } // Filter log data based on search term and filters export function filterLogData( logs: LogData[], searchTerm: string = ”, severity: string = 'all’, timeRange: string = ’24h’, source: string = 'all’ ): LogData[] { const now = new Date(); let filteredLogs = […logs]; // Filter by time range if (timeRange !== 'all’) { let cutoffDate; switch (timeRange) { case '1h’: cutoffDate = subHours(now, 1); break; case ’24h’: cutoffDate = subDays(now, 1); break; case '7d’: cutoffDate = subDays(now, 7); break; case ’30d’: cutoffDate = subDays(now, 30); break; default: cutoffDate = subDays(now, 1); // Default to 24h } filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= cutoffDate ); } // Filter by severity if (severity !== 'all’) { filteredLogs = filteredLogs.filter(log => log.severity.toLowerCase() === severity.toLowerCase() ); } // Filter by source if (source !== 'all’) { filteredLogs = filteredLogs.filter(log => log.source.toLowerCase() === source.toLowerCase() ); } // Filter by search term if (searchTerm.trim() !== ”) { const term = searchTerm.toLowerCase(); filteredLogs = filteredLogs.filter(log => log.message.toLowerCase().includes(term) || log.source.toLowerCase().includes(term) || log.severity.toLowerCase().includes(term) || (log.stackTrace && log.stackTrace.toLowerCase().includes(term)) ); } return filteredLogs; } // Helper functions for generating random log messages function getRandomErrorMessage(): string { const errorMessages = [ „Failed to connect to database: Connection timeout after 30 seconds”, „Uncaught TypeError: Cannot read property 'data’ of undefined”, „Authentication failed: Invalid token signature”, „Failed to process payment: Invalid card number”, „500 Internal Server Error: Unexpected condition encountered”, „OutOfMemoryError: Java heap space”, „Unhandled exception in API request: Invalid JSON payload”, „Database query failed: Syntax error in SQL statement”, „Cache miss for critical resource, fallback failed”, „Rate limit exceeded for API endpoint /api/users”, „Failed to read file: Permission denied”, „Network error: Unable to reach external service” ]; return errorMessages[Math.floor(Math.random() * errorMessages.length)]; } function getRandomWarningMessage(): string { const warningMessages = [ „High CPU usage detected: 85% for the last 5 minutes”, „Memory usage approaching threshold: 78% of available memory”, „Slow database query detected: Query took 4.2s to complete”, „API rate limit at 80% – consider throttling requests”, „Deprecated API endpoint still in use: /v1/legacy/users”, „Cache hit ratio below acceptable threshold (65%)”, „Session timeout shorter than recommended (10 minutes)”, „Disk space running low: 85% used on /data volume”, „Configuration using default values – recommended to set explicitly”, „CORS policy may be too permissive, consider restricting origins” ]; return warningMessages[Math.floor(Math.random() * warningMessages.length)]; } function getRandomInfoMessage(): string { const infoMessages = [ „User successfully authenticated: user_12345”, „Payment processed successfully for order #34567”, „New user registered: [email protected]”, „Email notification sent to [email protected]”, „Database backup completed successfully, size: 1.2GB”, „API request completed, response time: 235ms”, „Background job completed: Report generation”, „User session started: session_id_123456”, „Config reloaded with new environment variables”, „Cache purged for content region: product-catalog” ]; return infoMessages[Math.floor(Math.random() * infoMessages.length)]; } function getRandomDebugMessage(): string { const debugMessages = [ „Request parameters: {„userId”: 12345, „limit”: 50, „offset”: 0}”, „Database query executed: SELECT * FROM users WHERE status = 'active’ LIMIT 100”, „Cache lookup for key: user:profile:12345”, „Processing item 45 of 230 in batch job”, „Authentication flow starting for user: [email protected]”, „API response payload size: 24.5KB”, „Function execution time: method=getUserData, duration=125ms”, „Initializing service dependencies: [UserService, AuthService, CacheManager]”, „Route matched: GET /api/products/:id -> ProductController.getProductById”, „Object state before transform: {„status”: „pending”, „attempts”: 2}” ]; return debugMessages[Math.floor(Math.random() * debugMessages.length)]; } function getRandomStackTrace(): string { const stackTraces = [ `Error: Cannot read property 'id’ of undefined at UserService.getUserDetails (/src/services/UserService.js:42:23) at async APIController.getUserProfile (/src/controllers/APIController.js:57:12) at async processRequest (/src/middleware/requestHandler.js:28:7)`, `TypeError: undefined is not a function at Object.parseResponse [as parse] (/src/utils/responseParser.js:28:10) at APIClient.handleResponse (/src/clients/APIClient.js:112:45) at async fetchData (/src/hooks/useFetch.js:23:19) at async ComponentDidMount (/src/components/DataView.js:31:5)`, `ReferenceError: fetch is not defined at fetchData (/src/utils/api.js:15:3) at loadUserData (/src/components/UserProfile.js:47:19) at Component.componentDidMount (/src/components/UserProfile.js:32:10)`, `SyntaxError: Unexpected token } in JSON at position 42 at JSON.parse () at parseResponse (/src/utils/http.js:23:19) at async fetchData (/src/services/dataService.js:45:23)`, `Error: Failed to connect to database at connectToDatabase (/src/db/connection.js:28:11) at initializeServices (/src/app.js:42:8) at startServer (/src/server.js:15:3) at main (/src/index.js:10:1)`, `RangeError: Maximum call stack size exceeded at Object.processNode (/src/utils/treeProcessor.js:45:12) at Object.processNode (/src/utils/treeProcessor.js:47:14) at Object.processNode (/src/utils/treeProcessor.js:47:14)`, `DatabaseError: relation „users” does not exist at Connection.parseE (/node_modules/pg/lib/connection.js:614:13) at Connection.parseMessage (/node_modules/pg/lib/connection.js:413:19) at Socket. (/node_modules/pg/lib/connection.js:129:22)`, `AuthenticationError: Invalid credentials at verifyToken (/src/auth/tokenVerifier.js:35:11) at authenticateRequest (/src/middleware/auth.js:28:15) at async processRequest (/src/middleware/requestHandler.js:24:5)` ]; return stackTraces[Math.floor(Math.random() * stackTraces.length)]; } // Mock performance data export async function fetchPerformanceData(timeRange: string): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 1000)); const now = new Date(); const days = timeRange === ’24h’ ? 1 : timeRange === '7d’ ? 7 : timeRange === ’30d’ ? 30 : 90; // Generate history data const pageLoadTimeHistory = []; const firstContentfulPaintHistory = []; for (let i = days; i >= 0; i–) { const date = subDays(now, i); // Add some randomness but maintain a trend const basePageLoad = 2.2 + (Math.sin(i/10) * 0.3); const baseFCP = 1.4 + (Math.cos(i/8) * 0.2); pageLoadTimeHistory.push({ date: format(date, 'yyyy-MM-dd’), value: Number((basePageLoad + (Math.random() * 0.5 – 0.25)).toFixed(2)) }); firstContentfulPaintHistory.push({ date: format(date, 'yyyy-MM-dd’), value: Number((baseFCP + (Math.random() * 0.4 – 0.2)).toFixed(2)) }); } return { pageLoadTime: 2.4, pageLoadTimeChange: -5.2, firstContentfulPaint: 1.2, firstContentfulPaintChange: -8.5, timeToInteractive: 3.1, timeToInteractiveChange: -3.8, serverResponseTime: 0.42, serverResponseTimeChange: 12.5, pageLoadTimeHistory, firstContentfulPaintHistory, resourceSizesByType: [ { type: 'JavaScript’, size: 845000 }, { type: 'CSS’, size: 124000 }, { type: 'Images’, size: 1250000 }, { type: 'Fonts’, size: 298000 }, { type: 'HTML’, size: 45000 }, { type: 'Other’, size: 67000 }, ], loadTimesByDevice: [ { device: 'Desktop’, loadTime: 2.1 }, { device: 'Mobile’, loadTime: 3.4 }, { device: 'Tablet’, loadTime: 2.8 }, ] }; } // Mock performance route data export async function fetchPerformanceRouteData(timeRange: string): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 800)); return [ { route: '/dashboard’, avgLoadTime: 2.1, minLoadTime: 1.4, maxLoadTime: 3.8, pageViews: 15420, performanceScore: 0.82 }, { route: '/products’, avgLoadTime: 2.8, minLoadTime: 1.9, maxLoadTime: 5.2, pageViews: 8760, performanceScore: 0.75 }, { route: '/checkout’, avgLoadTime: 3.2, minLoadTime: 2.1, maxLoadTime: 6.5, pageViews: 4350, performanceScore: 0.65 }, { route: '/user/profile’, avgLoadTime: 1.9, minLoadTime: 1.2, maxLoadTime: 3.6, pageViews: 3280, performanceScore: 0.88 }, { route: '/blog’, avgLoadTime: 2.4, minLoadTime: 1.6, maxLoadTime: 4.2, pageViews: 7520, performanceScore: 0.79 }, { route: '/api/data’, avgLoadTime: 0.8, minLoadTime: 0.3, maxLoadTime: 3.2, pageViews: 142500, performanceScore: 0.92 }, { route: '/search’, avgLoadTime: 4.2, minLoadTime: 2.5, maxLoadTime: 8.7, pageViews: 6240, performanceScore: 0.42 }, { route: '/cart’, avgLoadTime: 2.6, minLoadTime: 1.5, maxLoadTime: 5.4, pageViews: 5870, performanceScore: 0.70 } ]; } // Mock resources performance data export async function fetchPerformanceResources(): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 700)); return [ { name: 'main.js’, type: 'script’, size: 287500, transferSize: 76400, loadTime: 420, cacheable: true, optimized: false }, { name: 'styles.css’, type: 'stylesheet’, size: 84200, transferSize: 23500, loadTime: 180, cacheable: true, optimized: true }, { name: 'vendor.js’, type: 'script’, size: 543000, transferSize: 168400, loadTime: 680, cacheable: true, optimized: false }, { name: 'hero-image.jpg’, type: 'image’, size: 485000, transferSize: 482000, loadTime: 540, cacheable: true, optimized: false }, { name: 'font-awesome.woff2′, type: 'font’, size: 98000, transferSize: 97400, loadTime: 220, cacheable: true, optimized: true }, { name: 'api/user-data’, type: 'fetch’, size: 12400, transferSize: 5200, loadTime: 350, cacheable: false, optimized: true }, { name: 'analytics.js’, type: 'script’, size: 67800, transferSize: 22400, loadTime: 310, cacheable: true, optimized: false }, { name: 'product-thumbnails.png’, type: 'image’, size: 265000, transferSize: 264300, loadTime: 480, cacheable: true, optimized: false }, { name: 'roboto.woff2′, type: 'font’, size: 142000, transferSize: 141600, loadTime: 290, cacheable: true, optimized: true }, { name: 'polyfill.js’, type: 'script’, size: 48600, transferSize: 12800, loadTime: 160, cacheable: true, optimized: true } ]; } // Mock user engagement data export async function fetchEngagementData(timeRange: string): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 900)); const now = new Date(); const days = timeRange === '7d’ ? 7 : timeRange === ’30d’ ? 30 : timeRange === ’90d’ ? 90 : 365; // Generate history data const sessionDurationHistory = []; const bounceRateHistory = []; for (let i = days; i >= 0; i–) { const date = subDays(now, i); // Add some randomness but maintain a trend const baseDuration = 3.2 + (Math.sin(i/15) * 0.4); const baseBounce = 48 + (Math.cos(i/20) * 5); sessionDurationHistory.push({ date: format(date, 'yyyy-MM-dd’), value: Number((baseDuration + (Math.random() * 0.6 – 0.3)).toFixed(2)) }); bounceRateHistory.push({ date: format(date, 'yyyy-MM-dd’), value: Number((baseBounce + (Math.random() * 3 – 1.5)).toFixed(1)) }); } return { avgSessionDuration: 186, // in seconds avgSessionDurationChange: 4.8, pagesPerSession: 3.4, pagesPerSessionChange: 2.1, bounceRate: 46.5, bounceRateChange: -3.2, returningUsers: 32.7, returningUsersChange: 8.4, sessionDurationHistory, bounceRateHistory, trafficByDevice: [ { name: 'Desktop’, value: 48 }, { name: 'Mobile’, value: 42 }, { name: 'Tablet’, value: 10 }, ], engagementByAge: [ { age: ’18-24′, sessionDuration: 2.8 }, { age: ’25-34′, sessionDuration: 3.5 }, { age: ’35-44′, sessionDuration: 4.2 }, { age: ’45-54′, sessionDuration: 3.7 }, { age: ’55-64′, sessionDuration: 3.1 }, { age: ’65+’, sessionDuration: 2.5 }, ] }; } // Mock referrers data export async function fetchReferrers(timeRange: string): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 700)); return [ { source: 'Google’, sessions: 42650, bounceRate: 42.3, conversionRate: 3.8 }, { source: 'Direct’, sessions: 31470, bounceRate: 38.7, conversionRate: 5.2 }, { source: 'Facebook’, sessions: 18920, bounceRate: 53.1, conversionRate: 2.7 }, { source: 'Instagram’, sessions: 14580, bounceRate: 64.5, conversionRate: 3.1 }, { source: 'Twitter’, sessions: 8240, bounceRate: 59.8, conversionRate: 1.8 }, { source: 'Email’, sessions: 7830, bounceRate: 31.4, conversionRate: 6.4 }, { source: 'LinkedIn’, sessions: 4920, bounceRate: 44.2, conversionRate: 4.3 }, { source: 'Reddit’, sessions: 3750, bounceRate: 66.7, conversionRate: 1.5 } ]; } // Mock popular content data export async function fetchPopularContent(timeRange: string): Promise { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 800)); return [ { id: 'page_1′, title: 'Getting Started Guide’, views: 28450, avgTimeOnPage: 285, engagementScore: 87 }, { id: 'page_2′, title: 'Product Features Overview’, views: 24180, avgTimeOnPage: 192, engagementScore: 76 }, { id: 'page_3′, title: 'Pricing Plans’, views: 19740, avgTimeOnPage: 147, engagementScore: 62 }, { id: 'page_4′, title: 'Latest Product Updates’, views: 17920, avgTimeOnPage: 240, engagementScore: 81 }, { id: 'page_5′, title: 'API Documentation’, views: 15840, avgTimeOnPage: 328, engagementScore: 93 }, { id: 'page_6′, title: 'Case Studies’, views: 14250, avgTimeOnPage: 265, engagementScore: 84 }, { id: 'page_7′, title: 'Contact Support’, views: 11380, avgTimeOnPage: 118, engagementScore: 54 }, { id: 'page_8′, title: 'Frequently Asked Questions’, views: 9740, avgTimeOnPage: 208, engagementScore: 72 } ]; } End File# myrta-ds/myrta // Types for dashboard data structures // Log data export interface LogData { id: string; timestamp: string; severity: 'info’ | 'warning’ | 'error’ | 'debug’; source: string; message: string; stackTrace?: string; metadata?: Record; } // Performance data export interface TimeSeriesDataPoint { date: string; value: number; } export interface ResourceSizeData { type: string; size: number; } export interface DeviceLoadTimeData { device: string; loadTime: number; } export interface PerformanceData { pageLoadTime: number; pageLoadTimeChange: number; firstContentfulPaint: number; firstContentfulPaintChange: number; timeToInteractive: number; timeToInteractiveChange: number; serverResponseTime: number; serverResponseTimeChange: number; pageLoadTimeHistory: TimeSeriesDataPoint[]; firstContentfulPaintHistory: TimeSeriesDataPoint[]; resourceSizesByType: ResourceSizeData[]; loadTimesByDevice: DeviceLoadTimeData[]; } export interface PerformanceRouteData { route: string; avgLoadTime: number; minLoadTime: number; maxLoadTime: number; pageViews: number; performanceScore: number; } export interface PerformanceResource { name: string; type: string; size: number; transferSize: number; loadTime: number; cacheable: boolean; optimized: boolean; } // Engagement data export interface DeviceTrafficData { name: string; value: number; } export interface AgeEngagementData { age: string; sessionDuration: number; } export interface EngagementData { avgSessionDuration: number; // in seconds avgSessionDurationChange: number; pagesPerSession: number; pagesPerSessionChange: number; bounceRate: number; bounceRateChange: number; returningUsers: number; returningUsersChange: number; sessionDurationHistory: TimeSeriesDataPoint[]; bounceRateHistory: TimeSeriesDataPoint[]; trafficByDevice: DeviceTrafficData[]; engagementByAge: AgeEngagementData[]; } export interface Referrer { source: string; sessions: number; bounceRate: number; conversionRate: number; } export interface ContentItem { id: string; title: string; views: number; avgTimeOnPage: number; // in seconds engagementScore: number; // 0-100 } End Fileimport { format, formatDistance } from 'date-fns’; /** * Format a date object or string to a readable date * @param date Date object or string * @param includeTime Include time in the formatted string * @returns Formatted date string */ export function formatDate(date: Date | string, includeTime: boolean = false): string { const dateObj = typeof date === 'string’ ? new Date(date) : date; return format(dateObj, includeTime ? 'PPp’ : 'PP’); } /** * Format a date with a relative time (e.g., '3 days ago’) * @param date Date object or string * @returns Relative time string */ export function formatRelativeTime(date: Date | string): string { const dateObj = typeof date === 'string’ ? new Date(date) : date; return formatDistance(dateObj, new Date(), { addSuffix: true }); } /** * Format a timestamp from a log entry * @param timestamp ISO date string * @param includeSeconds Include seconds in the output * @returns Formatted date/time string */ export function formatLogDate(timestamp: string, includeSeconds: boolean = false): string { const date = new Date(timestamp); if (includeSeconds) { return format(date, 'MMM d, yyyy HH:mm:ss’); } return format(date, 'MMM d, yyyy HH:mm’); } /** * Truncate the first line of a multi-line string * @param text Text to truncate * @param maxLength Maximum length before truncation * @returns Truncated first line */ export function truncateFirstLine(text: string, maxLength: number = 100): string { // Get only the first line const firstLine = text.split(’n’)[0]; // Truncate if necessary if (firstLine.length <= maxLength) { return firstLine; } return firstLine.substring(0, maxLength) + '…'; } /** * Get CSS class based on log severity * @param severity Log severity * @returns CSS class name */ export function getSeverityClass(severity: string): 'default' | 'destructive' | 'warning' | 'secondary' { switch (severity.toLowerCase()) { case 'error': return 'destructive'; case 'warning': return 'warning'; case 'info': return 'secondary'; case 'debug': default: return 'default'; } } /** * Format a stack trace for better readability * @param stackTrace Raw stack trace * @returns Formatted stack trace */ export function formatStackTrace(stackTrace: string): string { if (!stackTrace) return ''; // Already has line breaks if (stackTrace.includes('n')) { return stackTrace; } // Add line breaks at common delimiters return stackTrace .replace(/at /g, 'n at ') .replace(/^n/, ''); // Remove leading newline } /** * Format a number with thousands separators * @param num Number to format * @returns Formatted number string */ export function formatNumber(num: number): string { return new Intl.NumberFormat().format(num); } /** * Format time in seconds to a readable format * @param seconds Time in seconds * @returns Formatted time string */ export function formatTime(seconds: number): string { if (seconds string; /** * Text prefix for values */ valuePrefix?: string; /** * Text suffix for values */ valueSuffix?: string; } /** * Bar chart component using Recharts */ export function BarChart({ data, xAxisKey, yAxisKey, height = 300, width = „100%”, barColor = „hsl(var(–primary))”, showGrid = true, valueFormatter, valuePrefix = „”, valueSuffix = „”, }: BarChartProps) { // Default value formatter const defaultFormatter = (value: number) => { return `${valuePrefix}${value}${valueSuffix}`; }; // Use the provided formatter or default const formatValue = valueFormatter || defaultFormatter; return ( {showGrid && ( )} { if (active && payload && payload.length) { return ( {payload[0].payload[xAxisKey]} {formatValue(payload[0].value as number)} ); } return null; }} /> ); } End File# myrta-ds/myrta import * as React from „react”; import { DonutChart as RechartsDonutChart, PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from „recharts”; import { Card } from „@/components/ui/card”; /** * Data item for donut charts */ interface DonutChartDataItem { name: string; value: number; } interface DonutChartProps { /** * Data for the donut chart */ data: DonutChartDataItem[]; /** * Height of the chart in pixels * @default 300 */ height?: number; /** * Width of the chart (can be percentage or pixels) * @default '100%’ */ width?: number | string; /** * Inner radius as a percentage * @default 70 */ innerRadius?: number; /** * Outer radius as a percentage * @default 90 */ outerRadius?: number; /** * Colors for the segments * @default Automatically generated based on theme */ colors?: string[]; /** * Show a tooltip on hover * @default true */ showTooltip?: boolean; /** * Show a legend for the chart * @default false */ showLegend?: boolean; /** * Format value for display */ valueFormatter?: (value: number) => string; } /** * Donut chart component using Recharts */ export function DonutChart({ data, height = 300, width = „100%”, innerRadius = 70, outerRadius = 90, colors, showTooltip = true, showLegend = false, valueFormatter, }: DonutChartProps) { // Default colors based on theme const defaultColors = [ „hsl(var(–primary))”, „hsl(var(–secondary))”, „hsl(var(–accent))”, „hsl(var(–muted))”, „hsl(var(–primary) / 0.8)”, „hsl(var(–secondary) / 0.8)”, „hsl(var(–accent) / 0.8)”, „hsl(var(–muted) / 0.8)”, „hsl(var(–primary) / 0.6)”, „hsl(var(–secondary) / 0.6)”, ]; // Use the provided colors or default colors const chartColors = colors || defaultColors; // Default value formatter const defaultFormatter = (value: number) => { return value.toString(); }; // Use the provided formatter or default const formatValue = valueFormatter || defaultFormatter; return ( {data.map((entry, index) => ( ))} {showTooltip && ( { if (active && payload && payload.length) { return ( {payload[0].name} {formatValue(payload[0].value as number)} ); } return null; }} /> )} {showLegend && ( )} ); } End Fileimport * as React from „react”; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area } from „recharts”; import { Card } from „@/components/ui/card”; /** * Data point for time series charts */ interface TimeSeriesPoint { date: string; value: number; } interface TimeSeriesChartProps { /** * Data for the time series chart */ data: TimeSeriesPoint[]; /** * Height of the chart in pixels * @default 300 */ height?: number; /** * Width of the chart (can be percentage or pixels) * @default '100%’ */ width?: number | string; /** * Color of the line * @default 'hsl(var(–primary))’ */ lineColor?: string; /** * Color of the area under the line * @default 'hsl(var(–primary) / 0.2)’ */ areaColor?: string; /** * Whether to show the grid * @default true */ showGrid?: boolean; /** * Whether to show the tooltip * @default true */ showTooltip?: boolean; /** * Key to use for the x-axis * @default 'date’ */ xAxisKey?: string; /** * Key to use for the y-axis * @default 'value’ */ yAxisKey?: string; /** * Text prefix for values * @default ” */ valuePrefix?: string; /** * Text suffix for values * @default ” */ valueSuffix?: string; /** * Value formatter */ valueFormatter?: (value: number) => string; } /** * Time series chart component using Recharts */ export function TimeSeriesChart({ data, height = 300, width = „100%”, lineColor = „hsl(var(–primary))”, areaColor = „hsl(var(–primary) / 0.2)”, showGrid = true, showTooltip = true, xAxisKey = „date”, yAxisKey = „value”, valuePrefix = „”, valueSuffix = „”, valueFormatter, }: TimeSeriesChartProps) { // Format the value for display const formatValue = (value: number) => { if (valueFormatter) { return valueFormatter(value); } return `${valuePrefix}${value}${valueSuffix}`; }; return ( {showGrid && ( )} {showTooltip && ( { if (active && payload && payload.length) { return ( {payload[0].payload[xAxisKey]} {formatValue(payload[0].value as number)} ); } return null; }} /> )} ); } End File# myrta-ds/myrta import { Badge } from „@/components/ui/badge”; import { Button } from „@/components/ui/button”; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from „@/components/ui/table”; import { PerformanceResource } from „@/lib/definitions”; import { formatBytes } from „@/lib/utils”; import { ArrowDownIcon, ArrowUpIcon, SortIcon } from „lucide-react”; import { useState } from „react”; type SortDirection = „asc” | „desc” | null; interface ResourcesTableProps { resources: PerformanceResource[]; } export default function ResourcesTable({ resources }: ResourcesTableProps) { const [sortField, setSortField] = useState(„loadTime”); const [sortDirection, setSortDirection] = useState(„desc”); const handleSort = (field: string) => { if (sortField === field) { if (sortDirection === „asc”) { setSortDirection(„desc”); } else if (sortDirection === „desc”) { setSortDirection(null); setSortField(null); } else { setSortDirection(„asc”); } } else { setSortField(field); setSortDirection(„asc”); } }; const getSortIcon = (field: string) => { if (sortField !== field) { return ; } if (sortDirection === „asc”) { return ; } if (sortDirection === „desc”) { return ; } return ; }; // Sort resources based on current sort settings const sortedResources = […resources].sort((a, b) => { if (!sortField || !sortDirection) return 0; let valueA, valueB; switch (sortField) { case „name”: valueA = a.name.toLowerCase(); valueB = b.name.toLowerCase(); break; case „type”: valueA = a.type.toLowerCase(); valueB = b.type.toLowerCase(); break; case „size”: valueA = a.size; valueB = b.size; break; case „transferSize”: valueA = a.transferSize; valueB = b.transferSize; break; case „loadTime”: valueA = a.loadTime; valueB = b.loadTime; break; default: return 0; } if (sortDirection === „asc”) { return valueA > valueB ? 1 : -1; } else { return valueA { switch (type.toLowerCase()) { case „script”: return Script; case „stylesheet”: return CSS; case „image”: return Image; case „font”: return Font; case „fetch”: return API; default: return {type}; } }; return ( handleSort(„name”)} className=”px-0 font-medium” > Resource Name {getSortIcon(„name”)} handleSort(„type”)} className=”px-0 font-medium” > Type {getSortIcon(„type”)} handleSort(„size”)} className=”px-0 font-medium” > Size {getSortIcon(„size”)} handleSort(„transferSize”)} className=”px-0 font-medium” > Transfer {getSortIcon(„transferSize”)} handleSort(„loadTime”)} className=”px-0 font-medium” > Load Time {getSortIcon(„loadTime”)} Status {sortedResources.map((resource) => ( {resource.name} {getResourceTypeBadge(resource.type)} {formatBytes(resource.size)} {formatBytes(resource.transferSize)} 500 ? „text-red-500” : resource.loadTime > 300 ? „text-amber-500” : „text-green-500” } > {resource.loadTime}ms {!resource.optimized ? ( Needs Optimization ) : ( Optimized )} ))} Przewiń tabelę w poziomie ←→ ); } End File# myrta-ds/myrta import React from 'react’; import { Card, CardBody, CardHeader, CardTitle } from '@/components/ui/card’; import { Badge } from '@/components/ui/badge’; import { LogData } from '@/lib/definitions’; import { getSeverityClass, formatLogDate } from '@/lib/utils’; import { Button } from '@/components/ui/button’; import { CopyIcon } from 'lucide-react’; import copyToClipboard from '@/lib/clipboard’; interface LogMessageCardProps { log: LogData; isSelected?: boolean; onClick?: () => void; } const LogMessageCard: React.FC = ({ log, isSelected = false, onClick }) => { const handleCopy = (e: React.MouseEvent) => { e.stopPropagation(); copyToClipboard(`${log.timestamp} [${log.severity}] ${log.message}`); }; return ( {formatLogDate(log.timestamp)} • {log.source} {log.severity} {log.message.split(’n’)[0]} {log.message} {log.stackTrace && ( {log.stackTrace.split(’n’)[0]} {log.stackTrace.split(’n’).length > 1 && '…’} )} ); }; export default LogMessageCard; End File’use client’ import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable, SortingState, ColumnFiltersState, getFilteredRowModel, getSortedRowModel, } from „@tanstack/react-table” import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from „@/components/ui/table” import { Button } from „@/components/ui/button” import { useState } from 'react’ import { Input } from '@/components/ui/input’ interface DataTableProps { /** * Column definitions for the table */ columns: ColumnDef[] /** * Data to display in the table */ data: TData[] /** * Whether to show search functionality * @default true */ showSearch?: boolean /** * The column key to search on */ searchKey?: string /** * Placeholder text for the search input * @default „Search…” */ searchPlaceholder?: string /** * Whether to show pagination controls * @default true */ showPagination?: boolean /** * The number of rows per page * @default 10 */ pageSize?: number } /** * A reusable data table component with sorting, filtering, and pagination */ export function DataTable({ columns, data, showSearch = true, searchKey, searchPlaceholder = „Search…”, showPagination = true, pageSize = 10, }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), state: { sorting, columnFilters, }, initialState: { pagination: { pageSize, }, }, }) return ( {/* Search input */} {showSearch && searchKey && ( table.getColumn(searchKey)?.setFilterValue(event.target.value) } className=”max-w-sm” /> )} {/* Table */} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ) })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( No results. )} Przewiń tabelę w poziomie ←→ {/* Pagination */} {showPagination && ( table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous table.nextPage()} disabled={!table.getCanNextPage()} > Next )} ) } End File# myrta-ds/myrta import * as THREE from 'three’; import { useRef, useState, useEffect } from 'react’; import { Canvas, useFrame } from '@react-three/fiber’; import { useGLTF, useTexture, Environment, Float, MeshReflectorMaterial } from '@react-three/drei’; import { EffectComposer, Bloom } from '@react-three/postprocessing’; import { easing } from 'maath’; export function MeshStage({ children, color, …props }) { // Reflective floor setup const [hovered, setHovered] = useState(false); return ( (e.stopPropagation(), setHovered(true))} onPointerOut={() => setHovered(false)}> {children} ); } export function RoundedBox({ position = [0, 0, 0], args = [1, 1, 1], radius = 0.1, …props }) { const mesh = useRef(); const [hovered, setHovered] = useState(false); useFrame((state, delta) => { easing.dampC(mesh.current.material.color, hovered ? '#40a0ff’ : '#ff6080′, 0.1, delta); mesh.current.rotation.x = mesh.current.rotation.y += delta / 4; }); return ( setHovered(true)} onPointerOut={() => setHovered(false)} castShadow receiveShadow {…props} > ); } export function Shoe({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) { const mesh = useRef(); const { nodes, materials } = useGLTF(’/models/shoe.glb’); useFrame((state, delta) => { const t = state.clock.elapsedTime; mesh.current.rotation.set( Math.cos(t / 4) / 8, Math.sin(t / 3) / 4, Math.sin(t / 2) / 8 ); mesh.current.position.y = (1 + Math.sin(t / 2)) / 3; }); return ( ); } export function WatchModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) { const mesh = useRef(); const [hovered, setHovered] = useState(false); const { nodes, materials } = useGLTF(’/models/watch.glb’); useFrame((state, delta) => { const t = state.clock.elapsedTime; mesh.current.rotation.x = Math.cos(t / 2) / 8; mesh.current.rotation.y = Math.sin(t / 4) * 0.5; easing.dampC(materials.glass.color, hovered ? '#40a0ff’ : '#74b9ff’, 0.2, delta); easing.dampC(materials.body.color, hovered ? '#303030′ : '#202020′, 0.2, delta); }); return ( setHovered(true)} onPointerOut={() => setHovered(false)} {…props} dispose={null} > ); } export function PhoneModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) { const mesh = useRef(); const [hovered, setHovered] = useState(false); const texture = useTexture(’/textures/screen.jpg’); useEffect(() => { texture.flipY = false; }, [texture]); useFrame((state, delta) => { const t = state.clock.elapsedTime; mesh.current.rotation.x = Math.cos(t / 3) / 10; mesh.current.rotation.y = Math.sin(t / 2) * 0.3; }); return ( setHovered(true)} onPointerOut={() => setHovered(false)} {…props} > {/* Phone Body */} {/* Screen */} {/* Camera Module */} ); } export function LaptopModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) { const mesh = useRef(); const [hovered, setHovered] = useState(false); const screenTexture = useTexture(’/textures/laptop-screen.jpg’); useEffect(() => { screenTexture.flipY = false; }, [screenTexture]); useFrame((state, delta) => { const t = state.clock.elapsedTime; mesh.current.rotation.x = Math.cos(t / 4) / 10; mesh.current.rotation.y = Math.sin(t / 3) * 0.2; }); return ( setHovered(true)} onPointerOut={() => setHovered(false)} {…props} > {/* Base */} {/* Keyboard Area */} {/* Trackpad */} {/* Screen Housing */} {/* Screen */} {/* Logo on back */} ); } useGLTF.preload(’/models/shoe.glb’); useGLTF.preload(’/models/watch.glb’); End File# myrta-ds/myrta # apps/docs/app/components/design-system/color-palette.tsx import * as React from 'react’; import { Label } from '@/components/ui/label’; import { getContrast } from 'polished’; interface ColorSwatch { colorName: string; colorValue: string; isDark: boolean; } interface ColorPaletteProps { /** * The colors to display * Format: { colorName: colorValue } */ colors: Record; } export function ColorPalette({ colors }: ColorPaletteProps) { const swatches = React.useMemo(() => { return Object.entries(colors).map(([colorName, colorValue]) => { // Calculate if text should be white or black const contrastWithWhite = getContrast(colorValue, '#FFFFFF’); const isDark = contrastWithWhite >= 3; // WCAG AA for normal text return { colorName, colorValue, isDark, }; }); }, [colors]); const sortedSwatches = React.useMemo(() => { return […swatches].sort((a, b) => { // Extract numbers like „50”, „100”, „200” from colorName const valueA = parseInt((a.colorName.match(/d+/) || [0])[0], 10); const valueB = parseInt((b.colorName.match(/d+/) || [0])[0], 10); return valueA – valueB; }); }, [swatches]); return ( {sortedSwatches.map((swatch) => ( ))} ); } /** * Individual color swatch component */ function ColorSwatch({ colorName, colorValue, isDark }: ColorSwatch) { return ( {colorValue} {colorName} ); } /** * Color grid to display a set of colors */ export function ColorGrid({ children, className, }: { children: React.ReactNode; className?: string; }) { return ( {children} ); } /** * Color grid item */ export function ColorGridItem({ color, name, className, }: { color: string; name: string; className?: string; }) { // Calculate if text should be white or black const contrastWithWhite = getContrast(color, '#FFFFFF’); const isDark = contrastWithWhite >= 3; // WCAG AA for normal text return ( {color} {name} ); } End File”use client”; import React, { useState } from „react”; import { Tabs, TabsContent, TabsList, TabsTrigger } from „@/components/ui/tabs”; import { Copy } from „lucide-react”; import { Button } from „@/components/ui/button”; import { cn } from „@/lib/utils”; interface CodeBlockProps { /** * The code to display */ code: string; /** * The language of the code (for syntax highlighting) * @default „tsx” */ language?: string; /** * The file name to display */ fileName?: string; /** * Disable syntax highlighting * @default false */ disableSyntaxHighlighting?: boolean; /** * Show line numbers * @default false */ showLineNumbers?: boolean; /** * Hide copy button * @default false */ hideCopyButton?: boolean; /** * Additional HTML attributes for the code element */ codeProps?: React.HTMLAttributes; /** * Alternative code examples in different languages */ examples?: { language: string; code: string; fileName?: string; }[]; } /** * Code block component for displaying code snippets with syntax highlighting */ export function CodeBlock({ code, language = „tsx”, fileName, disableSyntaxHighlighting = false, showLineNumbers = false, hideCopyButton = false, codeProps, examples = [], }: CodeBlockProps) { const [copied, setCopied] = useState(false); const allExamples = [ { language, code, fileName, }, …examples, ]; const handleCopy = async (codeToСopy: string) => { await navigator.clipboard.writeText(codeToСopy); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( {/* File name header, if provided */} {allExamples.length === 1 && fileName && ( {fileName} {!hideCopyButton && ( handleCopy(code)} > Copy code )} )} {/* Multiple examples tabs */} {allExamples.length > 1 ? ( {allExamples.map((example, i) => ( {example.language.toUpperCase()} ))} {!hideCopyButton && ( handleCopy(code)} > Copy code )} {allExamples.map((example, i) => ( {example.code} ))} ) : ( {/* Single code block */} {code} {/* Copy button at the top right if no filename */} {!fileName && !hideCopyButton && ( handleCopy(code)} > Copy code )} )} ); } End File# myrta-ds/myrta # apps/docs/app/components/docs/installation-steps.tsx „use client”; import { Tabs, TabsContent, TabsList, TabsTrigger } from „@/components/ui/tabs”; import { CodeBlock } from „./code-block”; // Package manager tab content const packageManagers = [ { name: „npm”, installCmd: „npm install @myrta-ds/core @myrta-ds/react”, peerCmd: „npm install react react-dom”, }, { name: „yarn”, installCmd: „yarn add @myrta-ds/core @myrta-ds/react”, peerCmd: „yarn add react react-dom”, }, { name: „pnpm”, installCmd: „pnpm add @myrta-ds/core @myrta-ds/react”, peerCmd: „pnpm add react react-dom”, }, ]; // Component import examples const importExamples = { typescript: `import { Button } from '@myrta-ds/react’; export default function App() { return ( Click me ); }`, javascript: `import { Button } from '@myrta-ds/react’; export default function App() { return ( Click me ); }`, }; // CSS setup examples const cssSetupExamples = { typescript: `// In your entry file (e.g., main.tsx, index.tsx, or app.tsx) import '@myrta-ds/core/styles.css’; import { App } from ’./App’; // Rest of your application setup…`, javascript: `// In your entry file (e.g., main.js, index.js, or app.js) import '@myrta-ds/core/styles.css’; import { App } from ’./App’; // Rest of your application setup…`, }; export default function InstallationSteps() { return ( 1. Install the packages {packageManagers.map((pm) => ( {pm.name} ))} {packageManagers.map((pm) => ( Myrta DS has peer dependencies on React and React DOM: ))} 2. Import the CSS Import the CSS file in your main entry file to apply global styles: TypeScript JavaScript 3. Use components Now you can import and use components in your application: TypeScript JavaScript ); } End File”use client”; import * as React from „react”; import Link from „next/link”; import { usePathname } from „next/navigation”; import { cn } from „@/lib/utils”; import { ChevronRight } from „lucide-react”; export interface SidebarNavProps extends React.HTMLAttributes { items: { title: string; href?: string; disabled?: boolean; links?: { title: string; href: string; disabled?: boolean; }[]; }[]; } /** * SidebarNav component for documentation sidebar */ export function SidebarNav({ className, items, …props }: SidebarNavProps) { const pathname = usePathname(); // Track expanded sections const [expandedSections, setExpandedSections] = React.useState([]); // Check if section should be initially expanded const initializeExpandedSections = React.useCallback(() => { const initialExpanded: string[] = []; items.forEach((item) => { if ( item.links?.some((link) => pathname.startsWith(link.href)) || pathname === item.href ) { initialExpanded.push(item.title); } }); setExpandedSections(initialExpanded); }, [items, pathname]); // Initialize expanded sections on mount and pathname change React.useEffect(() => { initializeExpandedSections(); }, [initializeExpandedSections, pathname]); // Toggle a section’s expanded state const toggleSection = (title: string) => { setExpandedSections((prev) => prev.includes(title) ? prev.filter((item) => item !== title) : […prev, title] ); }; return ( {items.map((item) => { const isExpanded = expandedSections.includes(item.title); const hasLinks = item.links && item.links.length > 0; return ( {/* Section title/link */} {hasLinks ? ( toggleSection(item.title)} className={cn( „w-full flex items-center justify-between px-3 py-2 text-sm font-medium rounded-md”, item.disabled && „cursor-not-allowed opacity-60”, pathname === item.href ? „bg-muted text-foreground” : „hover:bg-muted/50 hover:text-foreground” )} disabled={item.disabled} > {item.title} ) : ( {item.title} )} {/* Section links (if expanded) */} {hasLinks && isExpanded && ( {item.links?.map((link) => ( {link.title} ))} )} ); })} ); } End File# apps/docs/app/components/docs/api-tbl.tsx /** * API table component for documenting props and other APIs */ import React from 'react’; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table’; import { Badge } from '@/components/ui/badge’; interface ApiTableProps { /** * The data to display in the table */ data: ApiItem[]; /** * Optional table caption/title */ caption?: string; /** * The name of the interface being documented */ interfaceName?: string; /** * Whether to display default values * @default true */ showDefaults?: boolean; } export interface ApiItem { /** * Property name */ name: string; /** * Property type (e.g. string, number, boolean) */ type: string; /** * Default value (if any) */ default?: string; /** * Property description */ description: string; /** * Whether the property is required * @default false */ required?: boolean; } export function ApiTable({ data, caption, interfaceName, showDefaults = true, }: ApiTableProps) { return ( {caption && {caption}} Prop Type {showDefaults && Default} Description {data.map((item) => ( {item.name} {item.required && Required} {formatType(item.type)} {showDefaults && ( {item.default !== undefined ? formatDefaultValue(item.default) : ’-’} )} {item.description} ))} Przewiń tabelę w poziomie ←→ ); } /** * Format a type string for display */ function formatType(type: string): React.ReactNode { // Highlight certain types if ([’string’, 'number’, 'boolean’, 'any’, 'void’].includes(type)) { return {type}; } // Format union types if (type.includes(’|’)) { return type.split(’|’).map((t, i) => ( {i > 0 && ’ | ’} {t.trim()} )); } return type; } /** * Format a default value for display */ function formatDefaultValue(value: string): React.ReactNode { // Boolean values if (value === 'true’ || value === 'false’) { return {value}; } // Numbers if (!isNaN(Number(value))) { return {value}; } // Strings (quoted) if ((value.startsWith(’”’) && value.endsWith(’”’)) || (value.startsWith(„’”) && value.endsWith(„’”))) { return {value}; } return value; } End File# myrta-ds/myrta import { MetadataRoute } from 'next’; interface SitemapEntry { url: string; lastModified?: string | Date; changeFrequency?: 'always’ | 'hourly’ | 'daily’ | 'weekly’ | 'monthly’ | 'yearly’ | 'never’; priority?: number; } export default function sitemap(): MetadataRoute.Sitemap { const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://myrta-ds.vercel.app’; const pages: SitemapEntry[] = [ { url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly’, priority: 1, }, ]; // Documentation pages const docsPaths = [ // Getting started '/docs/introduction’, '/docs/installation’, '/docs/theming’, '/docs/roadmap’, // Foundation '/docs/foundation/colors’, '/docs/foundation/typography’, '/docs/foundation/spacing’, '/docs/foundation/breakpoints’, // Components '/docs/components/accordion’, '/docs/components/avatar’, '/docs/components/badge’, '/docs/components/button’, '/docs/components/card’, '/docs/components/checkbox’, '/docs/components/dialog’, '/docs/components/icon’, '/docs/components/menu’, '/docs/components/radio’, '/docs/components/select’, '/docs/components/slider’, '/docs/components/spinner’, '/docs/components/table’, '/docs/components/tabs’, '/docs/components/textfield’, '/docs/components/toggle’, '/docs/components/tooltip’, // Examples '/examples/dashboard’, '/examples/landing’, ]; const docsEntries = docsPaths.map(path => ({ url: `${baseUrl}${path}`, lastModified: new Date(), changeFrequency: 'weekly’ as const, priority: 0.8, })); // Legal and auxiliary pages const auxiliaryPaths = [ '/examples’, '/docs’, '/changelog’, ]; const auxiliaryEntries = auxiliaryPaths.map(path => ({ url: `${baseUrl}${path}`, lastModified: new Date(), changeFrequency: 'monthly’ as const, priority: 0.5, })); // Combine all entries return [ …pages, …docsEntries, …auxiliaryEntries, ]; } End Fileimport { Metadata } from 'next’; import { ScrollArea } from '@/components/ui/scroll-area’; import { Products } from '@/app/examples/landing/components/Products’; import { Hero } from '@/app/examples/landing/components/Hero’; import { Features } from '@/app/examples/landing/components/Features’; import { Brands } from '@/app/examples/landing/components/Brands’; import { Faq } from '@/app/examples/landing/components/Faq’; import { Testimonials } from '@/app/examples/landing/components/Testimonials’; import { Pricing } from '@/app/examples/landing/components/Pricing’; import { Statistics } from '@/app/examples/landing/components/Statistics’; import { Footer } from '@/app/examples/landing/components/Footer’; import { Header } from '@/app/examples/landing/components/Header’; export const metadata: Metadata = { title: 'Landing Page Example | Myrta Design System’, description: 'Example landing page built with Myrta Design System components’, }; export default function LandingPage() { return ( ); } End File# myrta-ds/myrta # apps/docs/app/examples/landing/components/Hero.tsx 'use client’; import { Button } from '@/components/ui/button’; import Link from 'next/link’; import { ArrowRight, Play } from 'lucide-react’; import { motion } from 'framer-motion’; import { MeshStage, RoundedBox } from '@/components/3d/mesh-stage’; import Image from 'next/image’; export function Hero() { return ( {/* Left column – Text content */} Next Generation Platform Kolejne rozdziały .ticss-13062101 { margin-top:30px; } Zapraszamy do dalszego czytania naszego leksykonu. Wybierz kolejny rozdział z menu poniżej, aby otworzyć nową podstronę kompedium wiedzy i uzyskać szczegółowe informację o leku, substancji lub chorobie. .ticss-b75d3ce7 { text-indent:0 !important; } Dawkowanie i sposób podawaniaDziałania niepożądaneInterakcje lekuProfil bezpieczeństwa lekuPrzeciwwskazaniaPrzedawkowaniePrzedkliniczne dane o bezpieczeństwieSkład i postać lekuSpecjalne ostrzeżeniaWłaściwości farmakodynamiczneWłaściwości farmakokinetyczneWpływ na płodność, ciążę i laktacjęWpływ na zdolność prowadzenia pojazdów i obsługiwania maszynWskazania do stosowania .ticss-a64d1df6 { /*break-inside: avoid;*/ } .ticss-88b9ebd1 { margin-top:30px; }
  2. {title} {icon || }
  3. 1. Install the packages
  4. 2. Import the CSS
  5. 3. Use components
    1. Kolejne rozdziały

Specjalne ostrzeżenia i środki ostrożności dotyczące stosowania

Stosowanie produktu leczniczego Lactulosum Polfarmex wymaga szczególnej uwagi w określonych sytuacjach klinicznych. Poniżej przedstawiono szczegółowe informacje dotyczące środków ostrożności oraz potencjalnych zagrożeń związanych z terapią tym lekiem.1

Monitorowanie elektrolitów podczas długotrwałej terapii

W przypadku przedłużonego stosowania laktulozy, szczególnie u pacjentów geriatrycznych, należy regularnie kontrolować stężenie elektrolitów w osoczu, ze szczególnym uwzględnieniem potasu i chlorków. Monitorowanie to pozwala na wczesne wykrycie potencjalnych zaburzeń elektrolitowych wynikających z długotrwałego działania przeczyszczającego.2

Substancje pomocnicze o znanym działaniu

Produkt leczniczy Lactulosum Polfarmex zawiera szereg substancji pomocniczych, które mogą wywierać istotny wpływ kliniczny u określonych grup pacjentów. Substancje te obejmują: etanol (składnik aromatu), fruktozę, laktozę, galaktozę, siarczyny oraz butylohydroksyanizol.3

Zawartość etanolu

W skład produktu wchodzi aromat mangora, płynny, zawierający etanol (alkohol). W każdych 15 ml syropu znajduje się 0,00963 g alkoholu etylowego.4

Zawartość etanolu w poszczególnych dawkach terapeutycznych przedstawia się następująco:

Grupa pacjentów Wskazanie Dawka Zawartość etanolu Ekwiwalent alkoholu
Dorośli Działanie przeczyszczające 45 ml syropu 29,07 mg mniej niż 1 ml piwa lub 1 ml wina
15 ml syropu 9,63 mg mniej niż 1 ml piwa lub 1 ml wina
Niewydolność wątroby 180 ml syropu 116,28 mg mniej niż 3 ml piwa i 2 ml wina
Niemowlęta Działanie przeczyszczające 2,5 ml syropu 1,615 mg mniej niż 1 ml piwa lub 1 ml wina
Dzieci do 3 lat 5 ml syropu 3,23 mg mniej niż 1 ml piwa lub 1 ml wina
Dzieci powyżej 3 lat 15 ml syropu 9,69 mg mniej niż 1 ml piwa lub 1 ml wina

Należy podkreślić, że ilość alkoholu obecna w preparacie jest tak niewielka, że nie powoduje klinicznie istotnych efektów farmakologicznych.5

Zawartość fruktozy

Produkt zawiera 0,075 g fruktozy w każdych 15 ml syropu. W maksymalnej dawce dobowej zawartość fruktozy sięga 0,9 g. Należy mieć na uwadze, że fruktoza zawarta w syropie może wywierać niekorzystny wpływ na zęby, szczególnie przy długotrwałym stosowaniu.6

Zawartość laktozy

Każde 15 ml syropu zawiera 0,75 g laktozy, co daje 9 g laktozy w maksymalnej dawce dobowej. Ta informacja jest szczególnie istotna dla pacjentów z cukrzycą, u których należy uwzględnić zawartość laktozy w bilansie węglowodanów.7

Produkt jest przeciwwskazany u pacjentów z rzadko występującą dziedziczną nietolerancją galaktozy, brakiem laktazy lub zespołem złego wchłaniania glukozy-galaktozy ze względu na zawartość laktozy.8

Zawartość galaktozy

Produkt leczniczy zawiera 1,125 g galaktozy w każdych 15 ml syropu, co przekłada się na 13,5 g galaktozy w maksymalnej dawce dobowej. Z tego powodu produkt nie powinien być stosowany u pacjentów z rzadkimi dziedzicznymi zaburzeniami związanymi z nietolerancją galaktozy, takimi jak galaktozemia czy zespół złego wchłaniania glukozy-galaktozy.9

Zawartość siarczyn# kevinkirbyuk/Opta-F24-Data-Parser
# OptaParser.py
import json
import os
import time
import re
import pandas as pd
from time import sleep

def preprocess_text_to_valid_json(text):
„””
Ensure the text is valid JSON by handling common syntax errors.

Parameters:
– text (str): The JSON text to be preprocessed

Returns:
– str: Preprocessed JSON text that should be valid
„””
# Common preprocessing steps
# Remove trailing commas in arrays and objects
text = re.sub(r’,s*([}]])’, r’1′, text)
# Ensure properties are in double quotes
text = re.sub(r'([{,])s*([a-zA-Z0-9_]+):’, r’1″2″:’, text)
# Replace single quotes with double quotes for string values
text = re.sub(r”'([^’]*)’”, r’”1″’, text)
return text

def read_opta_files(filenames):
„””
Read multiple Opta F24 XML files and store them in a list.

Parameters:
– filenames (List[str]): List of Opta F24 filenames

Returns:
– List: List of parsed XML file contents
„””
opta_files = []

for filename in filenames:
# Skip if file doesn’t exist or is directory
if not os.path.isfile(filename):
print(f”Skipping {filename}: file not found.”)
continue

# Read the file
with open(filename, 'r’, encoding=’utf-8′) as file:
opta_files.append(file.read())

return opta_files

def parse_opta_f24_to_json(xml_string):
„””
Parse Opta F24 XML data into JSON format.

Parameters:
– xml_string (str): Opta F24 XML data as string

Returns:
– dict: Parsed data in JSON format
„””
# Extract necessary components
match_pattern = r’
match_time = re.search(match_pattern, xml_string)
timestamp = match_time.group(1) if match_time else „”

game_pattern = r’
match = re.search(game_pattern, xml_string)

if not match:
print(„Failed to extract game information.”)
return None # Or handle the error in a way that makes sense for your application

game_id = match.group(1)
away_team_id = match.group(2)
away_team_name = match.group(3)
competition_id = match.group(4)
competition_name = match.group(5)
game_date = match.group(6)
home_team_id = match.group(7)
home_team_name = match.group(8)
matchday = match.group(9)
period_1_start = match.group(10)
period_2_start = match.group(11)
season_id = match.group(12)
season_name = match.group(13)

# Extract all event data
pattern = r’)*’
r’.*?

events = []
for match in re.finditer(pattern, xml_string):
event_id = match.group(1)
type_id = match.group(3)
period_id = match.group(4)
minute = match.group(5)
second = match.group(6)
team_id = match.group(7)
outcome = match.group(8)
x_pos = match.group(9)
y_pos = match.group(10)
event_timestamp = match.group(11)

# Extract qualifiers for this event
qualifier_pattern = r’
qualifiers = []
for q_match in re.finditer(qualifier_pattern, match.group(0)):
q_id = q_match.group(1)
qualifier_id = q_match.group(2)
value = q_match.group(3)
qualifiers.append({
„id”: q_id,
„qualifier_id”: qualifier_id,
„value”: value
})

# Create event object
event = {
„id”: event_id,
„type_id”: type_id,
„period_id”: period_id,
„minute”: minute,
„second”: second,
„team_id”: team_id,
„outcome”: outcome,
„x”: x_pos,
„y”: y_pos,
„timestamp”: event_timestamp,
„qualifiers”: qualifiers
}

events.append(event)

# Create the final JSON structure
json_data = {
„OptaFeed”: {
„timestamp”: timestamp,
„Game”: {
„id”: game_id,
„away_team_id”: away_team_id,
„away_team_name”: away_team_name,
„competition_id”: competition_id,
„competition_name”: competition_name,
„game_date”: game_date,
„home_team_id”: home_team_id,
„home_team_name”: home_team_name,
„matchday”: matchday,
„period_1_start”: period_1_start,
„period_2_start”: period_2_start,
„season_id”: season_id,
„season_name”: season_name,
„Events”: events
}
}
}

return json_data

def save_to_json(data, filename):
„””
Save data as JSON to a file.

Parameters:
– data (dict): The data to save
– filename (str): The output filename

Returns:
– bool: True if successful, False otherwise
„””
try:
with open(filename, 'w’, encoding=’utf-8′) as file:
json.dump(data, file, indent=4)
return True
except Exception as e:
print(f”Error saving JSON to {filename}: {e}”)
return False

def extract_match_info(json_data):
„””
Extract match information from Opta F24 JSON data.

Parameters:
– json_data (dict): Parsed Opta F24 data

Returns:
– dict: Extracted match information
„””
game = json_data[„OptaFeed”][„Game”]

return {
„match_id”: game[„id”],
„home_team_id”: game[„home_team_id”],
„home_team”: game[„home_team_name”],
„away_team_id”: game[„away_team_id”],
„away_team”: game[„away_team_name”],
„competition”: game[„competition_name”],
„season”: game[„season_name”],
„date”: game[„game_date”]
}

def events_to_dataframe(json_data):
„””
Convert Opta F24 events from JSON to a pandas DataFrame.

Parameters:
– json_data (dict): Parsed Opta F24 data

Returns:
– DataFrame: Events data in tabular format
„””
events = json_data[„OptaFeed”][„Game”][„Events”]
match_info = extract_match_info(json_data)

# Initialize an empty list to store flattened events
flattened_events = []

for event in events:
# Start with basic event information
flat_event = {
„event_id”: event[„id”],
„type_id”: event[„type_id”],
„period_id”: event[„period_id”],
„minute”: event[„minute”],
„second”: event[„second”],
„team_id”: event[„team_id”],
„outcome”: event[„outcome”],
„x”: event[„x”],
„y”: event[„y”],
„timestamp”: event[„timestamp”],
„match_id”: match_info[„match_id”],
„home_team”: match_info[„home_team”],
„away_team”: match_info[„away_team”],
„competition”: match_info[„competition”],
„season”: match_info[„season”],
„date”: match_info[„date”]
}

# Add team information
if event[„team_id”] == match_info[„home_team_id”]:
flat_event[„team”] = match_info[„home_team”]
elif event[„team_id”] == match_info[„away_team_id”]:
flat_event[„team”] = match_info[„away_team”]

# Add qualifiers as separate columns
for qualifier in event[„qualifiers”]:
qualifier_key = f”q{qualifier[’qualifier_id’]}”
flat_event[qualifier_key] = qualifier[„value”]

flattened_events.append(flat_event)

# Convert to DataFrame
df = pd.DataFrame(flattened_events)

return df

def main(filenames, output_format=”json”):
„””
Main function to process Opta F24 files.

Parameters:
– filenames (List[str]): List of Opta F24 filenames to process
– output_format (str): Output format (json or csv)

Returns:
– None: Results are saved to files
„””
# Read in all the specified files
opta_files = read_opta_files(filenames)

# Process each file
for i, file_content in enumerate(opta_files):
try:
# Parse XML to JSON
json_data = parse_opta_f24_to_json(file_content)

if json_data:
# Extract match info for filename
match_info = extract_match_info(json_data)
base_filename = f”{match_info[’home_team’]}_{match_info[’away_team’]}_{match_info[’date’]}”
base_filename = base_filename.replace(” „, „_”).replace(„/”, „-„)

# Save in requested format(s)
if output_format.lower() == „json” or output_format.lower() == „both”:
json_filename = f”{base_filename}.json”
save_to_json(json_data, json_filename)
print(f”Saved {json_filename}”)

if output_format.lower() == „csv” or output_format.lower() == „both”:
# Convert to DataFrame and save as CSV
df = events_to_dataframe(json_data)
csv_filename = f”{base_filename}.csv”
df.to_csv(csv_filename, index=False)
print(f”Saved {csv_filename}”)

else:
print(f”Failed to parse file {filenames[i]}”)

except Exception as e:
print(f”Error processing {filenames[i]}: {e}”)

print(„Processing complete.”)

if __name__ == „__main__”:
# Example usage:
filenames = [„f24-20-2019-2245250-eventdetails.xml”]
main(filenames, output_format=”both”) # Save as both JSON and CSV
# serbogdanov/project
service = $service;
$this->repository = $repository;
$this->contactRepository = $contactRepository;
$this->appointmentRepository = $appointmentRepository;
}

/**
* @param Request $request
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function index(Request $request)
{
$sort_by = $request->get(’sort_by’, 'created_at’);
$sort = $request->get(’sort’, 'desc’);
$search = trim($request->get(’search’));

$user = Auth::user();

$per_page = config(’project.per_page’);

$hasPermission = auth()->user()->hasPermission(’patient’);

$patients = $this->repository->getUserWithContactByRole(’patient’, $sort_by, $sort, $search, $per_page, false);

return view(’users::patient.index’, compact(’patients’, 'user’, 'hasPermission’));
}

/**
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function create()
{
$isCreate = true;
$doctors = $this->repository->getUsersByRole(’doctor’);
$treatments = $this->service->getPreDefinedData(’treatment’);
$bloodGroups = $this->service->getPreDefinedData(’blood_group’);
$statuses = $this->service->getPreDefinedData(’status’);
return view(’users::patient.create_edit’, compact(’doctors’, 'isCreate’, 'bloodGroups’, 'treatments’, 'statuses’));
}

/**
* @param Request $request
* @return IlluminateHttpRedirectResponse
*/
public function store(CreateUserRequest $request)
{
try {
$data = $request->all();
$this->service->createPatientContact($data);
return redirect()->route(’patients.index’)
->with(’success’, trans(’users::messages.record_was_successfully_created’));
} catch (Exception $e) {
return back()
->with(’error’, „Cannot save patient: „.$e->getMessage());
}
}

/**
* @param $id
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function show($id)
{
$per_page = config(’project.per_page’);

$user = $this->service->findPatientById($id);
$doctors = $this->repository->getUsersByRole(’doctor’);
return view(’users::patient.view’, compact(’user’, 'doctors’));
}

/**
* @param $id
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function edit($id)
{
$isCreate = false;
$user = $this->service->findPatientById($id);
$doctors = $this->repository->getUsersByRole(’doctor’);
$bloodGroups = $this->service->getPreDefinedData(’blood_group’);
$treatments = $this->service->getPreDefinedData(’treatment’);
$statuses = $this->service->getPreDefinedData(’status’);
return view(’users::patient.create_edit’, compact(’user’, 'doctors’, 'isCreate’, 'bloodGroups’, 'treatments’, 'statuses’));
}

/**
* @param Request $request
* @param $id
* @return IlluminateHttpRedirectResponse
*/
public function update(UpdateUserRequest $request, $id)
{
try {
$data = $request->all();
$this->service->updatePatientContact($id, $data);
return redirect()->route(’patients.index’)
->with(’success’, trans(’users::messages.record_was_successfully_updated’));
} catch (Exception $e) {
return back()
->with(’error’, „Cannot update patient: „.$e->getMessage());
}
}

/**
* @param $id
* @return IlluminateHttpRedirectResponse
*/
public function destroy($id)
{
try {
$this->repository->delete($id);
return back()
->with(’success’, trans(’users::messages.record_was_successfully_deleted’));
} catch (Exception $e) {
return back()
->with(’error’, trans(’users::messages.record_was_not_deleted_due_to_system_error’).$e->getMessage());
}
}

/**
* @param Request $request
* @param $id
* @return string
*/
public function events(Request $request, $id) {
$data = $this->appointmentRepository->getAppointmentByContactId($id);
return json_encode($data);
}

/**
* @param $id
* @return IlluminateHttpJsonResponse
*/
public function getPatient($id)
{
try {
$data = $this->contactRepository->findById($id, true);
return response()->json([
'status’ => 'success’,
'data’ => $data
]);
} catch(Exception $e) {
return response()->json([
'status’ => 'error’,
'error’ => $e->getMessage()
]);
}
}

/**
* Copy patient data
*
* @param IlluminateHttpRequest $request
* @return IlluminateHttpJsonResponse
*/
public function copyPatientData(Request $request)
{
try {
$data = $request->only(’patient_ids’);
$patientIds = !empty($data[’patient_ids’]) ? explode(’,’, $data[’patient_ids’]) : [];
if (empty($patientIds)) {
throw new Exception(’Please choose patient(s) for copy.’);
}
$patients = [];
foreach ($patientIds as $patientId) {
$patients[] = $this->service->findPatientById($patientId);
}
$data = [
'datas’ => $patients
];
$view = view(’users::patient.includes.patient_data_mail_table’, $data)->render();
return response()->json([
'status’ => 'success’,
'view’ => $view
]);
} catch(Exception $e) {
return response()->json([
'status’ => 'error’,
'error’ => $e->getMessage()
]);
}
}

public function patientStatus($id)
{
try {
$this->service->updatePatientStatus($id);
return back()
->with(’success’, trans(’users::messages.patient_status_successfully_updated’));
} catch (Exception $e) {
return back()
->with(’error’, 'Cannot update patient: ’.$e->getMessage());
}
}
}
End FileuserModel = $userModel;
$this->roleUserModel = $roleUserModel;
$this->roleModel = $roleModel;
$this->contactModel = $contactModel;
$this->fileEntityModel = $fileEntityModel;
}

/**
* get All User Except Current User
* @return IlluminateDatabaseEloquentCollection|static[]
*/
public function getAllUsers()
{
return $this->userModel->where(’id’, ’<>’, Auth::user()->id)->get();
}

/**
* get users by role
* @param string $roleName
* @return mixed
*/
public function getUsersByRole($roleName = 'user’)
{
$role = $this->roleModel->select(’id’)->where(’name’, $roleName)->first();
$userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray();
return $this->userModel->whereIn(’id’, $userIds)->get();
}

/**
* @param String $search
* @return Collection
*/
public function searchUserName($search)
{
$users = DB::table(’users’)
->whereRaw(’CONCAT(first_name, ” „, last_name) like ?’, [„%{$search}%”])
->orderBy(’first_name’)
->orderBy(’last_name’)
->get();
$role = $this->roleModel->select(’id’)->where(’name’, 'patient’)->first();
return $users;
}

/**
* @param $userId
* @param bool $isDoctorView
* @return Contact|Contact[]|IlluminateDatabaseEloquentCollection|IlluminateDatabaseEloquentModel|null
*/
public function getUserContactProfileByUserId($userId, $isDoctorView = false) {
$role = $this->roleModel->select(’id’)->where(’name’, 'patient’)->first();
$userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray();
if ($isDoctorView) {
return $this->contactModel
->whereIn(’user_id’, $userIds)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’)
->where(’assigned_dr_id’, $userId)
->get();
} else {
return $this->contactModel
->whereIn(’user_id’, $userIds)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’)
->get();
}

}

/**
* @param $userId
* @return mixed
*/
public function getUserById($userId)
{
return $this->userModel->where(’id’, $userId)->with(’profile’)->first();
}

/**
* @param $roleName
* @param string $sort_by
* @param string $sort
* @param string $search
* @param int $per_page
* @param bool $isDoctorAssigned
* @return IlluminateContractsPaginationLengthAwarePaginator
*/
public function getUserWithContactByRole($roleName, $sort_by = 'created_at’,
$sort = 'desc’, $search = ”, $per_page = 10, $isDoctorAssigned = false)
{
$role = $this->roleModel->select(’id’)->where(’name’, $roleName)->first();
$userIds = $this->roleUserModel->select(’user_id’)->where(’role_id’, $role->id)->get()->pluck(’user_id’)->toArray();

$query = $this->contactModel->whereIn(’user_id’, $userIds)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’);
if ($isDoctorAssigned === true) {
$query = $query->where(’assigned_dr_id’, Auth::user()->id);
}

if (!is_null($search) && !empty($search)) {
$query = $query->where(function ($q) use ($search) {
$q->where(’first_name’, 'like’, '%’.$search.’%’)
->orWhere(’last_name’, 'like’, '%’.$search.’%’)
->orWhere(’surname’, 'like’, '%’.$search.’%’)
->orWhere(’email’, 'like’, '%’.$search.’%’)
;
});
}

return $query->orderBy($sort_by, $sort)->paginate($per_page);
}

/**
* @param $id
* @return bool
*/
public function delete($id)
{
$user = $this->userModel->where(’id’, $id)->first();
return $user->delete();
}

/**
* @param $userId
* @param $entityId
* @param $category
* @return bool
*/
public function saveUserAvatar($userId, $entityId, $category)
{
$object = $this->fileEntityModel
->where(’entity_id’, $userId)
->where(’category’, $category)
->first();
if (is_null($object)) {
$this->fileEntityModel->entity_id = $userId;
$this->fileEntityModel->file_id = $entityId;
$this->fileEntityModel->category = $category;
return $this->fileEntityModel->save();
} else {
return $object
->where(’entity_id’, $userId)
->where(’category’, $category)
->update([’file_id’ => $entityId]);
}
}

/**
* @param $userId
* @param $category
* @return FileEntity
*/
public function getUserAvatar($userId, $category)
{
return $this->fileEntityModel
->where(’entity_id’, $userId)
->where(’category’, $category)
->first();
}

/**
* @param $roleId
* @return IlluminateDatabaseEloquentCollection|static[]
*/
public function findAllUsersByRoleId($roleId)
{
return $this->userModel
->join(’role_user’, 'role_user.user_id’, '=’, 'users.id’)
->select(’users.*’, 'role_user.created_at as user_role_created_at’)
->where(’role_user.role_id’, $roleId)
->get();
}

/**
* @return IlluminateDatabaseEloquentCollection|static[]
*/
public function getAdminUser()
{
return $this->userModel
->join(’role_user’, 'role_user.user_id’, '=’, 'users.id’)
->join(’roles’, 'roles.id’, '=’, 'role_user.role_id’)
->where(’role_user.role_id’, env(’ROLE_ADMIN_USER_ID’))
->select(’users.*’)
->get();
}

/**
* @param $userId
* @return Contact[]|IlluminateDatabaseEloquentCollection|IlluminateDatabaseEloquentModel|mixed|null
*/
public function getContactData($userId)
{
$contact = $this->contactModel
->where(’assigned_dr_id’, $userId)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’)
->get();
return $contact;
}

/**
* @param $doctor_ids
* @return Contact[]|IlluminateDatabaseEloquentCollection
*/
public function getReferredDoctorPatients($doctor_ids)
{
return $this->contactModel
->whereIn(’assigned_dr_id’, $doctor_ids)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’)
->get();
}

/**
* @param $user_ids
* @return mixed
*/
public function getContactsByUserIds($user_ids)
{
return $this->contactModel
->whereIn(’user_id’, $user_ids)
->with(’avatar’, 'user’, 'patient_doctor’, 'treatment’, 'appointments’)
->get();
}
}End FileuserRepository = $userRepository;
$this->roleRepository = $roleRepository;
$this->contactService = $contactService;
$this->preDefinedService = $preDefinedService;
}

/**
* @param $data
* @param $is_creating
* @return User
*/
public function createOrUpdate($data, $is_creating)
{
$user = new User();
if (!$is_creating) {
$user = User::find($data[’id’]);
}

if ($is_creating) {
$user->email = $data[’email’];
$user->first_name = $data[’first_name’];
$user->last_name = $data[’last_name’];
$user->password = Hash::make($data[’password’]);
$user->status = true;
} else {
if (isset($data[’password’]) && !empty($data[’password’])) {
$user->password = Hash::make($data[’password’]);
}
}

$user->save();

return $user;
}

/**
* @param $user
* @param $data
* @param $entityId
* @param $type
* @param bool $is_creating
* @return mixed
*/
public function saveRole($user, $data, $entityId, $type, $is_creating = true)
{
if (!empty($data[’roles’])) {
$roles = [];
foreach ($data[’roles’] as $roleId => $value) {
$roles[] = $roleId;
}
$user->syncRoles($roles);
}

return $user;
}

/**
* @param array $data
* @return mixed
*/
public function createUserProfile(array $data)
{
try {
DB::beginTransaction();
$data[’password’] = str_random(8);
$data[’password_confirmation’] = $data[’password’];
$data[’email’] = (!empty($data[’email’])) ? $data[’email’] : 'john.’.$data[’last_name’].’@example.com’;
$user = $this->createOrUpdate($data, true);
$data[’roles’][$data[’role’]] = 1;
$this->saveRole($user, $data, $user->id, 'user’);
$contact = $this->contactService->setOwner($user->id);
$this->contactService->storeContact($data);
DB::commit();
return $contact;
} catch (Exception $e) {
DB::rollBack();
return $e->getMessage();
}
}

/**
* @param array $data
* @return mixed
*/
public function createPatientContact(array $data)
{
try {
DB::beginTransaction();
$data[’password’] = str_random(8);
$data[’password_confirmation’] = $data[’password’];
$user = $this->createOrUpdate($data, true);

$data[’roles’][config(’project.roles.patient’)] = 1;
$this->saveRole($user, $data, $user->id, 'user’);
$contact = $this->contactService->setOwner($user->id);
$this->contactService->storeContact($data);
$this->updateContactAssignedDoctor($contact->id, isset($data[’doctor’]) ? $data[’doctor’] : ”);
DB::commit();
return true;
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}

/**
* find patient information and details by id
* @param $id
* @return mixed
*/
public function findPatientById($id) {
$user = $this->contactService->findContactByUserId($id);
return $user;
}

/**
* update patient contact by id
* @param $id
* @param $data
* @return bool
* @throws Exception
*/
public function updatePatientContact($id, $data) {
try {
DB::beginTransaction();
$contact = $this->contactService->findById($id, true);
if (!empty($data[’password’])) {
$user = User::find($contact->user_id);
$user->password = Hash::make($data[’password’]);
$user->email = $data[’email’];
$user->save();
}
$this->contactService->updateContact($contact, $data);
$this->updateContactAssignedDoctor($id, isset($data[’doctor’]) ? $data[’doctor’] : ”);
DB::commit();
return true;
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}

/**
* @param $category
* @return mixed
*/
public function getPreDefinedData($category)
{
return $this->preDefinedService->getDataByCategory($category);
}

/**
* @param $contactId
* @param $doctorID
* @return mixed
*/
public function updateContactAssignedDoctor($contactId, $doctorID = ”)
{
return $this->contactService->updateContactAssignedDoctor($contactId, $doctorID);
}

/**
* @param $userId
* @return mixed
*/
public function updatePatientStatus($userId)
{
$contact = $this->contactService->findContactByUserId($userId);
return $this->contactService->updatePatientStatus($contact->id);
}
}
End File# serbogdanov/project
# app/Modules/Appointments/Repositories/AppointmentRepository.php
appointmentModel = $appointmentModel;
$this->userRepository = $userRepository;
}

/**
* @param array $data
* @return Appointment|IlluminateDatabaseEloquentModel
*/
public function create(array $data)
{
$start = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’start’]);
$end = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’end’]);
$query = $this->appointmentModel->create([
'contact_id’ => $data[’contact_id’],
'title’ => $data[’title’],
'dentist_id’ => $data[’dentist_id’],
'type_id’ => $data[’type_id’],
'duration’ => $data[’duration’],
'date’ => $data[’date’],
'treatment_id’ => $data[’treatment_id’],
'start_time’ => $data[’start’],
'end_time’ => $data[’end’],
'all_day’ => 0,
'notes’ => isset($data[’notes’]) ? $data[’notes’] : ”,
'color’ => isset($data[’color’]) ? $data[’color’] : ”,
'is_block’ => isset($data[’is_block’]) ? $data[’is_block’] : 0,
'is_initial’ => isset($data[’is_initial’]) ? $data[’is_initial’] : 0,
'status_id’ => $data[’status_id’],
'start’ => $start,
'end’ => $end,
]);
return $query;
}

/**
* Update appointment
* @param int $id
* @param array $data
* @return Appointment
* @throws Exception
*/
public function update($id, array $data)
{
$appointment = $this->findById($id);

if (empty($appointment)) {
throw new Exception(’No appointment found.’);
}

$start = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’start’]);
$end = Carbon::createFromFormat(’Y-m-d H:i’, $data[’date’].’ ’.$data[’end’]);

if (array_key_exists(’contact_id’, $data)) {
$appointment->contact_id = $data[’contact_id’];
}

if (array_key_exists(’title’, $data)) {
$appointment->title = $data[’title’];
}

if (array_key_exists(’dentist_id’, $data)) {
$appointment->dentist_id = $data[’dentist_id’];
}

if (array_key_exists(’type_id’, $data)) {
$appointment->type_id = $data[’type_id’];
}

if (array_key_exists(’duration’, $data)) {
$appointment->duration = $data[’duration’];
}

if (array_key_exists(’date’, $data)) {
$appointment->date = $data[’date’];
}

if (array_key_exists(’treatment_id’, $data)) {
$appointment->treatment_id = $data[’treatment_id’];
}

if (array_key_exists(’start’, $data)) {
$appointment->start_time = $data[’start’];
}

if (array_key_exists(’end’, $data)) {
$appointment->end_time = $data[’end’];
}

if (array_key_exists(’all_day’, $data)) {
$appointment->all_day = $data[’all_day’];
}

if (array_key_exists(’notes’, $data)) {
$appointment->notes = $data[’notes’];
}

if (array_key_exists(’color’, $data)) {
$appointment->color = $data[’color’];
}

if (array_key_exists(’is_block’, $data)) {
$appointment->is_block = $data[’is_block’];
}

if (array_key_exists(’is_initial’, $data)) {
$appointment->is_initial = $data[’is_initial’];
}

if (array_key_exists(’status_id’, $data)) {
$appointment->status_id = $data[’status_id’];
}

$appointment->start = $start;
$appointment->end = $end;

$appointment->save();

return $appointment;
}

/**
* Delete appointment
* @param $id
* @return bool
* @throws Exception
*/
public function delete($id)
{
$appointment = $this->findById($id);

if (empty($appointment)) {
throw new Exception(’No appointment found.’);
}

return $appointment->delete();
}

/**
* @param $id
* @return Appointment|IlluminateDatabaseEloquentModel|null|object
*/
public function findById($id)
{
return $this->appointmentModel->where(’id’, $id)->with([’contact’])->first();
}

/**
* @param $id
* @return Appointment|IlluminateDatabaseEloquentModel|null|object
*/
public function findDetailById($id)
{
return $this->appointmentModel->where(’id’, $id)
->with([’contact’,’dentist’,’type’, 'status’, 'treatment’])
->first();
}

/**
* @param null $doctorId
* @param null $startStr
* @param null $endStr
* @return mixed
*/
public function getDentistAppointment($doctorId = null, $startStr = null, $endStr = null)
{
$query = $this->appointmentModel->where(’dentist_id’, $doctorId);

if (!is_null($startStr) && !is_null($endStr)) {
$start = Carbon::parse($startStr)->format(’Y-m-d’);
$end = Carbon::parse($endStr)->format(’Y-m-d’);

// Use date based conditions directly on date column for more accurate search
$query = $query->whereRaw(’date(date) >= ?’, [$start])
->whereRaw(’date(date) <= ?', [$end]); } // Include related models for a richer response $query = $query->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]);

return $query->get();
}

/**
* @param $contactId
* @return mixed
*/
public function getAppointmentByContactId($contactId)
{
$query = $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’contact_id’, $contactId);
return $query->get();
}

/**
* @param $startDate
* @param $endDate
* @return mixed
*/
public function getAppointmentByRange($startDate, $endDate)
{
$doctorIds = [];
$contactIds = [];
$user = Auth::user();
if ($user->hasRole(’doctor’)) {
$doctorIds = [$user->id];
} else {
$result = $this->userRepository->getUsersByRole(’doctor’);
foreach($result as $doctor) {
$doctorIds[] = $doctor->id;
}
}
if ($user->hasRole(’doctor’)) {
$contacts = $this->userRepository->getContactData($user->id);
foreach($contacts as $contact) {
$contactIds[] = $contact->id;
}
} else {
$query = $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’date’, '>=’, $startDate)
->where(’date’, ’<=', $endDate) ->whereIn(’dentist_id’, $doctorIds);
}
return $query->get();
}

/**
* @param string $sort_by
* @param string $sort
* @param string $search
* @param int $per_page
* @return IlluminateContractsPaginationLengthAwarePaginator
*/
public function getAllAppointments($sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10)
{
$query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’]);

if (!is_null($search) && !empty($search)) {
$query = $query->where(function ($q) use ($search) {
$q->where(’title’, 'like’, '%’.$search.’%’)
->orWhere(’notes’, 'like’, '%’.$search.’%’)
->orWhereHas(’contact’, function ($query) use ($search) {
$query->where(’first_name’, 'like’, '%’.$search.’%’)
->orWhere(’last_name’, 'like’, '%’.$search.’%’);
})
->orWhereHas(’dentist’, function ($query) use ($search) {
$query->where(’first_name’, 'like’, '%’.$search.’%’)
->orWhere(’last_name’, 'like’, '%’.$search.’%’);
});
});
}

return $query->orderBy($sort_by, $sort)->paginate($per_page);
}

/**
* @param $user_id
* @param string $sort_by
* @param string $sort
* @param string $search
* @param int $per_page
* @return IlluminateContractsPaginationLengthAwarePaginator
*/
public function getAppointmentsByDentist($user_id, $sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10)
{
$query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’dentist_id’, $user_id);

if (!is_null($search) && !empty($search)) {
$query = $query->where(function ($q) use ($search) {
$q->where(’title’, 'like’, '%’.$search.’%’)
->orWhere(’notes’, 'like’, '%’.$search.’%’)
->orWhereHas(’contact’, function ($query) use ($search) {
$query->where(’first_name’, 'like’, '%’.$search.’%’)
->orWhere(’last_name’, 'like’, '%’.$search.’%’);
});
});
}

return $query->orderBy($sort_by, $sort)->paginate($per_page);
}

/**
* @param $contact_id
* @param string $sort_by
* @param string $sort
* @param string $search
* @param int $per_page
* @return IlluminateContractsPaginationLengthAwarePaginator
*/
public function getAppointmentsByContact($contact_id, $sort_by = 'date’, $sort = 'desc’, $search = ”, $per_page = 10)
{
$query = $this->appointmentModel->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’contact_id’, $contact_id);

if (!is_null($search) && !empty($search)) {
$query = $query->where(function ($q) use ($search) {
$q->where(’title’, 'like’, '%’.$search.’%’)
->orWhere(’notes’, 'like’, '%’.$search.’%’)
->orWhereHas(’dentist’, function ($query) use ($search) {
$query->where(’first_name’, 'like’, '%’.$search.’%’)
->orWhere(’last_name’, 'like’, '%’.$search.’%’);
});
});
}

return $query->orderBy($sort_by, $sort)->paginate($per_page);
}

/**
* @param $startDate
* @param $endDate
* @return mixed
*/
public function getWeeklyAppointments($startDate, $endDate)
{
$query = $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’date’, '>=’, $startDate)
->where(’date’, ’<=', $endDate); return $query->get();
}

/**
* @param string $status
* @return mixed
*/
public function getAppointmentsByStatus($status = 'booked’)
{
$query = $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->whereHas(’status’, function ($query) use ($status) {
$query->where(’value’, $status);
})
->where(’date’, '>=’, Carbon::now()->format(’Y-m-d’));

return $query->orderBy(’date’, 'asc’)->orderBy(’start_time’, 'asc’)->take(5)->get();
}

/**
* Get upcoming appointments count
* @return int
*/
public function getUpcomingAppointmentsCount()
{
return $this->appointmentModel
->where(’date’, '>=’, Carbon::now()->format(’Y-m-d’))
->count();
}

/**
* Get today’s appointments count
* @return int
*/
public function getTodayAppointmentsCount()
{
return $this->appointmentModel
->where(’date’, Carbon::now()->format(’Y-m-d’))
->count();
}

/**
* Get weekly appointments statistics
* @return array
*/
public function getWeeklyAppointmentStats()
{
$startOfWeek = Carbon::now()->startOfWeek()->format(’Y-m-d’);
$endOfWeek = Carbon::now()->endOfWeek()->format(’Y-m-d’);

$results = DB::table(’appointments’)
->select(DB::raw(’DATE(date) as day, COUNT(*) as count’))
->whereBetween(’date’, [$startOfWeek, $endOfWeek])
->groupBy(’day’)
->orderBy(’day’)
->get();

// Initialize stats array with zeros for each day
$stats = [];
$current = Carbon::parse($startOfWeek);
while ($current->format(’Y-m-d’) <= $endOfWeek) { $stats[$current->format(’Y-m-d’)] = 0;
$current->addDay();
}

// Fill in actual counts
foreach ($results as $result) {
$stats[$result->day] = $result->count;
}

return $stats;
}

/**
* Get dentist specific weekly appointment stats
* @param int $dentistId
* @return array
*/
public function getDentistWeeklyAppointmentStats($dentistId)
{
$startOfWeek = Carbon::now()->startOfWeek()->format(’Y-m-d’);
$endOfWeek = Carbon::now()->endOfWeek()->format(’Y-m-d’);

$results = DB::table(’appointments’)
->select(DB::raw(’DATE(date) as day, COUNT(*) as count’))
->where(’dentist_id’, $dentistId)
->whereBetween(’date’, [$startOfWeek, $endOfWeek])
->groupBy(’day’)
->orderBy(’day’)
->get();

// Initialize stats array with zeros for each day
$stats = [];
$current = Carbon::parse($startOfWeek);
while ($current->format(’Y-m-d’) <= $endOfWeek) { $stats[$current->format(’Y-m-d’)] = 0;
$current->addDay();
}

// Fill in actual counts
foreach ($results as $result) {
$stats[$result->day] = $result->count;
}

return $stats;
}

/**
* Get appointment statistics by treatment type
* @return array
*/
public function getAppointmentStatsByTreatment()
{
$results = DB::table(’appointments’)
->join(’pre_defined_data’, 'appointments.treatment_id’, '=’, 'pre_defined_data.id’)
->select(’pre_defined_data.value as treatment’, DB::raw(’COUNT(*) as count’))
->where(’pre_defined_data.category’, 'treatment’)
->groupBy(’pre_defined_data.value’)
->orderBy(’count’, 'desc’)
->take(5)
->get();

$stats = [];
foreach ($results as $result) {
$stats[$result->treatment] = $result->count;
}

return $stats;
}

/**
* Get upcoming appointments
* @param int $limit
* @return mixed
*/
public function getUpcomingAppointments($limit = 5)
{
return $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’date’, '>=’, Carbon::now()->format(’Y-m-d’))
->orderBy(’date’, 'asc’)
->orderBy(’start_time’, 'asc’)
->take($limit)
->get();
}

/**
* Get today’s appointments
* @return mixed
*/
public function getTodayAppointments()
{
return $this->appointmentModel
->with([’contact’, 'dentist’, 'type’, 'status’, 'treatment’])
->where(’date’, Carbon::now()->format(’Y-m-d’))
->orderBy(’start_time’, 'asc’)
->get();
}
}
End File# serbogdanov/project
# app/Modules/Appointments/Controllers/AppointmentController.php
repository = $repository;
$this->service = $service;
$this->preDefinedService = $preDefinedService;
$this->userRepository = $userRepository;
}

/**
* @param Request $request
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function index(Request $request)
{
$sort_by = $request->get(’sort_by’, 'date’);
$sort = $request->get(’sort’, 'desc’);
$search = trim($request->get(’search’));

$per_page = config(’project.per_page’);

$user = Auth::user();

if ($user->hasRole(’doctor’)) {
$appointments = $this->repository->getAppointmentsByDentist($user->id, $sort_by, $sort, $search, $per_page);
} else {
$appointments = $this->repository->getAllAppointments($sort_by, $sort, $search, $per_page);
}

return view(’appointments::index’, compact(’appointments’, 'user’));
}

/**
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function create()
{
$doctors = $this->userRepository->getUsersByRole(’doctor’);
$patients = $this->userRepository->getUsersByRole(’patient’);
$status = $this->preDefinedService->getDataByCategory(’status’);
$treatments = $this->preDefinedService->getDataByCategory(’treatment’);
$types = $this->preDefinedService->getDataByCategory(’appointment_type’);

return view(’appointments::create’, compact(’doctors’, 'patients’, 'status’, 'treatments’, 'types’));
}

/**
* @param CreateAppointmentRequest $request
* @return IlluminateHttpRedirectResponse
*/
public function store(CreateAppointmentRequest $request)
{
try {
$data = $request->all();

$this->service->createAppointment($data);

if ($request->has(’save_and_new’)) {
return redirect()->route(’appointments.create’)
->with(’success’, trans(’appointments::messages.record_was_successfully_created’));
}

return redirect()->route(’appointments.index’)
->with(’success’, trans(’appointments::messages.record_was_successfully_created’));
} catch (Exception $e) {
return back()
->with(’error’, „Cannot save appointment: ” . $e->getMessage());
}
}

/**
* @param $id
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function show($id)
{
$appointment = $this->repository->findDetailById($id);

if (empty($appointment)) {
return redirect()->route(’appointments.index’)
->with(’error’, trans(’appointments::messages.record_not_found’));
}

return view(’appointments::show’, compact(’appointment’));
}

/**
* @param $id
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function edit($id)
{
$appointment = $this->repository->findDetailById($id);

if (empty($appointment)) {
return redirect()->route(’appointments.index’)
->with(’error’, trans(’appointments::messages.record_not_found’));
}

$doctors = $this->userRepository->getUsersByRole(’doctor’);
$patients = $this->userRepository->getUsersByRole(’patient’);
$status = $this->preDefinedService->getDataByCategory(’status’);
$treatments = $this->preDefinedService->getDataByCategory(’treatment’);
$types = $this->preDefinedService->getDataByCategory(’appointment_type’);

return view(’appointments::edit’, compact(’appointment’, 'doctors’, 'patients’, 'status’, 'treatments’, 'types’));
}

/**
* @param UpdateAppointmentRequest $request
* @param $id
* @return IlluminateHttpRedirectResponse
*/
public function update(UpdateAppointmentRequest $request, $id)
{
try {
$data = $request->all();

$this->service->updateAppointment($id, $data);

return redirect()->route(’appointments.index’)
->with(’success’, trans(’appointments::messages.record_was_successfully_updated’));
} catch (Exception $e) {
return back()
->with(’error’, „Cannot update appointment: ” . $e->getMessage());
}
}

/**
* @param $id
* @return IlluminateHttpRedirectResponse
*/
public function destroy($id)
{
try {
$this->repository->delete($id);
return back()
->with(’success’, trans(’appointments::messages.record_was_successfully_deleted’));
} catch (Exception $e) {
return back()
->with(’error’, trans(’appointments::messages.record_was_not_deleted_due_to_system_error’));
}
}

/**
* @param Request $request
* @return IlluminateHttpJsonResponse
*/
public function events(Request $request)
{
try {
$start = $request->get(’start’);
$end = $request->get(’end’);
$doctorId = $request->get(’doctor_id’);

$data = $this->repository->getDentistAppointment($doctorId, $start, $end);

$formattedData = $this->service->formatAppointmentsForCalendar($data);

return response()->json([
'status’ => 'success’,
'data’ => $formattedData
]);
} catch (Exception $e) {
return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @param Request $request
* @return IlluminateHttpJsonResponse
*/
public function quickCreate(Request $request)
{
try {
DB::beginTransaction();

$data = $request->all();
$appointment = $this->service->createAppointment($data);

DB::commit();

return response()->json([
'status’ => 'success’,
'message’ => 'Appointment created successfully’,
'data’ => $appointment
]);
} catch (Exception $e) {
DB::rollBack();

return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @param Request $request
* @param $id
* @return IlluminateHttpJsonResponse
*/
public function updateEvent(Request $request, $id)
{
try {
DB::beginTransaction();

$data = $request->all();
$appointment = $this->service->updateEventTiming($id, $data);

DB::commit();

return response()->json([
'status’ => 'success’,
'message’ => 'Appointment updated successfully’,
'data’ => $appointment
]);
} catch (Exception $e) {
DB::rollBack();

return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @param Request $request
* @param $id
* @return IlluminateHttpJsonResponse
*/
public function quickUpdate(Request $request, $id)
{
try {
DB::beginTransaction();

$data = $request->all();
$appointment = $this->service->updateAppointment($id, $data);

DB::commit();

return response()->json([
'status’ => 'success’,
'message’ => 'Appointment updated successfully’,
'data’ => $appointment
]);
} catch (Exception $e) {
DB::rollBack();

return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @param $id
* @return IlluminateHttpJsonResponse
*/
public function getAppointment($id)
{
try {
$appointment = $this->repository->findDetailById($id);

if (empty($appointment)) {
throw new Exception(’Appointment not found’);
}

return response()->json([
'status’ => 'success’,
'data’ => $appointment
]);
} catch (Exception $e) {
return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function calendar()
{
$doctors = $this->userRepository->getUsersByRole(’doctor’);
$patients = $this->userRepository->getUsersByRole(’patient’);
$status = $this->preDefinedService->getDataByCategory(’status’);
$treatments = $this->preDefinedService->getDataByCategory(’treatment’);
$types = $this->preDefinedService->getDataByCategory(’appointment_type’);

// Get current user
$user = Auth::user();
$currentDoctorId = $user->hasRole(’doctor’) ? $user->id : null;

return view(’appointments::calendar’, compact(
'doctors’,
'patients’,
'status’,
'treatments’,
'types’,
'currentDoctorId’
));
}

/**
* @param Request $request
* @return IlluminateHttpJsonResponse
*/
public function weeklyView(Request $request)
{
try {
$startDate = $request->get(’start_date’, Carbon::now()->startOfWeek()->format(’Y-m-d’));
$endDate = $request->get(’end_date’, Carbon::now()->endOfWeek()->format(’Y-m-d’));

$appointments = $this->repository->getWeeklyAppointments($startDate, $endDate);
$formattedData = $this->service->formatAppointmentsForWeeklyView($appointments);

return response()->json([
'status’ => 'success’,
'data’ => $formattedData,
'period’ => [
'start’ => $startDate,
'end’ => $endDate
]
]);
} catch (Exception $e) {
return response()->json([
'status’ => 'error’,
'message’ => $e->getMessage()
]);
}
}

/**
* @return IlluminateContractsViewFactory|IlluminateViewView
*/
public function weeklySchedule()
{
$doctors = $this->userRepository->getUsersByRole(’doctor’);
$currentWeek = [
'start’ => Carbon::now()->startOfWeek()->format(’Y-m-d’),
'end’ => Carbon::now()->endOfWeek()->format(’Y-m-d’)
];

return view(’appointments::weekly’, compact(’doctors’, 'currentWeek’));
}

/**
* @param Request $request
* @return IlluminateHttpResponse|SymfonyComponentHttpFoundationBinaryFileResponse
*/
public function exportPdf(Request $request)
{
$data = $request->all();
$format = isset($data[’format’]) ? $data[’format’] : 'weekly’;

if ($format === 'weekly’) {
$startDate = $request->get(’start_date’, Carbon::now()->startOfWeek()->format(’Y-m-d’));
$endDate = $request->get(’end_date’, Carbon::now()->endOfWeek()->format(’Y-m-d’));

$appointments = $this->repository->getWeeklyAppointments($startDate, $endDate);
$formattedData = $this->service->formatAppointmentsForWeeklyView($appointments);

$pdf = PDF::loadView(’appointments::exports.weekly_pdf’, [
'appointments’ => $formattedData,
'startDate’ => Carbon::parse($startDate)->format(’d M Y’),
'endDate’ => Carbon::parse($endDate)->format(’d M Y’)
]);

return $pdf->download(’weekly-schedule-’.Carbon::now()->format(’Y-m-d’).’.pdf’);
} else {
// Handle other formats if needed
return response(’Unsupported format’, 400);
}
}
}
End File# app/Modules/Appointments/Services/AppointmentService.php
repository = $repository;
}

/**
* @param array $data
* @return mixed
* @throws Exception
*/
public function createAppointment(array $data)
{
// Validate appointment overlap if needed
if (isset($data[’check_overlap’]) && $data[’check_overlap’]) {
$this->validateAppointmentOverlap($data);
}

// Format data for creation
$appointmentData = $this->prepareAppointmentData($data);

// Create appointment
return $this->repository->create($appointmentData);
}

/**
* @param int $id
* @param array $data
* @return mixed
* @throws Exception
*/
public function updateAppointment($id, array $data)
{
// Validate appointment overlap if needed
if (isset($data[’check_overlap’]) && $data[’check_overlap’]) {
$this->validateAppointmentOverlap($data, $id);
}

// Format data for update
$appointmentData = $this->prepareAppointmentData($data);

// Update appointment
return $this->repository->update($id, $appointmentData);
}

/**
* @param $id
* @param array $data
* @return mixed
* @throws Exception
*/
public function updateEventTiming($id, array $data)
{
$appointment = $this->repository->findById($id);

if (empty($appointment)) {
throw new Exception(’Appointment not found.’);
}

// Extract date and time from start and end
$startDt = Carbon::parse($data[’start’]);
$endDt = Carbon::parse($data[’end’]);

$updateData = [
'date’ => $startDt->format(’Y-m-d’),
'start’ => $startDt->format(’H:i’),
'end’ => $endDt->format(’H:i’),
'duration’ => $endDt->diffInMinutes($startDt)
];

// Check for all_day flag
if (isset($data[’allDay’])) {
$updateData[’all_day’] = $data[’allDay’] ? 1 : 0;
}

return $this->repository->update($id, $updateData);
}

/**
* @param array $data
* @param int|null $excludeId
* @throws Exception
*/
protected function validateAppointmentOverlap(array $data, $excludeId = null)
{
// Implementation of overlap validation logic
// This should check if the new appointment time conflicts with existing ones
// for the same dentist

// Example implementation left for you to complete based on your requirements
}

/**
* @param array $data
* @return array
*/
protected function prepareAppointmentData(array $data)
{
// Clean and format the data for appointment creation/update
$appointmentData = [];

// Map fields from request to appointment model fields
$fieldMappings = [
'contact_id’, 'title’, 'dentist_id’, 'type_id’, 'duration’,
'date’, 'treatment_id’, 'start’, 'end’, 'all_day’,
'notes’, 'color’, 'is_block’, 'is_initial’, 'status_id’
];

foreach ($fieldMappings as $field) {
if (array_key_exists($field, $data)) {
$appointmentData[$field] = $data[$field];
}
}

// Handle special field transformations if needed

return $appointmentData;
}

/**
* Format appointments for calendar view
* @param $appointments
* @return array
*/
public function formatAppointmentsForCalendar($appointments)
{
$formattedAppointments = [];

foreach ($appointments as $appointment) {
$event = [
'id’ => $appointment->id,
'title’ => $appointment->title,
'start’ => Carbon::parse($appointment->date.’ ’.$appointment->start_time)->toIso8601String(),
'end’ => Carbon::parse($appointment->date.’ ’.$appointment->end_time)->toIso8601String(),
'allDay’ => (bool)$appointment->all_day,
'backgroundColor’ => !empty($appointment->color) ? $appointment->color : '#3788d8′,
'borderColor’ => !empty($appointment->color) ? $appointment->color : '#3788d8′,
'extendedProps’ => [
'contact_id’ => $appointment->contact_id,
'dentist_id’ => $appointment->dentist_id,
'type_id’ => $appointment->type_id,
'treatment_id’ => $appointment->treatment_id,
'status_id’ => $appointment->status_id,
'notes’ => $appointment->notes,
'is_block’ => $appointment->is_block,
'is_initial’ => $appointment->is_initial,
'duration’ => $appointment->duration,
]
];

// Add patient information if available
if ($appointment->contact) {
$event[’extendedProps’][’patient’] = [
'id’ => $appointment->contact->id,
'name’ => $appointment->contact->first_name . ’ ’ . $appointment->contact->last_name,
'phone’ => $appointment->contact->phone
];
}

// Add dentist information if available
if ($appointment->dentist) {
$event[’extendedProps’][’dentist’] = [
'id’ => $appointment->dentist->id,
'name’ => $appointment->dentist->first_name . ’ ’ . $appointment->dentist->last_name
];
}

$formattedAppointments[] = $event;
}

return $formattedAppointments;
}

/**
* Format appointments for weekly view
* @param $appointments
* @return array
*/
public function formatAppointmentsForWeeklyView($appointments)
{
$formattedData = [];

// Group appointments by date
$appointmentsByDate = [];
foreach ($appointments as $appointment) {
$date = $appointment->date;
if (!isset($appointmentsByDate[$date])) {
$appointmentsByDate[$date] = [];
}
$appointmentsByDate[$date][] = $appointment;
}

// Sort appointments by time for each date
foreach ($appointmentsByDate as $date => $dateAppointments) {
usort($dateAppointments, function($a, $b) {
return strtotime($a->start_time) – strtotime($b->start_time);
});

$formattedData[$date] = [];

foreach ($dateAppointments as $appointment) {
$formattedData[$date][] = [
'id’ => $appointment->id,
'title’ => $appointment->title,
'start_time’ => $appointment->start_time,
'end_time’ => $appointment->end_time,
'patient_name’ => $appointment->contact ? ($appointment->contact->first_name . ’ ’ . $appointment->contact->last_name) : 'N/A’,
'dentist_name’ => $appointment->dentist ? ($appointment->dentist->first_name . ’ ’ . $appointment->dentist->last_name) : 'N/A’,
'treatment’ => $appointment->treatment ? $appointment->treatment->value : 'N/A’,
'status’ => $appointment->status ? $appointment->status->value : 'N/A’,
'color’ => !empty($appointment->color) ? $appointment->color : '#3788d8′,
];
}
}

return $formattedData;
}
}
End FilegetClientOriginalExtension());
if (!in_array($fileExtension, $allowedTypes)) {
throw new Exception(„File type not allowed. Allowed types: ” . implode(’, ’, $allowedTypes));
}
}

// Validate the file size if maxSize is specified
if ($maxSize > 0 && $file->getSize() > $maxSize * 1024) {
throw new Exception(„File size exceeds maximum allowed size of {$maxSize}KB”);
}

// Generate a unique filename
$filename = Str::random(20) . ’.’ . $file->getClientOriginalExtension();

// Set storage path
$path = $folder ? $folder . '/’ . $filename : $filename;

// Store the file
Storage::disk($disk)->put($path, file_get_contents($file));

// Return the file path
return $path;
}

/**
* Upload and resize image
*
* @param IlluminateHttpUploadedFile $file
* @param string $folder
* @param bool $isPublic
* @param int $width
* @param int $height
* @param bool $maintainAspectRatio
* @return string|null
*/
public function uploadImage($file, $folder = null, $isPublic = true, $width = null, $height = null, $maintainAspectRatio = true)
{
$allowedTypes = [’jpg’, 'jpeg’, 'png’, 'gif’, 'webp’];

// Check if file is valid image
$fileExtension = strtolower($file->getClientOriginalExtension());
if (!in_array($fileExtension, $allowedTypes)) {
throw new Exception(„File type not allowed. Allowed types: ” . implode(’, ’, $allowedTypes));
}

// Generate a unique filename
$filename = Str::random(20) . ’.’ . $fileExtension;

// Set storage path
$path = $folder ? $folder . '/’ . $filename : $filename;

// Create image instance
$image = Image::make($file);

// Resize if dimensions provided
if ($width || $height) {
if ($maintainAspectRatio) {
$image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$image->resize($width, $height);
}
}

// Define the disk to use
$disk = $isPublic ? 'public’ : 'local’;

// Save the image
Storage::disk($disk)->put($path, (string) $image->encode());

// Return the file path
return $path;
}

/**
* Delete file from storage
*
* @param string $path
* @param bool $isPublic
* @return bool
*/
public function deleteFile($path, $isPublic = true)
{
if (!$path) {
return false;
}

$disk = $isPublic ? 'public’ : 'local’;

if (Storage::disk($disk)->exists($path)) {
Storage::disk($disk)->delete($path);
return true;
}

return false;
}

/**
* Get public URL for a file
*
* @param string $path
* @return string|null
*/
public function getFileUrl($path)
{
if (!$path) {
return null;
}

return Storage::url($path);
}
}
x: number, y: number, width: number, height: number, offset: number, borderRadius: number) {
// 3D的框子
context.save();

// 边框设置
context.strokeStyle = 'rgba(66, 77, 116, 0.7)’;
context.lineWidth = 2;

// 绘制立体框的内壁
const innerTop = y + offset;
const innerLeft = x + offset;
const innerWidth = width – offset * 2;
const innerHeight = height – offset * 2;

// 绘制底部矩形(带圆角)
this.drawRoundedRect(context, x, y, width, height, borderRadius);
context.fillStyle = 'rgba(23, 30, 46, 0.7)’;
context.fill();
context.stroke();

// 绘制顶部矩形(带圆角)
this.drawRoundedRect(context, innerLeft, innerTop, innerWidth, innerHeight, borderRadius – offset);
context.fillStyle = 'rgba(47, 57, 86, 0.7)’;
context.fill();
context.stroke();

// 绘制内壁连接线
context.beginPath();
context.moveTo(x + borderRadius, y);
context.lineTo(innerLeft + borderRadius – offset, innerTop);
context.stroke();

context.beginPath();
context.moveTo(x + width – borderRadius, y);
context.lineTo(innerLeft + innerWidth – borderRadius + offset, innerTop);
context.stroke();

context.beginPath();
context.moveTo(x + width, y + borderRadius);
context.lineTo(innerLeft + innerWidth, innerTop + borderRadius – offset);
context.stroke();

context.beginPath();
context.moveTo(x + width, y + height – borderRadius);
context.lineTo(innerLeft + innerWidth, innerTop + innerHeight – borderRadius + offset);
context.stroke();

context.beginPath();
context.moveTo(x + width – borderRadius, y + height);
context.lineTo(innerLeft + innerWidth – borderRadius + offset, innerTop + innerHeight);
context.stroke();

context.beginPath();
context.moveTo(x + borderRadius, y + height);
context.lineTo(innerLeft + borderRadius – offset, innerTop + innerHeight);
context.stroke();

context.beginPath();
context.moveTo(x, y + height – borderRadius);
context.lineTo(innerLeft, innerTop + innerHeight – borderRadius + offset);
context.stroke();

context.beginPath();
context.moveTo(x, y + borderRadius);
context.lineTo(innerLeft, innerTop + borderRadius – offset);
context.stroke();

context.restore();
}

private drawRoundedRect(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width – radius, y);
context.quadraticCurveTo(x + width, y, x + width, y + radius);
context.lineTo(x + width, y + height – radius);
context.quadraticCurveTo(x + width, y + height, x + width – radius, y + height);
context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height – radius);
context.lineTo(x, y + radius);
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
}

//绘制装饰性箭头
private drawDecorativeArrow(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) {
const centerX = x + width / 2;
const centerY = y + height / 2;
const arrowSize = Math.min(width, height) * 0.3;

context.save();
context.strokeStyle = 'rgba(66, 133, 244, 0.6)’;
context.lineWidth = 2;
context.lineCap = ’round’;
context.lineJoin = ’round’;

// 绘制四个方向的箭头
// 上箭头
context.beginPath();
context.moveTo(centerX, centerY – arrowSize);
context.lineTo(centerX, centerY – arrowSize / 3);
context.moveTo(centerX – arrowSize / 4, centerY – arrowSize * 0.6);
context.lineTo(centerX, centerY – arrowSize);
context.lineTo(centerX + arrowSize / 4, centerY – arrowSize * 0.6);
context.stroke();

// 右箭头
context.beginPath();
context.moveTo(centerX + arrowSize, centerY);
context.lineTo(centerX + arrowSize / 3, centerY);
context.moveTo(centerX + arrowSize * 0.6, centerY – arrowSize / 4);
context.lineTo(centerX + arrowSize, centerY);
context.lineTo(centerX + arrowSize * 0.6, centerY + arrowSize / 4);
context.stroke();

// 下箭头
context.beginPath();
context.moveTo(centerX, centerY + arrowSize);
context.lineTo(centerX, centerY + arrowSize / 3);
context.moveTo(centerX – arrowSize / 4, centerY + arrowSize * 0.6);
context.lineTo(centerX, centerY + arrowSize);
context.lineTo(centerX + arrowSize / 4, centerY + arrowSize * 0.6);
context.stroke();

// 左箭头
context.beginPath();
context.moveTo(centerX – arrowSize, centerY);
context.lineTo(centerX – arrowSize / 3, centerY);
context.moveTo(centerX – arrowSize * 0.6, centerY – arrowSize / 4);
context.lineTo(centerX – arrowSize, centerY);
context.lineTo(centerX – arrowSize * 0.6, centerY + arrowSize / 4);
context.stroke();

context.restore();
}

//绘制内部装饰线
private drawDecorativeLines(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) {
context.save();
context.strokeStyle = 'rgba(66, 133, 244, 0.3)’;
context.lineWidth = 1;

// 绘制网格状的装饰线
const gridSize = Math.min(width, height) / 8;

// 水平线
for (let i = 1; i < height / gridSize; i++) { context.beginPath(); context.moveTo(x, y + i * gridSize); context.lineTo(x + width, y + i * gridSize); context.stroke(); } // 垂直线 for (let i = 1; i < width / gridSize; i++) { context.beginPath(); context.moveTo(x + i * gridSize, y); context.lineTo(x + i * gridSize, y + height); context.stroke(); } context.restore(); } //绘制高科技圆形装饰 private drawTechCircle(context: CanvasRenderingContext2D, x: number, y: number, size: number) { context.save(); const centerX = x + size / 2; const centerY = y + size / 2; const radius = size / 2; // 绘制外圆环 context.beginPath(); context.arc(centerX, centerY, radius, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.4)'; context.lineWidth = 2; context.stroke(); // 绘制内圆环 context.beginPath(); context.arc(centerX, centerY, radius * 0.8, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.3)'; context.lineWidth = 1; context.stroke(); // 绘制十字标记 context.beginPath(); context.moveTo(centerX - radius * 0.6, centerY); context.lineTo(centerX + radius * 0.6, centerY); context.moveTo(centerX, centerY - radius * 0.6); context.lineTo(centerX, centerY + radius * 0.6); context.strokeStyle = 'rgba(66, 133, 244, 0.5)'; context.lineWidth = 1; context.stroke(); // 绘制四个点 const pointRadius = radius * 0.05; const pointDistance = radius * 0.7; context.fillStyle = 'rgba(66, 133, 244, 0.7)'; context.beginPath(); context.arc(centerX + pointDistance, centerY, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX - pointDistance, centerY, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX, centerY + pointDistance, pointRadius, 0, Math.PI * 2); context.fill(); context.beginPath(); context.arc(centerX, centerY - pointDistance, pointRadius, 0, Math.PI * 2); context.fill(); // 绘制弧线装饰 for (let i = 0; i < 4; i++) { const startAngle = i * Math.PI / 2 + Math.PI / 8; const endAngle = startAngle + Math.PI / 4; context.beginPath(); context.arc(centerX, centerY, radius * 0.9, startAngle, endAngle); context.strokeStyle = 'rgba(66, 133, 244, 0.6)'; context.lineWidth = 1.5; context.stroke(); } context.restore(); } /** * 绘制旋转中的装饰性圆 */ private drawRotatingCircles(context: CanvasRenderingContext2D, x: number, y: number, size: number, time: number) { const centerX = x + size / 2; const centerY = y + size / 2; context.save(); // 计算旋转角度 const rotation1 = (time / 2000) % (Math.PI * 2); const rotation2 = (time / 3000) % (Math.PI * 2); // 绘制外圆 context.beginPath(); context.arc(centerX, centerY, size * 0.45, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.2)'; context.lineWidth = 1; context.stroke(); // 绘制旋转的分割线 - 外圈 context.translate(centerX, centerY); context.rotate(rotation1); context.translate(-centerX, -centerY); for (let i = 0; i < 12; i++) { context.save(); context.translate(centerX, centerY); context.rotate(i * Math.PI / 6); context.beginPath(); context.moveTo(size * 0.35, 0); context.lineTo(size * 0.45, 0); context.strokeStyle = 'rgba(66, 133, 244, 0.6)'; context.lineWidth = 1; context.stroke(); context.restore(); } // 绘制内圆 context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); context.arc(centerX, centerY, size * 0.3, 0, Math.PI * 2); context.strokeStyle = 'rgba(66, 133, 244, 0.3)'; context.lineWidth = 1; context.stroke(); // 绘制旋转的分割线 - 内圈 context.translate(centerX, centerY); context.rotate(rotation2); context.translate(-centerX, -centerY); for (let i = 0; i < 8; i++) { context.save(); context.translate(centerX, centerY); context.rotate(i * Math.PI / 4); context.beginPath(); context.moveTo(size * 0.2, 0); context.lineTo(size * 0.3, 0); context.strokeStyle = 'rgba(66, 133, 244, 0.5)'; context.lineWidth = 1; context.stroke(); context.restore(); } // 绘制中心小圆 context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); context.arc(centerX, centerY, size * 0.05, 0, Math.PI * 2); context.fillStyle = 'rgba(66, 133, 244, 0.6)'; context.fill(); context.restore(); } /** * 绘制左下角的指示标记 */ private drawCornerMarker(context: CanvasRenderingContext2D, x: number, y: number, size: number) { const cornerX = x; const cornerY = y + size; context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.fillStyle = 'rgba(66, 133, 244, 0.1)'; context.lineWidth = 1; // 绘制L形标记 context.beginPath(); context.moveTo(cornerX, cornerY - size * 0.3); context.lineTo(cornerX, cornerY); context.lineTo(cornerX + size * 0.3, cornerY); context.stroke(); // 绘制小方块 context.beginPath(); context.rect(cornerX + size * 0.15 - size * 0.05, cornerY - size * 0.15 - size * 0.05, size * 0.1, size * 0.1); context.fill(); context.stroke(); context.restore(); } /** * 绘制右上角的指示标记 */ private drawTopRightMarker(context: CanvasRenderingContext2D, x: number, y: number, width: number, size: number) { const cornerX = x + width; const cornerY = y; context.save(); context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.fillStyle = 'rgba(66, 133, 244, 0.1)'; context.lineWidth = 1; // 绘制反L形标记 context.beginPath(); context.moveTo(cornerX, cornerY + size * 0.3); context.lineTo(cornerX, cornerY); context.lineTo(cornerX - size * 0.3, cornerY); context.stroke(); // 绘制小方块 context.beginPath(); context.rect(cornerX - size * 0.15 - size * 0.05, cornerY + size * 0.15 - size * 0.05, size * 0.1, size * 0.1); context.fill(); context.stroke(); context.restore(); } /** * 绘制数据流动的动画效果 */ private drawDataFlow(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, time: number) { context.save(); // 数据流的参数 const flowSpeed = 1000; // 流动速度的基准值(毫秒) const flowCount = 3; // 数据流的数量 const flowLength = Math.min(width, height) * 0.15; // 数据流的长度 const flowWidth = 2; // 数据流的宽度 // 设置数据流样式 context.strokeStyle = 'rgba(66, 133, 244, 0.7)'; context.lineWidth = flowWidth; context.lineCap = 'round'; // 绘制右边的数据流 for (let i = 0; i < flowCount; i++) { const offset = (time + i * (flowSpeed / flowCount)) % flowSpeed / flowSpeed; const startY = y + height * offset; if (startY < y + height) { // 计算可见长度 let visibleLength = flowLength; if (startY + flowLength > y + height) {
visibleLength = y + height – startY;
}

context.beginPath();
context.moveTo(x + width, startY);
context.lineTo(x + width, startY + visibleLength);
context.stroke();
}
}

// 绘制下边的数据流
for (let i = 0; i < flowCount; i++) { const offset = (time + i * (flowSpeed / flowCount) + flowSpeed / 2) % flowSpeed / flowSpeed; const startX = x + width * offset; if (startX < x + width) { // 计算可见长度 let visibleLength = flowLength; if (startX + flowLength > x + width) {
visibleLength = x + width – startX;
}

context.beginPath();
context.moveTo(startX, y + height);
context.lineTo(startX + visibleLength, y + height);
context.stroke();
}
}

context.restore();
}

/**
* 绘制高科技左上角标记
*/
private drawTopLeftMarker(context: CanvasRenderingContext2D, x: number, y: number, size: number) {
context.save();

// 设置样式
context.strokeStyle = 'rgba(66, 133, 244, 0.7)’;
context.lineWidth = 1;

// 绘制角标记
context.beginPath();
context.moveTo(x, y + size * 0.3);
context.lineTo(x, y);
context.lineTo(x + size * 0.3, y);
context.stroke();

// 绘制装饰点
context.fillStyle = 'rgba(66, 133, 244, 0.8)’;
context.beginPath();
context.arc(x + size * 0.15, y + size * 0.15, size * 0.03, 0, Math.PI * 2);
context.fill();

context.restore();
}

/**
* 绘制高科技右下角标记
*/
private drawBottomRightMarker(context: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, size: number) {
const cornerX = x + width;
const cornerY = y + height;

context.save();

// 设置样式
context.strokeStyle = 'rgba(66, 133, 244, 0.7)’;
context.lineWidth = 1;

// 绘制角标记
context.beginPath();
context.moveTo(cornerX, cornerY – size * 0.3);
context.lineTo(cornerX, cornerY);
context.lineTo(cornerX – size * 0.3, cornerY);
context.stroke();

// 绘制装饰点
context.fillStyle = 'rgba(66, 133, 244, 0.8)’;
context.beginPath();
context.arc(cornerX – size * 0.15, cornerY – size * 0.15, size * 0.03, 0, Math.PI * 2);
context.fill();

context.restore();
}

/**
* 绘制高科技装饰性刻度
*/
private drawTechScale(context: CanvasRenderingContext2D, x: number, y: number, width: number, time: number) {
context.save();

// 刻度参数
const scaleHeight = 5;
const scaleCount = 20;
const scaleSpacing = width / scaleCount;
const animOffset = (time / 1000) % 1 * scaleSpacing;

// 设置样式
context.strokeStyle = 'rgba(66, 133, 244, 0.5)’;
context.lineWidth = 1;

// 绘制刻度
for (let i = 0; i < scaleCount + 1; i++) { const scaleX = x + (i * scaleSpacing - animOffset) % width; const height = (i % 5 === 0) ? scaleHeight * 1.5 : scaleHeight; context.beginPath(); context.moveTo(scaleX, y); context.lineTo(scaleX, y + height); context.stroke(); } context.restore(); } } End File# src/app/components/list-loading-table/list-loading-controller.service.ts import { Injectable } from '@angular/core'; import { debounceTime, distinctUntilChanged, filter, map, startWith, Subject, Subscription, switchMap, tap, timer } from 'rxjs'; /** * 列表加载控制器服务 * 用于管理列表的加载状态、搜索和刷新 */ @Injectable({ providedIn: 'root' }) export class ListLoadingController { private refreshSubject = new Subject();
private searchSubject = new Subject();
private loadingState = false;
private searchSubscription: Subscription | null = null;
private refreshSubscription: Subscription | null = null;

// 是否正在加载
get isLoading(): boolean {
return this.loadingState;
}

// 设置加载状态
set isLoading(value: boolean) {
this.loadingState = value;
}

/**
* 设置搜索处理器
* @param searchHandler 搜索处理函数
* @param debounceTimeMs 去抖时间(毫秒)
*/
setupSearch(searchHandler: (searchText: string) => void, debounceTimeMs: number = 300) {
// 取消之前的订阅
if (this.searchSubscription) {
this.searchSubscription.unsubscribe();
}

// 创建新的订阅,处理搜索操作
this.searchSubscription = this.searchSubject.pipe(
debounceTime(debounceTimeMs),
distinctUntilChanged(),
tap(() => {
this.isLoading = true; // 搜索开始时设置loading状态
}),
switchMap(searchText => {
return timer(500).pipe(
map(() => searchText)
);
}),
).subscribe(searchText => {
searchHandler(searchText);
this.isLoading = false; // 搜索完成后关闭loading状态
});
}

/**
* 设置刷新处理器
* @param refreshHandler 刷新处理函数
*/
setupRefresh(refreshHandler: () => void) {
// 取消之前的订阅
if (this.refreshSubscription) {
this.refreshSubscription.unsubscribe();
}

// 创建新的订阅,处理刷新操作
this.refreshSubscription = this.refreshSubject.pipe(
tap(() => {
this.isLoading = true; // 刷新开始时设置loading状态
}),
switchMap(() => {
return timer(500).pipe(
map(() => {})
);
}),
).subscribe(() => {
refreshHandler();
this.isLoading = false; // 刷新完成后关闭loading状态
});
}

/**
* 触发搜索
* @param searchText 搜索文本
*/
search(searchText: string) {
this.searchSubject.next(searchText);
}

/**
* 触发刷新
*/
refresh() {
this.refreshSubject.next();
}

/**
* 清理订阅,避免内存泄漏
*/
destroy() {
if (this.searchSubscription) {
this.searchSubscription.unsubscribe();
this.searchSubscription = null;
}
if (this.refreshSubscription) {
this.refreshSubscription.unsubscribe();
this.refreshSubscription = null;
}
}
}
End Fileimport { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core’;
import { CommonModule } from '@angular/common’;
import { SciFiHUDRenderer } from ’./sci-fi-hud-renderer’;

// 定义HUD模式枚举
export enum HUDMode {
NORMAL = 'normal’, // 正常模式
ALERT = 'alert’, // 警告模式
SUCCESS = 'success’, // 成功模式
INACTIVE = 'inactive’ // 不活跃模式
}

// HUD显示数据接口
export interface HUDData {
title?: string; // 标题
data?: any; // 数据
targetValue?: number; // 目标值
currentValue?: number; // 当前值
percentage?: number; // 百分比
isLocked?: boolean; // 是否锁定
status?: string; // 状态文本
mode?: HUDMode; // HUD模式
subtitle?: string; // 副标题
}

@Component({
selector: 'app-sci-fi-hud’,
standalone: true,
imports: [CommonModule],
templateUrl: ’./sci-fi-hud.component.html’,
styleUrl: ’./sci-fi-hud.component.scss’
})
export class SciFiHudComponent implements OnInit {
@ViewChild(’hudCanvas’, { static: true }) hudCanvas!: ElementRef;
@Input() width: number = 300;
@Input() height: number = 200;
@Input() data: HUDData = {};

private renderer!: SciFiHUDRenderer;
private animationId: number = 0;

constructor() {}

ngOnInit(): void {
this.initializeCanvas();
this.startRendering();
}

ngOnChanges(): void {
if (this.renderer) {
this.renderer.setData(this.data);
}
}

ngOnDestroy(): void {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
}

private initializeCanvas(): void {
const canvas = this.hudCanvas.nativeElement;
canvas.width = this.width;
canvas.height = this.height;

const ctx = canvas.getContext(’2d’);
if (!ctx) {
console.error(’Cannot get 2D context from canvas’);
return;
}

// 初始化渲染器
this.renderer = new SciFiHUDRenderer(ctx, this.width, this.height);
this.renderer.setData(this.data);
}

private startRendering(): void {
const renderFrame = () => {
if (!this.renderer) return;

this.renderer.render();
this.animationId = requestAnimationFrame(renderFrame);
};

renderFrame();
}

/**
* 更新数据并刷新HUD显示
* @param data 要更新的HUD数据
*/
public updateData(data: Partial): void {
this.data = { …this.data, …data };
if (this.renderer) {
this.renderer.setData(this.data);
}
}

/**
* 设置HUD模式
* @param mode HUD模式
*/
public setMode(mode: HUDMode): void {
this.updateData({ mode });
}

/**
* 设置进度值
* @param currentValue 当前值
* @param targetValue 目标值(可选)
*/
public setProgress(currentValue: number, targetValue?: number): void {
const updateData: Partial = { currentValue };

if (targetValue !== undefined) {
updateData.targetValue = targetValue;
}

if (updateData.targetValue) {
updateData.percentage = Math.min(100, Math.max(0,
(updateData.currentValue || 0) / updateData.targetValue * 100));
}

this.updateData(updateData);
}

/**
* 设置锁定状态
* @param isLocked 是否锁定
*/
public setLocked(isLocked: boolean): void {
this.updateData({ isLocked });
}

/**
* 设置状态文本
* @param status 状态文本
*/
public setStatus(status: string): void {
this.updateData({ status });
}
}
End Fileimport { Component, EventEmitter, Input, OnInit, Output } from '@angular/core’;
import { CommonModule } from '@angular/common’;
import { FormsModule } from '@angular/forms’;
import { ListLoadingController } from ’../list-loading-controller.service’;

/**
* 表格分页事件接口
*/
export interface PageEvent {
pageIndex: number; // 当前页码索引(从0开始)
pageSize: number; // 每页显示条数
length: number; // 总条数
}

/**
* 表格列配置接口
*/
export interface TableColumn {
field: string; // 字段名
header: string; // 显示的表头
sortable?: boolean; // 是否可排序
class?: string; // 列样式类
type?: 'text’ | 'number’ | 'date’ | 'boolean’ | 'custom’; // 列数据类型
format?: (value: any) => any; // 自定义格式化函数
width?: string; // 列宽度
align?: 'left’ | 'center’ | 'right’; // 对齐方式
visible?: boolean; // 是否可见,默认为true
}

/**
* 加载中表格组件
* 一个现代化、高性能的数据表格组件,支持排序、分页和加载状态
*/
@Component({
selector: 'app-list-loading-table’,
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: ’./list-loading-table.component.html’,
styleUrl: ’./list-loading-table.component.scss’
})
export class ListLoadingTableComponent implements OnInit {
@Input() data: any[] = []; // 表格数据
@Input() columns: TableColumn[] = []; // 列配置
@Input() enablePagination: boolean = true; // 是否启用分页
@Input() pageSize: number = 10; // 每页数据量
@Input() pageSizeOptions: number[] = [5, 10, 25, 50]; // 可选每页数量
@Input() totalRecords: number = 0; // 总记录数
@Input() currentPageIndex: number = 0; // 当前页码(从0开始)
@Input() showSearch: boolean = true; // 是否显示搜索
@Input() searchPlaceholder: string = '搜索…’; // 搜索框提示文本
@Input() noDataMessage: string = '没有数据’; // 无数据时的提示信息
@Input() loading: boolean = false; // 加载状态
@Input() showRefresh: boolean = true; // 是否显示刷新按钮
@Input() stripped: boolean = true; // 是否使用条纹背景
@Input() bordered: boolean = false; // 是否显示边框
@Input() hoverable: boolean = true; // 行是否有悬停效果
@Input() emptyStateImage?: string; // 空状态图片
@Input() expandableRows: boolean = false; // 是否允许行展开
@Input() selectable: boolean = false; // 是否可选择行

@Output() pageChange = new EventEmitter(); // 分页变更事件
@Output() sortChange = new EventEmitter<{field: string, direction: 'asc' | 'desc'}>(); // 排序变更事件
@Output() searchChange = new EventEmitter(); // 搜索变更事件
@Output() refreshRequest = new EventEmitter(); // 刷新请求事件
@Output() rowClick = new EventEmitter(); // 行点击事件
@Output() rowSelect = new EventEmitter(); // 行选择事件

searchText: string = ”; // 搜索文本
selectedRows: Set = new Set(); // 已选择的行
expandedRows: Set = new Set(); // 已展开的行
sortField: string | null = null; // 当前排序字段
sortDirection: 'asc’ | 'desc’ = 'asc’; // 当前排序方向

get visibleColumns(): TableColumn[] {
return this.columns.filter(col => col.visible !== false);
}

constructor(public loadingController: ListLoadingController) {}

ngOnInit() {
// 设置搜索处理
this.loadingController.setupSearch((searchText) => {
this.searchChange.emit(searchText);
});

// 设置刷新处理
this.loadingController.setupRefresh(() => {
this.refreshRequest.emit();
});
}

/**
* 处理搜索变更
*/
onSearchChange() {
this.loadingController.search(this.searchText);
}

/**
* 处理刷新请求
*/
onRefreshClick() {
this.loadingController.refresh();
}

/**
* 处理排序变更
* @param field 排序字段
*/
onSort(field: string) {
// 查找列配置,检查是否可排序
const column = this.columns.find(col => col.field === field);
if (!column || column.sortable === false) return;

// 如果点击的是当前排序列,则切换排序方向
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc’ ? 'desc’ : 'asc’;
} else {
// 否则使用新的排序列,并设置默认排序方向为升序
this.sortField = field;
this.sortDirection = 'asc’;
}

this.sortChange.emit({ field, direction: this.sortDirection });
}

/**
* 处理页大小变更
* @param size 新的页大小
*/
onPageSizeChange(size: number) {
this.pageSize = size;
this.currentPageIndex = 0; // 重置页码
this.emitPageEvent();
}

/**
* 处理页面变更
* @param offset 页码偏移量(1 表示下一页,-1 表示上一页)
*/
onPageChange(offset: number) {
const newPageIndex = this.currentPageIndex + offset;
if (newPageIndex >= 0 && newPageIndex * this.pageSize < this.totalRecords) { this.currentPageIndex = newPageIndex; this.emitPageEvent(); } } /** * 发送分页事件 */ private emitPageEvent() { this.pageChange.emit({ pageIndex: this.currentPageIndex, pageSize: this.pageSize, length: this.totalRecords }); } /** * 处理行点击 * @param row 被点击的行数据 * @param event 点击事件 */ onRowClick(row: any, event: MouseEvent) { this.rowClick.emit(row); // 如果启用了行展开功能,则切换展开状态 if (this.expandableRows) { if (this.expandedRows.has(row)) { this.expandedRows.delete(row); } else { this.expandedRows.add(row); } } } /** * 处理行选择变更 * @param row 行数据 * @param event 变更事件 */ onRowSelectChange(row: any, event: Event) { event.stopPropagation(); // 防止触发行点击事件 const checkbox = event.target as HTMLInputElement; if (checkbox.checked) { this.selectedRows.add(row); } else { this.selectedRows.delete(row); } this.rowSelect.emit(Array.from(this.selectedRows)); } /** * 处理全选/取消全选 * @param event 变更事件 */ onSelectAll(event: Event) { const checkbox = event.target as HTMLInputElement; if (checkbox.checked) { this.data.forEach(row => this.selectedRows.add(row));
} else {
this.selectedRows.clear();
}

this.rowSelect.emit(Array.from(this.selectedRows));
}

/**
* 检查行是否被选择
* @param row 行数据
*/
isSelected(row: any): boolean {
return this.selectedRows.has(row);
}

/**
* 获取全选状态
* 如果所有行都被选中,返回true;
* 如果部分行被选中,返回null(indeterminate状态);
* 如果没有行被选中,返回false。
*/
get selectAllState(): boolean | null {
if (this.data.length === 0) return false;
if (this.selectedRows.size === 0) return false;
if (this.selectedRows.size === this.data.length) return true;
return null; // indeterminate state
}

/**
* 检查行是否展开
* @param row 行数据
*/
isExpanded(row: any): boolean {
return this.expandedRows.has(row);
}

/**
* 格式化单元格值
* @param column 列配置
* @param row 行数据
*/
getCellValue(column: TableColumn, row: any): any {
const value = row[column.field];

// 使用列格式化函数(如果有)
if (column.format) {
return column.format(value);
}

// 根据列类型进行格式化
switch (column.type) {
case 'date’:
return value ? new Date(value).toLocaleDateString() : ”;
case 'boolean’:
return value ? '是’ : '否’;
default:
return value;
}
}

/**
* 获取当前分页信息文本
*/
get paginationInfo(): string {
const start = this.currentPageIndex * this.pageSize + 1;
const end = Math.min(start + this.pageSize – 1, this.totalRecords);
return `${start}-${end} / ${this.totalRecords}`;
}

/**
* 获取列对齐样式类
* @param column 列配置
*/
getColumnAlignClass(column: TableColumn): string {
return column.align ? `text-${column.align}` : ”;
}

/**
* 获取当前页数据
*/
get pageData(): any[] {
if (!this.enablePagination) return this.data;

const start = this.currentPageIndex * this.pageSize;
return this.data.slice(start, start + this.pageSize);
}

/**
* 重置所有筛选和分页状态
*/
reset() {
this.searchText = ”;
this.currentPageIndex = 0;
this.sortField = null;
this.sortDirection = 'asc’;
this.selectedRows.clear();
this.expandedRows.clear();

this.searchChange.emit(”);
this.emitPageEvent();
}
}
End File# serbogdanov/project
/**
* 科幻风格HUD (Heads-Up Display)渲染器
* 负责绘制各种科幻元素和动画效果
*/
export class SciFiHUDRenderer {
private context: CanvasRenderingContext2D;
private width: number;
private height: number;
private startTime: number;
private data: any = {};
private animationValues: any = {
scale: 0,
opacity: 0,
progress: 0,
rotation: 0
};

// 颜色方案
private colorSchemes = {
normal: {
primary: 'rgba(0, 149, 255, 0.8)’,
secondary: 'rgba(0, 149, 255, 0.4)’,
background: 'rgba(10, 20, 40, 0.3)’,
text: 'rgba(150, 210, 255, 0.9)’,
highlight: 'rgba(0, 200, 255, 1)’
},
alert: {
primary: 'rgba(255, 60, 50, 0.8)’,
secondary: 'rgba(255, 60, 50, 0.4)’,
background: 'rgba(40, 15, 15, 0.3)’,
text: 'rgba(255, 180, 170, 0.9)’,
highlight: 'rgba(255, 100, 70, 1)’
},
success: {
primary: 'rgba(50, 200, 100, 0.8)’,
secondary: 'rgba(50, 200, 100, 0.4)’,
background: 'rgba(15, 30, 20, 0.3)’,
text: 'rgba(170, 255, 200, 0.9)’,
highlight: 'rgba(80, 240, 120, 1)’
},
inactive: {
primary: 'rgba(150, 150, 150, 0.5)’,
secondary: 'rgba(100, 100, 100, 0.3)’,
background: 'rgba(20, 20, 20, 0.3)’,
text: 'rgba(200, 200, 200, 0.7)’,
highlight: 'rgba(180, 180, 180, 0.8)’
}
};

// 当前颜色方案
private currentColorScheme: any;

constructor(context: CanvasRenderingContext2D, width: number, height: number) {
this.context = context;
this.width = width;
this.height = height;
this.startTime = Date.now();
this.currentColorScheme = this.colorSchemes.normal;
}

/**
* 设置HUD数据
* @param data HUD数据对象
*/
setData(data: any): void {
this.data = data;

// 根据模式更新颜色方案
if (data.mode) {
this.currentColorScheme = this.colorSchemes[data.mode] || this.colorSchemes.normal;
}
}

/**
* 渲染HUD帧
*/
render(): void {
const ctx = this.context;
const currentTime = Date.now();
const elapsed = currentTime – this.startTime;

// 清除画布
ctx.clearRect(0, 0, this.width, this.height);

// 更新动画值
this.updateAnimationValues(elapsed);

// 绘制背景
this.drawBackground();

// 绘制边框和装饰元素
this.drawBorder();
this.drawDecorativeElements(elapsed);

// 绘制内容
this.drawContent(elapsed);
}

/**
* 更新动画值
* @param elapsed 经过的时间(毫秒)
*/
private updateAnimationValues(elapsed: number): void {
// 平滑的进入动画
this.animationValues.scale = Math.min(1, elapsed / 500);
this.animationValues.opacity = Math.min(1, elapsed / 700);

// 进度动画(如果有目标值和当前值)
if (this.data.percentage !== undefined) {
const targetProgress = this.data.percentage / 100;
this.animationValues.progress += (targetProgress – this.animationValues.progress) * 0.1;
}

// 旋转动画
this.animationValues.rotation = (elapsed / 3000) % (Math.PI * 2);
}

/**
* 绘制HUD背景
*/
private drawBackground(): void {
const ctx = this.context;
const { width, height } = this;

// 半透明背景
ctx.fillStyle = this.currentColorScheme.background;
ctx.fillRect(0, 0, width, height);

// 网格效果
ctx.strokeStyle = this.currentColorScheme.secondary;
ctx.lineWidth = 0.5;
ctx.globalAlpha = 0.3;

const gridSize = 20;
for (let x = 0; x <= width; x += gridSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for (let y = 0; y <= height; y += gridSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } ctx.globalAlpha = 1; } /** * 绘制HUD边框 */ private drawBorder(): void { const ctx = this.context; const { width, height } = this; ctx.strokeStyle = this.currentColorScheme.primary; ctx.lineWidth = 2; // 外边框 ctx.beginPath(); ctx.rect(2, 2, width - 4, height - 4); ctx.stroke(); // 绘制角落装饰 const cornerSize = 15; // 左上角 ctx.beginPath(); ctx.moveTo(2, cornerSize); ctx.lineTo(2, 2); ctx.lineTo(cornerSize, 2); ctx.stroke(); // 右上角 ctx.beginPath(); ctx.moveTo(width - cornerSize, 2); ctx.lineTo(width - 2, 2); ctx.lineTo(width - 2, cornerSize); ctx.stroke(); // 右下角 ctx.beginPath(); ctx.moveTo(width - 2, height - cornerSize); ctx.lineTo(width - 2, height - 2); ctx.lineTo(width - cornerSize, height - 2); ctx.stroke(); // 左下角 ctx.beginPath(); ctx.moveTo(cornerSize, height - 2); ctx.lineTo(2, height - 2); ctx.lineTo(2, height - cornerSize); ctx.stroke(); } /** * 绘制装饰性元素 * @param elapsed 经过的时间(毫秒) */ private drawDecorativeElements(elapsed: number): void { const ctx = this.context; const { width, height } = this; // 标题栏装饰 ctx.fillStyle = this.currentColorScheme.primary; ctx.fillRect(10, 10, width - 20, 4); // 底部装饰线 ctx.fillRect(10, height - 14, width - 20, 2); // 动态旋转的装饰圆圈 const rotation = elapsed / 2000; const circleRadius = 8; ctx.save(); ctx.translate(20, 20); ctx.rotate(rotation); ctx.beginPath(); ctx.arc(0, 0, circleRadius, 0, Math.PI * 2); ctx.strokeStyle = this.currentColorScheme.highlight; ctx.lineWidth = 1.5; ctx.stroke(); // 内部十字 ctx.beginPath(); ctx.moveTo(-circleRadius, 0); ctx.lineTo(circleRadius, 0); ctx.moveTo(0, -circleRadius); ctx.lineTo(0, circleRadius); ctx.strokeStyle = this.currentColorScheme.primary; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); // 另一个旋转圆圈(在右上角) ctx.save(); ctx.translate(width - 20, 20); ctx.rotate(-rotation); ctx.beginPath(); ctx.arc(0, 0, circleRadius, 0, Math.PI * 2); ctx.strokeStyle = this.currentColorScheme.highlight; ctx.lineWidth = 1.5; ctx.stroke(); // 内部十字 ctx.beginPath(); ctx.moveTo(-circleRadius, 0); ctx.lineTo(circleRadius, 0); ctx.moveTo(0, -circleRadius); ctx.lineTo(0, circleRadius); ctx.strokeStyle = this.currentColorScheme.primary; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } /** * 绘制HUD内容 * @param elapsed 经过的时间(毫秒) */ private drawContent(elapsed: number): void { const ctx = this.context; const { width, height } = this; // 设置文本样式 ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = this.currentColorScheme.text; // 绘制标题 if (this.data.title) { ctx.font = 'bold 16px sans-serif'; ctx.fillText(this.data.title, width / 2, 25); } // 绘制副标题 if (this.data.subtitle) { ctx.font = '12px sans-serif'; ctx.fillText(this.data.subtitle, width / 2, 45); } // 绘制进度条(如果有百分比数据) if (this.data.percentage !== undefined) { this.drawProgressBar(width / 2 - 60, height / 2 - 10, 120, 20, this.animationValues.progress); // 绘制百分比文本 ctx.font = 'bold 14px sans-serif'; ctx.fillText(`${Math.round(this.data.percentage)}%`, width / 2, height / 2); // 绘制当前值/目标值(如果有) if (this.data.currentValue !== undefined && this.data.targetValue !== undefined) { ctx.font = '12px sans-serif'; ctx.fillText(`${this.data.currentValue}/${this.data.targetValue}`, width / 2, height / 2 + 25); } } // 绘制状态文本 if (this.data.status) { ctx.font = '12px sans-serif'; ctx.fillText(this.data.status, width / 2, height - 25); } // 绘制锁定状态(如果适用) if (this.data.isLocked) { this.drawLockIcon(width - 30, height - 30, 16); } } /** * 绘制进度条 * @param x X坐标 * @param y Y坐标 * @param width 宽度 * @param height 高度 * @param progress 进度值(0-1) */ private drawProgressBar(x: number, y: number, width: number, height: number, progress: number): void { const ctx = this.context; // 进度条背景 ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(x, y, width, height); // 进度条边框 ctx.strokeStyle = this.currentColorScheme.secondary; ctx.lineWidth = 1; ctx.strokeRect(x, y, width, height); // 进度部分 const progressWidth = width * progress; ctx.fillStyle = this.currentColorScheme.primary; ctx.fillRect(x, y, progressWidth, height); // 进度条纹理 if (progress > 0) {
ctx.save();
ctx.globalAlpha = 0.4;
ctx.fillStyle = this.currentColorScheme.highlight;

const stripeWidth = 10;
const stripeAngle = Math.PI / 4;
const stripeSpacing = 15;

ctx.beginPath();
for (let i = -width; i < width * 2; i += stripeSpacing) { const xPos = x + i; ctx.moveTo(xPos, y); ctx.lineTo(xPos + height / Math.tan(stripeAngle), y + height); } ctx.stroke(); ctx.restore(); } } /** * 绘制锁定图标 * @param x X坐标 * @param y Y坐标 * @param size 图标大小 */ private drawLockIcon(x: number, y: number, size: number): void { const ctx = this.context; ctx.save(); // 锁的底部(矩形部分) ctx.fillStyle = this.currentColorScheme.primary; ctx.fillRect(x - size/2, y - size/2, size, size); // 锁的顶部(弧形部分) ctx.strokeStyle = this.currentColorScheme.highlight; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(x, y - size/2, size/3, Math.PI, Math.PI * 2); ctx.stroke(); // 锁的钥匙孔 ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.beginPath(); ctx.arc(x, y, size/6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } End File# serbogdanov/project # 高科技组件库设计方案 ## 概述 本文档详细描述了高科技组件库的设计方案,包含组件的功能特性、UI设计风格、技术实现方案以及后续开发计划。我们的目标是打造一套现代化、高性能且视觉效果极具科技感的Angular组件库,满足企业级应用的需求。 ## 设计原则 我们的组件库设计遵循以下核心原则: 1. **科技感** - 所有组件应具有未来科技感的视觉效果,包括霓虹发光边框、全息投影效果、动态粒子背景等 2. **高性能** - 组件应当高效渲染,不引入不必要的性能开销 3. **可定制性** - 提供丰富的配置选项,允许用户自定义外观和行为 4. **响应式设计** - 支持各种屏幕尺寸和设备类型 5. **无障碍访问** - 遵循WCAG准则,确保组件库对所有用户都可用 6. **模块化设计** - 每个组件可独立使用,无不必要的依赖 ## 技术栈 - **框架**:Angular 17+ - **渲染技术**:HTML5 Canvas, WebGL, CSS3动画 - **状态管理**:RxJS - **样式处理**:SCSS with CSS Variables - **打包工具**:Angular CLI - **文档工具**:Storybook ## 视觉风格指南 ### 色彩系统 基于明暗两套主题,每套主题包含: - **主色调**:#1a73e8(蓝色系,代表科技) - **辅助色**:#e33b3b(红)、#4ade80(绿)、#f9b700(黄) - **中性色**:从#f8f9fa到#202124的灰度梯度 - **特效色**:霓虹蓝光色、全息光谱色 ### 排版 - **标题字体**:'Orbitron', sans-serif(科技感强) - **正文字体**:'Roboto', sans-serif(清晰易读) - **等宽字体**:'Source Code Pro', monospace(代码显示) ### 视觉元素 - **发光边框**:1-2px描边外围带有扩散光晕 - **全息投影**:多层次半透明元素与光谱渐变 - **玻璃态效果**:磨砂玻璃背景,微弱透明度 - **网格线**:细微网格背景,增强空间感 - **动态元素**:呼吸效果、脉冲动画、粒子流动 ## 组件规划 ### 第一阶段核心组件 1. **SciFiCard** - 科幻风格卡片 - 带发光边框和立体视觉效果 - 支持头部、内容、底部三区域布局 - 可选动态背景效果 2. **HologramButton** - 全息按钮 - 带有3D立体感和光效 - 支持不同状态:默认、悬停、按下、禁用 - 可自定义发光颜色和强度 3. **NeonInput** - 霓虹输入框 - 键入时边框发光效果 - 支持验证状态视觉反馈 - 带有浮动标签效果 4. **TechRadar** - 科技雷达图 - 动态扫描效果 - 可自定义数据点和区域 - 交互式提示 5. **DataMatrix** - 数据矩阵可视化 - 类似《黑客帝国》码雨效果 - 可用于数据加载状态展示 - 支持自定义文字和颜色 ### 第二阶段扩展组件 6. **HolographicChart** - 全息图表 - 3D立体数据可视化 - 支持柱状图、折线图、饼图等 - 带有动态过渡效果 7. **VirtualScroll** - 虚拟滚动列表 - 高效渲染大数据集 - 科技风格滚动条 - 自定义项目模板 8. **TechTimeline** - 科技时间线 - 垂直或水平布局 - 动态连接线效果 - 支持里程碑标记 9. **AIPoweredSearch** - AI搜索组件 - 带有语音输入功能 - 打字时的预测补全 - 结果高亮显示 10. **SciFiDashboard** - 科幻仪表盘 - 模块化布局系统 - 实时数据更新 - 支持拖拽重组 ### 第三阶段高级组件 11. **HologramMap** - 3D全息地图 - 基于WebGL的3D地球或地图 - 数据点动态显示 - 缩放和旋转交互 12. **BioAuthUI** - 生物识别UI模拟 - 指纹/面部扫描动画 - 识别过程视觉反馈 - 成功/失败状态展示 13. **Neural Connection** - 神经网络连接可视化 - 动态节点和连接线 - 数据流动画效果 - 交互式节点探索 14. **VirtualAssistant** - 虚拟助手界面 - 语音波形动画 - AI响应状态指示 - 对话界面设计 15. **QuantumState** - 量子态可视化 - 粒子效果展示 - 概率分布动画 - 状态变化过渡效果 ## 组件库架构 ### 模块结构 ``` hightech-ui/ ├── core/ # 核心功能和工具 │ ├── animations/ # 通用动画 │ ├── directives/ # 通用指令 │ ├── services/ # 共享服务 │ └── styles/ # 基础样式 ├── components/ # 所有组件 │ ├── sci-fi-card/ │ ├── hologram-button/ │ ├── neon-input/ │ └── ... ├── theme/ # 主题系统 │ ├── dark-theme.scss │ └── light-theme.scss └── utils/ # 工具函数 ├── canvas-utils.ts ├── color-utils.ts └── ... ``` ### 渲染策略 - **简单组件**:使用CSS3和HTML实现 - **中等复杂度**:结合Canvas 2D API提升视觉效果 - **高复杂度**:利用WebGL/Three.js实现3D效果 ### 状态管理 - 通过RxJS管理组件内部状态和交互 - 提供`Input()`和`Output()`接口与外部系统交互 - 复杂组件状态提供独立的状态服务 ## 技术实现挑战与解决方案 ### 挑战1:高性能动画效果 **问题**:科技感组件通常需要复杂动画,可能导致性能问题。 **解决方案**: - 使用CSS硬件加速属性(transform, opacity) - 实现动画节流和防抖 - 复杂动画使用`requestAnimationFrame`精确控制 - 非可视区域组件自动暂停动画 ### 挑战2:兼容性问题 **问题**:高级CSS和WebGL特性在某些浏览器可能不完全支持。 **解决方案**: - 实现功能降级策略 - 使用特性检测而非浏览器检测 - 为关键效果提供备选方案 - 清晰文档说明兼容性要求 ### 挑战3:可定制性与简便使用之间的平衡 **问题**:既要支持高度定制,又要保持API简洁易用。 **解决方案**: - 采用合理默认值设计 - 使用分层配置:基础配置 + 高级配置选项 - 提供主题系统统一控制视觉风格 - 组件API设计遵循一致性原则 ## 开发路线图 ### 阶段1(1-2个月) - 设计系统基础建设 - 实现5个核心组件 - 建立基本开发文档 - 发布0.1.0版本用于内部测试 ### 阶段2(2-3个月) - 完善核心组件功能 - 实现第二阶段5个扩展组件 - 建立单元测试和集成测试 - 完善组件文档和示例 - 发布0.5.0版本用于受限公开测试 ### 阶段3(3-4个月) - 实现第三阶段5个高级组件 - 优化组件性能和可访问性 - 完成全面的组件文档 - 添加动画和交互示例 - 发布1.0.0正式版本 ## 贡献指南 我们欢迎社区贡献,但需要遵循以下原则: 1. **代码风格**:遵循Angular风格指南 2. **测试覆盖**:新组件需有单元测试 3. **文档完整**:提供使用示例和API说明 4. **可访问性**:确保组件支持键盘导航和屏幕阅读器 5. **性能考虑**:避免不必要的计算和重绘 ## 总结 本组件库旨在为现代Web应用提供令人惊艳的科技感UI组件,同时保证高性能和良好的用户体验。通过分阶段实现,我们将逐步完善组件功能,并根据社区反馈持续优化。我们相信,这套组件库将为科技感十足的网站和应用提供绝佳的视觉表现力。 # myrta-ds/myrta # packages/colors/scripts/generate.js /* eslint-disable no-console */ import fs from 'fs'; import path from 'path'; import prettier from 'prettier'; import { ESLint } from 'eslint'; import chroma from 'chroma-js'; import swatch from '../swatch/index.js'; export const JS_PATH = path.resolve('src/js/swatch.js'); export const SCSS_PATH = path.resolve('src/scss/_swatch.scss'); export const DOCS_PATH = path.resolve('docs', 'swatch.md'); export const SWATCH_BASE_PATH = path.resolve('swatch'); export const SWATCH_JS_PATH = path.resolve('swatch', 'index.js'); // Utility to format code const formatJS = async (jsStr) => {
const prettierRC = JSON.parse(fs.readFileSync(’.prettierrc’, 'utf8′));
const eslint = new ESLint({
fix: true,
useEslintrc: true,
});
// Format with Prettier
const formattedJS = prettier.format(jsStr, {
…prettierRC,
parser: 'babel’,
});
// Apply ESLint fixes
const [result] = await eslint.lintText(formattedJS);
return result.output || formattedJS;
};

// Utility to format SCSS code
const formatSCSS = (scssStr) => {
const prettierRC = JSON.parse(fs.readFileSync(’.prettierrc’, 'utf8′));
return prettier.format(scssStr, {
…prettierRC,
parser: 'scss’,
});
};

// Format number with 0 decimal places.
const formatNumber = (num) => parseFloat(num.toFixed(0));

// Create and detect dark/light colors
const generateColorInfo = (color) => {
const chromaColor = chroma(color);
const luminance = chromaColor.luminance();
const isDark = luminance < 0.25; // Enhanced threshold for determining dark colors const contrast = { white: chroma.contrast(color, '#FFFFFF'), black: chroma.contrast(color, '#1A1A1A'), }; // Create text color based on best contrast const textColor = contrast.white > contrast.black ? '#FFFFFF’ : '#1A1A1A’;

return {
hex: color.toUpperCase(),
rgb: chromaColor.rgb().map(formatNumber),
isDark,
textColor,
};
};

// Generate JS output
const generateJS = async () => {
// Generate JavaScript version of the color swatch
let jsOutput = `/**
* Auto-generated color swatch.
* DO NOT EDIT DIRECTLY.
*/
const swatch = ${JSON.stringify(swatch, null, 2)};n
export default swatch;`;

// Format and write the JS file
const formattedJS = await formatJS(jsOutput);
fs.writeFileSync(JS_PATH, formattedJS);
console.log(`✅ Generated JavaScript color swatch at ${JS_PATH}`);
};

// Generate SCSS output
const generateSCSS = () => {
let scssOutput = '// Auto-generated color swatch variables.n// DO NOT EDIT DIRECTLY.nn’;

// Process each palette
Object.entries(swatch).forEach(([paletteName, palette]) => {
scssOutput += `// ${paletteName} paletten`;

// Process each color in the palette
Object.entries(palette).forEach(([colorName, color]) => {
if (typeof color === 'string’) {
// For base colors (strings)
const varName = `$color-${paletteName}-${colorName}`;
scssOutput += `${varName}: ${color.toUpperCase()};n`;
} else {
// For color objects with shades
Object.entries(color).forEach(([shadeName, shadeValue]) => {
const varName = `$color-${paletteName}-${colorName}-${shadeName}`;
scssOutput += `${varName}: ${shadeValue.toUpperCase()};n`;
});
}
});
scssOutput += 'n’;
});

// Add CSS custom properties (variables)
scssOutput += '// CSS custom propertiesn’;
scssOutput += ’:root {n’;

// Process each palette for CSS variables
Object.entries(swatch).forEach(([paletteName, palette]) => {
scssOutput += ` // ${paletteName} paletten`;

// Process each color in the palette
Object.entries(palette).forEach(([colorName, color]) => {
if (typeof color === 'string’) {
// For base colors (strings)
const varName = `–color-${paletteName}-${colorName}`;
scssOutput += ` ${varName}: ${color.toUpperCase()};n`;
} else {
// For color objects with shades
Object.entries(color).forEach(([shadeName, shadeValue]) => {
const varName = `–color-${paletteName}-${colorName}-${shadeName}`;
scssOutput += ` ${varName}: ${shadeValue.toUpperCase()};n`;
});
}
});
});

scssOutput += ’}n’;

// Format and write the SCSS file
const formattedSCSS = formatSCSS(scssOutput);
fs.writeFileSync(SCSS_PATH, formattedSCSS);
console.log(`✅ Generated SCSS color variables at ${SCSS_PATH}`);
};

// Generate documentation
const generateDocs = () => {
let mdOutput = '# Color SwatchnnAuto-generated color documentation.nn’;

// Process each palette
Object.entries(swatch).forEach(([paletteName, palette]) => {
mdOutput += `## ${paletteName} Palettenn`;

// Process each color in the palette
Object.entries(palette).forEach(([colorName, color]) => {
if (typeof color === 'string’) {
// For base colors (strings)
const colorInfo = generateColorInfo(color);
mdOutput += `### ${colorName}nn`;
mdOutput += `

n`;
mdOutput += ` ${colorName}
n`;
mdOutput += ` HEX: ${colorInfo.hex}
n`;
mdOutput += ` RGB: ${colorInfo.rgb.join(’, ’)}n`;
mdOutput += `

nn`;
mdOutput += `CSS Variable: `–color-${paletteName}-${colorName}`nn`;
mdOutput += `SCSS Variable: `$color-${paletteName}-${colorName}`nn`;
} else {
// For color objects with shades
mdOutput += `### ${colorName}nn`;
mdOutput += ’

n’;

Object.entries(color).forEach(([shadeName, shadeValue]) => {
const colorInfo = generateColorInfo(shadeValue);
mdOutput += `

n`;
mdOutput += ` ${colorName}-${shadeName}
n`;
mdOutput += ` HEX: ${colorInfo.hex}
n`;
mdOutput += ` RGB: ${colorInfo.rgb.join(’, ’)}n`;
mdOutput += `

n`;
});

mdOutput += ’

nn’;

// Add variables reference
mdOutput += 'CSS Variables:nn’;
Object.keys(color).forEach((shadeName) => {
mdOutput += `- `–color-${paletteName}-${colorName}-${shadeName}`n`;
});
mdOutput += 'n’;

mdOutput += 'SCSS Variables:nn’;
Object.keys(color).forEach((shadeName) => {
mdOutput += `- `$color-${paletteName}-${colorName}-${shadeName}`n`;
});
mdOutput += 'n’;
}
});
});

fs.writeFileSync(DOCS_PATH, mdOutput);
console.log(`✅ Generated color documentation at ${DOCS_PATH}`);
};

// Main function to run all generators
const main = async () => {
try {
// Make sure the directories exist
if (!fs.existsSync(path.dirname(JS_PATH))) {
fs.mkdirSync(path.dirname(JS_PATH), { recursive: true });
}

if (!fs.existsSync(path.dirname(SCSS_PATH))) {
fs.mkdirSync(path.dirname(SCSS_PATH), { recursive: true });
}

if (!fs.existsSync(path.dirname(DOCS_PATH))) {
fs.mkdirSync(path.dirname(DOCS_PATH), { recursive: true });
}

// Generate files
await generateJS();
generateSCSS();
generateDocs();

console.log(’✨ All color files generated successfully!’);
} catch (error) {
console.error(’Error generating color files:’, error);
process.exit(1);
}
};

main();
End File# packages/react/src/hooks/useMediaQuery.ts
import { useEffect, useState } from 'react’;

/**
* Media query breakpoints
*/
export const breakpoints = {
xs: '(max-width: 575px)’,
sm: '(min-width: 576px)’,
md: '(min-width: 768px)’,
lg: '(min-width: 992px)’,
xl: '(min-width: 1200px)’,
xxl: '(min-width: 1400px)’,
};

export type Breakpoint = keyof typeof breakpoints;

interface UseMediaQueryOptions {
/* If true, will return false on the server regardless of actual matches */
noSSR?: boolean;
/* Default value to return before the media query is evaluated */
defaultValue?: boolean;
}

/**
* React hook for using media queries in components
*
* @param query The media query string or a breakpoint key
* @param options Configuration options
* @returns Boolean indicating if the media query matches
*
* @example
* // Basic usage with breakpoint
* const isMobile = useMediaQuery(’sm’);
*
* // Custom media query
* const isPrint = useMediaQuery(’print’);
* const isLandscape = useMediaQuery('(orientation: landscape)’);
*
* // With options
* const isDesktop = useMediaQuery(’lg’, { noSSR: true, defaultValue: false });
*/
export function useMediaQuery(
query: string | Breakpoint,
options: UseMediaQueryOptions = {},
): boolean {
const { noSSR = false, defaultValue = false } = options;
const [matches, setMatches] = useState(() => {
// Handle SSR case
if (typeof window === 'undefined’) {
return noSSR ? false : defaultValue;
}

// Resolve the query string
const mediaQuery = breakpoints[query as Breakpoint] || query;
// Initial match
return window.matchMedia(mediaQuery).matches;
});

useEffect(() => {
// Resolve the query string
const mediaQuery = breakpoints[query as Breakpoint] || query;
// Get the media query list
const mediaQueryList = window.matchMedia(mediaQuery);
// Set initial value
setMatches(mediaQueryList.matches);

// Define handler for media query changes
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);

// Add event listener
mediaQueryList.addEventListener(’change’, handler);

// Cleanup
return () => {
mediaQueryList.removeEventListener(’change’, handler);
};
}, [query]);

return matches;
}
End File# myrta-ds/myrta
import { useState, useEffect, RefObject } from 'react’;

/**
* Options for the click outside hook
*/
interface UseClickOutsideOptions {
/**
* Whether the hook is active
* @default true
*/
active?: boolean;
/**
* Callback to execute when click outside occurs
*/
onClickOutside: () => void;
}

/**
* React hook to detect clicks outside of the specified element
*
* @param ref Reference to the element to detect clicks outside of
* @param options Configuration options
*
* @example
* „`jsx
* const MyComponent = () => {
* const ref = useRef(null);
* const [isOpen, setIsOpen] = useState(false);
*
* useClickOutside(ref, {
* active: isOpen,
* onClickOutside: () => setIsOpen(false),
* });
*
* return (
*

*
* {isOpen && (
*

* This will close when clicking outside
*

* )}
*

* );
* };
* „`
*/
export function useClickOutside(
ref: RefObject,
options: UseClickOutsideOptions,
): void {
const { active = true, onClickOutside } = options;
const [initialized, setInitialized] = useState(false);

useEffect(() => {
setInitialized(true);
}, []);

useEffect(() => {
// Skip if not active or not initialized (for SSR compatibility)
if (!active || !initialized) {
return undefined;
}

const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
onClickOutside();
}
};

// Add event listener with capture to ensure it runs before other click handlers
document.addEventListener(’mousedown’, handleClickOutside, true);

// Cleanup function
return () => {
document.removeEventListener(’mousedown’, handleClickOutside, true);
};
}, [ref, active, initialized, onClickOutside]);
}
End Fileimport { useRef, useCallback, Dispatch, SetStateAction, useState } from 'react’;

/**
* Options for useDisclosure hook
*/
interface UseDisclosureOptions {
/**
* The default state
* @default false
*/
defaultIsOpen?: boolean;

/**
* Function called when state changes
*/
onStateChange?: (isOpen: boolean) => void;
}

/**
* The return value of useDisclosure
*/
interface UseDisclosureReturn {
/** Current state */
isOpen: boolean;

/** Function to open */
open: () => void;

/** Function to close */
close: () => void;

/** Function to toggle */
toggle: () => void;

/** Function to set state directly */
setOpen: Dispatch>;
}

/**
* Hook for managing disclosure state (open/close)
* Useful for modals, dropdowns, etc.
*
* @param options Configuration options
* @returns Object with state and handlers
*
* @example
* „`jsx
* function Modal() {
* const { isOpen, open, close } = useDisclosure();
*
* return (
* <>
*
* {isOpen && (
*

*

Modal content

*
*

* )}
*
* );
* }
* „`
*/
export function useDisclosure(
options: UseDisclosureOptions = {}
): UseDisclosureReturn {
const { defaultIsOpen = false, onStateChange } = options;

const [isOpen, setIsOpen] = useState(defaultIsOpen);
const onStateChangeRef = useRef(onStateChange);

// Keep the onStateChange ref up to date
onStateChangeRef.current = onStateChange;

const setOpen = useCallback((value: boolean | ((prevState: boolean) => boolean)) => {
setIsOpen((prev) => {
const next = typeof value === 'function’ ? value(prev) : value;
if (prev !== next) {
onStateChangeRef.current?.(next);
}
return next;
});
}, []);

const open = useCallback(() => setOpen(true), [setOpen]);
const close = useCallback(() => setOpen(false), [setOpen]);
const toggle = useCallback(() => setOpen((prev) => !prev), [setOpen]);

return {
isOpen,
open,
close,
toggle,
setOpen,
};
}
End Fileimport { useEffect, useRef, useState } from 'react’;

/**
* Options for useCopyToClipboard hook
*/
interface UseCopyToClipboardOptions {
/**
* Reset the copied state after this many milliseconds
* @default null (no reset)
*/
resetAfter?: number | null;

/**
* Callback called after copy success
*/
onSuccess?: (text: string) => void;

/**
* Callback called after copy failure
*/
onError?: (error: Error) => void;
}

/**
* The return value of useCopyToClipboard
*/
interface UseCopyToClipboardReturn {
/** The currently copied text or null if nothing copied */
copied: string | null;

/** Is the copy operation in progress */
copying: boolean;

/** Whether the last copy operation succeeded */
success: boolean | null;

/** Function to copy text to clipboard */
copy: (text: string) => Promise;

/** Function to clear the copied state */
reset: () => void;
}

/**
* Hook to copy text to clipboard
*
* @param options Configuration options
* @returns Object with copied state and copy function
*
* @example
* „`jsx
* function CopyButton() {
* const { copied, copying, success, copy } = useCopyToClipboard({
* resetAfter: 2000,
* onSuccess: () => toast.success(’Copied to clipboard!’),
* });
*
* return (
*
* );
* }
* „`
*/
export function useCopyToClipboard(
options: UseCopyToClipboardOptions = {}
): UseCopyToClipboardReturn {
const { resetAfter = null, onSuccess, onError } = options;

const [copied, setCopied] = useState(null);
const [copying, setCopying] = useState(false);
const [success, setSuccess] = useState(null);
const resetTimeoutRef = useRef(null);

// Clear timeout on unmount
useEffect(() => {
return () => {
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
}
};
}, []);

const reset = () => {
setCopied(null);
setSuccess(null);
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}
};

const copy = async (text: string): Promise => {
if (!navigator?.clipboard) {
const error = new Error(’Clipboard API not supported’);
onError?.(error);
return false;
}

// Clear any existing reset timeout
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}

setCopying(true);
setSuccess(null);

try {
await navigator.clipboard.writeText(text);
setCopied(text);
setSuccess(true);
onSuccess?.(text);

// Set up reset timeout if specified
if (resetAfter !== null) {
resetTimeoutRef.current = window.setTimeout(reset, resetAfter);
}

return true;
} catch (error) {
setCopied(null);
setSuccess(false);
onError?.(error as Error);
return false;
} finally {
setCopying(false);
}
};

return {
copied,
copying,
success,
copy,
reset,
};
}
End File# packages/react/src/hooks/useCountdown.ts
import { useState, useEffect, useRef, useCallback } from 'react’;

/**
* Options for useCountdown hook
*/
interface UseCountdownOptions {
/**
* Time in milliseconds
*/
milliseconds: number;

/**
* Interval for countdown updates in milliseconds
* @default 1000
*/
interval?: number;

/**
* Whether to start the countdown automatically
* @default true
*/
autoStart?: boolean;

/**
* Callback when countdown reaches zero
*/
onComplete?: () => void;

/**
* Callback on each countdown tick
*/
onTick?: (millisecondsLeft: number) => void;
}

/**
* The return value of useCountdown
*/
interface UseCountdownReturn {
/** Whether the countdown is currently running */
isRunning: boolean;

/** Whether the countdown is complete */
isComplete: boolean;

/** Time left in milliseconds */
millisecondsLeft: number;

/** Function to start/resume the countdown */
start: () => void;

/** Function to pause the countdown */
pause: () => void;

/** Function to stop and reset the countdown */
reset: () => void;

/** Percentage of time elapsed (0-100) */
progress: number;

/** Formatted time left as mm:ss */
formatted: string;
}

/**
* Hook to create a countdown timer
*
* @param options Configuration options
* @returns Object with countdown state and controls
*
* @example
* „`jsx
* function Timer() {
* const {
* isRunning,
* formatted,
* progress,
* start,
* pause,
* reset
* } = useCountdown({
* milliseconds: 60000, // 1 minute
* onComplete: () => console.log(’Countdown finished!’),
* });
*
* return (
*

*

Time left: {formatted}

* *
*
*

* );
* }
* „`
*/
export function useCountdown(
options: UseCountdownOptions
): UseCountdownReturn {
const {
milliseconds,
interval = 1000,
autoStart = true,
onComplete,
onTick,
} = options;

const [millisecondsLeft, setMillisecondsLeft] = useState(milliseconds);
const [isRunning, setIsRunning] = useState(autoStart);
const [isComplete, setIsComplete] = useState(false);
const intervalRef = useRef(null);
const startTimeRef = useRef(0);
const timeRemainingRef = useRef(milliseconds);

// Format time as mm:ss
const formatTime = (ms: number): string => {
const totalSeconds = Math.ceil(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0′)}:${seconds.toString().padStart(2, '0′)}`;
};

// Calculate progress percentage
const calculateProgress = (ms: number): number => {
return ((milliseconds – ms) / milliseconds) * 100;
};

const stop = useCallback(() => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
setIsRunning(false);
}, []);

const reset = useCallback(() => {
stop();
setMillisecondsLeft(milliseconds);
timeRemainingRef.current = milliseconds;
setIsComplete(false);
}, [milliseconds, stop]);

const tick = useCallback(() => {
const currentTime = Date.now();
const elapsedTime = currentTime – startTimeRef.current;
startTimeRef.current = currentTime;

const newTimeRemaining = Math.max(0, timeRemainingRef.current – elapsedTime);
timeRemainingRef.current = newTimeRemaining;
setMillisecondsLeft(newTimeRemaining);

onTick?.(newTimeRemaining);

if (newTimeRemaining <= 0) { stop(); setIsComplete(true); onComplete?.(); } }, [onComplete, onTick, stop]); const start = useCallback(() => {
if (isRunning || isComplete) return;

setIsRunning(true);
startTimeRef.current = Date.now();

// Clear any existing interval to be safe
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}

// Start the interval
intervalRef.current = window.setInterval(tick, interval);
}, [interval, isComplete, isRunning, tick]);

const pause = useCallback(() => {
stop();
}, [stop]);

// Set up interval when running
useEffect(() => {
if (isRunning) {
startTimeRef.current = Date.now();
intervalRef.current = window.setInterval(tick, interval);
}

return () => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}
};
}, [interval, isRunning, tick]);

return {
isRunning,
isComplete,
millisecondsLeft,
start,
pause,
reset,
progress: calculateProgress(millisecondsLeft),
formatted: formatTime(millisecondsLeft),
};
}
End Fileimport React, { useState, useRef, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import { Box, BoxProps } from ’../Box/Box’;
import ’./Tooltip.scss’;

export interface TooltipProps extends BoxProps {
/**
* Content to display inside the tooltip
*/
content: React.ReactNode;

/**
* Tooltip position relative to the trigger
* @default 'top’
*/
position?: 'top’ | 'right’ | 'bottom’ | 'left’;

/**
* Trigger element to hook the tooltip to
*/
children: React.ReactElement;

/**
* Tooltip width in pixels
* @default 200
*/
width?: number;

/**
* Delay before showing the tooltip in milliseconds
* @default 200
*/
showDelay?: number;

/**
* Delay before hiding the tooltip in milliseconds
* @default 100
*/
hideDelay?: number;

/**
* Whether the tooltip should show on click instead of hover
* @default false
*/
clickTrigger?: boolean;

/**
* Whether the tooltip should be visible initially
* @default false
*/
defaultVisible?: boolean;

/**
* Whether the tooltip is disabled
* @default false
*/
disabled?: boolean;
}

export const Tooltip: React.FC = ({
content,
position = 'top’,
children,
width = 200,
showDelay = 200,
hideDelay = 100,
clickTrigger = false,
defaultVisible = false,
disabled = false,
className,
…props
}) => {
const [visible, setVisible] = useState(defaultVisible);
const [tooltipStyle, setTooltipStyle] = useState({});
const triggerRef = useRef(null);
const tooltipRef = useRef(null);
const timeoutRef = useRef(null);
const tooltipId = useId();

const calculatePosition = () => {
if (!triggerRef.current || !tooltipRef.current) return;

const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;

let top, left;

switch (position) {
case 'top’:
top = triggerRect.top + scrollTop – tooltipRect.height – 8;
left = triggerRect.left + scrollLeft + (triggerRect.width / 2) – (tooltipRect.width / 2);
break;
case 'right’:
top = triggerRect.top + scrollTop + (triggerRect.height / 2) – (tooltipRect.height / 2);
left = triggerRect.right + scrollLeft + 8;
break;
case 'bottom’:
top = triggerRect.bottom + scrollTop + 8;
left = triggerRect.left + scrollLeft + (triggerRect.width / 2) – (tooltipRect.width / 2);
break;
case 'left’:
top = triggerRect.top + scrollTop + (triggerRect.height / 2) – (tooltipRect.height / 2);
left = triggerRect.left + scrollLeft – tooltipRect.width – 8;
break;
}

// Ensure tooltip stays within viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

if (left < 0) left = 0; if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth – tooltipRect.width;
}

if (top < 0) top = 0; if (top + tooltipRect.height > viewportHeight + scrollTop) {
top = viewportHeight + scrollTop – tooltipRect.height;
}

setTooltipStyle({
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
opacity: 1,
visibility: 'visible’
});
};

const handleShowTooltip = () => {
if (disabled) return;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
setVisible(true);
}, showDelay);
};

const handleHideTooltip = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
setVisible(false);
}, hideDelay);
};

const handleToggleTooltip = () => {
if (disabled) return;
setVisible(prevVisible => !prevVisible);
};

useEffect(() => {
if (visible) {
calculatePosition();
// Recalculate on window resize
window.addEventListener(’resize’, calculatePosition);
// Recalculate on scroll
window.addEventListener(’scroll’, calculatePosition);
}

return () => {
window.removeEventListener(’resize’, calculatePosition);
window.removeEventListener(’scroll’, calculatePosition);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, [visible]);

// Clone the trigger element to add event handlers
const triggerElement = React.cloneElement(children, {
ref: (node: HTMLElement | null) => {
triggerRef.current = node;
// Forward ref if the child has one
const { ref } = children as any;
if (typeof ref === 'function’) ref(node);
else if (ref) ref.current = node;
},
…(clickTrigger
? {
onClick: (e: React.MouseEvent) => {
handleToggleTooltip();
// Call the original onClick if it exists
if (children.props.onClick) children.props.onClick(e);
},
}
: {
onMouseEnter: (e: React.MouseEvent) => {
handleShowTooltip();
// Call the original onMouseEnter if it exists
if (children.props.onMouseEnter) children.props.onMouseEnter(e);
},
onMouseLeave: (e: React.MouseEvent) => {
handleHideTooltip();
// Call the original onMouseLeave if it exists
if (children.props.onMouseLeave) children.props.onMouseLeave(e);
},
onFocus: (e: React.FocusEvent) => {
handleShowTooltip();
// Call the original onFocus if it exists
if (children.props.onFocus) children.props.onFocus(e);
},
onBlur: (e: React.FocusEvent) => {
handleHideTooltip();
// Call the original onBlur if it exists
if (children.props.onBlur) children.props.onBlur(e);
},
}),
'aria-describedby’: visible ? tooltipId : undefined,
});

return (
<>
{triggerElement}
{visible && (

{content}

)}

);
};
End Fileimport React, { useEffect, useRef, useState } from 'react’;
import clsx from 'clsx’;
import { Icon } from ’../Icon/Icon’;
import { useId } from ’../../hooks/useId’;
import ’./Accordion.scss’;

export interface AccordionProps {
/**
* Unique identifier for the accordion
*/
id?: string;

/**
* Accordion item title
*/
title: React.ReactNode;

/**
* Whether the accordion is expanded by default
* @default false
*/
defaultExpanded?: boolean;

/**
* Whether the accordion is expanded (controlled component)
*/
expanded?: boolean;

/**
* Function called when the expanded state changes
*/
onExpandedChange?: (expanded: boolean) => void;

/**
* Content of the accordion
*/
children: React.ReactNode;

/**
* Additional CSS class for the accordion
*/
className?: string;

/**
* Whether to show the divider between accordion items
* @default true
*/
showDivider?: boolean;

/**
* Custom icon to be displayed instead of the default
*/
icon?: React.ReactNode;

/**
* Additional CSS class for the title button
*/
titleClassName?: string;

/**
* Additional CSS class for the content panel
*/
contentClassName?: string;

/**
* Callback when accordion is focused
*/
onFocus?: React.FocusEventHandler;

/**
* Callback when accordion loses focus
*/
onBlur?: React.FocusEventHandler;

/**
* Whether the accordion is disabled
* @default false
*/
disabled?: boolean;
}

export const Accordion = React.forwardRef(
(
{
id,
title,
defaultExpanded = false,
expanded: expandedProp,
onExpandedChange,
children,
className,
showDivider = true,
icon,
titleClassName,
contentClassName,
onFocus,
onBlur,
disabled = false,
…rest
},
ref,
) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const generatedId = useId();
const accordionId = id || `accordion-${generatedId}`;
const headingId = `${accordionId}-heading`;
const contentId = `${accordionId}-content`;

const contentRef = useRef(null);
const [contentHeight, setContentHeight] = useState(
defaultExpanded ? undefined : 0,
);

// Handle controlled component
const expanded = expandedProp !== undefined ? expandedProp : isExpanded;

const handleToggle = () => {
if (disabled) return;

const newExpanded = !expanded;
setIsExpanded(newExpanded);
onExpandedChange?.(newExpanded);
};

// Update height when expanded state changes
useEffect(() => {
if (!contentRef.current) return;

if (expanded) {
const height = contentRef.current.scrollHeight;
setContentHeight(height);

// Remove fixed height after transition to allow for content changes
const timer = setTimeout(() => {
setContentHeight(undefined);
}, 300); // Should match transition duration
return () => clearTimeout(timer);
} else {
// Set current height before collapsing for smooth animation
setContentHeight(contentRef.current.scrollHeight);

// Force a reflow
void contentRef.current.offsetHeight;

// Set to 0 for animation
const timer = setTimeout(() => {
setContentHeight(0);
}, 10);
return () => clearTimeout(timer);
}
}, [expanded]);

return (


);
},
);

Accordion.displayName = 'Accordion’;
End File# packages/react/src/components/Button/Button.tsx
import React from 'react’;
import clsx from 'clsx’;
import ’./Button.scss’;
import { Spinner } from ’../Spinner/Spinner’;

export type ButtonVariant = 'primary’ | 'secondary’ | 'tertiary’ | 'danger’;
export type ButtonSize = 'small’ | 'medium’ | 'large’;

export interface ButtonProps
extends React.ButtonHTMLAttributes {
/**
* Button variant
* @default 'primary’
*/
variant?: ButtonVariant;

/**
* Button size
* @default 'medium’
*/
size?: ButtonSize;

/**
* Whether the button extends to fill its container
* @default false
*/
fullWidth?: boolean;

/**
* Icon to display at the start of the button
*/
startIcon?: React.ReactNode;

/**
* Icon to display at the end of the button
*/
endIcon?: React.ReactNode;

/**
* Whether the button is in a loading state
* @default false
*/
loading?: boolean;

/**
* Text to display when in loading state
*/
loadingText?: string;

/**
* CSS class for the loading spinner
*/
spinnerClassName?: string;

/**
* Whether the button should be rendered as a link
*/
href?: string;

/**
* Target attribute for link buttons
*/
target?: string;

/**
* Rel attribute for link buttons
*/
rel?: string;
}

const ButtonRoot = React.forwardRef< HTMLButtonElement | HTMLAnchorElement, ButtonProps & { as: React.ElementType } >(
(
{
as: Component,
className,
variant = 'primary’,
size = 'medium’,
disabled,
fullWidth = false,
startIcon,
endIcon,
loading = false,
loadingText,
spinnerClassName,
children,
…rest
},
ref
) => {
const isDisabled = disabled || loading;

// Use proper DOM props based on the component type
const domProps = Component === 'button’ ? { disabled: isDisabled } : { 'aria-disabled’: isDisabled };

return (

{loading && (



)}

{startIcon && {startIcon}}
{loading && loadingText ? loadingText : children}
{endIcon && {endIcon}}


);
}
);

export const Button = React.forwardRef(
(props, ref) => {
if (props.href && !props.disabled) {
return ;
}
return ;
}
);

Button.displayName = 'Button’;
End File# myrta-ds/myrta
# packages/react/src/components/Toggle/Toggle.tsx
import React, { useState, useEffect } from 'react’;
import clsx from 'clsx’;
import ’./Toggle.scss’;
import { useId } from ’../../hooks/useId’;

export interface ToggleProps {
/**
* Whether the toggle is checked
*/
checked?: boolean;

/**
* Default checked state when uncontrolled
* @default false
*/
defaultChecked?: boolean;

/**
* Whether the toggle is disabled
* @default false
*/
disabled?: boolean;

/**
* Function called when the toggle state changes
*/
onChange?: (checked: boolean, event: React.ChangeEvent) => void;

/**
* Size of the toggle
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’;

/**
* An accessible label for the toggle
*/
label?: string;

/**
* ID for the toggle input
*/
id?: string;

/**
* Additional CSS class for the toggle
*/
className?: string;

/**
* Additional CSS class for the toggle track
*/
trackClassName?: string;

/**
* Additional CSS class for the toggle thumb
*/
thumbClassName?: string;

/**
* Name attribute for the hidden input
*/
name?: string;

/**
* ARIA label for the toggle
*/
'aria-label’?: string;

/**
* ARIA labelledby for the toggle
*/
'aria-labelledby’?: string;

/**
* Loading state
* @default false
*/
loading?: boolean;

/**
* Required state
* @default false
*/
required?: boolean;
}

export const Toggle = React.forwardRef(
(
{
checked,
defaultChecked = false,
disabled = false,
onChange,
size = 'medium’,
label,
id,
className,
trackClassName,
thumbClassName,
name,
'aria-label’: ariaLabel,
'aria-labelledby’: ariaLabelledBy,
loading = false,
required = false,
…rest
},
ref
) => {
const [isChecked, setIsChecked] = useState(defaultChecked);
const generatedId = useId();
const toggleId = id || `toggle-${generatedId}`;

// Handle the controlled component case
useEffect(() => {
if (checked !== undefined) {
setIsChecked(checked);
}
}, [checked]);

const handleChange = (event: React.ChangeEvent) => {
if (disabled || loading) return;

const newChecked = event.target.checked;
// Only update internal state for uncontrolled component
if (checked === undefined) {
setIsChecked(newChecked);
}
onChange?.(newChecked, event);
};

return (

{loading && }

{label && {label}}

);
}
);

Toggle.displayName = 'Toggle’;
End Fileimport React, { useState, useRef, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./Select.scss’;
import { Icon } from ’../Icon/Icon’;

export type SelectOption = {
value: string;
label: React.ReactNode;
disabled?: boolean;
};

export type SelectSize = 'small’ | 'medium’ | 'large’;

export interface SelectProps
extends Omit, 'size’> {
/**
* Options for the select dropdown
*/
options: SelectOption[];

/**
* Label for the select field
*/
label?: string;

/**
* Helper text to be displayed below the select
*/
helperText?: string;

/**
* Error message to be displayed below the select
*/
error?: string;

/**
* Size of the select
* @default 'medium’
*/
size?: SelectSize;

/**
* Placeholder text displayed when no option is selected
* @default 'Select an option’
*/
placeholder?: string;

/**
* Called when the selected option changes
*/
onValueChange?: (value: string) => void;

/**
* Whether the select should take the full width of its container
* @default false
*/
fullWidth?: boolean;

/**
* Additional CSS class for the select container
*/
className?: string;

/**
* Additional CSS class for the select element
*/
selectClassName?: string;

/**
* Additional CSS class for the label
*/
labelClassName?: string;

/**
* Whether the select is loading
* @default false
*/
loading?: boolean;

/**
* Loading text to display when loading
* @default 'Loading…’
*/
loadingText?: string;

/**
* Required field indicator
*/
required?: boolean;
}

export const Select = React.forwardRef(
(
{
options,
label,
helperText,
error,
size = 'medium’,
placeholder = 'Select an option’,
value,
defaultValue,
onChange,
onValueChange,
fullWidth = false,
className,
selectClassName,
labelClassName,
disabled = false,
loading = false,
loadingText = 'Loading…’,
required = false,
id,
name,
…props
},
ref
) => {
const [selectedValue, setSelectedValue] = useState(
value !== undefined ? String(value) : defaultValue !== undefined ? String(defaultValue) : undefined
);
const selectRef = useRef(null);
const generatedId = useId();
const selectId = id || `select-${generatedId}`;
const helperTextId = `helper-text-${generatedId}`;
const errorId = `error-${generatedId}`;

// Update internal state when controlled value changes
useEffect(() => {
if (value !== undefined) {
setSelectedValue(String(value));
}
}, [value]);

const handleChange = (e: React.ChangeEvent) => {
const newValue = e.target.value;

// Only update internal state if it’s an uncontrolled component
if (value === undefined) {
setSelectedValue(newValue);
}

// Call the provided onChange handler
if (onChange) {
onChange(e);
}

// Call the custom onValueChange handler
if (onValueChange) {
onValueChange(newValue);
}
};

return (


{label && (

)}

{error ? (

{error}

) : helperText ? (

{helperText}

) : null}

);
}
);

Select.displayName = 'Select’;
End File# myrta-ds/myrta
# packages/react/src/components/Table/Table.tsx
import React from 'react’;
import clsx from 'clsx’;
import ’./Table.scss’;

// Table component
export interface TableProps extends React.TableHTMLAttributes {
/**
* Whether the table has zebra-striped rows
* @default false
*/
striped?: boolean;

/**
* Whether the table has borders
* @default false
*/
bordered?: boolean;

/**
* Whether the table’s cells are compact
* @default false
*/
compact?: boolean;

/**
* Whether the table has hover state on rows
* @default false
*/
hoverable?: boolean;

/**
* Whether the table is in a loading state
* @default false
*/
loading?: boolean;

/**
* Width of the table
* @default '100%’
*/
width?: string | number;

/**
* Custom CSS class for table container
*/
wrapperClassName?: string;
}

export const Table = React.forwardRef(
(
{
children,
className,
striped = false,
bordered = false,
compact = false,
hoverable = false,
loading = false,
width = '100%’,
wrapperClassName,
…rest
},
ref
) => (


{children}

{loading && (

)}

)
);

Table.displayName = 'Table’;

// Table Head component
export interface TableHeadProps extends React.HTMLAttributes {
/**
* Whether the header is sticky
* @default false
*/
sticky?: boolean;
}

export const TableHead = React.forwardRef(
({ children, className, sticky = false, …rest }, ref) => (


{children}

)
);

TableHead.displayName = 'TableHead’;

// Table Body component
export const TableBody = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes
>(({ children, className, …rest }, ref) => (


{children}

));

TableBody.displayName = 'TableBody’;

// Table Row component
export interface TableRowProps extends React.HTMLAttributes {
/**
* Whether the row is selected
* @default false
*/
selected?: boolean;

/**
* Whether the row is clickable
* @default false
*/
clickable?: boolean;

/**
* Whether the row is disabled
* @default false
*/
disabled?: boolean;
}

export const TableRow = React.forwardRef(
({ children, className, selected = false, clickable = false, disabled = false, …rest }, ref) => (


{children}

)
);

TableRow.displayName = 'TableRow’;

// Table Cell component
export interface TableCellProps extends React.TdHTMLAttributes {
/**
* Text alignment within the cell
*/
align?: 'left’ | 'center’ | 'right’;

/**
* Truncate text with ellipsis if it’s too long
* @default false
*/
truncate?: boolean;

/**
* Maximum width of the cell
*/
maxWidth?: string | number;
}

export const TableCell = React.forwardRef(
({ children, className, align, truncate = false, maxWidth, …rest }, ref) => (


{children}

)
);

TableCell.displayName = 'TableCell’;

// Table Header Cell component
export interface TableHeaderCellProps extends React.ThHTMLAttributes {
/**
* Text alignment within the header cell
*/
align?: 'left’ | 'center’ | 'right’;

/**
* Whether the column is sortable
* @default false
*/
sortable?: boolean;

/**
* Current sort direction for this column
*/
sortDirection?: 'asc’ | 'desc’ | null;

/**
* Width of the column
*/
width?: string | number;
}

export const TableHeaderCell = React.forwardRef(
(
{ children, className, align, sortable = false, sortDirection, width, …rest },
ref
) => (

{children}
{sortable && (

{sortDirection === 'asc’ ? '▲’ : sortDirection === 'desc’ ? '▼’ : '⇅’}

)}

)
);

TableHeaderCell.displayName = 'TableHeaderCell’;

// Table Footer component
export const TableFooter = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes
>(({ children, className, …rest }, ref) => (


{children}

));

TableFooter.displayName = 'TableFooter’;

// Table Pagination component
export interface TablePaginationProps extends React.HTMLAttributes {
/**
* Total number of items
*/
count: number;

/**
* Number of items per page
* @default 10
*/
rowsPerPage?: number;

/**
* Available options for rows per page
* @default [5, 10, 25]
*/
rowsPerPageOptions?: number[];

/**
* Current page (0-based)
* @default 0
*/
page: number;

/**
* Callback fired when the page changes
*/
onPageChange: (page: number) => void;

/**
* Callback fired when rows per page changes
*/
onRowsPerPageChange?: (rowsPerPage: number) => void;

/**
* Label for the rows per page selector
* @default 'Rows per page:’
*/
labelRowsPerPage?: string;
}

export const TablePagination: React.FC = ({
count,
rowsPerPage = 10,
rowsPerPageOptions = [5, 10, 25],
page,
onPageChange,
onRowsPerPageChange,
labelRowsPerPage = 'Rows per page:’,
className,
…rest
}) => {
const totalPages = Math.ceil(count / rowsPerPage);
const from = page * rowsPerPage + 1;
const to = Math.min(count, (page + 1) * rowsPerPage);

const handlePrevPage = () => {
if (page > 0) {
onPageChange(page – 1);
}
};

const handleNextPage = () => {
if (page < totalPages - 1) { onPageChange(page + 1); } }; const handleRowsPerPageChange = (e: React.ChangeEvent) => {
onRowsPerPageChange?.(Number(e.target.value));
};

return (

{labelRowsPerPage}
{count > 0 ? `${from}-${to} of ${count}` : '0-0 of 0′}

);
};

TablePagination.displayName = 'TablePagination’;
End File# packages/react/src/components/Dialog/Dialog.tsx
import React, { useEffect, useRef } from 'react’;
import { createPortal } from 'react-dom’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./Dialog.scss’;

export interface DialogProps {
/**
* Whether the dialog is open
*/
open: boolean;

/**
* Title of the dialog
*/
title?: React.ReactNode;

/**
* Content of the dialog
*/
children: React.ReactNode;

/**
* Function called when the dialog should close
*/
onClose: () => void;

/**
* Additional content for the dialog’s footer
*/
footer?: React.ReactNode;

/**
* CSS class for the dialog container
*/
className?: string;

/**
* CSS class for the dialog content
*/
contentClassName?: string;

/**
* CSS class for the dialog backdrop
*/
backdropClassName?: string;

/**
* Whether to show a close button in the top-right corner
* @default true
*/
showCloseButton?: boolean;

/**
* The size of the dialog
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’ | 'fullscreen’;

/**
* Whether to close the dialog when the backdrop is clicked
* @default true
*/
closeOnBackdropClick?: boolean;

/**
* Whether to close the dialog when Escape key is pressed
* @default true
*/
closeOnEsc?: boolean;

/**
* The maximum height of the dialog content
*/
maxContentHeight?: string;

/**
* The ID of the dialog
*/
id?: string;

/**
* Whether the dialog is in a loading state
* @default false
*/
loading?: boolean;

/**
* Whether to prevent scrolling of the body when the dialog is open
* @default true
*/
preventBodyScroll?: boolean;
}

export const Dialog: React.FC = ({
open,
title,
children,
onClose,
footer,
className,
contentClassName,
backdropClassName,
showCloseButton = true,
size = 'medium’,
closeOnBackdropClick = true,
closeOnEsc = true,
maxContentHeight,
id,
loading = false,
preventBodyScroll = true,
}) => {
const dialogRef = useRef(null);
const generatedId = useId();
const dialogId = id || `dialog-${generatedId}`;
const dialogTitleId = `${dialogId}-title`;

useEffect(() => {
// Focus the dialog when it opens
if (open && dialogRef.current) {
// Set focus after a small delay to ensure the dialog is fully rendered
const timeoutId = setTimeout(() => {
if (dialogRef.current) {
dialogRef.current.focus();
}
}, 50);

return () => clearTimeout(timeoutId);
}
}, [open]);

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape’ && open && closeOnEsc) {
onClose();
}
};

document.addEventListener(’keydown’, handleKeyDown);
return () => {
document.removeEventListener(’keydown’, handleKeyDown);
};
}, [open, closeOnEsc, onClose]);

useEffect(() => {
if (preventBodyScroll) {
if (open) {
// Store the original overflow style
const originalOverflow = document.body.style.overflow;
// Prevent scrolling on the body
document.body.style.overflow = 'hidden’;

return () => {
// Restore original overflow style
document.body.style.overflow = originalOverflow;
};
}
}
}, [open, preventBodyScroll]);

const handleBackdropClick = (event: React.MouseEvent) => {
if (
closeOnBackdropClick &&
event.target === event.currentTarget
) {
onClose();
}
};

if (!open) return null;

const dialogContent = (

);

// Use createPortal to render the dialog at the end of the document body
return createPortal(dialogContent, document.body);
};
End File# myrta-ds/myrta
import React, { useState, useRef, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./TextField.scss’;

export interface TextFieldProps
extends Omit, 'size’> {
/**
* Label for the text field
*/
label?: string;

/**
* Helper text to be displayed below the text field
*/
helperText?: string;

/**
* Error message to be displayed below the text field
*/
error?: string;

/**
* Size of the text field
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’;

/**
* Whether the text field should take the full width of its container
* @default false
*/
fullWidth?: boolean;

/**
* Icon to display at the start of the text field
*/
startIcon?: React.ReactNode;

/**
* Icon to display at the end of the text field
*/
endIcon?: React.ReactNode;

/**
* Additional CSS class for the text field container
*/
className?: string;

/**
* Additional CSS class for the input element
*/
inputClassName?: string;

/**
* Additional CSS class for the label
*/
labelClassName?: string;

/**
* Whether the text field is loading
* @default false
*/
loading?: boolean;

/**
* Type of the input
* @default 'text’
*/
type?: string;

/**
* Maximum length of text input
*/
maxLength?: number;

/**
* Whether to show character count
* @default false
*/
showCharacterCount?: boolean;
}

export const TextField = React.forwardRef(
(
{
label,
helperText,
error,
size = 'medium’,
fullWidth = false,
startIcon,
endIcon,
className,
inputClassName,
labelClassName,
disabled = false,
loading = false,
type = 'text’,
maxLength,
showCharacterCount = false,
id,
value,
defaultValue,
onChange,
required,
…props
},
ref
) => {
const [inputValue, setInputValue] = useState(
value !== undefined
? String(value)
: defaultValue !== undefined
? String(defaultValue)
: ”
);
const inputRef = useRef(null);
const generatedId = useId();
const inputId = id || `text-field-${generatedId}`;
const helperTextId = `helper-text-${generatedId}`;
const errorId = `error-${generatedId}`;

// Update internal state when controlled value changes
useEffect(() => {
if (value !== undefined) {
setInputValue(String(value));
}
}, [value]);

const handleChange = (e: React.ChangeEvent) => {
const newValue = e.target.value;

// Only update internal state if it’s an uncontrolled component
if (value === undefined) {
setInputValue(newValue);
}

if (onChange) {
onChange(e);
}
};

return (

0,
},
className
)}
>
{label && (

)}

{startIcon && (

{startIcon}

)}

{
// Handle both the ref prop and the ref object
if (ref) {
if (typeof ref === 'function’) {
ref(node);
} else {
ref.current = node;
}
}
inputRef.current = node;
}}
id={inputId}
type={type}
value={value !== undefined ? value : inputValue}
onChange={handleChange}
disabled={disabled || loading}
className={clsx(’mds-text-field__input’, inputClassName)}
aria-invalid={Boolean(error)}
aria-describedby={
error
? errorId
: helperText
? helperTextId
: undefined
}
maxLength={maxLength}
required={required}
{…props}
/>

{endIcon && (

{endIcon}

)}

{loading && (

)}

{error ? (

{error}

) : helperText ? (

{helperText}

) : null}

{(maxLength && showCharacterCount) && (

{inputValue.length}/{maxLength}

)}

);
}
);

TextField.displayName = 'TextField’;
End File# myrta-ds/myrta
import React, { useState, useRef, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./Tabs.scss’;

export interface TabProps extends React.ButtonHTMLAttributes {
/**
* Value of the tab, used for selection
*/
value: string;

/**
* Whether the tab is disabled
* @default false
*/
disabled?: boolean;

/**
* Icon to display before the label
*/
icon?: React.ReactNode;

/**
* Badge or notification indicator
*/
badge?: React.ReactNode;
}

export const Tab = React.forwardRef(
(
{
children,
className,
value,
disabled = false,
icon,
badge,
…rest
},
ref
) => {
// This component is not rendered directly
// It serves as a configuration component for TabsRoot
return null;
}
);

Tab.displayName = 'Tab’;

export interface TabPanelProps extends React.HTMLAttributes {
/**
* Value of the tab this panel is associated with
*/
value: string;

/**
* Additional CSS class
*/
className?: string;
}

export const TabPanel = React.forwardRef(
({ children, className, value, …rest }, ref) => {
// This component is not rendered directly
// It serves as a configuration component for TabsRoot
return null;
}
);

TabPanel.displayName = 'TabPanel’;

export interface TabsProps extends React.HTMLAttributes {
/**
* Current active tab value
*/
value: string;

/**
* Callback when tab changes
*/
onChange: (value: string) => void;

/**
* Orientation of the tabs
* @default 'horizontal’
*/
orientation?: 'horizontal’ | 'vertical’;

/**
* Variant of the tabs
* @default 'default’
*/
variant?: 'default’ | 'enclosed’ | 'pills’;

/**
* Size of the tabs
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’;

/**
* Whether tabs should fill the container width
* @default false
*/
fullWidth?: boolean;

/**
* Whether to center the tabs
* @default false
*/
centered?: boolean;

/**
* Custom CSS class for the tabs container
*/
className?: string;

/**
* Custom CSS class for the tab list
*/
tabListClassName?: string;

/**
* Custom CSS class for the tab panels container
*/
tabPanelsClassName?: string;

/**
* Children should be Tab and TabPanel components
*/
children: React.ReactNode;
}

/**
* A component that displays a set of tabs
*/
export const Tabs = React.forwardRef(
(
{
children,
value,
onChange,
orientation = 'horizontal’,
variant = 'default’,
size = 'medium’,
fullWidth = false,
centered = false,
className,
tabListClassName,
tabPanelsClassName,
…rest
},
ref
) => {
const [tabs, setPanels] = useState([]);
const [panels, setTabs] = useState([]);
const [indicatorStyle, setIndicatorStyle] = useState({});
const tabRefs = useRef>(new Map());
const tabListRef = useRef(null);
const generatedId = useId();

// Separate Tab and TabPanel components
useEffect(() => {
const tabsArray: React.ReactElement[] = [];
const panelsArray: React.ReactElement[] = [];

React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) return;

if (child.type === Tab) {
tabsArray.push(child);
} else if (child.type === TabPanel) {
panelsArray.push(child);
}
});

setPanels(tabsArray);
setTabs(panelsArray);
}, [children]);

// Update indicator position when value changes
useEffect(() => {
const updateIndicator = () => {
const activeTab = tabRefs.current.get(value);
if (!activeTab || !tabListRef.current) return;

const tabRect = activeTab.getBoundingClientRect();
const tabListRect = tabListRef.current.getBoundingClientRect();

if (orientation === 'horizontal’) {
setIndicatorStyle({
left: `${activeTab.offsetLeft}px`,
width: `${tabRect.width}px`,
bottom: '0′,
});
} else {
setIndicatorStyle({
top: `${activeTab.offsetTop}px`,
height: `${tabRect.height}px`,
right: '0′,
});
}
};

updateIndicator();
window.addEventListener(’resize’, updateIndicator);

return () => {
window.removeEventListener(’resize’, updateIndicator);
};
}, [value, orientation, tabs]);

return (


{tabs.map((tab, index) => {
const {
value: tabValue,
disabled,
icon,
badge,
children: tabChildren,
className: tabClassName,
…tabProps
} = tab.props;

const isSelected = value === tabValue;
const tabId = `tab-${generatedId}-${tabValue}`;
const panelId = `panel-${generatedId}-${tabValue}`;

return (

);
})}

{panels.map((panel) => {
const { value: panelValue, children: panelChildren, className: panelClassName, …panelProps } = panel.props;
const isSelected = value === panelValue;
const tabId = `tab-${generatedId}-${panelValue}`;
const panelId = `panel-${generatedId}-${panelValue}`;

if (!isSelected) return null;

return (


{panelChildren}

);
})}

);
}
);

Tabs.displayName = 'Tabs’;
End File# myrta-ds/myrta
# packages/react/src/components/Slider/Slider.tsx
import React, { useRef, useState, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./Slider.scss’;

export interface SliderProps {
/**
* The current value of the slider
*/
value?: number;

/**
* Default value for uncontrolled component
* @default 0
*/
defaultValue?: number;

/**
* Minimum allowed value
* @default 0
*/
min?: number;

/**
* Maximum allowed value
* @default 100
*/
max?: number;

/**
* Step size
* @default 1
*/
step?: number;

/**
* Callback when value changes
*/
onChange?: (value: number) => void;

/**
* Whether the slider is disabled
* @default false
*/
disabled?: boolean;

/**
* Size of the slider
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’;

/**
* Label for the slider
*/
label?: string;

/**
* Whether to show the current value
* @default false
*/
showValue?: boolean;

/**
* Format function for the displayed value
*/
formatValue?: (value: number) => string;

/**
* Mark specific values on the track
*/
marks?: { value: number; label?: string }[];

/**
* Custom CSS class
*/
className?: string;

/**
* Custom CSS class for the track
*/
trackClassName?: string;

/**
* Custom CSS class for the thumb
*/
thumbClassName?: string;

/**
* ID attribute for the slider
*/
id?: string;

/**
* Callback when dragging starts
*/
onDragStart?: () => void;

/**
* Callback when dragging ends
*/
onDragEnd?: () => void;
}

export const Slider = React.forwardRef(
(
{
value: valueProp,
defaultValue = 0,
min = 0,
max = 100,
step = 1,
onChange,
disabled = false,
size = 'medium’,
label,
showValue = false,
formatValue = (value) => value.toString(),
marks,
className,
trackClassName,
thumbClassName,
id,
onDragStart,
onDragEnd,
…props
},
ref
) => {
const [value, setValue] = useState(
valueProp !== undefined ? valueProp : defaultValue
);
const [isDragging, setIsDragging] = useState(false);
const trackRef = useRef(null);
const thumbRef = useRef(null);
const generatedId = useId();
const sliderId = id || `slider-${generatedId}`;
const labelId = `${sliderId}-label`;
const valueId = `${sliderId}-value`;

// Handle controlled component updates
useEffect(() => {
if (valueProp !== undefined) {
setValue(valueProp);
}
}, [valueProp]);

// Calculate percentage for positioning
const percentage = ((value – min) / (max – min)) * 100;

const handleTrackClick = (e: React.MouseEvent) => {
if (disabled || !trackRef.current) return;

const rect = trackRef.current.getBoundingClientRect();
const clickPosition = e.clientX – rect.left;
const percentageClicked = (clickPosition / rect.width) * 100;
const rawValue = min + (percentageClicked / 100) * (max – min);
const snappedValue = Math.round(rawValue / step) * step;
const clampedValue = Math.max(min, Math.min(max, snappedValue));

setValue(clampedValue);
onChange?.(clampedValue);
};

const startDragging = (e: React.MouseEvent | React.TouchEvent) => {
if (disabled) return;

setIsDragging(true);
onDragStart?.();

// Capture initial position
const clientX = 'touches’ in e
? e.touches[0].clientX
: e.clientX;

updateValueFromClientX(clientX);

// Prevent text selection during drag
document.body.style.userSelect = 'none’;
};

const stopDragging = () => {
if (isDragging) {
setIsDragging(false);
onDragEnd?.();
document.body.style.userSelect = ”;
}
};

const updateValueFromClientX = (clientX: number) => {
if (!trackRef.current || !isDragging) return;

const rect = trackRef.current.getBoundingClientRect();
const clickPosition = Math.max(0, Math.min(rect.width, clientX – rect.left));
const percentageClicked = (clickPosition / rect.width) * 100;
const rawValue = min + (percentageClicked / 100) * (max – min);
const snappedValue = Math.round(rawValue / step) * step;
const clampedValue = Math.max(min, Math.min(max, snappedValue));

setValue(clampedValue);
onChange?.(clampedValue);
};

const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
updateValueFromClientX(e.clientX);
}
};

const handleTouchMove = (e: TouchEvent) => {
if (isDragging && e.touches.length > 0) {
updateValueFromClientX(e.touches[0].clientX);
}
};

// Set up event listeners for drag
useEffect(() => {
if (isDragging) {
document.addEventListener(’mousemove’, handleMouseMove);
document.addEventListener(’touchmove’, handleTouchMove);
document.addEventListener(’mouseup’, stopDragging);
document.addEventListener(’touchend’, stopDragging);
}

return () => {
document.removeEventListener(’mousemove’, handleMouseMove);
document.removeEventListener(’touchmove’, handleTouchMove);
document.removeEventListener(’mouseup’, stopDragging);
document.removeEventListener(’touchend’, stopDragging);
};
}, [isDragging]);

// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (disabled) return;

let newValue = value;

switch (e.key) {
case 'ArrowRight’:
case 'ArrowUp’:
newValue = Math.min(max, value + step);
break;
case 'ArrowLeft’:
case 'ArrowDown’:
newValue = Math.max(min, value – step);
break;
case 'Home’:
newValue = min;
break;
case 'End’:
newValue = max;
break;
default:
return;
}

if (newValue !== value) {
e.preventDefault();
setValue(newValue);
onChange?.(newValue);
}
};

return (


{(label || showValue) && (

{label && (

)}
{showValue && (

{formatValue(value)}

)}

)}

{/* Render marks if provided */}
{marks && marks.length > 0 && (

{marks.map((mark) => {
const markPercentage = ((mark.value – min) / (max – min)) * 100;
return (

= mark.value,
})}
style={{ left: `${markPercentage}%` }}
>
{mark.label && (

{mark.label}

)}

);
})}

)}

);
}
);

Slider.displayName = 'Slider’;
End Fileimport React, { useState, useRef, useEffect } from 'react’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import ’./Checkbox.scss’;

export interface CheckboxProps extends Omit, 'size’> {
/**
* Label for the checkbox
*/
label?: React.ReactNode;

/**
* Additional helper text
*/
helperText?: React.ReactNode;

/**
* Error message
*/
error?: React.ReactNode;

/**
* Whether the checkbox is checked
*/
checked?: boolean;

/**
* Default checked state for uncontrolled component
* @default false
*/
defaultChecked?: boolean;

/**
* Whether the checkbox is in an indeterminate state
* @default false
*/
indeterminate?: boolean;

/**
* Whether the checkbox is disabled
* @default false
*/
disabled?: boolean;

/**
* Callback when the checked state changes
*/
onChange?: (checked: boolean, event: React.ChangeEvent) => void;

/**
* Size of the checkbox
* @default 'medium’
*/
size?: 'small’ | 'medium’ | 'large’;

/**
* Additional CSS class for the checkbox container
*/
className?: string;

/**
* Additional CSS class for the checkbox input element
*/
inputClassName?: string;

/**
* Additional CSS class for the checkbox label
*/
labelClassName?: string;

/**
* Name attribute for the input element
*/
name?: string;

/**
* ID attribute for the input element
*/
id?: string;

/**
* Whether the checkbox is in a loading state
* @default false
*/
loading?: boolean;
}

export const Checkbox = React.forwardRef(
(
{
label,
helperText,
error,
checked,
defaultChecked = false,
indeterminate = false,
disabled = false,
onChange,
size = 'medium’,
className,
inputClassName,
labelClassName,
name,
id,
loading = false,
…props
},
ref
) => {
const [isChecked, setIsChecked] = useState(
checked !== undefined ? checked : defaultChecked
);
const inputRef = useRef(null);
const generatedId = useId();
const checkboxId = id || `checkbox-${generatedId}`;
const helperTextId = `helper-text-${generatedId}`;
const errorId = `error-${generatedId}`;

// Update internal state when controlled value changes
useEffect(() => {
if (checked !== undefined) {
setIsChecked(checked);
}
}, [checked]);

// Set the indeterminate prop
useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);

const handleChange = (event: React.ChangeEvent) => {
if (disabled || loading) return;

const newChecked = event.target.checked;

// Only update internal state for uncontrolled component
if (checked === undefined) {
setIsChecked(newChecked);
}

if (onChange) {
onChange(newChecked, event);
}
};

return (

{
// Handle both the ref prop and the ref object
if (ref) {
if (typeof ref === 'function’) {
ref(node);
} else {
ref.current = node;
}
}
inputRef.current = node;
}}
type=”checkbox”
id={checkboxId}
name={name}
checked={isChecked}
onChange={handleChange}
disabled={disabled || loading}
className={clsx(’mds-checkbox__input’, inputClassName)}
aria-invalid={Boolean(error)}
aria-describedby={
error
? errorId
: helperText
? helperTextId
: undefined
}
{…props}
/>

{loading &&

}

{label && (

)}

{error ? (

{error}

) : helperText ? (

{helperText}

) : null}

);
}
);

Checkbox.displayName = 'Checkbox’;
End File# myrta-ds/myrta
# packages/react/src/components/Menu/Menu.tsx
import React, { useState, useRef, useEffect } from 'react’;
import { createPortal } from 'react-dom’;
import clsx from 'clsx’;
import { useId } from ’../../hooks/useId’;
import { useClickOutside } from ’../../hooks/useClickOutside’;
import ’./Menu.scss’;

export interface MenuProps {
/**
* The trigger element
*/
trigger: React.ReactElement;

/**
* The content of the menu
*/
children: React.ReactNode;

/**
* Whether the menu is open
*/
open?: boolean;

/**
* Callback when the menu should open or close
*/
onOpenChange?: (open: boolean) => void;

/**
* Position of the menu relative to the trigger
* @default 'bottom-start’
*/
placement?: 'top’ | 'top-start’ | 'top-end’ | 'bottom’ | 'bottom-start’ | 'bottom-end’ | 'left’ | 'right’;

/**
* Custom CSS class for the menu
*/
className?: string;

/**
* Width of the menu
*/
width?: number | string;

/**
* Maximum height of the menu before scrolling
*/
maxHeight?: number | string;

/**
* Whether to close the menu when an item is clicked
* @default true
*/
closeOnItemClick?: boolean;

/**
* CSS z-index for the menu
* @default 1000
*/
zIndex?: number;

/**
* Whether to match the width of the menu with the trigger
* @default false
*/
matchWidth?: boolean;

/**
* Distance between the menu and the trigger
* @default 4
*/
offset?: number;
}

export interface MenuItemProps extends React.HTMLAttributes {
/**
* Whether the item is disabled
* @default false
*/
disabled?: boolean;

/**
* Icon to display before item text
*/
icon?: React.ReactNode;

/**
* Custom CSS class for the menu item
*/
className?: string;
}

export const MenuItem: React.FC = ({
children,
disabled = false,
icon,
className,
onClick,
…rest
}) => {
const menuContext = useMenuContext();

const handleClick = (e: React.MouseEvent) => {
if (disabled) {
e.preventDefault();
return;
}

// Call the original onClick handler
onClick?.(e);

// Close the menu if closeOnItemClick is true
if (menuContext?.closeOnItemClick) {
menuContext.onClose();
}
};

return (


  • {icon && {icon}}
    {children}
  • );
    };

    MenuItem.displayName = 'MenuItem’;

    export interface MenuDividerProps extends React.HTMLAttributes {
    /**
    * Custom CSS class for the divider
    */
    className?: string;
    }

    export const MenuDivider: React.FC = ({
    className,
    …rest
    }) => (


  • );

    MenuDivider.displayName = 'MenuDivider’;

    export interface MenuGroupProps extends React.HTMLAttributes {
    /**
    * Group title
    */
    title?: React.ReactNode;

    /**
    * Custom CSS class for the group
    */
    className?: string;
    }

    export const MenuGroup: React.FC = ({
    title,
    children,
    className,
    …rest
    }) => (

    {title &&

    {title}

    }

      {children}

    );

    MenuGroup.displayName = 'MenuGroup’;

    // Create a context for the menu
    interface MenuContextValue {
    closeOnItemClick: boolean;
    onClose: () => void;
    }

    const MenuContext = React.createContext(null);

    const useMenuContext = () => React.useContext(MenuContext);

    export const Menu: React.FC = ({
    trigger,
    children,
    open: openProp,
    onOpenChange,
    placement = 'bottom-start’,
    className,
    width,
    maxHeight,
    closeOnItemClick = true,
    zIndex = 1000,
    matchWidth = false,
    offset = 4,
    }) => {
    const [isOpen, setIsOpen] = useState(Boolean(openProp));
    const [menuStyle, setMenuStyle] = useState({});
    const triggerRef = useRef(null);
    const menuRef = useRef(null);
    const generatedId = useId();
    const menuId = `menu-${generatedId}`;

    // Set up click outside handling
    useClickOutside(menuRef, {
    active: isOpen,
    onClickOutside: () => {
    setIsOpen(false);
    onOpenChange?.(false);
    },
    });

    // Handle controlled component
    useEffect(() => {
    if (openProp !== undefined) {
    setIsOpen(openProp);
    }
    }, [openProp]);

    // Calculate and set menu position
    const updateMenuPosition = () => {
    if (!triggerRef.current || !menuRef.current) return;

    const triggerRect = triggerRef.current.getBoundingClientRect();
    const menuRect = menuRef.current.getBoundingClientRect();
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;

    let top, left;

    // Calculate position based on placement
    switch (placement) {
    case 'top’:
    top = triggerRect.top – menuRect.height – offset;
    left = triggerRect.left + triggerRect.width / 2 – menuRect.width / 2;
    break;
    case 'top-start’:
    top = triggerRect.top – menuRect.height – offset;
    left = triggerRect.left;
    break;
    case 'top-end’:
    top = triggerRect.top – menuRect.height – offset;
    left = triggerRect.right – menuRect.width;
    break;
    case 'bottom’:
    top = triggerRect.bottom + offset;
    left = triggerRect.left + triggerRect.width / 2 – menuRect.width / 2;
    break;
    case 'bottom-start’:
    top = triggerRect.bottom + offset;
    left = triggerRect.left;
    break;
    case 'bottom-end’:
    top = triggerRect.bottom + offset;
    left = triggerRect.right – menuRect.width;
    break;
    case 'left’:
    top = triggerRect.top + triggerRect.height / 2 – menuRect.height / 2;
    left = triggerRect.left – menuRect.width – offset;
    break;
    case 'right’:
    top = triggerRect.top + triggerRect.height / 2 – menuRect.height / 2;
    left = triggerRect.right + offset;
    break;
    default:
    top = triggerRect.bottom + offset;
    left = triggerRect.left;
    }

    // Adjust for viewport boundaries
    if (left < 0) left = 0; if (left + menuRect.width > viewportWidth) left = viewportWidth – menuRect.width;
    if (top < 0) top = 0; if (top + menuRect.height > viewportHeight) top = viewportHeight – menuRect.height;

    const style: React.CSSProperties = {
    position: 'fixed’,
    top: `${top}px`,
    left: `${left}px`,
    zIndex,
    };

    if (width) {
    style.width = typeof width === 'number’ ? `${width}px` : width;
    } else if (matchWidth) {
    style.width = `${triggerRect.width}px`;
    }

    if (maxHeight) {
    style.maxHeight = typeof maxHeight === 'number’ ? `${maxHeight}px` : maxHeight;
    }

    setMenuStyle(style);
    };

    // Update position when menu opens
    useEffect(() => {
    if (isOpen) {
    updateMenuPosition();

    // Update position on resize/scroll
    window.addEventListener(’resize’, updateMenuPosition);
    window.addEventListener(’scroll’, updateMenuPosition);
    }

    return () => {
    window.removeEventListener(’resize’, updateMenuPosition);
    window.removeEventListener(’scroll’, updateMenuPosition);
    };
    }, [isOpen]);

    // Toggle menu
    const toggleMenu = () => {
    const newState = !isOpen;
    setIsOpen(newState);
    onOpenChange?.(newState);
    };

    // Close menu
    const closeMenu = () => {
    setIsOpen(false);
    onOpenChange?.(false);
    };

    // Clone trigger with ref and event handlers
    const triggerElement = React.cloneElement(trigger, {
    ref: (node: HTMLElement | null) => {
    triggerRef.current = node;
    // Forward ref if the child has one
    const { ref } = trigger as any;
    if (typeof ref === 'function’) ref(node);
    else if (ref) ref.current = node;
    },
    onClick: (e: React.MouseEvent) => {
    toggleMenu();
    // Call the original onClick if it exists
    if (trigger.props.onClick) trigger.props.onClick(e);
    },
    'aria-haspopup’: 'menu’,
    'aria-expanded’: isOpen,
    'aria-controls’: isOpen ? menuId : undefined,
    });

    return (
    <>
    {triggerElement}

    {isOpen && createPortal(

    ,
    document.body
    )}

    );
    };

    Menu.displayName = 'Menu’;
    End File# packages/react/src/components/Avatar/Avatar.tsx
    import React, { useState } from 'react’;
    import clsx from 'clsx’;
    import ’./Avatar.scss’;

    // Status types for avatar
    type AvatarStatus = 'online’ | 'offline’ | 'away’ | 'busy’ | 'invisible’;

    // Size variants for avatar
    type AvatarSize = 'xs’ | 'small’ | 'medium’ | 'large’ | 'xl’;

    // Shape variants for avatar
    type AvatarShape = 'circle’ | 'square’ | ’rounded’;

    export interface AvatarProps extends React.HTMLAttributes {
    /**
    * Image source for the avatar
    */
    src?: string;

    /**
    * Alt text for the avatar image
    */
    alt?: string;

    /**
    * Text to display when no image is available
    */
    name?: string;

    /**
    * Size of the avatar
    * @default 'medium’
    */
    size?: AvatarSize;

    /**
    * Shape of the avatar
    * @default 'circle’
    */
    shape?: AvatarShape;

    /**
    * Status indicator for the avatar
    */
    status?: AvatarStatus;

    /**
    * Whether to show a border around the avatar
    * @default false
    */
    bordered?: boolean;

    /**
    * Background color for the avatar when displaying initials
    */
    backgroundColor?: string;

    /**
    * Text color for the avatar when displaying initials
    */
    textColor?: string;

    /**
    * Custom CSS class
    */
    className?: string;

    /**
    * Optional icon to display instead of initials when no image is available
    */
    icon?: React.ReactNode;

    /**
    * Callback when avatar fails to load
    */
    onError?: (e: React.SyntheticEvent) => void;

    /**
    * Callback when avatar is clicked
    */
    onClick?: (e: React.MouseEvent) => void;
    }

    export const Avatar = React.forwardRef(
    (
    {
    src,
    alt,
    name,
    size = 'medium’,
    shape = 'circle’,
    status,
    bordered = false,
    backgroundColor,
    textColor,
    className,
    icon,
    onError,
    onClick,
    style,
    …rest
    },
    ref
    ) => {
    const [imgError, setImgError] = useState(false);

    // Generate initials from name
    const getInitials = () => {
    if (!name) return ”;

    return name
    .split(’ ’)
    .map(n => n[0])
    .join(”)
    .toUpperCase()
    .substring(0, 2);
    };

    // Handle image load error
    const handleError = (e: React.SyntheticEvent) => {
    setImgError(true);
    onError?.(e);
    };

    // Combine styles
    const avatarStyle = {
    …style,
    …(backgroundColor && !src && { backgroundColor }),
    …(textColor && !src && { color: textColor }),
    };

    // Determine if we should render the image
    const shouldRenderImage = src && !imgError;

    return (


    {shouldRenderImage ? (
    {alt
    ) : icon ? (

    {icon}

    ) : (

    {getInitials()}

    )}

    {status && (

    )}

    );
    }
    );

    Avatar.displayName = 'Avatar’;
    End File# packages/react/src/components/Card/Card.tsx
    import React from 'react’;
    import clsx from 'clsx’;
    import ’./Card.scss’;

    export interface CardProps extends React.HTMLAttributes {
    /**
    * Variant of the card
    * @default 'outlined’
    */
    variant?: 'outlined’ | 'elevated’ | 'filled’;

    /**
    * Whether the card has a hover effect
    * @default false
    */
    hoverable?: boolean;

    /**
    * Whether the card is clickable
    * @default false
    */
    clickable?: boolean;

    /**
    * Whether the card is disabled
    * @default false
    */
    disabled?: boolean;

    /**
    * Additional CSS class for the card
    */
    className?: string;
    }

    export const Card = React.forwardRef(
    (
    {
    children,
    variant = 'outlined’,
    hoverable = false,
    clickable = false,
    disabled = false,
    className,
    …rest
    },
    ref
    ) => (


    {children}

    )
    );

    Card.displayName = 'Card’;

    export interface CardHeaderProps extends React.HTMLAttributes {
    /**
    * Title of the card
    */
    title?: React.ReactNode;

    /**
    * Subtitle of the card
    */
    subtitle?: React.ReactNode;

    /**
    * Action element to display in the header
    */
    action?: React.ReactNode;

    /**
    * Avatar or icon to display in the header
    */
    avatar?: React.ReactNode;

    /**
    * Additional CSS class for the header
    */
    className?: string;
    }

    export const CardHeader = React.forwardRef(
    (
    {
    title,
    subtitle,
    action,
    avatar,
    className,
    children,
    …rest
    },
    ref
    ) => (


    {avatar &&

    {avatar}

    }

    {title &&

    {title}

    }
    {subtitle &&

    {subtitle}

    }
    {children}

    {action &&

    {action}

    }

    )
    );

    CardHeader.displayName = 'CardHeader’;

    export interface CardContentProps extends React.HTMLAttributes {
    /**
    * Additional CSS class for the content
    */
    className?: string;
    }

    export const CardContent = React.forwardRef(
    ({ className, children, …rest }, ref) => (


    {children}

    )
    );

    CardContent.displayName = 'CardContent’;

    export interface CardFooterProps extends React.HTMLAttributes {
    /**
    * Additional CSS class for the footer
    */
    className?: string;

    /**
    * Align the footer content
    * @default 'left’
    */
    align?: 'left’ | 'center’ | 'right’ | 'space-between’;
    }

    export const CardFooter = React.forwardRef(
    (
    {
    className,
    align = 'left’,
    children,
    …rest
    },
    ref
    ) => (


    {children}

    )
    );

    CardFooter.displayName = 'CardFooter’;

    export interface CardMediaProps extends React.HTMLAttributes {
    /**
    * Image source
    */
    image?: string;

    /**
    * Alt text for the image
    */
    alt?: string;

    /**
    * Height of the media area
    */
    height?: number | string;

    /**
    * Position of the background image
    * @default 'center’
    */
    position?: string;

    /**
    * Title overlay on the media
    */
    title?: React.ReactNode;

    /**
    * Additional CSS class for the media
    */
    className?: string;
    }

    export const CardMedia = React.forwardRef(
    (
    {
    image,
    alt,
    height = 200,
    position = 'center’,
    title,
    className,
    children,
    style,
    …rest
    },
    ref
    ) => {
    const mediaStyle = {
    …style,
    height: typeof height === 'number’ ? `${height}px` : height,
    backgroundImage: image ? `url(${image})` : undefined,
    backgroundPosition: position,
    };

    return (


    {children}
    {title &&

    {title}

    }

    );
    }
    );

    CardMedia.displayName = 'CardMedia’;

    export interface CardDividerProps extends React.HTMLAttributes {
    /**
    * Whether the divider is vertical
    * @default false
    */
    vertical?: boolean;

    /**
    * Additional CSS class for the divider
    */
    className?: string;
    }

    export const CardDivider = React.forwardRef(
    ({ vertical = false, className, …rest }, ref) => (



    )
    );

    CardDivider.displayName = 'CardDivider’;
    End Fileimport { Component, ElementType, HTMLAttributes, MouseEvent, ReactNode } from 'react’;
    import clsx from 'clsx’;

    interface SpinnerProps extends HTMLAttributes {
    /**
    * Custom spinner size
    */
    size?: 'small’ | 'medium’ | 'large’;

    /**
    * Change the color of the spinner
    */
    color?: string;

    /**
    * Display a text message along the spinner
    */
    message?: string;

    /**
    * Custom classes to apply to the spinner
    */
    className?: string;

    /**
    * Display the spinner as an inline element
    */
    inline?: boolean;

    /**
    * Whether to center the spinner
    */
    centered?: boolean;

    /**
    * Spinner overlay mode with backdrop
    */
    overlay?: boolean;

    /**
    * Custom icon for the spinner
    */
    customIcon?: ReactNode;

    /**
    * Icon to display instead of spinner
    */
    icon?: string;

    /**
    * Whether the spinner should take up the entire height of the parent
    */
    fullHeight?: boolean;

    /**
    * Aria label for accessibility
    */
    'aria-label’?: string;

    /**
    * Component to render as
    */
    as?: ElementType;

    /**
    * Whether the spinner should spin faster
    */
    speedUp?: boolean;

    /**
    * Called when spinner is clicked
    */
    onClick?: (event: MouseEvent) => void;
    }

    class Spinner extends Component {
    render() {
    const {
    size = 'medium’,
    color,
    message,
    className,
    inline = false,
    centered = false,
    overlay = false,
    customIcon,
    icon,
    fullHeight = false,
    'aria-label’: ariaLabel = 'Loading’,
    as: Component = 'div’,
    speedUp = false,
    onClick,
    …rest
    } = this.props;

    const spinnerClasses = clsx(
    'mds-spinner’,
    `mds-spinner–${size}`,
    {
    'mds-spinner–inline’: inline,
    'mds-spinner–centered’: centered,
    'mds-spinner–overlay’: overlay,
    'mds-spinner–with-message’: !!message,
    'mds-spinner–full-height’: fullHeight,
    'mds-spinner–speed-up’: speedUp,
    },
    className
    );

    const spinnerStyle = color ? { ’–spinner-color’: color } as React.CSSProperties : undefined;

    return (

    {customIcon ? (

    {customIcon}

    ) : icon ? (

    {icon}

    ) : (

    )}
    {message &&

    {message}

    }
    {ariaLabel}


    );
    }
    }

    export { Spinner, SpinnerProps };
    End File# myrta-ds/myrta
    # packages/react/src/components/Radio/Radio.tsx
    import React, { useState, useRef, useEffect } from 'react’;
    import clsx from 'clsx’;
    import { useId } from ’../../hooks/useId’;
    import ’./Radio.scss’;

    export interface RadioProps
    extends Omit, 'size’> {
    /**
    * Label for the radio
    */
    label?: React.ReactNode;

    /**
    * Helper text to display
    */
    helperText?: React.ReactNode;

    /**
    * Error message to display
    */
    error?: React.ReactNode;

    /**
    * Whether the radio is checked
    */
    checked?: boolean;

    /**
    * Default checked state for uncontrolled component
    * @default false
    */
    defaultChecked?: boolean;

    /**
    * Whether the radio is disabled
    * @default false
    */
    disabled?: boolean;

    /**
    * Callback when checked state changes
    */
    onChange?: (checked: boolean, event: React.ChangeEvent) => void;

    /**
    * Size of the radio
    * @default 'medium’
    */
    size?: 'small’ | 'medium’ | 'large’;

    /**
    * CSS class for the radio container
    */
    className?: string;

    /**
    * CSS class for the radio input
    */
    inputClassName?: string;

    /**
    * CSS class for the radio label
    */
    labelClassName?: string;

    /**
    * Name for the radio input
    */
    name?: string;

    /**
    * ID for the radio input
    */
    id?: string;

    /**
    * Whether the radio is in a loading state
    * @default false
    */
    loading?: boolean;

    /**
    * Value of the radio
    */
    value?: string | number;
    }

    export const Radio = React.forwardRef(
    (
    {
    label,
    helperText,
    error,
    checked,
    defaultChecked = false,
    disabled = false,
    onChange,
    size = 'medium’,
    className,
    inputClassName,
    labelClassName,
    name,
    id,
    loading = false,
    value,
    …props
    },
    ref
    ) => {
    const [isChecked, setIsChecked] = useState(
    checked !== undefined ? checked : defaultChecked
    );
    const inputRef = useRef(null);
    const generatedId = useId();
    const radioId = id || `radio-${generatedId}`;
    const helperTextId = `helper-text-${generatedId}`;
    const errorId = `error-${generatedId}`;

    // Update internal state when controlled value changes
    useEffect(() => {
    if (checked !== undefined) {
    setIsChecked(checked);
    }
    }, [checked]);

    const handleChange = (event: React.ChangeEvent) => {
    if (disabled || loading) return;

    const newChecked = event.target.checked;

    // Only update internal state for uncontrolled component
    if (checked === undefined) {
    setIsChecked(newChecked);
    }

    if (onChange) {
    onChange(newChecked, event);
    }
    };

    return (

    {
    // Handle both the ref prop and the ref object
    if (ref) {
    if (typeof ref === 'function’) {
    ref(node);
    } else {
    ref.current = node;
    }
    }
    inputRef.current = node;
    }}
    type=”radio”
    id={radioId}
    name={name}
    checked={isChecked}
    onChange={handleChange}
    disabled={disabled || loading}
    className={clsx(’mds-radio__input’, inputClassName)}
    aria-invalid={Boolean(error)}
    aria-describedby={
    error
    ? errorId
    : helperText
    ? helperTextId
    : undefined
    }
    value={value}
    {…props}
    />

    {loading &&

    }

    {label && (

    )}

    {error ? (

    {error}

    ) : helperText ? (

    {helperText}

    ) : null}

    );
    }
    );

    Radio.displayName = 'Radio’;

    export interface RadioGroupProps extends Omit, 'onChange’> {
    /**
    * Name attribute to be applied to all radios
    */
    name: string;

    /**
    * Value of the selected radio
    */
    value?: string | number;

    /**
    * Default value for uncontrolled component
    */
    defaultValue?: string | number;

    /**
    * Called when a radio is selected
    */
    onChange?: (value: string | number, event: React.ChangeEvent) => void;

    /**
    * CSS class for the radio group
    */
    className?: string;

    /**
    * Orientation of the radio group
    * @default 'vertical’
    */
    orientation?: 'horizontal’ | 'vertical’;

    /**
    * Whether the radio group is disabled
    * @default false
    */
    disabled?: boolean;

    /**
    * Error message to display
    */
    error?: React.ReactNode;

    /**
    * Label for the radio group
    */
    label?: React.ReactNode;

    /**
    * CSS class for the label
    */
    labelClassName?: string;

    /**
    * Size for all radios in the group
    * @default 'medium’
    */
    size?: 'small’ | 'medium’ | 'large’;

    /**
    * Whether the radio group is required
    * @default false
    */
    required?: boolean;
    }

    export const RadioGroup = React.forwardRef(
    (
    {
    children,
    name,
    value,
    defaultValue,
    onChange,
    className,
    orientation = 'vertical’,
    disabled = false,
    error,
    label,
    labelClassName,
    size = 'medium’,
    required = false,
    …rest
    },
    ref
    ) => {
    const [selectedValue, setSelectedValue] = useState(
    value !== undefined ? value : defaultValue
    );
    const generatedId = useId();
    const groupId = `radio-group-${generatedId}`;
    const errorId = `radio-group-error-${generatedId}`;

    // Update internal state when controlled value changes
    useEffect(() => {
    if (value !== undefined) {
    setSelectedValue(value);
    }
    }, [value]);

    const handleChange = (checked: boolean, event: React.ChangeEvent) => {
    if (checked) {
    const newValue = event.target.value;

    // Only update internal state for uncontrolled component
    if (value === undefined) {
    setSelectedValue(newValue);
    }

    if (onChange) {
    onChange(newValue, event);
    }
    }
    };

    // Clone children to pass props
    const radioButtons = React.Children.map(children, (child) => {
    if (!React.isValidElement(child)) return child;

    // Only modify Radio components
    if (child.type === Radio) {
    return React.cloneElement(child, {
    name,
    size,
    disabled: disabled || child.props.disabled,
    checked: child.props.value === selectedValue,
    onChange: handleChange,
    });
    }

    return child;
    });

    return (


    {label && (

    {label}
    {required && *}

    )}

    {radioButtons}

    {error && (

    {error}

    )}

    );
    }
    );

    RadioGroup.displayName = 'RadioGroup’;
    End Fileimport React from 'react’;
    import clsx from 'clsx’;
    import ’./Badge.scss’;

    export interface BadgeProps extends React.HTMLAttributes {
    /**
    * Content to be displayed inside the badge
    */
    content?: React.ReactNode;

    /**
    * Maximum count to show (e.g., 99+)
    * @default 99
    */
    max?: number;

    /**
    * Badge color variant
    * @default 'primary’
    */
    variant?: 'primary’ | 'secondary’ | 'success’ | 'warning’ | 'error’ | 'info’;

    /**
    * Badge size
    * @default 'medium’
    */
    size?: 'small’ | 'medium’ | 'large’;

    /**
    * Whether the badge is a dot style (without content)
    * @default false
    */
    dot?: boolean;

    /**
    * Position of the badge relative to its children
    * @default 'top-right’
    */
    position?: 'top-right’ | 'top-left’ | 'bottom-right’ | 'bottom-left’;

    /**
    * Whether to show the badge even when content is zero
    * @default false
    */
    showZero?: boolean;

    /**
    * Whether the badge should be visible
    * @default true
    */
    visible?: boolean;

    /**
    * Horizontal offset of the badge
    * @default 0
    */
    offsetX?: number;

    /**
    * Vertical offset of the badge
    * @default 0
    */
    offsetY?: number;

    /**
    * Whether the badge is standalone (not positioned relative to children)
    * @default false
    */
    standalone?: boolean;

    /**
    * Elements that the badge wraps
    */
    children?: React.ReactNode;
    }

    export const Badge = React.forwardRef(
    (
    {
    content,
    max = 99,
    variant = 'primary’,
    size = 'medium’,
    dot = false,
    position = 'top-right’,
    showZero = false,
    visible = true,
    offsetX = 0,
    offsetY = 0,
    standalone = false,
    children,
    className,
    style,
    …rest
    },
    ref
    ) => {
    // Determine if the badge should be shown
    const shouldShowBadge = () => {
    if (!visible) return false;
    if (dot) return true;
    if (content === 0 || content === '0′) return showZero;
    return Boolean(content);
    };

    // Format the content for the badge
    const formattedContent = () => {
    if (dot) return null;
    if (typeof content === 'number’ && content > max) {
    return `${max}+`;
    }
    return content;
    };

    // Create style with offsets
    const badgeStyle = {
    …style,
    …(offsetX && { ’–badge-offset-x’: `${offsetX}px` }),
    …(offsetY && { ’–badge-offset-y’: `${offsetY}px` }),
    };

    // For standalone badge, just render the badge itself
    if (standalone) {
    return (

    {formattedContent()}

    );
    }

    // For badge with children, render the badge positioned relative to children
    return (

    {children}
    {shouldShowBadge() && (

    {formattedContent()}

    )}

    );
    }
    );

    Badge.displayName = 'Badge’;
    End Fileimport React, { useId } from 'react’;
    import clsx from 'clsx’;

    // Icon type definition
    export type IconName =
    | 'alert-circle’
    | 'alert-triangle’
    | 'arrow-down’
    | 'arrow-left’
    | 'arrow-right’
    | 'arrow-up’
    | 'bell’
    | 'calendar’
    | 'check’
    | 'check-circle’
    | 'chevron-down’
    | 'chevron-left’
    | 'chevron-right’
    | 'chevron-up’
    | 'clock’
    | 'close’
    | 'download’
    | 'edit’
    | 'eye’
    | 'eye-off’
    | 'file’
    | 'folder’
    | 'globe’
    | 'heart’
    | 'home’
    | 'info’
    | 'link’
    | 'lock’
    | 'mail’
    | 'menu’
    | 'message’
    | 'more-horizontal’
    | 'more-vertical’
    | 'phone’
    | 'plus’
    | 'search’
    | 'settings’
    | 'share’
    | 'star’
    | 'trash’
    | 'unlock’
    | 'upload’
    | 'user’
    | 'users’
    | 'x’;

    // Icon size definition
    export type IconSize = 'small’ | 'medium’ | 'large’ | number;

    // Icon props interface
    export interface IconProps {
    /**
    * Name of the icon
    */
    name: IconName;

    /**
    * Size of the icon
    * @default 'medium’
    */
    size?: IconSize;

    /**
    * Color of the icon
    * @default 'currentColor’
    */
    color?: string;

    /**
    * Additional CSS class
    */
    className?: string;

    /**
    * ARIA label for accessibility
    */
    'aria-label’?: string;

    /**
    * ARIA hidden attribute
    * @default false when aria-label is provided, otherwise true
    */
    'aria-hidden’?: boolean;

    /**
    * Viewbox for the SVG
    * @default '0 0 24 24′
    */
    viewBox?: string;

    /**
    * Custom stroke width
    * @default 2
    */
    strokeWidth?: number;

    /**
    * HTML title for the icon
    */
    title?: string;

    /**
    * Whether the icon should spin
    * @default false
    */
    spin?: boolean;

    /**
    * Whether the icon should pulse
    * @default false
    */
    pulse?: boolean;

    /**
    * Custom CSS style
    */
    style?: React.CSSProperties;

    /**
    * Custom SVG path
    */
    customPath?: string;

    /**
    * Custom SVG content
    */
    customSvg?: React.ReactNode;

    /**
    * onClick event handler
    */
    onClick?: (event: React.MouseEvent) => void;
    }

    // Icon component
    export const Icon: React.FC = ({
    name,
    size = 'medium’,
    color = 'currentColor’,
    className,
    'aria-label’: ariaLabel,
    'aria-hidden’: ariaHiddenProp,
    viewBox = '0 0 24 24′,
    strokeWidth = 2,
    title,
    spin = false,
    pulse = false,
    style,
    customPath,
    customSvg,
    onClick,
    …props
    }) => {
    // Generate unique IDs for accessibility
    const titleId = useId();

    // Determine if the icon should be hidden from screen readers
    const ariaHidden = ariaHiddenProp ?? (ariaLabel ? false : true);

    // Convert size to pixel value if needed
    const getSizeInPixels = () => {
    if (typeof size === 'number’) return size;

    switch (size) {
    case 'small’: return 16;
    case 'large’: return 32;
    case 'medium’:
    default: return 24;
    }
    };

    // Get the icon path based on name
    const getIconPath = (): string => {
    if (customPath) return customPath;

    // Return path data based on icon name
    switch (name) {
    case 'alert-circle’:
    return 'M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v7h-2V7zm0 8h2v2h-2v-2z’;

    case 'alert-triangle’:
    return 'M12.866 3l9.526 16.5a1 1 0 0 1-.866 1.5H2.474a1 1 0 0 1-.866-1.5L11.134 3a1 1 0 0 1 1.732 0zM11 16v2h2v-2h-2zm0-7v5h2V9h-2z’;

    case 'arrow-down’:
    return 'M12 4v12.25L17.25 11l1.42 1.41L12 19.08l-6.67-6.67L6.75 11 12 16.25V4h2z’;

    case 'arrow-left’:
    return 'M7.75 12L13 6.75 11.59 5.34 4.92 12l6.67 6.66L13 17.25 7.75 12z’;

    case 'arrow-right’:
    return 'M16.25 12L11 17.25l1.41 1.41L19.08 12l-6.67-6.66L11 6.75 16.25 12z’;

    case 'arrow-up’:
    return 'M12 20V7.75L6.75 13 5.34 11.59 12 4.92l6.66 6.67L17.25 13 12 7.75V20h-2z’;

    case 'bell’:
    return 'M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z’;

    case 'calendar’:
    return 'M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2zM7 12h5v5H7v-5z’;

    case 'check’:
    return 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z’;

    case 'check-circle’:
    return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z’;

    case 'chevron-down’:
    return 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z’;

    case 'chevron-left’:
    return 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z’;

    case 'chevron-right’:
    return 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z’;

    case 'chevron-up’:
    return 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z’;

    case 'clock’:
    return 'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z’;

    case 'close’:
    return 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z’;

    case 'download’:
    return 'M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z’;

    case 'edit’:
    return 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z’;

    case 'eye’:
    return 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z’;

    case 'eye-off’:
    return 'M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z’;

    case 'file’:
    return 'M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z’;

    case 'folder’:
    return 'M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z’;

    case 'globe’:
    return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z’;

    case 'heart’:
    return 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z’;

    case 'home’:
    return 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z’;

    case 'info’:
    return 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z’;

    case 'link’:
    return 'M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z’;

    case 'lock’:
    return 'M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z’;

    case 'mail’:
    return 'M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z’;

    case 'menu’:
    return 'M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z’;

    case 'message’:
    return 'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z’;

    case 'more-horizontal’:
    return 'M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z’;

    case 'more-vertical’:
    return 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z’;

    case 'phone’:
    return 'M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z’;

    case 'plus’:
    return 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z’;

    case 'search’:
    return 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z’;

    case 'settings’:
    return 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z’;

    case 'share’:
    return 'M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z’;

    case 'star’:
    return 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z’;

    case 'trash’:
    return 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z’;

    case 'unlock’:
    return 'M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z’;

    case 'upload’:
    return 'M5 20h14v-2H5v2zm0-10h4v7h6v-7h4l-7-7-7 7z’;

    case 'user’:
    return 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z’;

    case 'users’:
    return 'M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z’;

    case 'x’:
    return 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z’;

    default:
    return ”;
    }
    };

    // Style object with size and animation properties
    const sizeInPixels = getSizeInPixels();
    const iconStyle: React.CSSProperties = {
    …style,
    width: sizeInPixels,
    height: sizeInPixels,
    color,
    };

    // Default SVG attributes
    const svgProps = {
    xmlns: 'http://www.w3.org/2000/svg’,
    viewBox,
    width: sizeInPixels,
    height: sizeInPixels,
    fill: 'currentColor’,
    stroke: 'none’,
    className: clsx(
    'mds-icon’,
    {
    'mds-icon–spin’: spin,
    'mds-icon–pulse’: pulse,
    },
    className
    ),
    style: iconStyle,
    'aria-hidden’: ariaHidden,
    'aria-labelledby’: title ? titleId : undefined,
    onClick,
    …props,
    };

    // Render the icon
    return (
    {title && {title}} {customSvg || }
    );
    };
    End File# packages/react/rollup.config.mjs
    import resolve from '@rollup/plugin-node-resolve’;
    import commonjs from '@rollup/plugin-commonjs’;
    import typescript from '@rollup/plugin-typescript’;
    import terser from '@rollup/plugin-terser’;
    import external from 'rollup-plugin-peer-deps-external’;
    import dts from 'rollup-plugin-dts’;
    import sass from 'rollup-plugin-sass’;
    import autoprefixer from 'autoprefixer’;
    import postcss from 'postcss’;
    import url from '@rollup/plugin-url’;
    import { readFileSync } from 'fs’;

    // Parse package.json
    const packageJson = JSON.parse(readFileSync(’./package.json’, 'utf8′));

    // Define output directory
    const outputDir = 'dist’;
    const createPath = path => `${outputDir}/${path}`;

    // Configure SASS processing
    const sassOptions = {
    // Process and insert CSS directly
    insert: true,
    // Process SASS with PostCSS for autoprefixing
    processor: css => postcss([autoprefixer])
    .process(css, { from: undefined })
    .then(result => result.css)
    };

    // Main Rollup configuration
    export default [
    // ESM and CJS bundles (code)
    {
    input: 'src/index.ts’,
    output: [
    {
    file: createPath(packageJson.module),
    format: 'esm’,
    sourcemap: true
    },
    {
    file: createPath(packageJson.main),
    format: 'cjs’,
    sourcemap: true
    }
    ],
    plugins: [
    // Handle external dependencies
    external(),
    // Resolve node_modules
    resolve(),
    // Convert CommonJS modules to ES6
    commonjs(),
    // Process TypeScript
    typescript({
    tsconfig: ’./tsconfig.json’,
    exclude: [’**/*.test.tsx’, '**/*.test.ts’, '**/*.stories.tsx’]
    }),
    // Process SASS files
    sass(sassOptions),
    // Handle assets (limit: 8KB, otherwise use file)
    url({
    limit: 8 * 1024, // 8KB
    include: [’**/*.svg’, '**/*.png’, '**/*.jpg’, '**/*.gif’],
    fileName: '[dirname][name][extname]’
    }),
    // Minify for production
    terser()
    ],
    // Explicitly mark React as external
    external: [’react’, 'react-dom’]
    },

    // TypeScript declaration files
    {
    input: 'src/index.ts’,
    output: [
    {
    file: createPath(packageJson.types),
    format: 'esm’
    }
    ],
    plugins: [
    dts({
    tsconfig: ’./tsconfig.json’,
    compilerOptions: {
    emitDeclarationOnly: true,
    }
    })
    ],
    external: [/.scss$/]
    }
    ];
    End File# myrta-ds/myrta
    „use client”;

    import {
    Card,
    CardContent,
    Button,
    CardFooter,
    CardHeader,
    CardTitle,
    } from „@/components/ui/card”;
    import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
    } from „@/components/ui/table”;
    import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
    } from „@/components/ui/select”;
    import { Tabs, TabsList, TabsTrigger, TabsContent } from „@/components/ui/tabs”;
    import { useCallback, useEffect, useState } from „react”;
    import { Input } from „@/components/ui/input”;
    import { fetchLogData, filterLogData } from „@/lib/data”;
    import { LogData } from „@/lib/definitions”;
    import { CopyIcon, LoaderIcon, SearchIcon } from „lucide-react”;
    import { Label } from „@/components/ui/label”;
    import {
    Pagination,
    PaginationContent,
    PaginationItem,
    PaginationPrev,
    PaginationLink,
    PaginationEllipsis,
    PaginationNext,
    } from „@/components/ui/pagination”;
    import { useDebounce } from „@/lib/hooks”;
    import {
    truncateFirstLine,
    formatLogDate,
    getSeverityClass,
    formatStackTrace,
    } from „@/lib/utils”;
    import { Badge } from „@/components/ui/badge”;
    import copyToClipboard from „@/lib/clipboard”;
    import { ScrollArea } from „@/components/ui/scroll-area”;
    import LogMessageCard from „@/components/logs/log-message-card”;

    const DEBOUNCE_MS = 300;
    const LOGS_PER_PAGE = 10;

    export default function LogsPage() {
    // State for logs data and filters
    const [logs, setLogs] = useState([]);
    const [filteredLogs, setFilteredLogs] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [selectedLog, setSelectedLog] = useState(null);
    const [tableView, setTableView] = useState(true);

    // Filters state
    const [searchTerm, setSearchTerm] = useState(„”);
    const [severity, setSeverity] = useState(„all”);
    const [timeRange, setTimeRange] = useState(„24h”);
    const [source, setSource] = useState(„all”);

    // Pagination state
    const [currentPage, setCurrentPage] = useState(1);
    const [totalPages, setTotalPages] = useState(1);

    // Debounced search term
    const debouncedSearchTerm = useDebounce(searchTerm, DEBOUNCE_MS);

    // Load logs data
    const loadLogs = useCallback(async () => {
    setIsLoading(true);
    try {
    const data = await fetchLogData();
    setLogs(data);
    setIsLoading(false);
    } catch (error) {
    console.error(„Failed to fetch log data:”, error);
    setIsLoading(false);
    }
    }, []);

    // Initial data load
    useEffect(() => {
    loadLogs();
    }, [loadLogs]);

    // Apply filters whenever filter criteria change
    useEffect(() => {
    const applyFilters = () => {
    const filtered = filterLogData(
    logs,
    debouncedSearchTerm,
    severity,
    timeRange,
    source
    );
    setFilteredLogs(filtered);
    setTotalPages(Math.ceil(filtered.length / LOGS_PER_PAGE));
    setCurrentPage(1); // Reset to first page when filters change
    // Clear selected log when filters change
    setSelectedLog(null);
    };

    applyFilters();
    }, [logs, debouncedSearchTerm, severity, timeRange, source]);

    // Handle pagination
    const getPaginatedLogs = () => {
    const startIndex = (currentPage – 1) * LOGS_PER_PAGE;
    const endIndex = startIndex + LOGS_PER_PAGE;
    return filteredLogs.slice(startIndex, endIndex);
    };

    const handleCopySelected = () => {
    if (selectedLog) {
    copyToClipboard(
    `${selectedLog.timestamp} [${selectedLog.severity}] ${selectedLog.message}n${selectedLog.stackTrace}`
    );
    }
    };

    // Render loading state
    if (isLoading) {
    return (


    Loading logs…

    );
    }

    const renderPagination = () => {
    if (totalPages <= 1) return null; const renderPageNumbers = () => {
    const pages = [];
    const showEllipsisStart = currentPage > 3;
    const showEllipsisEnd = currentPage < totalPages - 2; // Always show first page if (currentPage > 3) {
    pages.push(

    setCurrentPage(1)}>1

    );
    }

    // Show ellipsis if needed
    if (showEllipsisStart) {
    pages.push(



    );
    }

    // Show current page and adjacent pages
    for (
    let i = Math.max(2, currentPage – 1);
    i <= Math.min(totalPages - 1, currentPage + 1); i++ ) { pages.push(
    setCurrentPage(i)}
    >
    {i}


    );
    }

    // Show ellipsis if needed
    if (showEllipsisEnd) {
    pages.push(



    );
    }

    // Always show last page
    if (currentPage < totalPages - 2 && totalPages > 3) {
    pages.push(

    setCurrentPage(totalPages)}>
    {totalPages}


    );
    }

    return pages;
    };

    return (



    setCurrentPage((prev) => Math.max(prev – 1, 1))}
    isDisabled={currentPage === 1}
    />

    {renderPageNumbers()}


    setCurrentPage((prev) => Math.min(prev + 1, totalPages))
    }
    isDisabled={currentPage === totalPages}
    />



    );
    };

    const paginatedLogs = getPaginatedLogs();

    const renderTableView = () => (




    Timestamp
    Severity
    Source
    Message



    {paginatedLogs.length === 0 ? (


    No logs found matching the current filters.


    ) : (
    paginatedLogs.map((log) => (
    setSelectedLog(log)}
    style={{ cursor: 'pointer’ }}
    >

    {formatLogDate(log.timestamp)}



    {log.severity}



    {log.source}


    {truncateFirstLine(log.message)}


    ))
    )}


    {renderPagination()}

    );

    const renderCardView = () => (

    {paginatedLogs.length === 0 ? (

    No logs found matching the current filters.

    ) : (
    paginatedLogs.map((log) => (
    setSelectedLog(log)}
    />
    ))
    )}
    {renderPagination()}

    );

    return (

    System Logs

    {/* Filters panel */}


    setSearchTerm(e.target.value)}
    className=”pl-10″
    />

    {/* Main content area */}


    Log Events
    setTableView(v === „table”)}
    >

    Table
    Cards



    {tableView ? renderTableView() : renderCardView()}


    Showing {Math.min(filteredLogs.length, LOGS_PER_PAGE)} of{” „}
    {filteredLogs.length} logs

    {/* Log details panel */}


    Log Details
    {selectedLog && (

    )}



    {selectedLog ? (

    {formatLogDate(selectedLog.timestamp, true)}


    {selectedLog.severity}

    {selectedLog.source}

    {selectedLog.message}

    {selectedLog.stackTrace && (


                              {formatStackTrace(selectedLog.stackTrace)}
                            

    )}

    {selectedLog.metadata && (


                              {JSON.stringify(selectedLog.metadata, null, 2)}
                            

    )}

    ) : (

    Select a log entry to view details

    )}

    );
    }
    End File# apps/dashboard-demo/app/analytics/performance/page.tsx
    „use client”;

    import { Card, CardContent, CardHeader, CardTitle } from „@/components/ui/card”;
    import {
    ArrowDownIcon,
    ArrowUpIcon,
    LoaderIcon,
    MoreHorizontalIcon,
    } from „lucide-react”;
    import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
    } from „@/components/ui/select”;
    import { useState, useEffect } from „react”;
    import { Button } from „@/components/ui/button”;
    import { Tabs, TabsList, TabsTrigger } from „@/components/ui/tabs”;
    import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
    } from „@/components/ui/table”;
    import { Progress } from „@/components/ui/progress”;
    import { TimeSeriesChart } from „@/components/charts/time-series-chart”;
    import { BarChart } from „@/components/charts/bar-chart”;
    import { DonutChart } from „@/components/charts/donut-chart”;
    import {
    fetchPerformanceData,
    fetchPerformanceRouteData,
    fetchPerformanceResources,
    } from „@/lib/data”;
    import {
    PerformanceData,
    PerformanceRouteData,
    PerformanceResource,
    } from „@/lib/definitions”;
    import { formatBytes, formatNumber } from „@/lib/utils”;
    import ResourcesTable from „@/components/performance/resources-table”;

    // Page component
    export default function PerformancePage() {
    const [timeRange, setTimeRange] = useState(„7d”);
    const [isLoading, setIsLoading] = useState(true);
    const [performanceData, setPerformanceData] = useState(
    null
    );
    const [routeData, setRouteData] = useState([]);
    const [resources, setResources] = useState([]);

    useEffect(() => {
    async function loadData() {
    setIsLoading(true);
    try {
    const perfData = await fetchPerformanceData(timeRange);
    const routesData = await fetchPerformanceRouteData(timeRange);
    const resourcesData = await fetchPerformanceResources();

    setPerformanceData(perfData);
    setRouteData(routesData);
    setResources(resourcesData);
    setIsLoading(false);
    } catch (error) {
    console.error(„Error loading performance data:”, error);
    setIsLoading(false);
    }
    }

    loadData();
    }, [timeRange]);

    if (isLoading) {
    return (


    Loading performance data…

    );
    }

    if (!performanceData) {
    return (

    No performance data available.

    );
    }

    return (

    Performance Dashboard


    {/* Key Metrics Cards */}

    0 ? „slower” : „faster”
    }
    isGoodWhenNegative={true}
    />
    0 ? „slower” : „faster”
    }
    isGoodWhenNegative={true}
    />
    0 ? „slower” : „faster”
    }
    isGoodWhenNegative={true}
    />
    0 ? „slower” : „faster”
    }
    isGoodWhenNegative={true}
    />

    {/* Time Series Charts */}



    Page Load Time (seconds)







    First Contentful Paint (seconds)





    {/* Routes Performance */}



    Routes Performance



    Slowest
    All Routes






    Route
    Avg. Load Time
    Min
    Max
    Page Views
    Performance



    {routeData.map((route) => (


    {route.route}

    {route.avgLoadTime.toFixed(2)}s
    {route.minLoadTime.toFixed(2)}s
    {route.maxLoadTime.toFixed(2)}s
    {formatNumber(route.pageViews)}

    0.7
    ? „bg-green-500”
    : route.performanceScore > 0.4
    ? „bg-amber-500”
    : „bg-red-500”
    }
    />
    0.7
    ? „text-green-500”
    : route.performanceScore > 0.4
    ? „text-amber-500”
    : „text-red-500”
    }
    >
    {(route.performanceScore * 100).toFixed(0)}%



    ))}



    {/* Charts and Resources */}




    Page Sizes by Type



    ({
    name: item.type,
    value: item.size,
    }))}
    height={300}
    valueFormatter={(value) => formatBytes(value)}
    />




    Loading Times by Device Type





    {/* Resources Table */}




    Resource Performance





    );
    }

    // Metric Card Component
    function MetricCard({
    title,
    value,
    change,
    changeLabel,
    isGoodWhenNegative = false,
    }: {
    title: string;
    value: string;
    change: number;
    changeLabel: string;
    isGoodWhenNegative?: boolean;
    }) {
    // Determine if the change is positive or negative from a business perspective
    const isPositiveChange = isGoodWhenNegative ? change < 0 : change > 0;

    return (


    {title}

    {value}

    {change >= 0 ? (

    ) : (

    )}
    {Math.abs(change).toFixed(1)}% {changeLabel}


    vs. previous period



    );
    }
    End File# apps/dashboard-demo/app/analytics/engagement/page.tsx
    „use client”;

    import { Card, CardContent, CardHeader, CardTitle } from „@/components/ui/card”;
    import {
    ArrowDownIcon,
    ArrowUpIcon,
    LoaderIcon,
    MoreHorizontalIcon,
    } from „lucide-react”;
    import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
    } from „@/components/ui/select”;
    import { useState, useEffect } from „react”;
    import { Button } from „@/components/ui/button”;
    import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
    } from „@/components/ui/table”;
    import { TimeSeriesChart } from „@/components/charts/time-series-chart”;
    import { BarChart } from „@/components/charts/bar-chart”;
    import { DonutChart } from „@/components/charts/donut-chart”;
    import {
    fetchEngagementData,
    fetchReferrers,
    fetchPopularContent,
    } from „@/lib/data”;
    import { EngagementData, Referrer, ContentItem } from „@/lib/definitions”;
    import { formatNumber, formatTime } from „@/lib/utils”;

    // Page component
    export default function EngagementPage() {
    const [timeRange, setTimeRange] = useState(„30d”);
    const [isLoading, setIsLoading] = useState(true);
    const [engagementData, setEngagementData] = useState(
    null
    );
    const [referrers, setReferrers] = useState([]);
    const [popularContent, setPopularContent] = useState([]);

    useEffect(() => {
    async function loadData() {
    setIsLoading(true);
    try {
    const data = await fetchEngagementData(timeRange);
    const referrerData = await fetchReferrers(timeRange);
    const contentData = await fetchPopularContent(timeRange);

    setEngagementData(data);
    setReferrers(referrerData);
    setPopularContent(contentData);
    setIsLoading(false);
    } catch (error) {
    console.error(„Error loading engagement data:”, error);
    setIsLoading(false);
    }
    }

    loadData();
    }, [timeRange]);

    if (isLoading) {
    return (


    Loading engagement data…

    );
    }

    if (!engagementData) {
    return (

    No engagement data available.

    );
    }

    return (

    User Engagement


    {/* Key Metrics Cards */}




    {/* Time Series Charts */}



    Average Session Duration



    formatTime(value * 60)}
    />



    Bounce Rate Over Time





    {/* Charts and Referrers */}




    Traffic by Device



    `${value}%`}
    />

    {engagementData.trafficByDevice.map((item) => (

    {item.name}

    {item.value}%

    ))}





    User Engagement by Age Group



    formatTime(value * 60)}
    />

    {/* Popular Content & Referrers */}




    Most Popular Content






    Title
    Views
    Avg. Time
    Engagement



    {popularContent.map((item) => (

    {item.title}

    {formatNumber(item.views)}


    {formatTime(item.avgTimeOnPage)}


    75
    ? „text-green-500 font-medium”
    : item.engagementScore > 50
    ? „text-amber-500 font-medium”
    : „text-muted-foreground font-medium”
    }
    >
    {item.engagementScore}%



    ))}






    Top Referrers






    Source
    Sessions
    Bounce Rate
    Conversion



    {referrers.map((referrer) => (


    {referrer.source}


    {formatNumber(referrer.sessions)}


    70
    ? „text-red-500 font-medium”
    : referrer.bounceRate > 50
    ? „text-amber-500 font-medium”
    : „text-green-500 font-medium”
    }
    >
    {referrer.bounceRate.toFixed(1)}%



    {referrer.conversionRate.toFixed(1)}%


    ))}



    );
    }

    // Metric Card Component
    function MetricCard({
    title,
    value,
    change,
    isGoodWhenPositive = true,
    }: {
    title: string;
    value: string;
    change: number;
    isGoodWhenPositive?: boolean;
    }) {
    // Determine if the change is positive or negative from a business perspective
    const isPositiveChange = isGoodWhenPositive
    ? change > 0
    : change < 0; return (

    {title}

    {value}

    {change >= 0 ? (

    ) : (

    )}
    {Math.abs(change).toFixed(1)}%


    vs. previous period



    );
    }
    End File# apps/dashboard-demo/lib/data.ts
    import { addDays, addHours, format, subDays, subHours, subMonths } from „date-fns”;
    import {
    LogData,
    PerformanceData,
    PerformanceRouteData,
    PerformanceResource,
    EngagementData,
    Referrer,
    ContentItem
    } from „./definitions”;

    // Mock log data
    export async function fetchLogData(): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 800));

    const now = new Date();
    const logs: LogData[] = [];

    // Generate random logs
    for (let i = 0; i < 200; i++) { const randomHours = Math.floor(Math.random() * 72); // Logs from the last 72 hours const timestamp = subHours(now, randomHours).toISOString(); const severities = ['info', 'warning', 'error', 'debug']; const severity = severities[Math.floor(Math.random() * severities.length)]; const sources = ['api', 'frontend', 'backend', 'database']; const source = sources[Math.floor(Math.random() * sources.length)]; let message = ''; let stackTrace = ''; switch (severity) { case 'error': message = getRandomErrorMessage(); stackTrace = getRandomStackTrace(); break; case 'warning': message = getRandomWarningMessage(); break; case 'info': message = getRandomInfoMessage(); break; case 'debug': message = getRandomDebugMessage(); break; } // Add metadata for some logs const metadata = Math.random() > 0.7 ? {
    userId: `user_${Math.floor(Math.random() * 10000)}`,
    requestId: `req_${Math.random().toString(36).substring(2, 12)}`,
    path: `/api/${[’users’, 'products’, 'orders’][Math.floor(Math.random() * 3)]}/${Math.floor(Math.random() * 1000)}`,
    browser: [’Chrome’, 'Firefox’, 'Safari’, 'Edge’][Math.floor(Math.random() * 4)],
    os: [’Windows’, 'MacOS’, 'Linux’, 'iOS’, 'Android’][Math.floor(Math.random() * 5)]
    } : undefined;

    logs.push({
    id: `log_${i}`,
    timestamp,
    severity,
    source,
    message,
    stackTrace,
    metadata
    });
    }

    // Sort by most recent first
    return logs.sort((a, b) => new Date(b.timestamp).getTime() – new Date(a.timestamp).getTime());
    }

    // Filter log data based on search term and filters
    export function filterLogData(
    logs: LogData[],
    searchTerm: string = ”,
    severity: string = 'all’,
    timeRange: string = ’24h’,
    source: string = 'all’
    ): LogData[] {
    const now = new Date();
    let filteredLogs = […logs];

    // Filter by time range
    if (timeRange !== 'all’) {
    let cutoffDate;
    switch (timeRange) {
    case '1h’:
    cutoffDate = subHours(now, 1);
    break;
    case ’24h’:
    cutoffDate = subDays(now, 1);
    break;
    case '7d’:
    cutoffDate = subDays(now, 7);
    break;
    case ’30d’:
    cutoffDate = subDays(now, 30);
    break;
    default:
    cutoffDate = subDays(now, 1); // Default to 24h
    }

    filteredLogs = filteredLogs.filter(log =>
    new Date(log.timestamp) >= cutoffDate
    );
    }

    // Filter by severity
    if (severity !== 'all’) {
    filteredLogs = filteredLogs.filter(log =>
    log.severity.toLowerCase() === severity.toLowerCase()
    );
    }

    // Filter by source
    if (source !== 'all’) {
    filteredLogs = filteredLogs.filter(log =>
    log.source.toLowerCase() === source.toLowerCase()
    );
    }

    // Filter by search term
    if (searchTerm.trim() !== ”) {
    const term = searchTerm.toLowerCase();
    filteredLogs = filteredLogs.filter(log =>
    log.message.toLowerCase().includes(term) ||
    log.source.toLowerCase().includes(term) ||
    log.severity.toLowerCase().includes(term) ||
    (log.stackTrace && log.stackTrace.toLowerCase().includes(term))
    );
    }

    return filteredLogs;
    }

    // Helper functions for generating random log messages
    function getRandomErrorMessage(): string {
    const errorMessages = [
    „Failed to connect to database: Connection timeout after 30 seconds”,
    „Uncaught TypeError: Cannot read property 'data’ of undefined”,
    „Authentication failed: Invalid token signature”,
    „Failed to process payment: Invalid card number”,
    „500 Internal Server Error: Unexpected condition encountered”,
    „OutOfMemoryError: Java heap space”,
    „Unhandled exception in API request: Invalid JSON payload”,
    „Database query failed: Syntax error in SQL statement”,
    „Cache miss for critical resource, fallback failed”,
    „Rate limit exceeded for API endpoint /api/users”,
    „Failed to read file: Permission denied”,
    „Network error: Unable to reach external service”
    ];

    return errorMessages[Math.floor(Math.random() * errorMessages.length)];
    }

    function getRandomWarningMessage(): string {
    const warningMessages = [
    „High CPU usage detected: 85% for the last 5 minutes”,
    „Memory usage approaching threshold: 78% of available memory”,
    „Slow database query detected: Query took 4.2s to complete”,
    „API rate limit at 80% – consider throttling requests”,
    „Deprecated API endpoint still in use: /v1/legacy/users”,
    „Cache hit ratio below acceptable threshold (65%)”,
    „Session timeout shorter than recommended (10 minutes)”,
    „Disk space running low: 85% used on /data volume”,
    „Configuration using default values – recommended to set explicitly”,
    „CORS policy may be too permissive, consider restricting origins”
    ];

    return warningMessages[Math.floor(Math.random() * warningMessages.length)];
    }

    function getRandomInfoMessage(): string {
    const infoMessages = [
    „User successfully authenticated: user_12345”,
    „Payment processed successfully for order #34567”,
    „New user registered: [email protected]”,
    „Email notification sent to [email protected]”,
    „Database backup completed successfully, size: 1.2GB”,
    „API request completed, response time: 235ms”,
    „Background job completed: Report generation”,
    „User session started: session_id_123456”,
    „Config reloaded with new environment variables”,
    „Cache purged for content region: product-catalog”
    ];

    return infoMessages[Math.floor(Math.random() * infoMessages.length)];
    }

    function getRandomDebugMessage(): string {
    const debugMessages = [
    „Request parameters: {„userId”: 12345, „limit”: 50, „offset”: 0}”,
    „Database query executed: SELECT * FROM users WHERE status = 'active’ LIMIT 100”,
    „Cache lookup for key: user:profile:12345”,
    „Processing item 45 of 230 in batch job”,
    „Authentication flow starting for user: [email protected]”,
    „API response payload size: 24.5KB”,
    „Function execution time: method=getUserData, duration=125ms”,
    „Initializing service dependencies: [UserService, AuthService, CacheManager]”,
    „Route matched: GET /api/products/:id -> ProductController.getProductById”,
    „Object state before transform: {„status”: „pending”, „attempts”: 2}”
    ];

    return debugMessages[Math.floor(Math.random() * debugMessages.length)];
    }

    function getRandomStackTrace(): string {
    const stackTraces = [
    `Error: Cannot read property 'id’ of undefined
    at UserService.getUserDetails (/src/services/UserService.js:42:23)
    at async APIController.getUserProfile (/src/controllers/APIController.js:57:12)
    at async processRequest (/src/middleware/requestHandler.js:28:7)`,

    `TypeError: undefined is not a function
    at Object.parseResponse [as parse] (/src/utils/responseParser.js:28:10)
    at APIClient.handleResponse (/src/clients/APIClient.js:112:45)
    at async fetchData (/src/hooks/useFetch.js:23:19)
    at async ComponentDidMount (/src/components/DataView.js:31:5)`,

    `ReferenceError: fetch is not defined
    at fetchData (/src/utils/api.js:15:3)
    at loadUserData (/src/components/UserProfile.js:47:19)
    at Component.componentDidMount (/src/components/UserProfile.js:32:10)`,

    `SyntaxError: Unexpected token } in JSON at position 42
    at JSON.parse ()
    at parseResponse (/src/utils/http.js:23:19)
    at async fetchData (/src/services/dataService.js:45:23)`,

    `Error: Failed to connect to database
    at connectToDatabase (/src/db/connection.js:28:11)
    at initializeServices (/src/app.js:42:8)
    at startServer (/src/server.js:15:3)
    at main (/src/index.js:10:1)`,

    `RangeError: Maximum call stack size exceeded
    at Object.processNode (/src/utils/treeProcessor.js:45:12)
    at Object.processNode (/src/utils/treeProcessor.js:47:14)
    at Object.processNode (/src/utils/treeProcessor.js:47:14)`,

    `DatabaseError: relation „users” does not exist
    at Connection.parseE (/node_modules/pg/lib/connection.js:614:13)
    at Connection.parseMessage (/node_modules/pg/lib/connection.js:413:19)
    at Socket. (/node_modules/pg/lib/connection.js:129:22)`,

    `AuthenticationError: Invalid credentials
    at verifyToken (/src/auth/tokenVerifier.js:35:11)
    at authenticateRequest (/src/middleware/auth.js:28:15)
    at async processRequest (/src/middleware/requestHandler.js:24:5)`
    ];

    return stackTraces[Math.floor(Math.random() * stackTraces.length)];
    }

    // Mock performance data
    export async function fetchPerformanceData(timeRange: string): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 1000));

    const now = new Date();
    const days = timeRange === ’24h’ ? 1 :
    timeRange === '7d’ ? 7 :
    timeRange === ’30d’ ? 30 : 90;

    // Generate history data
    const pageLoadTimeHistory = [];
    const firstContentfulPaintHistory = [];

    for (let i = days; i >= 0; i–) {
    const date = subDays(now, i);
    // Add some randomness but maintain a trend
    const basePageLoad = 2.2 + (Math.sin(i/10) * 0.3);
    const baseFCP = 1.4 + (Math.cos(i/8) * 0.2);

    pageLoadTimeHistory.push({
    date: format(date, 'yyyy-MM-dd’),
    value: Number((basePageLoad + (Math.random() * 0.5 – 0.25)).toFixed(2))
    });

    firstContentfulPaintHistory.push({
    date: format(date, 'yyyy-MM-dd’),
    value: Number((baseFCP + (Math.random() * 0.4 – 0.2)).toFixed(2))
    });
    }

    return {
    pageLoadTime: 2.4,
    pageLoadTimeChange: -5.2,
    firstContentfulPaint: 1.2,
    firstContentfulPaintChange: -8.5,
    timeToInteractive: 3.1,
    timeToInteractiveChange: -3.8,
    serverResponseTime: 0.42,
    serverResponseTimeChange: 12.5,
    pageLoadTimeHistory,
    firstContentfulPaintHistory,
    resourceSizesByType: [
    { type: 'JavaScript’, size: 845000 },
    { type: 'CSS’, size: 124000 },
    { type: 'Images’, size: 1250000 },
    { type: 'Fonts’, size: 298000 },
    { type: 'HTML’, size: 45000 },
    { type: 'Other’, size: 67000 },
    ],
    loadTimesByDevice: [
    { device: 'Desktop’, loadTime: 2.1 },
    { device: 'Mobile’, loadTime: 3.4 },
    { device: 'Tablet’, loadTime: 2.8 },
    ]
    };
    }

    // Mock performance route data
    export async function fetchPerformanceRouteData(timeRange: string): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 800));

    return [
    {
    route: '/dashboard’,
    avgLoadTime: 2.1,
    minLoadTime: 1.4,
    maxLoadTime: 3.8,
    pageViews: 15420,
    performanceScore: 0.82
    },
    {
    route: '/products’,
    avgLoadTime: 2.8,
    minLoadTime: 1.9,
    maxLoadTime: 5.2,
    pageViews: 8760,
    performanceScore: 0.75
    },
    {
    route: '/checkout’,
    avgLoadTime: 3.2,
    minLoadTime: 2.1,
    maxLoadTime: 6.5,
    pageViews: 4350,
    performanceScore: 0.65
    },
    {
    route: '/user/profile’,
    avgLoadTime: 1.9,
    minLoadTime: 1.2,
    maxLoadTime: 3.6,
    pageViews: 3280,
    performanceScore: 0.88
    },
    {
    route: '/blog’,
    avgLoadTime: 2.4,
    minLoadTime: 1.6,
    maxLoadTime: 4.2,
    pageViews: 7520,
    performanceScore: 0.79
    },
    {
    route: '/api/data’,
    avgLoadTime: 0.8,
    minLoadTime: 0.3,
    maxLoadTime: 3.2,
    pageViews: 142500,
    performanceScore: 0.92
    },
    {
    route: '/search’,
    avgLoadTime: 4.2,
    minLoadTime: 2.5,
    maxLoadTime: 8.7,
    pageViews: 6240,
    performanceScore: 0.42
    },
    {
    route: '/cart’,
    avgLoadTime: 2.6,
    minLoadTime: 1.5,
    maxLoadTime: 5.4,
    pageViews: 5870,
    performanceScore: 0.70
    }
    ];
    }

    // Mock resources performance data
    export async function fetchPerformanceResources(): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 700));

    return [
    {
    name: 'main.js’,
    type: 'script’,
    size: 287500,
    transferSize: 76400,
    loadTime: 420,
    cacheable: true,
    optimized: false
    },
    {
    name: 'styles.css’,
    type: 'stylesheet’,
    size: 84200,
    transferSize: 23500,
    loadTime: 180,
    cacheable: true,
    optimized: true
    },
    {
    name: 'vendor.js’,
    type: 'script’,
    size: 543000,
    transferSize: 168400,
    loadTime: 680,
    cacheable: true,
    optimized: false
    },
    {
    name: 'hero-image.jpg’,
    type: 'image’,
    size: 485000,
    transferSize: 482000,
    loadTime: 540,
    cacheable: true,
    optimized: false
    },
    {
    name: 'font-awesome.woff2′,
    type: 'font’,
    size: 98000,
    transferSize: 97400,
    loadTime: 220,
    cacheable: true,
    optimized: true
    },
    {
    name: 'api/user-data’,
    type: 'fetch’,
    size: 12400,
    transferSize: 5200,
    loadTime: 350,
    cacheable: false,
    optimized: true
    },
    {
    name: 'analytics.js’,
    type: 'script’,
    size: 67800,
    transferSize: 22400,
    loadTime: 310,
    cacheable: true,
    optimized: false
    },
    {
    name: 'product-thumbnails.png’,
    type: 'image’,
    size: 265000,
    transferSize: 264300,
    loadTime: 480,
    cacheable: true,
    optimized: false
    },
    {
    name: 'roboto.woff2′,
    type: 'font’,
    size: 142000,
    transferSize: 141600,
    loadTime: 290,
    cacheable: true,
    optimized: true
    },
    {
    name: 'polyfill.js’,
    type: 'script’,
    size: 48600,
    transferSize: 12800,
    loadTime: 160,
    cacheable: true,
    optimized: true
    }
    ];
    }

    // Mock user engagement data
    export async function fetchEngagementData(timeRange: string): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 900));

    const now = new Date();
    const days = timeRange === '7d’ ? 7 :
    timeRange === ’30d’ ? 30 :
    timeRange === ’90d’ ? 90 : 365;

    // Generate history data
    const sessionDurationHistory = [];
    const bounceRateHistory = [];

    for (let i = days; i >= 0; i–) {
    const date = subDays(now, i);
    // Add some randomness but maintain a trend
    const baseDuration = 3.2 + (Math.sin(i/15) * 0.4);
    const baseBounce = 48 + (Math.cos(i/20) * 5);

    sessionDurationHistory.push({
    date: format(date, 'yyyy-MM-dd’),
    value: Number((baseDuration + (Math.random() * 0.6 – 0.3)).toFixed(2))
    });

    bounceRateHistory.push({
    date: format(date, 'yyyy-MM-dd’),
    value: Number((baseBounce + (Math.random() * 3 – 1.5)).toFixed(1))
    });
    }

    return {
    avgSessionDuration: 186, // in seconds
    avgSessionDurationChange: 4.8,
    pagesPerSession: 3.4,
    pagesPerSessionChange: 2.1,
    bounceRate: 46.5,
    bounceRateChange: -3.2,
    returningUsers: 32.7,
    returningUsersChange: 8.4,
    sessionDurationHistory,
    bounceRateHistory,
    trafficByDevice: [
    { name: 'Desktop’, value: 48 },
    { name: 'Mobile’, value: 42 },
    { name: 'Tablet’, value: 10 },
    ],
    engagementByAge: [
    { age: ’18-24′, sessionDuration: 2.8 },
    { age: ’25-34′, sessionDuration: 3.5 },
    { age: ’35-44′, sessionDuration: 4.2 },
    { age: ’45-54′, sessionDuration: 3.7 },
    { age: ’55-64′, sessionDuration: 3.1 },
    { age: ’65+’, sessionDuration: 2.5 },
    ]
    };
    }

    // Mock referrers data
    export async function fetchReferrers(timeRange: string): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 700));

    return [
    {
    source: 'Google’,
    sessions: 42650,
    bounceRate: 42.3,
    conversionRate: 3.8
    },
    {
    source: 'Direct’,
    sessions: 31470,
    bounceRate: 38.7,
    conversionRate: 5.2
    },
    {
    source: 'Facebook’,
    sessions: 18920,
    bounceRate: 53.1,
    conversionRate: 2.7
    },
    {
    source: 'Instagram’,
    sessions: 14580,
    bounceRate: 64.5,
    conversionRate: 3.1
    },
    {
    source: 'Twitter’,
    sessions: 8240,
    bounceRate: 59.8,
    conversionRate: 1.8
    },
    {
    source: 'Email’,
    sessions: 7830,
    bounceRate: 31.4,
    conversionRate: 6.4
    },
    {
    source: 'LinkedIn’,
    sessions: 4920,
    bounceRate: 44.2,
    conversionRate: 4.3
    },
    {
    source: 'Reddit’,
    sessions: 3750,
    bounceRate: 66.7,
    conversionRate: 1.5
    }
    ];
    }

    // Mock popular content data
    export async function fetchPopularContent(timeRange: string): Promise {
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 800));

    return [
    {
    id: 'page_1′,
    title: 'Getting Started Guide’,
    views: 28450,
    avgTimeOnPage: 285,
    engagementScore: 87
    },
    {
    id: 'page_2′,
    title: 'Product Features Overview’,
    views: 24180,
    avgTimeOnPage: 192,
    engagementScore: 76
    },
    {
    id: 'page_3′,
    title: 'Pricing Plans’,
    views: 19740,
    avgTimeOnPage: 147,
    engagementScore: 62
    },
    {
    id: 'page_4′,
    title: 'Latest Product Updates’,
    views: 17920,
    avgTimeOnPage: 240,
    engagementScore: 81
    },
    {
    id: 'page_5′,
    title: 'API Documentation’,
    views: 15840,
    avgTimeOnPage: 328,
    engagementScore: 93
    },
    {
    id: 'page_6′,
    title: 'Case Studies’,
    views: 14250,
    avgTimeOnPage: 265,
    engagementScore: 84
    },
    {
    id: 'page_7′,
    title: 'Contact Support’,
    views: 11380,
    avgTimeOnPage: 118,
    engagementScore: 54
    },
    {
    id: 'page_8′,
    title: 'Frequently Asked Questions’,
    views: 9740,
    avgTimeOnPage: 208,
    engagementScore: 72
    }
    ];
    }
    End File# myrta-ds/myrta
    // Types for dashboard data structures

    // Log data
    export interface LogData {
    id: string;
    timestamp: string;
    severity: 'info’ | 'warning’ | 'error’ | 'debug’;
    source: string;
    message: string;
    stackTrace?: string;
    metadata?: Record;
    }

    // Performance data
    export interface TimeSeriesDataPoint {
    date: string;
    value: number;
    }

    export interface ResourceSizeData {
    type: string;
    size: number;
    }

    export interface DeviceLoadTimeData {
    device: string;
    loadTime: number;
    }

    export interface PerformanceData {
    pageLoadTime: number;
    pageLoadTimeChange: number;
    firstContentfulPaint: number;
    firstContentfulPaintChange: number;
    timeToInteractive: number;
    timeToInteractiveChange: number;
    serverResponseTime: number;
    serverResponseTimeChange: number;
    pageLoadTimeHistory: TimeSeriesDataPoint[];
    firstContentfulPaintHistory: TimeSeriesDataPoint[];
    resourceSizesByType: ResourceSizeData[];
    loadTimesByDevice: DeviceLoadTimeData[];
    }

    export interface PerformanceRouteData {
    route: string;
    avgLoadTime: number;
    minLoadTime: number;
    maxLoadTime: number;
    pageViews: number;
    performanceScore: number;
    }

    export interface PerformanceResource {
    name: string;
    type: string;
    size: number;
    transferSize: number;
    loadTime: number;
    cacheable: boolean;
    optimized: boolean;
    }

    // Engagement data
    export interface DeviceTrafficData {
    name: string;
    value: number;
    }

    export interface AgeEngagementData {
    age: string;
    sessionDuration: number;
    }

    export interface EngagementData {
    avgSessionDuration: number; // in seconds
    avgSessionDurationChange: number;
    pagesPerSession: number;
    pagesPerSessionChange: number;
    bounceRate: number;
    bounceRateChange: number;
    returningUsers: number;
    returningUsersChange: number;
    sessionDurationHistory: TimeSeriesDataPoint[];
    bounceRateHistory: TimeSeriesDataPoint[];
    trafficByDevice: DeviceTrafficData[];
    engagementByAge: AgeEngagementData[];
    }

    export interface Referrer {
    source: string;
    sessions: number;
    bounceRate: number;
    conversionRate: number;
    }

    export interface ContentItem {
    id: string;
    title: string;
    views: number;
    avgTimeOnPage: number; // in seconds
    engagementScore: number; // 0-100
    }
    End Fileimport { format, formatDistance } from 'date-fns’;

    /**
    * Format a date object or string to a readable date
    * @param date Date object or string
    * @param includeTime Include time in the formatted string
    * @returns Formatted date string
    */
    export function formatDate(date: Date | string, includeTime: boolean = false): string {
    const dateObj = typeof date === 'string’ ? new Date(date) : date;
    return format(dateObj, includeTime ? 'PPp’ : 'PP’);
    }

    /**
    * Format a date with a relative time (e.g., '3 days ago’)
    * @param date Date object or string
    * @returns Relative time string
    */
    export function formatRelativeTime(date: Date | string): string {
    const dateObj = typeof date === 'string’ ? new Date(date) : date;
    return formatDistance(dateObj, new Date(), { addSuffix: true });
    }

    /**
    * Format a timestamp from a log entry
    * @param timestamp ISO date string
    * @param includeSeconds Include seconds in the output
    * @returns Formatted date/time string
    */
    export function formatLogDate(timestamp: string, includeSeconds: boolean = false): string {
    const date = new Date(timestamp);
    if (includeSeconds) {
    return format(date, 'MMM d, yyyy HH:mm:ss’);
    }
    return format(date, 'MMM d, yyyy HH:mm’);
    }

    /**
    * Truncate the first line of a multi-line string
    * @param text Text to truncate
    * @param maxLength Maximum length before truncation
    * @returns Truncated first line
    */
    export function truncateFirstLine(text: string, maxLength: number = 100): string {
    // Get only the first line
    const firstLine = text.split(’n’)[0];

    // Truncate if necessary
    if (firstLine.length <= maxLength) { return firstLine; } return firstLine.substring(0, maxLength) + '...'; } /** * Get CSS class based on log severity * @param severity Log severity * @returns CSS class name */ export function getSeverityClass(severity: string): 'default' | 'destructive' | 'warning' | 'secondary' { switch (severity.toLowerCase()) { case 'error': return 'destructive'; case 'warning': return 'warning'; case 'info': return 'secondary'; case 'debug': default: return 'default'; } } /** * Format a stack trace for better readability * @param stackTrace Raw stack trace * @returns Formatted stack trace */ export function formatStackTrace(stackTrace: string): string { if (!stackTrace) return ''; // Already has line breaks if (stackTrace.includes('n')) { return stackTrace; } // Add line breaks at common delimiters return stackTrace .replace(/at /g, 'n at ') .replace(/^n/, ''); // Remove leading newline } /** * Format a number with thousands separators * @param num Number to format * @returns Formatted number string */ export function formatNumber(num: number): string { return new Intl.NumberFormat().format(num); } /** * Format time in seconds to a readable format * @param seconds Time in seconds * @returns Formatted time string */ export function formatTime(seconds: number): string { if (seconds < 60) { return `${seconds.toFixed(1)}s`; } const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}m ${remainingSeconds.toFixed(0)}s`; } /** * Format a file size in bytes to a human-readable format * @param bytes File size in bytes * @returns Formatted size string */ export function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } End File# myrta-ds/myrta import * as React from "react"; import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import { Card } from "@/components/ui/card"; interface BarChartProps { /** * Data for the bar chart */ data: any[]; /** * Key to use for the x-axis */ xAxisKey: string; /** * Key to use for the y-axis */ yAxisKey: string; /** * Height of the chart in pixels * @default 300 */ height?: number; /** * Width of the chart (can be percentage or pixels) * @default '100%' */ width?: number | string; /** * Color of the bars * @default 'hsl(var(--primary))' */ barColor?: string; /** * Whether to show the grid * @default true */ showGrid?: boolean; /** * Format value for display */ valueFormatter?: (value: number) => string;

    /**
    * Text prefix for values
    */
    valuePrefix?: string;

    /**
    * Text suffix for values
    */
    valueSuffix?: string;
    }

    /**
    * Bar chart component using Recharts
    */
    export function BarChart({
    data,
    xAxisKey,
    yAxisKey,
    height = 300,
    width = „100%”,
    barColor = „hsl(var(–primary))”,
    showGrid = true,
    valueFormatter,
    valuePrefix = „”,
    valueSuffix = „”,
    }: BarChartProps) {
    // Default value formatter
    const defaultFormatter = (value: number) => {
    return `${valuePrefix}${value}${valueSuffix}`;
    };

    // Use the provided formatter or default
    const formatValue = valueFormatter || defaultFormatter;

    return (


    {showGrid && (

    )}


    {
    if (active && payload && payload.length) {
    return (


    {payload[0].payload[xAxisKey]}

    {formatValue(payload[0].value as number)}


    );
    }

    return null;
    }}
    />



    );
    }
    End File# myrta-ds/myrta
    import * as React from „react”;
    import { DonutChart as RechartsDonutChart, PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from „recharts”;
    import { Card } from „@/components/ui/card”;

    /**
    * Data item for donut charts
    */
    interface DonutChartDataItem {
    name: string;
    value: number;
    }

    interface DonutChartProps {
    /**
    * Data for the donut chart
    */
    data: DonutChartDataItem[];

    /**
    * Height of the chart in pixels
    * @default 300
    */
    height?: number;

    /**
    * Width of the chart (can be percentage or pixels)
    * @default '100%’
    */
    width?: number | string;

    /**
    * Inner radius as a percentage
    * @default 70
    */
    innerRadius?: number;

    /**
    * Outer radius as a percentage
    * @default 90
    */
    outerRadius?: number;

    /**
    * Colors for the segments
    * @default Automatically generated based on theme
    */
    colors?: string[];

    /**
    * Show a tooltip on hover
    * @default true
    */
    showTooltip?: boolean;

    /**
    * Show a legend for the chart
    * @default false
    */
    showLegend?: boolean;

    /**
    * Format value for display
    */
    valueFormatter?: (value: number) => string;
    }

    /**
    * Donut chart component using Recharts
    */
    export function DonutChart({
    data,
    height = 300,
    width = „100%”,
    innerRadius = 70,
    outerRadius = 90,
    colors,
    showTooltip = true,
    showLegend = false,
    valueFormatter,
    }: DonutChartProps) {
    // Default colors based on theme
    const defaultColors = [
    „hsl(var(–primary))”,
    „hsl(var(–secondary))”,
    „hsl(var(–accent))”,
    „hsl(var(–muted))”,
    „hsl(var(–primary) / 0.8)”,
    „hsl(var(–secondary) / 0.8)”,
    „hsl(var(–accent) / 0.8)”,
    „hsl(var(–muted) / 0.8)”,
    „hsl(var(–primary) / 0.6)”,
    „hsl(var(–secondary) / 0.6)”,
    ];

    // Use the provided colors or default colors
    const chartColors = colors || defaultColors;

    // Default value formatter
    const defaultFormatter = (value: number) => {
    return value.toString();
    };

    // Use the provided formatter or default
    const formatValue = valueFormatter || defaultFormatter;

    return (



    {data.map((entry, index) => (

    ))}

    {showTooltip && (
    {
    if (active && payload && payload.length) {
    return (


    {payload[0].name}

    {formatValue(payload[0].value as number)}


    );
    }

    return null;
    }}
    />
    )}
    {showLegend && (

    )}


    );
    }
    End Fileimport * as React from „react”;
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area } from „recharts”;
    import { Card } from „@/components/ui/card”;

    /**
    * Data point for time series charts
    */
    interface TimeSeriesPoint {
    date: string;
    value: number;
    }

    interface TimeSeriesChartProps {
    /**
    * Data for the time series chart
    */
    data: TimeSeriesPoint[];

    /**
    * Height of the chart in pixels
    * @default 300
    */
    height?: number;

    /**
    * Width of the chart (can be percentage or pixels)
    * @default '100%’
    */
    width?: number | string;

    /**
    * Color of the line
    * @default 'hsl(var(–primary))’
    */
    lineColor?: string;

    /**
    * Color of the area under the line
    * @default 'hsl(var(–primary) / 0.2)’
    */
    areaColor?: string;

    /**
    * Whether to show the grid
    * @default true
    */
    showGrid?: boolean;

    /**
    * Whether to show the tooltip
    * @default true
    */
    showTooltip?: boolean;

    /**
    * Key to use for the x-axis
    * @default 'date’
    */
    xAxisKey?: string;

    /**
    * Key to use for the y-axis
    * @default 'value’
    */
    yAxisKey?: string;

    /**
    * Text prefix for values
    * @default ”
    */
    valuePrefix?: string;

    /**
    * Text suffix for values
    * @default ”
    */
    valueSuffix?: string;

    /**
    * Value formatter
    */
    valueFormatter?: (value: number) => string;
    }

    /**
    * Time series chart component using Recharts
    */
    export function TimeSeriesChart({
    data,
    height = 300,
    width = „100%”,
    lineColor = „hsl(var(–primary))”,
    areaColor = „hsl(var(–primary) / 0.2)”,
    showGrid = true,
    showTooltip = true,
    xAxisKey = „date”,
    yAxisKey = „value”,
    valuePrefix = „”,
    valueSuffix = „”,
    valueFormatter,
    }: TimeSeriesChartProps) {
    // Format the value for display
    const formatValue = (value: number) => {
    if (valueFormatter) {
    return valueFormatter(value);
    }
    return `${valuePrefix}${value}${valueSuffix}`;
    };

    return (


    {showGrid && (

    )}


    {showTooltip && (
    {
    if (active && payload && payload.length) {
    return (


    {payload[0].payload[xAxisKey]}

    {formatValue(payload[0].value as number)}


    );
    }

    return null;
    }}
    />
    )}




    );
    }
    End File# myrta-ds/myrta
    import { Badge } from „@/components/ui/badge”;
    import { Button } from „@/components/ui/button”;
    import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
    } from „@/components/ui/table”;
    import { PerformanceResource } from „@/lib/definitions”;
    import { formatBytes } from „@/lib/utils”;
    import { ArrowDownIcon, ArrowUpIcon, SortIcon } from „lucide-react”;
    import { useState } from „react”;

    type SortDirection = „asc” | „desc” | null;

    interface ResourcesTableProps {
    resources: PerformanceResource[];
    }

    export default function ResourcesTable({ resources }: ResourcesTableProps) {
    const [sortField, setSortField] = useState(„loadTime”);
    const [sortDirection, setSortDirection] = useState(„desc”);

    const handleSort = (field: string) => {
    if (sortField === field) {
    if (sortDirection === „asc”) {
    setSortDirection(„desc”);
    } else if (sortDirection === „desc”) {
    setSortDirection(null);
    setSortField(null);
    } else {
    setSortDirection(„asc”);
    }
    } else {
    setSortField(field);
    setSortDirection(„asc”);
    }
    };

    const getSortIcon = (field: string) => {
    if (sortField !== field) {
    return ;
    }
    if (sortDirection === „asc”) {
    return ;
    }
    if (sortDirection === „desc”) {
    return ;
    }
    return ;
    };

    // Sort resources based on current sort settings
    const sortedResources = […resources].sort((a, b) => {
    if (!sortField || !sortDirection) return 0;

    let valueA, valueB;

    switch (sortField) {
    case „name”:
    valueA = a.name.toLowerCase();
    valueB = b.name.toLowerCase();
    break;
    case „type”:
    valueA = a.type.toLowerCase();
    valueB = b.type.toLowerCase();
    break;
    case „size”:
    valueA = a.size;
    valueB = b.size;
    break;
    case „transferSize”:
    valueA = a.transferSize;
    valueB = b.transferSize;
    break;
    case „loadTime”:
    valueA = a.loadTime;
    valueB = b.loadTime;
    break;
    default:
    return 0;
    }

    if (sortDirection === „asc”) {
    return valueA > valueB ? 1 : -1;
    } else {
    return valueA < valueB ? 1 : -1; } }); // Helper function to get resource type badge const getResourceTypeBadge = (type: string) => {
    switch (type.toLowerCase()) {
    case „script”:
    return Script;
    case „stylesheet”:
    return CSS;
    case „image”:
    return Image;
    case „font”:
    return Font;
    case „fetch”:
    return API;
    default:
    return {type};
    }
    };

    return (



















    Status



    {sortedResources.map((resource) => (

    {resource.name}
    {getResourceTypeBadge(resource.type)}

    {formatBytes(resource.size)}


    {formatBytes(resource.transferSize)}


    500
    ? „text-red-500”
    : resource.loadTime > 300
    ? „text-amber-500”
    : „text-green-500”
    }
    >
    {resource.loadTime}ms



    {!resource.optimized ? (
    Needs Optimization
    ) : (
    Optimized
    )}


    ))}

    );
    }
    End File# myrta-ds/myrta
    import React from 'react’;
    import { Card, CardBody, CardHeader, CardTitle } from '@/components/ui/card’;
    import { Badge } from '@/components/ui/badge’;
    import { LogData } from '@/lib/definitions’;
    import { getSeverityClass, formatLogDate } from '@/lib/utils’;
    import { Button } from '@/components/ui/button’;
    import { CopyIcon } from 'lucide-react’;
    import copyToClipboard from '@/lib/clipboard’;

    interface LogMessageCardProps {
    log: LogData;
    isSelected?: boolean;
    onClick?: () => void;
    }

    const LogMessageCard: React.FC = ({
    log,
    isSelected = false,
    onClick
    }) => {
    const handleCopy = (e: React.MouseEvent) => {
    e.stopPropagation();
    copyToClipboard(`${log.timestamp} [${log.severity}] ${log.message}`);
    };

    return (

    {formatLogDate(log.timestamp)} • {log.source}

    {log.severity}


    {log.message.split(’n’)[0]}



    {log.message}

    {log.stackTrace && (

    {log.stackTrace.split(’n’)[0]}
    {log.stackTrace.split(’n’).length > 1 && '…’}

    )}


    );
    };

    export default LogMessageCard;
    End File’use client’

    import {
    ColumnDef,
    flexRender,
    getCoreRowModel,
    getPaginationRowModel,
    useReactTable,
    SortingState,
    ColumnFiltersState,
    getFilteredRowModel,
    getSortedRowModel,
    } from „@tanstack/react-table”

    import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
    } from „@/components/ui/table”
    import { Button } from „@/components/ui/button”
    import { useState } from 'react’
    import { Input } from '@/components/ui/input’

    interface DataTableProps {
    /**
    * Column definitions for the table
    */
    columns: ColumnDef[]

    /**
    * Data to display in the table
    */
    data: TData[]

    /**
    * Whether to show search functionality
    * @default true
    */
    showSearch?: boolean

    /**
    * The column key to search on
    */
    searchKey?: string

    /**
    * Placeholder text for the search input
    * @default „Search…”
    */
    searchPlaceholder?: string

    /**
    * Whether to show pagination controls
    * @default true
    */
    showPagination?: boolean

    /**
    * The number of rows per page
    * @default 10
    */
    pageSize?: number
    }

    /**
    * A reusable data table component with sorting, filtering, and pagination
    */
    export function DataTable({
    columns,
    data,
    showSearch = true,
    searchKey,
    searchPlaceholder = „Search…”,
    showPagination = true,
    pageSize = 10,
    }: DataTableProps) {
    const [sorting, setSorting] = useState([])
    const [columnFilters, setColumnFilters] = useState([])

    const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    onColumnFiltersChange: setColumnFilters,
    getFilteredRowModel: getFilteredRowModel(),
    state: {
    sorting,
    columnFilters,
    },
    initialState: {
    pagination: {
    pageSize,
    },
    },
    })

    return (

    {/* Search input */}
    {showSearch && searchKey && (


    table.getColumn(searchKey)?.setFilterValue(event.target.value)
    }
    className=”max-w-sm”
    />

    )}

    {/* Table */}



    {table.getHeaderGroups().map((headerGroup) => (

    {headerGroup.headers.map((header) => {
    return (

    {header.isPlaceholder
    ? null
    : flexRender(
    header.column.columnDef.header,
    header.getContext()
    )}

    )
    })}

    ))}


    {table.getRowModel().rows?.length ? (
    table.getRowModel().rows.map((row) => (

    {row.getVisibleCells().map((cell) => (

    {flexRender(cell.column.columnDef.cell, cell.getContext())}

    ))}

    ))
    ) : (


    No results.


    )}

    {/* Pagination */}
    {showPagination && (


    )}

    )
    }
    End File# myrta-ds/myrta
    import * as THREE from 'three’;
    import { useRef, useState, useEffect } from 'react’;
    import { Canvas, useFrame } from '@react-three/fiber’;
    import { useGLTF, useTexture, Environment, Float, MeshReflectorMaterial } from '@react-three/drei’;
    import { EffectComposer, Bloom } from '@react-three/postprocessing’;
    import { easing } from 'maath’;

    export function MeshStage({ children, color, …props }) {
    // Reflective floor setup
    const [hovered, setHovered] = useState(false);
    return (





    (e.stopPropagation(), setHovered(true))} onPointerOut={() => setHovered(false)}>
    {children}








    );
    }

    export function RoundedBox({ position = [0, 0, 0], args = [1, 1, 1], radius = 0.1, …props }) {
    const mesh = useRef();
    const [hovered, setHovered] = useState(false);

    useFrame((state, delta) => {
    easing.dampC(mesh.current.material.color, hovered ? '#40a0ff’ : '#ff6080′, 0.1, delta);
    mesh.current.rotation.x = mesh.current.rotation.y += delta / 4;
    });

    return (
    setHovered(true)}
    onPointerOut={() => setHovered(false)}
    castShadow
    receiveShadow
    {…props}
    >



    );
    }

    export function Shoe({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) {
    const mesh = useRef();
    const { nodes, materials } = useGLTF(’/models/shoe.glb’);

    useFrame((state, delta) => {
    const t = state.clock.elapsedTime;
    mesh.current.rotation.set(
    Math.cos(t / 4) / 8,
    Math.sin(t / 3) / 4,
    Math.sin(t / 2) / 8
    );
    mesh.current.position.y = (1 + Math.sin(t / 2)) / 3;
    });

    return (
























    );
    }

    export function WatchModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) {
    const mesh = useRef();
    const [hovered, setHovered] = useState(false);
    const { nodes, materials } = useGLTF(’/models/watch.glb’);

    useFrame((state, delta) => {
    const t = state.clock.elapsedTime;
    mesh.current.rotation.x = Math.cos(t / 2) / 8;
    mesh.current.rotation.y = Math.sin(t / 4) * 0.5;
    easing.dampC(materials.glass.color, hovered ? '#40a0ff’ : '#74b9ff’, 0.2, delta);
    easing.dampC(materials.body.color, hovered ? '#303030′ : '#202020′, 0.2, delta);
    });

    return (
    setHovered(true)}
    onPointerOut={() => setHovered(false)}
    {…props}
    dispose={null}
    >









    );
    }

    export function PhoneModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) {
    const mesh = useRef();
    const [hovered, setHovered] = useState(false);
    const texture = useTexture(’/textures/screen.jpg’);

    useEffect(() => {
    texture.flipY = false;
    }, [texture]);

    useFrame((state, delta) => {
    const t = state.clock.elapsedTime;
    mesh.current.rotation.x = Math.cos(t / 3) / 10;
    mesh.current.rotation.y = Math.sin(t / 2) * 0.3;
    });

    return (
    setHovered(true)}
    onPointerOut={() => setHovered(false)}
    {…props}
    >
    {/* Phone Body */}



    {/* Screen */}



    {/* Camera Module */}











    );
    }

    export function LaptopModel({ position = [0, 0, 0], rotation = [0, 0, 0], …props }) {
    const mesh = useRef();
    const [hovered, setHovered] = useState(false);
    const screenTexture = useTexture(’/textures/laptop-screen.jpg’);

    useEffect(() => {
    screenTexture.flipY = false;
    }, [screenTexture]);

    useFrame((state, delta) => {
    const t = state.clock.elapsedTime;
    mesh.current.rotation.x = Math.cos(t / 4) / 10;
    mesh.current.rotation.y = Math.sin(t / 3) * 0.2;
    });

    return (
    setHovered(true)}
    onPointerOut={() => setHovered(false)}
    {…props}
    >
    {/* Base */}



    {/* Keyboard Area */}



    {/* Trackpad */}



    {/* Screen Housing */}



    {/* Screen */}



    {/* Logo on back */}





    );
    }

    useGLTF.preload(’/models/shoe.glb’);
    useGLTF.preload(’/models/watch.glb’);
    End File# myrta-ds/myrta
    # apps/docs/app/components/design-system/color-palette.tsx
    import * as React from 'react’;
    import { Label } from '@/components/ui/label’;
    import { getContrast } from 'polished’;

    interface ColorSwatch {
    colorName: string;
    colorValue: string;
    isDark: boolean;
    }

    interface ColorPaletteProps {
    /**
    * The colors to display
    * Format: { colorName: colorValue }
    */
    colors: Record;
    }

    export function ColorPalette({ colors }: ColorPaletteProps) {
    const swatches = React.useMemo(() => {
    return Object.entries(colors).map(([colorName, colorValue]) => {
    // Calculate if text should be white or black
    const contrastWithWhite = getContrast(colorValue, '#FFFFFF’);
    const isDark = contrastWithWhite >= 3; // WCAG AA for normal text

    return {
    colorName,
    colorValue,
    isDark,
    };
    });
    }, [colors]);

    const sortedSwatches = React.useMemo(() => {
    return […swatches].sort((a, b) => {
    // Extract numbers like „50”, „100”, „200” from colorName
    const valueA = parseInt((a.colorName.match(/d+/) || [0])[0], 10);
    const valueB = parseInt((b.colorName.match(/d+/) || [0])[0], 10);
    return valueA – valueB;
    });
    }, [swatches]);

    return (

    {sortedSwatches.map((swatch) => (

    ))}

    );
    }

    /**
    * Individual color swatch component
    */
    function ColorSwatch({ colorName, colorValue, isDark }: ColorSwatch) {
    return (


    {colorValue}

    );
    }

    /**
    * Color grid to display a set of colors
    */
    export function ColorGrid({
    children,
    className,
    }: {
    children: React.ReactNode;
    className?: string;
    }) {
    return (


    {children}

    );
    }

    /**
    * Color grid item
    */
    export function ColorGridItem({
    color,
    name,
    className,
    }: {
    color: string;
    name: string;
    className?: string;
    }) {
    // Calculate if text should be white or black
    const contrastWithWhite = getContrast(color, '#FFFFFF’);
    const isDark = contrastWithWhite >= 3; // WCAG AA for normal text

    return (


    {color}

    );
    }
    End File”use client”;

    import React, { useState } from „react”;
    import { Tabs, TabsContent, TabsList, TabsTrigger } from „@/components/ui/tabs”;
    import { Copy } from „lucide-react”;
    import { Button } from „@/components/ui/button”;
    import { cn } from „@/lib/utils”;

    interface CodeBlockProps {
    /**
    * The code to display
    */
    code: string;

    /**
    * The language of the code (for syntax highlighting)
    * @default „tsx”
    */
    language?: string;

    /**
    * The file name to display
    */
    fileName?: string;

    /**
    * Disable syntax highlighting
    * @default false
    */
    disableSyntaxHighlighting?: boolean;

    /**
    * Show line numbers
    * @default false
    */
    showLineNumbers?: boolean;

    /**
    * Hide copy button
    * @default false
    */
    hideCopyButton?: boolean;

    /**
    * Additional HTML attributes for the code element
    */
    codeProps?: React.HTMLAttributes;

    /**
    * Alternative code examples in different languages
    */
    examples?: {
    language: string;
    code: string;
    fileName?: string;
    }[];
    }

    /**
    * Code block component for displaying code snippets with syntax highlighting
    */
    export function CodeBlock({
    code,
    language = „tsx”,
    fileName,
    disableSyntaxHighlighting = false,
    showLineNumbers = false,
    hideCopyButton = false,
    codeProps,
    examples = [],
    }: CodeBlockProps) {
    const [copied, setCopied] = useState(false);

    const allExamples = [
    {
    language,
    code,
    fileName,
    },
    …examples,
    ];

    const handleCopy = async (codeToСopy: string) => {
    await navigator.clipboard.writeText(codeToСopy);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
    };

    return (

    {/* File name header, if provided */}
    {allExamples.length === 1 && fileName && (

    {fileName}
    {!hideCopyButton && (

    )}

    )}

    {/* Multiple examples tabs */}
    {allExamples.length > 1 ? (


    {allExamples.map((example, i) => (

    {example.language.toUpperCase()}

    ))}

    {!hideCopyButton && (

    )}

    {allExamples.map((example, i) => (

                    
                      {example.code}
                    
                  


    ))}

    ) : (
    <>
    {/* Single code block */}

                
                  {code}
                
              

    {/* Copy button at the top right if no filename */}
    {!fileName && !hideCopyButton && (

    )}

    )}

    );
    }
    End File# myrta-ds/myrta
    # apps/docs/app/components/docs/installation-steps.tsx
    „use client”;

    import { Tabs, TabsContent, TabsList, TabsTrigger } from „@/components/ui/tabs”;
    import { CodeBlock } from „./code-block”;

    // Package manager tab content
    const packageManagers = [
    {
    name: „npm”,
    installCmd: „npm install @myrta-ds/core @myrta-ds/react”,
    peerCmd: „npm install react react-dom”,
    },
    {
    name: „yarn”,
    installCmd: „yarn add @myrta-ds/core @myrta-ds/react”,
    peerCmd: „yarn add react react-dom”,
    },
    {
    name: „pnpm”,
    installCmd: „pnpm add @myrta-ds/core @myrta-ds/react”,
    peerCmd: „pnpm add react react-dom”,
    },
    ];

    // Component import examples
    const importExamples = {
    typescript: `import { Button } from '@myrta-ds/react’;

    export default function App() {
    return (

    );
    }`,
    javascript: `import { Button } from '@myrta-ds/react’;

    export default function App() {
    return (

    );
    }`,
    };

    // CSS setup examples
    const cssSetupExamples = {
    typescript: `// In your entry file (e.g., main.tsx, index.tsx, or app.tsx)
    import '@myrta-ds/core/styles.css’;
    import { App } from ’./App’;

    // Rest of your application setup…`,
    javascript: `// In your entry file (e.g., main.js, index.js, or app.js)
    import '@myrta-ds/core/styles.css’;
    import { App } from ’./App’;

    // Rest of your application setup…`,
    };

    export default function InstallationSteps() {
    return (

    1. Install the packages



    {packageManagers.map((pm) => (

    {pm.name}

    ))}

    {packageManagers.map((pm) => (

    Myrta DS has peer dependencies on React and React DOM:



    ))}

    2. Import the CSS

    Import the CSS file in your main entry file to apply global styles:



    TypeScript
    JavaScript








    3. Use components

    Now you can import and use components in your application:



    TypeScript
    JavaScript








    );
    }
    End File”use client”;

    import * as React from „react”;
    import Link from „next/link”;
    import { usePathname } from „next/navigation”;
    import { cn } from „@/lib/utils”;
    import { ChevronRight } from „lucide-react”;

    export interface SidebarNavProps extends React.HTMLAttributes {
    items: {
    title: string;
    href?: string;
    disabled?: boolean;
    links?: {
    title: string;
    href: string;
    disabled?: boolean;
    }[];
    }[];
    }

    /**
    * SidebarNav component for documentation sidebar
    */
    export function SidebarNav({ className, items, …props }: SidebarNavProps) {
    const pathname = usePathname();

    // Track expanded sections
    const [expandedSections, setExpandedSections] = React.useState([]);

    // Check if section should be initially expanded
    const initializeExpandedSections = React.useCallback(() => {
    const initialExpanded: string[] = [];
    items.forEach((item) => {
    if (
    item.links?.some((link) => pathname.startsWith(link.href)) ||
    pathname === item.href
    ) {
    initialExpanded.push(item.title);
    }
    });
    setExpandedSections(initialExpanded);
    }, [items, pathname]);

    // Initialize expanded sections on mount and pathname change
    React.useEffect(() => {
    initializeExpandedSections();
    }, [initializeExpandedSections, pathname]);

    // Toggle a section’s expanded state
    const toggleSection = (title: string) => {
    setExpandedSections((prev) =>
    prev.includes(title)
    ? prev.filter((item) => item !== title)
    : […prev, title]
    );
    };

    return (

    );
    }
    End File# apps/docs/app/components/docs/api-tbl.tsx
    /**
    * API table component for documenting props and other APIs
    */

    import React from 'react’;
    import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table’;
    import { Badge } from '@/components/ui/badge’;

    interface ApiTableProps {
    /**
    * The data to display in the table
    */
    data: ApiItem[];

    /**
    * Optional table caption/title
    */
    caption?: string;

    /**
    * The name of the interface being documented
    */
    interfaceName?: string;

    /**
    * Whether to display default values
    * @default true
    */
    showDefaults?: boolean;
    }

    export interface ApiItem {
    /**
    * Property name
    */
    name: string;

    /**
    * Property type (e.g. string, number, boolean)
    */
    type: string;

    /**
    * Default value (if any)
    */
    default?: string;

    /**
    * Property description
    */
    description: string;

    /**
    * Whether the property is required
    * @default false
    */
    required?: boolean;
    }

    export function ApiTable({
    data,
    caption,
    interfaceName,
    showDefaults = true,
    }: ApiTableProps) {
    return (


    {caption && {caption}}


    Prop
    Type
    {showDefaults && Default}
    Description



    {data.map((item) => (


    {item.name}
    {item.required && Required}


    {formatType(item.type)}

    {showDefaults && (

    {item.default !== undefined ? formatDefaultValue(item.default) : ’-’}

    )}
    {item.description}

    ))}

    );
    }

    /**
    * Format a type string for display
    */
    function formatType(type: string): React.ReactNode {
    // Highlight certain types
    if ([’string’, 'number’, 'boolean’, 'any’, 'void’].includes(type)) {
    return {type};
    }

    // Format union types
    if (type.includes(’|’)) {
    return type.split(’|’).map((t, i) => (

    {i > 0 && ’ | ’}
    {t.trim()}

    ));
    }

    return type;
    }

    /**
    * Format a default value for display
    */
    function formatDefaultValue(value: string): React.ReactNode {
    // Boolean values
    if (value === 'true’ || value === 'false’) {
    return {value};
    }

    // Numbers
    if (!isNaN(Number(value))) {
    return {value};
    }

    // Strings (quoted)
    if ((value.startsWith(’”’) && value.endsWith(’”’)) ||
    (value.startsWith(„’”) && value.endsWith(„’”))) {
    return {value};
    }

    return value;
    }
    End File# myrta-ds/myrta
    import { MetadataRoute } from 'next’;

    interface SitemapEntry {
    url: string;
    lastModified?: string | Date;
    changeFrequency?: 'always’ | 'hourly’ | 'daily’ | 'weekly’ | 'monthly’ | 'yearly’ | 'never’;
    priority?: number;
    }

    export default function sitemap(): MetadataRoute.Sitemap {
    const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://myrta-ds.vercel.app’;

    const pages: SitemapEntry[] = [
    {
    url: baseUrl,
    lastModified: new Date(),
    changeFrequency: 'monthly’,
    priority: 1,
    },
    ];

    // Documentation pages
    const docsPaths = [
    // Getting started
    '/docs/introduction’,
    '/docs/installation’,
    '/docs/theming’,
    '/docs/roadmap’,

    // Foundation
    '/docs/foundation/colors’,
    '/docs/foundation/typography’,
    '/docs/foundation/spacing’,
    '/docs/foundation/breakpoints’,

    // Components
    '/docs/components/accordion’,
    '/docs/components/avatar’,
    '/docs/components/badge’,
    '/docs/components/button’,
    '/docs/components/card’,
    '/docs/components/checkbox’,
    '/docs/components/dialog’,
    '/docs/components/icon’,
    '/docs/components/menu’,
    '/docs/components/radio’,
    '/docs/components/select’,
    '/docs/components/slider’,
    '/docs/components/spinner’,
    '/docs/components/table’,
    '/docs/components/tabs’,
    '/docs/components/textfield’,
    '/docs/components/toggle’,
    '/docs/components/tooltip’,

    // Examples
    '/examples/dashboard’,
    '/examples/landing’,
    ];

    const docsEntries = docsPaths.map(path => ({
    url: `${baseUrl}${path}`,
    lastModified: new Date(),
    changeFrequency: 'weekly’ as const,
    priority: 0.8,
    }));

    // Legal and auxiliary pages
    const auxiliaryPaths = [
    '/examples’,
    '/docs’,
    '/changelog’,
    ];

    const auxiliaryEntries = auxiliaryPaths.map(path => ({
    url: `${baseUrl}${path}`,
    lastModified: new Date(),
    changeFrequency: 'monthly’ as const,
    priority: 0.5,
    }));

    // Combine all entries
    return [
    …pages,
    …docsEntries,
    …auxiliaryEntries,
    ];
    }
    End Fileimport { Metadata } from 'next’;
    import { ScrollArea } from '@/components/ui/scroll-area’;
    import { Products } from '@/app/examples/landing/components/Products’;
    import { Hero } from '@/app/examples/landing/components/Hero’;
    import { Features } from '@/app/examples/landing/components/Features’;
    import { Brands } from '@/app/examples/landing/components/Brands’;
    import { Faq } from '@/app/examples/landing/components/Faq’;
    import { Testimonials } from '@/app/examples/landing/components/Testimonials’;
    import { Pricing } from '@/app/examples/landing/components/Pricing’;
    import { Statistics } from '@/app/examples/landing/components/Statistics’;
    import { Footer } from '@/app/examples/landing/components/Footer’;
    import { Header } from '@/app/examples/landing/components/Header’;

    export const metadata: Metadata = {
    title: 'Landing Page Example | Myrta Design System’,
    description: 'Example landing page built with Myrta Design System components’,
    };

    export default function LandingPage() {
    return (










    );
    }
    End File# myrta-ds/myrta
    # apps/docs/app/examples/landing/components/Hero.tsx
    'use client’;

    import { Button } from '@/components/ui/button’;
    import Link from 'next/link’;
    import { ArrowRight, Play } from 'lucide-react’;
    import { motion } from 'framer-motion’;
    import { MeshStage, RoundedBox } from '@/components/3d/mesh-stage’;
    import Image from 'next/image’;

    export function Hero() {
    return (

    {/* Left column – Text content */}


    Next Generation Platform

    1. 11.04.2026
    2. www.leksykon.com.pl

    Powiązane tematy: