Refactors UI components with new card header structure

Replaces `swp-section-label` with standardized `swp-card-header` and `swp-card-title`

Improves component consistency across multiple features:
- Adds structured card headers
- Introduces more semantic HTML elements
- Enhances layout and readability of card components

Updates CSS and component styles to support new structure
This commit is contained in:
Janus C. H. Knudsen 2026-01-19 14:23:41 +01:00
parent 33c338345e
commit c1d2df9327
31 changed files with 250 additions and 149 deletions

View file

@ -75,29 +75,58 @@ Standard icon wrapper (40×40px cirkel):
| `swp-page-title` | Titel-wrapper med h1 og p | h1 + subtitle |
| `swp-page-actions` | Action buttons i header | Flex gap |
| `swp-card` | Standard card wrapper | Border, padding, rounded |
| `swp-section-label` | Card section label | Uppercase, 11px, border-bottom |
| `swp-section-header` | Wrapper for label + action | Flex, space-between |
| `swp-section-action` | Action link i section header | Teal, clickable |
| `swp-card-header` | Card header wrapper | Flex, space-between, border-bottom |
| `swp-card-title` | Card title med ikon | Flex, icon + text |
| `swp-section-action` | Action link i header | Teal, clickable |
| `swp-section-label` | Subsection label (inde i card) | Uppercase, 11px, border-bottom |
| `swp-card-content` | Card indhold | Block |
### Card Header Eksempler
**Simpel label (uden action):**
**Standard card header (ANBEFALET):**
```html
<swp-card>
<swp-section-label>Kontakter</swp-section-label>
<swp-card-content>...</swp-card-content>
<swp-card-header>
<swp-card-title>Kontakter</swp-card-title>
</swp-card-header>
<!-- content -->
</swp-card>
```
**Label med action:**
**Card header med ikon:**
```html
<swp-card>
<swp-section-header>
<swp-section-label>Dagens bookinger</swp-section-label>
<swp-card-header>
<swp-card-title>
<i class="ph ph-envelope"></i>
Email
</swp-card-title>
</swp-card-header>
<!-- content -->
</swp-card>
```
**Card header med action:**
```html
<swp-card>
<swp-card-header>
<swp-card-title>Dagens bookinger</swp-card-title>
<swp-section-action>Se alle</swp-section-action>
</swp-section-header>
<swp-card-content>...</swp-card-content>
</swp-card-header>
<!-- content -->
</swp-card>
```
**Subsection label (inde i card):**
```html
<swp-card>
<swp-card-header>
<swp-card-title>Grundlæggende</swp-card-title>
</swp-card-header>
<!-- first section content -->
<swp-section-label class="spaced">Interne noter</swp-section-label>
<!-- second section content -->
</swp-card>
```
@ -434,7 +463,9 @@ Bruger `swp-data-table` med `.salary-history` context class.
```html
<swp-card class="salary-history">
<swp-section-label>Lønhistorik</swp-section-label>
<swp-card-header>
<swp-card-title>Lønhistorik</swp-card-title>
</swp-card-header>
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell>Periode</swp-data-table-cell>

View file

@ -57,15 +57,9 @@ swp-filter-spacer {
/* ===========================================
ACTION BAR (Table Header)
=========================================== */
swp-action-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-6) var(--spacing-8);
background: var(--color-surface);
border: 1px solid var(--color-border);
swp-action-bar.grid-top {
border-bottom: none;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
margin-bottom: 0;
}
swp-selection-info {
@ -279,20 +273,6 @@ swp-row-arrow {
}
}
/* ===========================================
TWO-COLUMN GRID (Detail View)
=========================================== */
swp-cash-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-12);
}
swp-cash-column {
display: grid;
align-content: start;
}
/* ===========================================
DATA TABLE (Dagens Tal)
=========================================== */
@ -690,12 +670,6 @@ swp-system-note {
}
}
@media (max-width: 900px) {
swp-cash-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
swp-filter-bar {
flex-direction: column;

View file

@ -570,6 +570,7 @@ swp-card-title {
gap: var(--spacing-3);
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
color: var(--color-text);
& i {
@ -1322,12 +1323,17 @@ swp-back-link {
swp-detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-8);
gap: var(--card-gap);
align-items: start;
}
> div {
display: flex;
flex-direction: column;
}
/* ===========================================
CARD COLUMN (Stacked cards container)
=========================================== */
swp-card-column {
display: grid;
gap: var(--card-gap);
align-content: start;
}
@media (max-width: 900px) {
@ -1582,9 +1588,3 @@ swp-two-column-grid {
}
}
/* Stacked cards in a grid column */
.stacked-cards {
display: flex;
flex-direction: column;
gap: var(--card-gap);
}

View file

@ -12,6 +12,8 @@ swp-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--card-gap);
margin-bottom: var(--section-gap);
}
swp-stats-row {

View file

@ -88,10 +88,31 @@ export class CashController {
toBankInput.addEventListener('input', calculate);
actualCashInput.addEventListener('input', calculate);
// Setup Enter key navigation between fields
this.setupEnterNavigation([payoutsInput, toBankInput, actualCashInput]);
// Initial calculation
calculate();
}
/**
* Setup Enter key to move focus to next input field
*/
private setupEnterNavigation(inputs: HTMLInputElement[]): void {
inputs.forEach((input, index) => {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const nextIndex = index + 1;
if (nextIndex < inputs.length) {
inputs[nextIndex].focus();
inputs[nextIndex].select();
}
}
});
});
}
/**
* Calculate expected cash and difference
*/
@ -113,13 +134,13 @@ export class CashController {
}
// Calculate and display difference
this.updateDifference(actual, expectedCash, actualCashInput.value);
this.updateDifference(actual, expectedCash);
}
/**
* Update difference box with color coding
*/
private updateDifference(actual: number, expected: number, rawValue: string): void {
private updateDifference(actual: number, expected: number): void {
const box = document.getElementById('differenceBox');
const value = document.getElementById('differenceValue');
if (!box || !value) return;
@ -129,11 +150,7 @@ export class CashController {
// Remove all state classes
box.classList.remove('positive', 'negative', 'neutral');
if (actual === 0 && rawValue === '') {
// No input yet
value.textContent = ' kr';
box.classList.add('neutral');
} else if (diff > 0) {
if (diff > 0) {
// More cash than expected
value.textContent = '+' + this.formatNumber(diff) + ' kr';
box.classList.add('positive');