adopting agent instructions

for a more consistent coding style
This commit is contained in:
2026-04-12 08:03:59 +02:00
parent 2cac1cc41d
commit 7fadf82c4c
5 changed files with 115 additions and 885 deletions
+4 -47
View File
@@ -1,5 +1,7 @@
# Bamort Development Instructions
READ THIS FILE CAREFULLY AND COMPLETE BEFORE STARTING DEVELOPMENT!
Bamort is a role-playing game character management system (MOAM replacement) with a Go backend and Vue.js frontend.
## Project Overview
@@ -56,34 +58,6 @@ module/
}
```
## Frontend Architecture (`frontend/`)
### Component Structure
- **Views**: `src/views/` - Page-level components (CharacterView, DashboardView)
- **Components**: `src/components/` - Reusable components (CharacterDetails, SkillView)
- **Utils**: `src/utils/api.js` - Axios instance with JWT interceptor
- **Locales**: `src/locales/de` and `src/locales/en` - i18n translations (JS objects, not JSON)
- **Store**: Pinia stores in `src/stores/`
### API Communication
- Use `API.get()`, `API.post()` from `utils/api.js` (auto-adds auth headers)
- Base URL: `import.meta.env.VITE_API_URL` (defaults to `http://localhost:8180`)
- Example:
```js
const response = await API.get(`/api/characters/${this.id}`)
```
### Translation Pattern
Add to both `locales/de` and `locales/en`:
```js
export default {
export: {
selectTemplate: 'Vorlage wählen', // DE
exportPDF: 'PDF Export'
}
}
```
## Docker Development Workflow
### Verify Containers
@@ -141,21 +115,6 @@ if err != nil {
}
```
### Modal Dialogs (Frontend)
```vue
<div v-if="showDialog" class="modal-overlay" @click.self="showDialog = false">
<div class="modal-content"><!-- content --></div>
</div>
```
### Popup Blocker Workaround
Open window **synchronously** before async calls:
```js
const newWindow = window.open('', '_blank')
// ... then await API call ...
newWindow.location.href = url
```
## Critical Rules
1. **NEVER** write example/demo code - only production code
@@ -163,13 +122,11 @@ newWindow.location.href = url
3. **ALWAYS** check `docker ps` before assuming containers are running
4. **ALWAYS** use TDD: write failing test first, then implement
5. **ALWAYS** use KISS principle: simplest solution that works
6. **ALWAYS** add translations to both DE and EN locales
7. **ALWAYS** use global CSS definition to ensure consistent style
## File-Specific Instructions
Load additional instructions for specific file types:
- Go files: See `.github/instructions/go.instructions.md`
- Vue files: See `.github/instructions/vue.instructions.md`
- js files: See `.github/instructions/js.instructions.md`
- css files: See `.github/instructions/css.instructions.md`
- CSS files: See `.github/instructions/css.instructions.md`
- JS files: See `.github/instructions/js.instructions.md`
+29 -468
View File
@@ -5,480 +5,41 @@ applyTo: '**/*.css,**/*.vue'
# CSS Development Instructions
Follow project-specific CSS conventions and modern best practices.
## Cascading Design & Style Hierarchy
## Scoped Styles in Vue Components
**Central styles in `src/assets/main.css` are ALWAYS the preferred way to style.** They ensure a consistent design across all views and components.
### Always Use Scoped Styles
```vue
<style scoped>
.component-class {
/* Styles only apply to this component */
}
</style>
```
### Cascade Layers (top to bottom)
1. **`src/assets/base.css`** — CSS variables, resets, root typography and color tokens
2. **`src/assets/main.css`** — Global layout, nav, buttons, modals, forms, loading states, typography, spacing — **all shared UI patterns go here**
3. **`<style scoped>` in `.vue` files** — Only for component-specific layout overrides that have no equivalent in `main.css`
**Critical**: Use `scoped` attribute to prevent style conflicts between components.
### Rule: Central vs. Scoped CSS
- **Use `main.css`** for: buttons, modals, forms, inputs, nav, loading spinners, typography, color tokens, flex layout utilities — anything reused in 2+ places
- **Use `<style scoped>`** only when a style is truly unique to one component and cannot be expressed by an existing global class
- **Never** duplicate a style from `main.css` in a scoped block
## Layout Patterns
## Key CSS Techniques
### Flexbox for Component Layouts
Standard pattern for headers, modals, and lists:
- **Flexbox** for all layouts — use `gap` instead of margins between flex children
- **CSS custom properties** (variables) from `base.css` — always reference via `var(--token-name)`
- **Transitions** (`transition: all 0.2s ease`) on all interactive elements
- **`@media` queries** in `main.css` for responsive breakpoints (mobile-first)
- **`z-index` layers** defined centrally: modals `1000`, dropdowns `100`, tooltips `200`
```css
.header-content {
display: flex;
align-items: center;
gap: 15px; /* Use gap instead of margin */
}
## Style Conventions
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
```
- Class selectors only — no IDs, no inline styles, no `!important`
- `rem`/`em` for font sizes; `px` only for borders and fixed-size icons
- Max 3 levels of selector nesting
- Always include `:hover`, `:focus`, `:disabled` states for interactive elements
- `box-sizing: border-box` set globally — do not override
### Common Flex Patterns
```css
/* Horizontal layout with spacing */
.horizontal-layout {
display: flex;
gap: 10px;
align-items: center;
}
## Anti-Patterns
/* Vertical centering */
.centered {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
/* Space between items */
.space-between {
display: flex;
justify-content: space-between;
align-items: center;
}
```
## Modal Dialog Styling
### Standard Modal Pattern
```css
/* Overlay - covers entire viewport */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* Modal content container */
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Modal sections */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #dee2e6;
}
.modal-body {
padding: 20px;
position: relative; /* For loading overlays */
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px;
border-top: 1px solid #dee2e6;
}
```
## Button Styling
### Standard Button Styles
```css
/* Primary action button */
.btn-primary,
.btn-export {
padding: 10px 20px;
border: 1px solid #007bff;
border-radius: 6px;
background: #007bff;
color: white;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
border-color: #0056b3;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Cancel/secondary button */
.btn-cancel {
padding: 10px 20px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: #f8f9fa;
color: #495057;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-cancel:hover {
background: #e9ecef;
border-color: #adb5bd;
}
/* Icon-only button */
.export-button-small {
width: 40px;
height: 40px;
padding: 0;
border: 1px solid #007bff;
border-radius: 8px;
background: #007bff;
color: white;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.export-button-small:hover {
background: #0056b3;
transform: scale(1.05);
}
```
## Form Elements
### Input Styling
```css
.template-select,
input[type="text"],
input[type="email"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: white;
color: #495057;
font-size: 0.95rem;
}
.template-select:focus,
input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
input:disabled,
select:disabled {
opacity: 0.6;
cursor: not-allowed;
background: #e9ecef;
}
```
### Checkbox Styling
```css
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
```
## Loading Animations
### Spinner Animation
```css
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
```
### Loading Overlay
```css
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.loading-overlay p {
color: #007bff;
font-weight: 500;
margin: 0;
}
```
## Color Scheme
### Standard Colors
```css
/* Primary */
--primary: #007bff;
--primary-hover: #0056b3;
/* Text */
--text-primary: #333;
--text-secondary: #495057;
--text-muted: #666;
/* Backgrounds */
--bg-light: #f8f9fa;
--bg-gray: #e9ecef;
/* Borders */
--border-light: #dee2e6;
--border-dark: #adb5bd;
/* Semantic colors */
--success: #28a745;
--danger: #dc3545;
--warning: #ffc107;
```
Use these consistently across components for visual coherence.
## Spacing System
### Use Consistent Spacing
```css
/* Prefer these spacing values */
gap: 8px; /* Tight spacing */
gap: 10px; /* Default spacing */
gap: 15px; /* Medium spacing */
gap: 20px; /* Large spacing */
padding: 10px 12px; /* Inputs */
padding: 20px; /* Modal sections */
```
## Typography
### Font Sizing
```css
h2 {
font-size: 1.5rem;
margin: 0;
color: #333;
}
h3 {
font-size: 1.25rem;
margin: 0;
color: #333;
}
p, span {
font-size: 0.95rem;
color: #495057;
}
small {
font-size: 0.875rem;
color: #666;
}
```
### Font Weight
```css
font-weight: 400; /* Normal */
font-weight: 500; /* Medium (buttons, labels) */
font-weight: 600; /* Semibold (headings) */
```
## Transitions and Animations
### Standard Transitions
```css
/* Buttons, interactive elements */
transition: all 0.2s ease;
/* Background changes */
transition: background 0.3s ease;
/* Transform animations */
transition: transform 0.2s ease;
```
### Hover Effects
```css
button:hover {
transform: scale(1.02); /* Subtle scale */
}
.card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
```
## Responsive Design
### Mobile-First Approach
```css
/* Base styles (mobile) */
.modal-content {
width: 90%;
max-width: 500px;
}
/* Tablet and up */
@media (min-width: 768px) {
.modal-content {
width: 600px;
}
}
/* Desktop */
@media (min-width: 1024px) {
.modal-content {
width: 700px;
}
}
```
## Best Practices
1. **Always use `scoped`** on Vue component styles
2. **Use flexbox for layouts** instead of floats or positioning
3. **Use `gap` property** instead of margins for spacing
4. **Keep selectors simple** - avoid deep nesting
5. **Use relative units** (`rem`, `em`) for font sizes
6. **Add transitions** for interactive elements
7. **Use CSS variables** for repeated values
8. **Keep z-index organized** (modals: 1000, dropdowns: 100, etc.)
9. **Test hover states** for all interactive elements
10. **Include disabled states** for buttons and inputs
## Anti-Patterns to Avoid
❌ Don't use `!important` unless absolutely necessary
❌ Don't use inline styles in templates - use classes
❌ Don't use fixed pixel widths for responsive layouts
❌ Don't nest selectors more than 3 levels deep
❌ Don't use IDs for styling - use classes
❌ Don't forget `:hover`, `:focus`, `:disabled` states
❌ Don't use `position: absolute` unless necessary
❌ Don't forget to test in different viewport sizes
❌ Don't use vendor prefixes manually - use autoprefixer
## Common Component Patterns
### Close Button
```css
.close-button {
background: none;
border: none;
font-size: 1.5rem;
color: #999;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
```
### Full-Height Container
```css
.character-details {
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
```
### Submenu/Tabs
```css
.submenu {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
.submenu button {
padding: 10px 16px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: #f8f9fa;
color: #495057;
cursor: pointer;
transition: all 0.2s ease;
}
.submenu button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
```
❌ Don't add a scoped style that duplicates a `main.css` rule
❌ Don't use inline styles in Vue templates
❌ Don't use `!important`
❌ Don't use IDs as CSS selectors
❌ Don't use floats or `position: absolute` for layout — use flexbox
❌ Don't add vendor prefixes manually — autoprefixer handles this
+4 -2
View File
@@ -5,6 +5,8 @@ applyTo: '**/*.go,**/go.mod,**/go.sum'
# Go Development Instructions
READ THIS FILE CAREFULLY AND COMPLETE BEFORE STARTING DEVELOPMENT!
Follow idiomatic Go practices and community standards when writing Go code. These instructions are based on [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), and [Google's Go Style Guide](https://google.github.io/styleguide/go/).
## General Instructions
@@ -266,10 +268,10 @@ Follow idiomatic Go practices and community standards when writing Go code. Thes
### Essential Tools
- `go fmt`: Format code
- `go vet`: Find suspicious constructs
- `golint` or `golangci-lint`: Additional linting
- `go test`: Run tests
- `go fmt`: Format code
- `golint` or `golangci-lint`: Additional linting
- `go mod`: Manage dependencies
- `go generate`: Code generation
+56 -368
View File
@@ -5,394 +5,82 @@ applyTo: '**/*.vue, **/*.ts, **/*.js, **/*.scss'
# Vue 3 Development Instructions
Follow Vue 3 best practices and project-specific conventions when writing components.
## Architecture & Code Organisation
## Component Structure
### Folder Responsibilities
| Path | Purpose |
|---|---|
| `src/views/` | Page-level components mounted by the router — orchestrate layout and child components |
| `src/components/` | Reusable UI components — self-contained, receive data via props, emit events upward |
| `src/utils/` | Shared utility functions and the central API client — **always prefer these over local re-implementations** |
| `src/stores/` | Pinia stores for global state (auth, language, etc.) |
| `src/locales/` | i18n translation objects (`de` and `en`, `.js` files, not JSON) |
| `src/assets/main.css` | Central styles — see css.instructions.md |
### Standard Component Layout
```vue
<template>
<!-- HTML template -->
</template>
### Component vs. Inline Code
- **Extract to a component** whenever UI logic would be repeated in more than one view, or when a dialog/panel is complex enough to have its own state
- **Prefer a dedicated component** for modal dialogs over writing inline modal markup inside a view — keeps views clean and the dialog reusable
- **Write utility functions in `src/utils/`** for any logic that could be used in more than one component (formatting, validation helpers, API wrappers)
- **Local methods** are fine for view/component-specific logic that will never be reused
<style scoped>
/* Component-specific styles */
</style>
<script>
// Component logic
</script>
### Component File Layout (order matters)
```
**Order matters**: Template, Style, Script (as seen throughout the codebase)
### Options API Pattern (Primary)
Use Options API for consistency with existing codebase:
```vue
<script>
export default {
name: "ComponentName",
props: ["id"],
data() {
return {
items: [],
isLoading: false
}
},
async created() {
// Initialization logic
},
methods: {
async methodName() {
// Use const/let, never var
const response = await API.get('/api/endpoint')
this.items = response.data
}
}
}
</script>
<template> → <style scoped> → <script>
```
Use **Options API** for consistency with the existing codebase (`data`, `created`, `methods`).
## API Communication
### Using the API Utility
Always use `API` from `utils/api.js` - it handles authentication automatically:
All HTTP calls go through `src/utils/api.js` — an Axios instance with JWT and 401-redirect interceptors.
```js
import API from '../utils/api'
- Import `API` from `utils/api.js`; **never** construct raw `axios` calls or add `Authorization` headers manually
- Base URL is read from `import.meta.env.VITE_API_URL` (default `http://localhost:8180`)
- Catch errors with `try/catch`; extract the message via `error.response?.data?.error ?? error.message` and show it to the user
// In methods:
const response = await API.get(`/api/characters/${this.id}`)
const data = await API.post('/api/characters', character)
```
## Modal Dialogs
**Never** manually add Authorization headers - the interceptor handles this.
**Always implement modals as separate components** in `src/components/`, not as inline markup in a view.
### Environment Variables
API base URL from Vite:
```js
import API from '../utils/api'
// API uses: import.meta.env.VITE_API_URL || 'http://localhost:8180'
```
- The view renders `<MyDialog v-if="showDialog" @close="showDialog = false" />`
- The dialog component handles its own internal state and emits `close` (and any result events) to the parent
- Use global CSS classes `modal-overlay`, `modal-content`, `modal-header`, `modal-body`, `modal-footer`, `btn-primary`, `btn-cancel`, `close-button` — all defined in `main.css`, do not redefine them in scoped styles
- Close on overlay click with `@click.self`; always include a `×` close button in the header
### API Configuration (`utils/api.js`)
Standard Axios instance with interceptors:
```js
import axios from 'axios'
## State & Reactivity
const API = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8180'
})
- **Pinia stores** (`src/stores/`) for state shared across multiple views (language, auth token, etc.)
- **Component-local `data()`** for state that belongs only to that component
- Always track async operations with an `isLoading` flag; disable interactive elements and show feedback while loading
- Use `v-if` for infrequently toggled elements, `v-show` for frequent toggles; always use `:key` with `v-for`
// Request interceptor - adds auth token
API.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
## Internationalization
// Response interceptor - handles 401
API.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
}
return Promise.reject(error)
}
)
export default API
```
### Error Handling Pattern
```js
try {
const response = await API.get(`/api/endpoint`)
this.data = response.data
} catch (error) {
console.error('Failed to load data:', error)
const errorMsg = error.response?.data?.error ?? error.message
alert(`${this.$t('errors.loadFailed')}: ${errorMsg}`)
}
```
## Internationalization (i18n)
### Using Translations
```vue
<template>
<h2>{{ $t('char') }}: {{ character.name }}</h2>
<button>{{ $t('export.exportPDF') }}</button>
</template>
```
### Adding New Translations
**ALWAYS** add to both `src/locales/de` and `src/locales/en`:
```js
// src/locales/de
export default {
export: {
selectTemplate: 'Vorlage wählen',
exportPDF: 'PDF Export'
}
}
// src/locales/en
export default {
export: {
selectTemplate: 'Select Template',
exportPDF: 'Export PDF'
}
}
```
**Note**: Locale files use `.js` extension and export objects, not JSON.
## Modal Dialog Pattern
### Standard Modal Structure
```vue
<template>
<!-- Trigger -->
<button @click="showDialog = true">Open</button>
<!-- Modal -->
<div v-if="showDialog" class="modal-overlay" @click.self="showDialog = false">
<div class="modal-content">
<div class="modal-header">
<h3>{{ $t('modal.title') }}</h3>
<button @click="showDialog = false" class="close-button">&times;</button>
</div>
<div class="modal-body">
<!-- Content -->
</div>
<div class="modal-footer">
<button @click="showDialog = false" class="btn-cancel">{{ $t('cancel') }}</button>
<button @click="handleSubmit" class="btn-primary">{{ $t('submit') }}</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showDialog: false
}
}
}
</script>
```
**Key conventions:**
- Use `@click.self` on overlay to close on outside click
- Include close button (×) in header
- Separate header, body, footer sections
## Component Communication
### Props (Parent → Child)
```vue
<script>
export default {
props: {
character: Object,
id: [String, Number]
}
}
</script>
```
### Events (Child → Parent)
```vue
<template>
<button @click="notifyParent">Update</button>
</template>
<script>
export default {
methods: {
notifyParent() {
this.$emit('character-updated', this.character)
}
}
}
</script>
<!-- Parent component -->
<template>
<ChildComponent @character-updated="refreshCharacter" />
</template>
```
## State Management
### Pinia Store Pattern (`stores/`)
For global state (language, auth, etc.):
```js
import { defineStore } from 'pinia'
export const useLanguageStore = defineStore('language', {
state: () => ({
currentLanguage: localStorage.getItem('language') || 'de'
}),
actions: {
setLanguage(lang) {
this.currentLanguage = lang
localStorage.setItem('language', lang)
}
}
})
```
### Loading States
Always show feedback for async operations:
```vue
<template>
<button @click="submit" :disabled="isLoading">
<span v-if="!isLoading">{{ $t('submit') }}</span>
<span v-else>{{ $t('loading') }}</span>
</button>
</template>
<script>
export default {
data() {
return { isLoading: false }
},
methods: {
async submit() {
this.isLoading = true
try {
await API.post('/api/endpoint', this.data)
} finally {
this.isLoading = false
}
}
}
}
</script>
```
### Disabling Form Elements During Loading
```vue
<select v-model="selected" :disabled="isLoading">
<input type="checkbox" v-model="option" :disabled="isLoading">
```
## Form Validation
### Validation Pattern
```js
methods: {
validateForm() {
if (!this.selectedTemplate) {
alert(this.$t('export.pleaseSelectTemplate'))
return false
}
return true
},
async submit() {
if (!this.validateForm()) return
this.isLoading = true
try {
await API.post('/api/endpoint', this.data)
} finally {
this.isLoading = false
}
}
}
```
- All user-visible strings use `$t('key')` — never hardcode text in templates
- **Always add translations to both** `src/locales/de` and `src/locales/en` at the same time
- Use nested keys that reflect the domain (e.g., `export.selectTemplate`)
## Browser Compatibility
### Popup Blocker Workaround
Open windows **synchronously** before async operations:
**Popup blocker**: `window.open()` must be called **synchronously** in the click handler — before any `await`. Open the window first, then perform the async API call and update `window.location.href` afterwards.
```js
async exportToPDF() {
// Open window FIRST (synchronously)
const pdfWindow = window.open('', '_blank')
if (!pdfWindow) {
alert(this.$t('export.popupBlocked'))
return
}
// Show loading page
pdfWindow.document.write('<html>...</html>')
// Then do async work
const response = await API.get('/api/pdf/export')
// Update window with result
pdfWindow.location.href = url
}
```
## Code Conventions
**Critical**: `window.open()` must be called synchronously in the click handler, not after `await`.
- `const` by default, `let` when reassignment is needed — never `var`
- `===` for all comparisons, never `==`
- Optional chaining `?.` and nullish coalescing `??` for safe property access
- Keep template expressions simple — move logic to `methods`
- PascalCase for component file names (`CharacterDetails.vue`)
- Handle errors gracefully — with user-friendly messages
- Test with actual API — running in Docker container
- Check HMR reload — view logs: `docker logs bamort-frontend-dev`
## Common Patterns
## Anti-Patterns
### Dynamic Component Loading
```vue
<template>
<component :is="currentView" :character="character" @character-updated="refresh"/>
</template>
<script>
import ViewA from './ViewA.vue'
import ViewB from './ViewB.vue'
export default {
components: { ViewA, ViewB },
data() {
return { currentView: 'ViewA' }
}
}
</script>
```
### Conditional Rendering
- Use `v-if` for elements that toggle rarely
- Use `v-show` for frequent toggles
- Use `v-for` with `:key` attribute always
### Event Modifiers
- `@click.self` - only trigger if clicked element itself
- `@submit.prevent` - prevent form submission
- `@keyup.enter` - keyboard event handling
## Best Practices
1. **Use global CSS definition** to ensure consistent styling
2. **Always use scoped styles** to avoid CSS conflicts
3. **Name components with PascalCase** (e.g., `CharacterDetails.vue`)
4. **Use `const` by default, `let` when needed** - never use `var`
5. **Use optional chaining `?.` and nullish coalescing `??`** for safe property access
6. **Handle errors gracefully** with user-friendly messages
7. **Keep template logic simple** - move complex logic to methods
8. **Clean up resources** in `beforeUnmount` if needed
9. **Test with actual API** running in Docker container
10. **Check HMR reload** - view logs: `docker logs bamort-frontend-dev`
## Anti-Patterns to Avoid
❌ Don't use `v-if` and `v-for` on the same element
❌ Don't mutate props directly
❌ Don't use `var` - use `const` or `let`
❌ Don't use `==` - always use `===` for comparisons
❌ Don't forget to handle loading and error states
❌ Don't use inline styles - use scoped CSS
❌ Don't call API methods in template expressions
❌ Don't forget translations - add to both DE and EN
❌ Don't write inline modal markup in views — extract to a component
❌ Don't duplicate API call setup — use `utils/api.js`
❌ Don't duplicate utility logic — add it to `src/utils/` and import it
❌ Don't use inline styles — use global CSS classes from `main.css`; scoped CSS only for genuinely unique local overrides
❌ Don't use `v-if` and `v-for` on the same element
❌ Don't mutate props directly
❌ Don't forget translations — always update both `de` and `en`
❌ Don't call API methods in template expressions