diff --git a/src/web/components/MobileCard.tsx b/src/web/components/MobileCard.tsx
new file mode 100644
index 0000000..108f977
--- /dev/null
+++ b/src/web/components/MobileCard.tsx
@@ -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 (
+
+
+
{title}
+ {actions ?
{actions}
: null}
+
+
{children}
+
+ );
+}
+
+export function MobileField({ label, value }: MobileFieldProps) {
+ return (
+
+ );
+}
diff --git a/src/web/components/mobile-card.test.tsx b/src/web/components/mobile-card.test.tsx
new file mode 100644
index 0000000..45a3d84
--- /dev/null
+++ b/src/web/components/mobile-card.test.tsx
@@ -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(
+
+
+ ,
+ );
+
+ 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');
+ });
+});
diff --git a/src/web/index.css b/src/web/index.css
index a616567..6238769 100644
--- a/src/web/index.css
+++ b/src/web/index.css
@@ -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);