feat: add mobile card components
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
type MobileCardProps = {
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type MobileFieldProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
};
|
||||
|
||||
export function MobileCard({ title, actions, children }: MobileCardProps) {
|
||||
return (
|
||||
<div className="mobile-card">
|
||||
<div className="mobile-card-header">
|
||||
<div className="mobile-card-title">{title}</div>
|
||||
{actions ? <div className="mobile-card-actions">{actions}</div> : null}
|
||||
</div>
|
||||
<div className="mobile-card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MobileField({ label, value }: MobileFieldProps) {
|
||||
return (
|
||||
<div className="mobile-field">
|
||||
<div className="mobile-field-label">{label}</div>
|
||||
<div className="mobile-field-value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { create } from 'react-test-renderer';
|
||||
import { MobileCard, MobileField } from './MobileCard.js';
|
||||
|
||||
describe('MobileCard', () => {
|
||||
it('renders title and fields', () => {
|
||||
const root = create(
|
||||
<MobileCard title="CardTitle">
|
||||
<MobileField label="Status" value="OK" />
|
||||
</MobileCard>,
|
||||
);
|
||||
|
||||
const text = root.root.findAll(() => true)
|
||||
.flatMap((instance) => instance.children)
|
||||
.filter((child): child is string => typeof child === 'string')
|
||||
.join('');
|
||||
|
||||
expect(text).toContain('CardTitle');
|
||||
expect(text).toContain('Status');
|
||||
expect(text).toContain('OK');
|
||||
});
|
||||
});
|
||||
@@ -964,6 +964,65 @@ body {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* ===== Mobile Card ===== */
|
||||
.mobile-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-card {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.mobile-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mobile-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.mobile-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mobile-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-field {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-field-label {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.mobile-field-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ===== Stat Card (New API style) ===== */
|
||||
.stat-card {
|
||||
background: var(--color-bg-card);
|
||||
|
||||
Reference in New Issue
Block a user