7.3 KiB
7.3 KiB
AGENTS.md - Coding Guidelines for CLQMS Frontend
SvelteKit frontend for Clinical Laboratory Quality Management System. Uses Svelte 5 runes, TailwindCSS 4, DaisyUI, and Lucide icons.
Commands
# Development
pnpm run dev # Start development server
pnpm run build # Production build (outputs to build/)
pnpm run preview # Preview production build
pnpm run prepare # Sync SvelteKit (runs on install)
# Package management
pnpm install # Install dependencies (use pnpm, not npm/yarn)
# Testing (not configured yet - add when needed)
# vitest run src/path/to/test.js # Run single test
# vitest # Run tests in watch mode
# npx playwright test # E2E tests
No ESLint/Prettier configured yet. When adding: configure in vite.config.js or separate config files.
Code Style
- ES Modules:
import/export(type: "module") - Semicolons: Required
- Quotes: Single quotes
- Indentation: 2 spaces
- Trailing commas: In multi-line objects/arrays
- JSDoc: Document all exported functions with
@paramand@returns
Imports Order
- Svelte (
svelte,$app/*) $lib/*(stores, api, components, utils)- External libraries (
lucide-svelte) - Relative imports (minimize, prefer
$lib)
Naming Conventions
- Components: PascalCase (
LoginForm.svelte,PatientFormModal.svelte) - Files/Routes: lowercase with hyphens (
+page.svelte,user-profile/) - Variables: camelCase (
isLoading,userName) - Constants: UPPER_SNAKE_CASE (
API_URL,STORAGE_KEY) - Stores: camelCase, descriptive (
auth,config,userStore) - Event handlers: prefix with
handle(handleSubmit,handleClick) - Form state:
formLoading,formError,deleteConfirmOpen
Svelte 5 Component Structure
<script>
// 1. Imports
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { auth } from '$lib/stores/auth.js';
import { User } from 'lucide-svelte';
// 2. Props with $bindable
let { open = $bindable(false), title = '', children, footer } = $props();
// 3. State
let loading = $state(false);
let formData = $state({ name: '' });
// 4. Derived
let isValid = $derived(formData.name.length > 0);
// 5. Effects
$effect(() => { /* side effects */ });
// 6. Handlers
function handleSubmit() { /* impl */ }
</script>
API Patterns
// src/lib/api/client.js - Use these helpers
import { get, post, put, patch, del } from '$lib/api/client.js';
// Feature endpoints (with JSDoc)
/** @param {Object} params - Query parameters */
export async function fetchItems(params = {}) {
const query = new URLSearchParams(params).toString();
return get(query ? `/api/items?${query}` : '/api/items');
}
export async function createItem(data) {
return post('/api/items', data);
}
export async function updateItem(id, data) {
return patch(`/api/items/${id}`, data);
}
export async function deleteItem(id) {
return del(`/api/items/${id}`);
}
Store Patterns
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
function createStore() {
const getInitialState = () => {
if (!browser) return { data: null };
return JSON.parse(localStorage.getItem('key'));
};
const { subscribe, set, update } = writable(getInitialState());
return {
subscribe,
setData: (data) => {
if (browser) localStorage.setItem('key', JSON.stringify(data));
set({ data });
}
};
}
Component Patterns
Modal with Snippets
<Modal bind:open={showModal} title="Edit Item" size="lg">
{#snippet children()}
<form onsubmit={handleSubmit}>
<input class="input input-bordered" bind:value={formData.name} />
</form>
{/snippet}
{#snippet footer()}
<button class="btn btn-ghost" onclick={() => showModal = false}>Cancel</button>
<button class="btn btn-primary" onclick={handleSubmit}>Save</button>
{/snippet}
</Modal>
Form with Validation
let formLoading = $state(false);
let formError = $state('');
let formData = $state({ username: '', password: '' });
function validateForm() {
formError = '';
if (!formData.username.trim()) {
formError = 'Username is required';
return false;
}
return true;
}
async function handleSubmit() {
if (!validateForm()) return;
formLoading = true;
try {
await api.submit(formData);
toastSuccess('Success');
} catch (err) {
formError = err.message;
} finally {
formLoading = false;
}
}
Error Handling
try {
const result = await api.fetchData();
toastSuccess('Operation successful');
} catch (err) {
const message = err.message || 'An unexpected error occurred';
toastError(message);
console.error('Operation failed:', err);
}
Styling (DaisyUI + Tailwind)
- Components:
btn,card,alert,input,navbar,modal,dropdown,menu - Colors:
primary(emerald),secondary(dark blue),accent(royal blue) - Sizes:
btn-sm,input-sm,select-smfor compact forms - Custom:
.compact-y,.compact-p,.compact-input,.compact-btn,.compact-card
Input with Icons (DaisyUI Pattern)
Correct way - Use DaisyUI's built-in input with icon support:
<!-- WRONG: Absolute positioning (icons may not render) -->
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2">
<User class="w-4 h-4" />
</span>
<input class="input input-sm input-bordered pl-9" />
</div>
<!-- CORRECT: DaisyUI input with flex layout -->
<label class="input input-sm input-bordered flex items-center gap-2 w-full focus-within:input-primary">
<User class="w-4 h-4 text-gray-400" />
<input
type="text"
class="grow bg-transparent outline-none"
placeholder="Enter value..."
bind:value={someValue}
/>
</label>
Key points:
- Use
<label>element as the input container withinput input-borderedclasses - Add
flex items-center gap-2for layout - Icon is a sibling of the input, not absolutely positioned
- Input uses
class="grow bg-transparent outline-none"to fill space - Use
focus-within:input-primaryfor focus state on the container - Always add
text-gray-400or similar color to the icon for visibility
Project Structure
src/
lib/
api/ # API clients per feature
stores/ # Svelte stores (auth, config, valuesets)
components/ # Reusable components (Modal, DataTable, Sidebar)
utils/ # Utilities (toast, helpers)
types/ # TypeScript type definitions
routes/ # SvelteKit routes
(app)/ # Route groups (protected)
dashboard/
patients/
master-data/
login/ # Public routes
Authentication
- Check
$auth.isAuthenticatedin layoutonMount - Redirect to
/loginif unauthenticated usinggoto('/login') - API client auto-redirects on 401
LocalStorage
- Only access in browser: check
browserfrom$app/environment - Use descriptive keys:
clqms_username,clqms_remember,auth_token
Important Notes
- Uses Svelte 5:
$props,$state,$derived,$effect,$bindable - Static adapter configured for static export
- Runtime config allows API URL changes without rebuild
- API proxy:
/api→http://localhost:8000(dev) - API Docs: https://clqms01-api.services-summit.my.id/swagger/