Styling fix and scraper bug fix

This commit is contained in:
2026-06-14 18:39:55 +02:00
parent 6ed2a3ae0c
commit 566595984f
21 changed files with 2150 additions and 119 deletions
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
import './loading-spinner.css'
interface LoadingSpinnerProps {
message?: string
@@ -7,9 +8,9 @@ interface LoadingSpinnerProps {
export function LoadingSpinner({ message = 'Loading...', ariaLabel = 'Loading' }: LoadingSpinnerProps): ReactNode {
return (
<div role="status" aria-label={ariaLabel} aria-busy={true} data-testid="loading-spinner">
<div data-testid="spinner-icon" role="presentation">⊙</div>
<p data-testid="spinner-message">{message}</p>
<div className="loading-spinner" role="status" aria-label={ariaLabel} aria-busy={true} data-testid="loading-spinner">
<div className="loading-spinner__icon" data-testid="spinner-icon" role="presentation">⊙</div>
<p className="loading-spinner__message" data-testid="spinner-message">{message}</p>
</div>
)
}
@@ -0,0 +1,39 @@
/* Loading Spinner */
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--sp-8);
gap: var(--sp-4);
min-height: 300px;
}
.loading-spinner__icon {
font-size: 48px;
color: var(--c-accent);
animation: spin 2s linear infinite;
display: block;
}
.loading-spinner__message {
font-size: var(--fs-base);
color: var(--c-text-muted);
margin: 0;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Reduce Motion */
@media (prefers-reduced-motion: reduce) {
.loading-spinner__icon {
animation: none;
}
}
@@ -13,18 +13,19 @@ interface OverviewMetricsProps {
export function OverviewMetrics({ metrics, title = 'Overview' }: OverviewMetricsProps): ReactNode {
return (
<section role="region" aria-label={title} data-testid="overview-metrics">
<h2 data-testid="metrics-title">{title}</h2>
<div role="list" data-testid="metrics-list">
<section role="region" aria-label={title} data-testid="overview-metrics" className="overview-metrics">
<h2 data-testid="metrics-title" className="overview-metrics__title">{title}</h2>
<div role="list" data-testid="metrics-list" className="overview-metrics__list">
{metrics.map((metric, index) => (
<div
key={index}
role="listitem"
data-testid={metric.testId || `metric-${index}`}
data-label={metric.label}
className="overview-metrics__item"
>
<span data-testid={`metric-label-${index}`}>{metric.label}:</span>
<span data-testid={`metric-value-${index}`}>{metric.value}</span>
<span data-testid={`metric-label-${index}`} className="overview-metrics__label">{metric.label}:</span>
<span data-testid={`metric-value-${index}`} className="overview-metrics__value">{metric.value}</span>
</div>
))}
</div>
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
import './footer.css'
export function Footer(): ReactNode {
const currentYear = new Date().getFullYear()
@@ -4,6 +4,7 @@ import { useAuth } from '@/context/AuthContext'
import { useWorkspace } from '@/context/WorkspaceContext'
import { Header, HeaderProps } from './Header'
import { Footer } from './Footer'
import './layout.css'
interface LayoutProps {
children: ReactNode
+13
View File
@@ -0,0 +1,13 @@
footer {
margin-top: var(--sp-8);
padding: var(--sp-5);
border-top: 1px solid var(--c-border);
background-color: var(--c-bg);
text-align: center;
color: var(--c-text-muted);
}
footer p {
margin: 0;
font-size: var(--fs-sm);
}
+63 -33
View File
@@ -1,83 +1,113 @@
.header {
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
padding: 16px 20px;
position: sticky;
top: 0;
z-index: 100;
background-color: var(--c-surface);
border-bottom: 1px solid var(--c-border);
padding: var(--sp-4) var(--sp-5);
box-shadow: var(--shadow-sm);
}
.header__top {
max-width: var(--maxw);
margin: 0 auto var(--sp-4);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header__title {
font-size: 18px;
font-weight: 600;
color: #333;
font-size: var(--fs-lg);
font-weight: 700;
color: var(--c-text);
}
.header__identity {
display: flex;
align-items: center;
gap: 12px;
gap: var(--sp-4);
}
.header__parent-name {
font-size: 14px;
color: #666;
font-size: var(--fs-sm);
color: var(--c-text-muted);
font-weight: 500;
}
.header__child-select {
padding: 6px 10px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 14px;
background-color: #fff;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
font-size: var(--fs-sm);
background-color: var(--c-surface);
color: var(--c-text);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.header__child-select:focus {
outline: none;
border-color: var(--c-accent);
box-shadow: 0 0 0 3px rgba(47, 111, 79, 0.1);
}
.header__child-name {
font-size: 14px;
color: #0066cc;
font-size: var(--fs-sm);
color: var(--c-accent);
font-weight: 500;
}
.header__nav {
max-width: var(--maxw);
margin: 0 auto;
display: flex;
gap: 8px;
gap: var(--sp-3);
flex-wrap: wrap;
justify-content: center;
}
.header__nav-link {
padding: 8px 14px;
font-size: 14px;
padding: var(--sp-2) var(--sp-4);
font-size: var(--fs-sm);
text-decoration: none;
color: #333;
border-radius: 4px;
transition: background-color 0.2s;
color: var(--c-text);
border-radius: var(--radius);
border-bottom: 2px solid transparent;
transition: background-color 0.2s, border-color 0.2s;
}
.header__nav-link:hover {
background-color: #e8e8e8;
background-color: var(--c-bg);
}
.header__nav-link:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
.header__nav-link--active {
background-color: #0066cc;
color: #fff;
background-color: var(--c-accent);
color: var(--c-surface);
border-bottom-color: var(--c-accent);
}
.header__logout-button {
padding: 8px 14px;
font-size: 14px;
background-color: #f8f8f8;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: var(--sp-2) var(--sp-4);
font-size: var(--fs-sm);
background-color: transparent;
border: 1px solid var(--c-border);
border-radius: var(--radius);
color: var(--c-text);
cursor: pointer;
transition: background-color 0.2s;
transition: background-color 0.2s, border-color 0.2s;
}
.header__logout-button:hover {
background-color: #e8e8e8;
background-color: var(--c-bg);
border-color: var(--c-accent);
}
.header__logout-button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
+112
View File
@@ -0,0 +1,112 @@
/* Layout container */
[data-testid='layout'] {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--c-bg);
}
[data-testid='layout-main'] {
flex: 1;
max-width: var(--maxw);
width: 100%;
margin: 0 auto;
padding: var(--sp-5);
}
/* Dashboard & Pages */
.dashboard,
.calendar,
.settings-page,
.tips-page,
.auth-page {
width: 100%;
}
.dashboard__section,
.settings-page__section,
.calendar__section,
.tips-page__section {
background-color: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: var(--sp-5);
margin-bottom: var(--sp-5);
box-shadow: var(--shadow-sm);
}
.dashboard__section:last-child,
.settings-page__section:last-child,
.calendar__section:last-child,
.tips-page__section:last-child {
margin-bottom: 0;
}
.dashboard__section h2,
.settings-page__section h2,
.calendar__section h2,
.tips-page__section h2 {
margin-top: 0;
}
.dashboard__error {
padding: var(--sp-4);
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
margin-bottom: var(--sp-5);
}
.dashboard__break-list {
list-style: none;
margin: 0;
padding: 0;
}
.dashboard__break-item {
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--c-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.dashboard__break-item:last-child {
border-bottom: none;
}
.dashboard__break-item strong {
display: block;
margin-bottom: var(--sp-2);
}
.dashboard__break-item small {
color: var(--c-text-muted);
font-size: var(--fs-sm);
}
/* Status & Freshness Info */
.status-info,
.freshness-info {
font-size: var(--fs-sm);
color: var(--c-text-muted);
margin-top: var(--sp-3);
padding-top: var(--sp-3);
border-top: 1px solid var(--c-border);
}
/* Responsive */
@media (max-width: 640px) {
[data-testid='layout-main'] {
padding: var(--sp-4);
}
.dashboard__section,
.settings-page__section,
.calendar__section,
.tips-page__section {
padding: var(--sp-4);
margin-bottom: var(--sp-4);
}
}
+35 -59
View File
@@ -1,5 +1,6 @@
import { ReactNode } from 'react'
import type { ScraperStatus } from '@/hooks/useScraperFetch'
import './qrcode-modal.css'
interface QRCodeModalProps {
isOpen: boolean
@@ -34,102 +35,77 @@ export function QRCodeModal({
}
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: 'white', borderRadius: 8, padding: '2rem', maxWidth: 500, width: '90%', boxShadow: '0 2px 8px rgba(0,0,0,0.2)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ margin: 0, fontSize: '1.2rem' }}>Hämta från Försäkringskassan</h2>
<div className="qrcode-modal-overlay">
<div className="qrcode-modal">
<div className="qrcode-modal__header">
<h2 className="qrcode-modal__title">Hämta från Försäkringskassan</h2>
<button
className="qrcode-modal__close-btn"
onClick={handleClose}
disabled={isRunning}
style={{
background: 'none',
border: 'none',
fontSize: '1.5rem',
cursor: isRunning ? 'not-allowed' : 'pointer',
opacity: isRunning ? 0.5 : 1,
}}
aria-label="Close modal"
>
×
</button>
</div>
{status && (
<div>
<div style={{ marginBottom: '1rem' }}>
<strong>Status:</strong>{' '}
<span style={{ color: status === 'done' ? 'green' : status === 'error' ? 'red' : 'inherit' }}>
<div className="qrcode-modal__status-container">
<div className="qrcode-modal__status-row">
<span className="qrcode-modal__status-label">Status:</span>
<span
className={`qrcode-modal__status-value qrcode-modal__status-value--${
status === 'done' ? 'success' : status === 'error' ? 'error' : 'pending'
}`}
>
{status}
</span>
{message && <span style={{ color: '#888', marginLeft: '0.5rem' }}>— {message}</span>}
{message && <span className="qrcode-modal__status-message">— {message}</span>}
</div>
{status === 'awaiting_scan' && qrData && (
<div style={{ marginBottom: '1rem', textAlign: 'center' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: '0.9rem' }}>Öppna BankID-appen och skanna QR-koden:</p>
<div className="qrcode-modal__qr-section">
<p className="qrcode-modal__qr-instruction">Öppna BankID-appen och skanna QR-koden:</p>
<img
src={`data:image/png;base64,${qrData}`}
alt="BankID QR-kod"
style={{ width: 220, height: 220, imageRendering: 'pixelated', border: '1px solid #ccc' }}
className="qrcode-modal__qr-image"
/>
</div>
)}
{(status === 'authenticating' || status === 'fetching') && (
<p style={{ color: '#555', textAlign: 'center' }}>
<p className="qrcode-modal__loading-message">
⏳ {status === 'authenticating' ? 'Autentiserar…' : 'Hämtar data från FK…'}
</p>
)}
{status === 'done' && result && (
<div>
<p style={{ color: 'green', marginBottom: '0.5rem' }}>✅ Klar! Data hämtad från Försäkringskassan.</p>
</div>
<p className="qrcode-modal__success-message">✅ Klar! Data hämtad från Försäkringskassan.</p>
)}
{status === 'cancelled' && (
<p style={{ color: '#888' }}>Sessionen avbröts.</p>
<p className="qrcode-modal__cancelled-message">Sessionen avbröts.</p>
)}
{status === 'done' && (
<button
onClick={onDone}
style={{
width: '100%',
padding: '0.5rem 1rem',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
marginTop: '1rem',
}}
>
Stäng
</button>
)}
<div className="qrcode-modal__button-group">
{status === 'done' && (
<button className="qrcode-modal__button qrcode-modal__button--success" onClick={onDone}>
Stäng
</button>
)}
{isRunning && (
<button
onClick={onCancel}
style={{
width: '100%',
padding: '0.5rem 1rem',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
marginTop: '1rem',
}}
>
Avbryt
</button>
)}
{isRunning && (
<button className="qrcode-modal__button qrcode-modal__button--danger" onClick={onCancel}>
Avbryt
</button>
)}
</div>
</div>
)}
{error && (
<div style={{ background: '#fee', border: '1px solid #f88', padding: '0.75rem', borderRadius: 4, color: '#c00' }}>
<div className="qrcode-modal__error">
<strong>Fel:</strong> {error}
</div>
)}
@@ -0,0 +1,226 @@
/* QRCode Modal Container */
.qrcode-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(16, 24, 40, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.qrcode-modal {
background-color: var(--c-surface);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
max-width: 500px;
width: 90%;
padding: var(--sp-6);
max-height: 90vh;
overflow-y: auto;
}
/* Modal Header */
.qrcode-modal__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--sp-5);
}
.qrcode-modal__title {
font-size: var(--fs-xl);
font-weight: 700;
margin: 0;
}
.qrcode-modal__close-btn {
background: none;
border: none;
font-size: var(--fs-lg);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: background-color 0.2s;
color: var(--c-text);
}
.qrcode-modal__close-btn:hover:not(:disabled) {
background-color: var(--c-bg);
}
.qrcode-modal__close-btn:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
.qrcode-modal__close-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Status Display */
.qrcode-modal__status-container {
margin-bottom: var(--sp-5);
}
.qrcode-modal__status-row {
margin-bottom: var(--sp-4);
}
.qrcode-modal__status-label {
font-weight: 600;
margin-right: var(--sp-2);
}
.qrcode-modal__status-value {
font-weight: 600;
}
.qrcode-modal__status-value--pending {
color: var(--c-warning);
}
.qrcode-modal__status-value--success {
color: var(--c-success);
}
.qrcode-modal__status-value--error {
color: var(--c-danger);
}
.qrcode-modal__status-message {
color: var(--c-text-muted);
margin-left: var(--sp-2);
font-size: var(--fs-sm);
}
/* QR Code Display */
.qrcode-modal__qr-section {
margin-bottom: var(--sp-5);
text-align: center;
}
.qrcode-modal__qr-instruction {
font-size: var(--fs-sm);
color: var(--c-text-muted);
margin-bottom: var(--sp-4);
}
.qrcode-modal__qr-image {
max-width: 300px;
width: 100%;
height: auto;
border: 1px solid var(--c-border);
border-radius: var(--radius);
background-color: var(--c-surface);
padding: var(--sp-2);
image-rendering: pixelated;
}
/* Status Messages */
.qrcode-modal__loading-message {
color: var(--c-text-muted);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__success-message {
color: var(--c-success);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__cancelled-message {
color: var(--c-text-muted);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__error {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-danger-weak);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
color: var(--c-danger);
margin-bottom: var(--sp-4);
font-size: var(--fs-sm);
}
.qrcode-modal__error strong {
display: block;
margin-bottom: var(--sp-2);
}
/* Button Group */
.qrcode-modal__button-group {
display: flex;
gap: var(--sp-3);
margin-top: var(--sp-5);
}
.qrcode-modal__button {
flex: 1;
padding: var(--sp-3) var(--sp-4);
border: none;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.qrcode-modal__button--success {
background-color: var(--c-success);
color: var(--c-surface);
}
.qrcode-modal__button--success:hover {
background-color: #0d6a37;
}
.qrcode-modal__button--danger {
background-color: var(--c-danger);
color: var(--c-surface);
}
.qrcode-modal__button--danger:hover {
background-color: #8a1a15;
}
.qrcode-modal__button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.qrcode-modal__button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
/* Responsive */
@media (max-width: 640px) {
.qrcode-modal {
width: 95%;
padding: var(--sp-4);
}
.qrcode-modal__qr-image {
max-width: 250px;
}
.qrcode-modal__button-group {
flex-direction: column-reverse;
}
.qrcode-modal__button {
width: 100%;
}
}
+1
View File
@@ -1,6 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './styles/global.css'
const root = document.getElementById('root')
if (!root) throw new Error('Root element #root not found')
+391
View File
@@ -0,0 +1,391 @@
/* Overview Metrics */
.overview-metrics {
display: grid;
gap: var(--sp-4);
}
.overview-metrics__title {
margin: 0 0 var(--sp-4) 0;
}
.overview-metrics__list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--sp-4);
}
.overview-metrics__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--sp-5);
background-color: var(--c-bg);
border-radius: var(--radius);
border: 1px solid var(--c-border);
}
.overview-metrics__value {
display: block;
font-size: var(--fs-2xl);
font-weight: 700;
color: var(--c-accent);
margin-bottom: var(--sp-2);
line-height: 1;
}
.overview-metrics__label {
font-size: var(--fs-sm);
color: var(--c-text-muted);
text-align: center;
}
/* Days Breakdown */
.days-breakdown {
width: 100%;
}
.days-breakdown__empty {
color: var(--c-text-muted);
font-size: var(--fs-sm);
text-align: center;
padding: var(--sp-5);
margin: 0;
}
.days-breakdown__table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
.days-breakdown__table th {
padding: var(--sp-4);
background-color: var(--c-bg);
border-bottom: 2px solid var(--c-border);
font-weight: 600;
color: var(--c-text-muted);
text-align: left;
font-size: var(--fs-sm);
}
.days-breakdown__table td {
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--c-border);
font-size: var(--fs-base);
}
.days-breakdown__table tbody tr:hover {
background-color: rgba(47, 111, 79, 0.02);
}
.days-breakdown__table tbody tr:last-child td {
border-bottom: none;
}
.days-breakdown__table td:last-child {
text-align: right;
font-weight: 600;
color: var(--c-accent);
}
/* Parental Leave Breakdown */
.parental-leave-breakdown {
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.parental-leave-breakdown__empty {
color: var(--c-text-muted);
font-size: var(--fs-sm);
padding: var(--sp-4);
margin: 0;
}
.parental-leave-breakdown__section {
width: 100%;
overflow-x: auto;
}
.parental-leave-breakdown__table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
.parental-leave-breakdown__table th {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-bg);
border-bottom: 2px solid var(--c-border);
font-weight: 600;
color: var(--c-text-muted);
text-align: left;
font-size: var(--fs-sm);
}
.parental-leave-breakdown__table td {
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--c-border);
font-size: var(--fs-sm);
}
.parental-leave-breakdown__table tbody tr:hover {
background-color: rgba(47, 111, 79, 0.02);
}
.parental-leave-breakdown__table tbody tr:last-child td {
border-bottom: none;
}
.parental-leave-breakdown__table td:nth-child(2),
.parental-leave-breakdown__table td:nth-child(3) {
text-align: right;
font-weight: 600;
}
.parental-leave-breakdown__provenance {
padding-top: var(--sp-3);
border-top: 1px solid var(--c-border);
font-size: var(--fs-sm);
color: var(--c-text-muted);
}
/* Progress Chart */
.progress-chart {
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.progress-chart__header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin: 0 0 var(--sp-3) 0;
}
.progress-chart__header h3 {
margin: 0;
}
.progress-chart__age {
font-size: var(--fs-sm);
color: var(--c-text-muted);
}
.progress-chart__bar {
width: 100%;
height: 24px;
background-color: var(--c-bg);
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--c-border);
}
.progress-chart__fill {
height: 100%;
background-color: var(--c-accent);
transition: width 0.3s ease;
}
.progress-chart__stats {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--fs-sm);
color: var(--c-text-muted);
}
.progress-chart__percentage {
font-weight: 600;
color: var(--c-accent);
}
/* Button Styles */
button {
background-color: var(--c-accent);
color: var(--c-surface);
font-weight: 500;
}
button:hover:not(:disabled) {
background-color: #1f5a3b;
}
button:active:not(:disabled) {
background-color: #0f4a2f;
}
/* Secondary button */
.button--secondary,
button.secondary {
background-color: transparent;
color: var(--c-accent);
border: 1px solid var(--c-accent);
}
.button--secondary:hover:not(:disabled),
button.secondary:hover:not(:disabled) {
background-color: var(--c-accent-weak);
}
/* Ghost button */
.button--ghost,
button.ghost {
background-color: transparent;
color: var(--c-text);
border: 1px solid var(--c-border);
}
.button--ghost:hover:not(:disabled),
button.ghost:hover:not(:disabled) {
background-color: var(--c-bg);
border-color: var(--c-text);
}
/* Form styles */
.form-group {
margin-bottom: var(--sp-5);
}
.form-group label {
display: block;
margin-bottom: var(--sp-2);
font-weight: 500;
color: var(--c-text);
font-size: var(--fs-base);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
max-width: 400px;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
/* Input Grid */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--sp-4);
}
.form-grid .form-group {
margin-bottom: 0;
}
.form-grid .form-group input,
.form-grid .form-group select {
max-width: none;
}
/* Settings Page Messages */
.settings-page__message {
padding: var(--sp-4);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
font-weight: 500;
}
.settings-page__message--success {
background-color: var(--c-success-weak);
color: var(--c-success);
border-left: 4px solid var(--c-success);
}
.settings-page__message--error {
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
}
/* Fieldsets */
fieldset {
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: var(--sp-4);
margin-bottom: var(--sp-5);
}
legend {
font-weight: 600;
color: var(--c-text);
padding: 0 var(--sp-2);
margin-left: calc(var(--sp-2) * -1);
}
/* Input Components */
.vacation-days-input,
.planning-goal-selector,
.break-periods-input,
.parent-profile-input,
.child-birthday-input {
margin-bottom: var(--sp-5);
}
.vacation-days-input__label,
.planning-goal-selector__label,
.break-periods-input__label,
.parent-profile-input__label,
.child-birthday-input__label {
display: block;
margin-bottom: var(--sp-2);
font-weight: 500;
color: var(--c-text);
font-size: var(--fs-base);
}
.vacation-days-input__field,
.planning-goal-selector__select,
.break-periods-input__field,
.parent-profile-input__field,
.child-birthday-input__field {
width: 100%;
max-width: 400px;
}
.vacation-days-input__range,
.break-periods-input__help,
.parent-profile-input__help,
.child-birthday-input__help {
font-size: var(--fs-sm);
color: var(--c-text-muted);
margin-top: var(--sp-2);
}
/* Responsive */
@media (max-width: 640px) {
[data-testid='metrics-list'] {
grid-template-columns: 1fr;
}
.form-group input,
.form-group select,
.form-group textarea,
.vacation-days-input__field,
.planning-goal-selector__select,
.break-periods-input__field,
.parent-profile-input__field,
.child-birthday-input__field {
max-width: none;
}
.form-grid {
grid-template-columns: 1fr;
}
.days-breakdown__table {
font-size: var(--fs-sm);
}
.days-breakdown__table th,
.days-breakdown__table td {
padding: var(--sp-2) var(--sp-3);
}
}
+343
View File
@@ -0,0 +1,343 @@
/* Component styles - must come first */
@import './components.css';
@import './modals.css';
@import './responsive.css';
/* Design Tokens */
:root {
/* Color - calm, trustworthy palette */
--c-bg: #f7f8fa;
--c-surface: #ffffff;
--c-border: #e3e6eb;
--c-text: #1c2733;
--c-text-muted: #5b6675;
--c-accent: #2f6f4f; /* calm green */
--c-accent-weak: #e8f1ec;
--c-danger: #b3261e;
--c-danger-weak: #fdeceb;
--c-success: #1e7d4f;
--c-success-weak: #e7f4ec;
--c-warning: #8a6d1f;
/* Spacing scale (8px base) */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--sp-8: 48px;
/* Type scale */
--fs-sm: 0.875rem;
--fs-base: 1rem;
--fs-lg: 1.25rem;
--fs-xl: 1.5rem;
--fs-2xl: 2rem;
--lh: 1.5;
/* Shape & depth */
--radius: 8px;
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
--shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
/* Content width */
--maxw: 960px;
color-scheme: light;
}
/* Reset & Base */
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
main,
mark,
meter,
nav,
output,
progress,
section,
summary,
time,
video {
margin: 0;
padding: 0;
border: 0;
}
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
background: var(--c-bg);
color: var(--c-text);
line-height: var(--lh);
font-size: var(--fs-base);
}
/* Headings */
h1 {
font-size: var(--fs-2xl);
font-weight: 700;
margin-bottom: var(--sp-4);
}
h2 {
font-size: var(--fs-xl);
font-weight: 700;
margin-bottom: var(--sp-3);
}
h3 {
font-size: var(--fs-lg);
font-weight: 600;
margin-bottom: var(--sp-3);
}
/* Text */
p {
margin-bottom: var(--sp-3);
}
small {
font-size: var(--fs-sm);
color: var(--c-text-muted);
}
/* Lists */
ul,
ol {
margin-left: var(--sp-5);
margin-bottom: var(--sp-3);
}
li {
margin-bottom: var(--sp-2);
}
/* Links */
a {
color: var(--c-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
border-radius: 2px;
}
/* Buttons - base */
button {
font-family: inherit;
font-size: var(--fs-base);
cursor: pointer;
border: none;
border-radius: var(--radius);
padding: var(--sp-3) var(--sp-4);
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
}
button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Inputs & Selects */
input,
select,
textarea {
font-family: inherit;
font-size: var(--fs-base);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: var(--sp-3) var(--sp-4);
background: var(--c-surface);
color: var(--c-text);
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--c-accent);
box-shadow: 0 0 0 3px rgba(47, 111, 79, 0.1);
}
input:disabled,
select:disabled,
textarea:disabled {
background-color: #f0f0f0;
cursor: not-allowed;
}
/* Label */
label {
display: block;
margin-bottom: var(--sp-2);
font-weight: 500;
color: var(--c-text);
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
font-weight: 600;
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-bg);
border-bottom: 1px solid var(--c-border);
color: var(--c-text-muted);
}
td {
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--c-border);
}
tr:hover td {
background-color: rgba(47, 111, 79, 0.02);
}
/* Section container */
main {
max-width: var(--maxw);
margin: 0 auto;
padding: var(--sp-5);
}
/* Utility classes for form */
.form-group {
margin-bottom: var(--sp-5);
}
.form-group label {
margin-bottom: var(--sp-2);
}
/* Message boxes */
.error-message {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
.success-message {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-success-weak);
color: var(--c-success);
border-left: 4px solid var(--c-success);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
.warning-message {
padding: var(--sp-3) var(--sp-4);
background-color: #fef8e7;
color: var(--c-warning);
border-left: 4px solid var(--c-warning);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
+206
View File
@@ -0,0 +1,206 @@
/* Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(16, 24, 40, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* Modal Content */
.modal-content {
background-color: var(--c-surface);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
padding: var(--sp-6);
}
.modal-content h2 {
margin-top: 0;
}
.modal-content h3 {
margin-top: var(--sp-5);
}
/* Modal Header */
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--sp-5);
}
.modal__title {
font-size: var(--fs-xl);
font-weight: 700;
margin: 0;
}
.modal__close {
background: none;
border: none;
font-size: var(--fs-lg);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: background-color 0.2s;
}
.modal__close:hover {
background-color: var(--c-bg);
}
.modal__close:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
/* Modal Body */
.modal__body {
margin-bottom: var(--sp-5);
}
/* Modal Footer */
.modal__footer {
display: flex;
gap: var(--sp-3);
justify-content: flex-end;
padding-top: var(--sp-4);
border-top: 1px solid var(--c-border);
}
.modal__footer button {
min-width: 100px;
}
/* QRCode Modal */
.qrcode-modal__content {
text-align: center;
}
.qrcode-modal__qr {
margin: var(--sp-5) 0;
display: flex;
justify-content: center;
}
.qrcode-modal__qr img {
max-width: 300px;
width: 100%;
height: auto;
border: 1px solid var(--c-border);
border-radius: var(--radius);
background-color: var(--c-bg);
padding: var(--sp-2);
}
.qrcode-modal__status {
padding: var(--sp-4);
background-color: var(--c-bg);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
font-size: var(--fs-sm);
}
.qrcode-modal__status--pending {
background-color: #fef8e7;
color: var(--c-warning);
}
.qrcode-modal__status--success {
background-color: var(--c-success-weak);
color: var(--c-success);
}
.qrcode-modal__status--error {
background-color: var(--c-danger-weak);
color: var(--c-danger);
}
.qrcode-modal__error {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
font-size: var(--fs-sm);
}
/* Loading Spinner */
[data-testid='loading-spinner'] {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--sp-8);
gap: var(--sp-4);
}
[data-testid='spinner-icon'] {
font-size: 48px;
color: var(--c-accent);
animation: spin 2s linear infinite;
}
[data-testid='spinner-message'] {
font-size: var(--fs-base);
color: var(--c-text-muted);
margin: 0;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Reduce Motion */
@media (prefers-reduced-motion: reduce) {
[data-testid='spinner-icon'] {
animation: none;
}
.modal-content {
animation: none;
}
}
/* Responsive */
@media (max-width: 640px) {
.modal-content {
width: 95%;
padding: var(--sp-4);
max-height: 95vh;
}
.qrcode-modal__qr img {
max-width: 250px;
}
.modal__footer {
flex-direction: column-reverse;
}
.modal__footer button {
width: 100%;
}
}
+240
View File
@@ -0,0 +1,240 @@
/* S7 - Responsive & Accessibility */
/* Mobile - Small screens (max 640px) */
@media (max-width: 640px) {
/* Sticky header with less padding */
.header {
padding: var(--sp-3) var(--sp-4);
}
.header__top {
margin-bottom: var(--sp-3);
}
.header__nav {
gap: var(--sp-2);
}
.header__nav-link {
padding: var(--sp-2) var(--sp-3);
font-size: var(--fs-sm);
}
.header__title {
font-size: var(--fs-base);
}
.header__identity {
gap: var(--sp-2);
font-size: var(--fs-sm);
}
/* Main content */
[data-testid='layout-main'] {
padding: var(--sp-4);
}
.dashboard__section,
.settings-page__section,
.calendar__section,
.tips-page__section {
padding: var(--sp-4);
margin-bottom: var(--sp-4);
}
/* Metrics - single column */
.overview-metrics__list {
grid-template-columns: 1fr;
}
/* Forms - full width */
.form-group input,
.form-group select,
.form-group textarea {
max-width: none;
width: 100%;
}
.form-grid {
grid-template-columns: 1fr;
}
.form-grid .form-group input,
.form-grid .form-group select {
max-width: none;
}
/* Tables */
.days-breakdown__table,
.parental-leave-breakdown__table {
font-size: var(--fs-sm);
}
.days-breakdown__table th,
.days-breakdown__table td,
.parental-leave-breakdown__table th,
.parental-leave-breakdown__table td {
padding: var(--sp-2) var(--sp-3);
}
/* Modals */
.qrcode-modal {
width: 95%;
padding: var(--sp-4);
max-height: 95vh;
}
.qrcode-modal__qr-image {
max-width: 250px;
}
.qrcode-modal__button-group {
flex-direction: column-reverse;
}
.qrcode-modal__button {
width: 100%;
}
/* Footer */
footer {
padding: var(--sp-4);
font-size: var(--fs-sm);
}
/* Button group */
.modal__footer {
flex-direction: column-reverse;
}
.modal__footer button {
width: 100%;
}
/* Remove max-width on modal content */
.modal-content {
width: 95%;
padding: var(--sp-4);
max-height: 95vh;
}
/* Lists */
.dashboard__break-item {
flex-direction: column;
align-items: flex-start;
gap: var(--sp-2);
}
/* Loading spinner */
.loading-spinner {
padding: var(--sp-5);
min-height: 200px;
}
.loading-spinner__icon {
font-size: 36px;
}
/* Progress chart */
.progress-chart__stats {
flex-direction: column;
gap: var(--sp-2);
}
/* Calendar responsiveness */
.calendar__grid {
font-size: var(--fs-sm);
}
.calendar__weekday,
.calendar-day {
padding: var(--sp-2);
}
.calendar-day__number {
font-size: var(--fs-sm);
}
}
/* Tablet - Medium screens (641px to 1024px) */
@media (min-width: 641px) and (max-width: 1024px) {
.overview-metrics__list {
grid-template-columns: repeat(2, 1fr);
}
.form-grid {
grid-template-columns: repeat(2, 1fr);
}
[data-testid='layout-main'] {
padding: var(--sp-4);
}
}
/* Accessibility - Reduced Motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.loading-spinner__icon {
animation: none;
}
.progress-chart__fill {
transition: none;
}
.header__nav-link,
.header__logout-button,
.header__child-select,
input,
select,
textarea,
button {
transition: none;
}
}
/* High Contrast Mode */
@media (prefers-contrast: more) {
:root {
--c-text: #000;
--c-accent: #1f4d2b;
--c-border: #333;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline-width: 3px;
}
}
/* Color Scheme Preference - Dark Mode (future-proofing) */
@media (prefers-color-scheme: dark) {
/* Currently not implemented, but prepared */
}
/* Keyboard Navigation */
:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
/* High Contrast Focus */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}