forked from boos/BoosAPI
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);
|
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 (New API style) ===== */
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: var(--color-bg-card);
|
background: var(--color-bg-card);
|
||||||
|
|||||||
Reference in New Issue
Block a user