{visibleItems.map((item, i) => {
@@ -290,12 +277,8 @@ export function DynamicVirtualList
({
setItemHeight(index, height);
}
};
-
- return (
-
- {renderItem(item, index, measureRef)}
-
- );
+
+ return {renderItem(item, index, measureRef)}
;
})}
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index 49be5bf..71fa114 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -1,58 +1,47 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+/* eslint-disable react-refresh/only-export-components */
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '../../lib/utils';
const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-[6px] text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#4f8ef7] disabled:pointer-events-none disabled:opacity-40',
+ 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-lg text-[11px] font-semibold transition-all duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ds-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ds-bg-base disabled:pointer-events-none disabled:opacity-40 disabled:grayscale will-change-transform',
{
variants: {
variant: {
default:
- 'bg-[#1d2632] text-[#dce8ef] border border-[#2a3545] hover:bg-[#253041] hover:border-[#3a4d62]',
+ 'bg-gradient-to-b from-ds-bg-panel-2 to-ds-bg-panel text-ds-text border-2 border-ds-line shadow-[inset_0_1px_0_rgba(255,255,255,0.1),0_2px_4px_rgba(0,0,0,0.3)] hover:from-ds-bg-hover hover:to-ds-bg-panel-2 hover:border-ds-line-strong hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.15),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] active:scale-[0.98]',
primary:
- 'bg-[#4f8ef7] text-white border border-[#3a7ae0] hover:bg-[#3a7ae0]',
- teal:
- 'bg-[#62dbc1] text-[#0b0e13] border border-[#4ecfb5] hover:bg-[#4ecfb5]',
+ 'bg-gradient-to-br from-ds-accent to-ds-accent-hover text-white border-2 border-ds-accent-hover shadow-[inset_0_1px_0_rgba(255,255,255,0.2),0_4px_8px_rgba(90,158,255,0.4),0_2px_4px_rgba(0,0,0,0.3)] hover:from-ds-accent-hover hover:to-ds-accent hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.25),0_6px_12px_rgba(90,158,255,0.5),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] active:scale-[0.98]',
+ teal: 'bg-gradient-to-br from-ds-teal to-[#4dd4ba] text-ds-bg-base border-2 border-ds-teal shadow-[inset_0_1px_0_rgba(255,255,255,0.3),0_4px_8px_rgba(94,234,212,0.4),0_2px_4px_rgba(0,0,0,0.3)] hover:brightness-110 hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_6px_12px_rgba(94,234,212,0.5),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.2)] active:scale-[0.98]',
danger:
- 'bg-[#e76d78] text-white border border-[#d05060] hover:bg-[#d05060]',
- warn:
- 'bg-[#d9a65f] text-[#0b0e13] border border-[#c49048] hover:bg-[#c49048]',
+ 'bg-gradient-to-br from-ds-danger to-[#dc5a64] text-white border-2 border-ds-danger shadow-[inset_0_1px_0_rgba(255,255,255,0.2),0_4px_8px_rgba(248,113,113,0.4),0_2px_4px_rgba(0,0,0,0.3)] hover:brightness-110 hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.25),0_6px_12px_rgba(248,113,113,0.5),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] active:scale-[0.98]',
+ warn: 'bg-gradient-to-br from-ds-warn to-[#f59e0b] text-ds-bg-base border-2 border-ds-warn shadow-[inset_0_1px_0_rgba(255,255,255,0.3),0_4px_8px_rgba(251,191,36,0.4),0_2px_4px_rgba(0,0,0,0.3)] hover:brightness-110 hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_6px_12px_rgba(251,191,36,0.5),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.2)] active:scale-[0.98]',
ghost:
- 'bg-transparent text-[#8da0af] border border-transparent hover:bg-[#121821] hover:text-[#dce8ef]',
- wide:
- 'w-full bg-[#2dbb7f] text-white border border-[#25a86e] hover:bg-[#25a86e]',
+ 'bg-transparent text-ds-muted border-2 border-transparent hover:bg-ds-bg-hover hover:text-ds-text hover:border-ds-line hover:shadow-[0_2px_4px_rgba(0,0,0,0.2)] active:scale-[0.98]',
+ wide: 'w-full bg-gradient-to-br from-ds-accent to-ds-accent-hover text-white border-2 border-ds-accent-hover shadow-[inset_0_1px_0_rgba(255,255,255,0.2),0_4px_8px_rgba(90,158,255,0.4),0_2px_4px_rgba(0,0,0,0.3)] hover:brightness-110 hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.25),0_6px_12px_rgba(90,158,255,0.5),0_3px_6px_rgba(0,0,0,0.4)] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] active:scale-[0.98]',
},
size: {
- default: 'min-h-[28px] px-2.5 py-1',
- sm: 'min-h-[24px] px-2 py-0.5 text-[10px]',
- lg: 'min-h-[34px] px-4 py-1.5 text-[12px]',
- icon: 'h-7 w-7 p-0',
+ default: 'h-8 px-3 py-1',
+ sm: 'h-7 px-2.5 py-0.5 text-[10px]',
+ lg: 'h-9 px-4 py-1.5 text-[12px]',
+ icon: 'h-8 w-8 p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
- }
-)
+ },
+);
export interface ButtonProps
- extends React.ButtonHTMLAttributes
,
- VariantProps {
- asChild?: boolean
+ extends React.ButtonHTMLAttributes, VariantProps {
+ asChild?: boolean;
}
-const Button = React.forwardRef(
- ({ className, variant, size, ...props }, ref) => {
- return (
-
- )
- }
-)
-Button.displayName = 'Button'
+const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => {
+ return ;
+});
+Button.displayName = 'Button';
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/frontend/src/components/ui/design-system-components.test.tsx b/frontend/src/components/ui/design-system-components.test.tsx
new file mode 100644
index 0000000..5246c48
--- /dev/null
+++ b/frontend/src/components/ui/design-system-components.test.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ ActionButtonGroup,
+ Button,
+ Card,
+ CheckboxRow,
+ FieldRow,
+ PresetButtonGroup,
+ Section,
+ SliderControl,
+ StatRow,
+ WorkflowStepper,
+} from './design-system-components';
+
+describe('design system components', () => {
+ it('associates FieldRow label with child input', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByLabelText('Output Pattern')).toBeInTheDocument();
+ });
+
+ it('renders accessible SliderControl and emits numeric changes', () => {
+ const handleChange = vi.fn();
+
+ render();
+
+ fireEvent.change(screen.getByRole('slider', { name: 'Opacity' }), {
+ target: { value: '85' },
+ });
+
+ expect(screen.getByText('70%')).toBeInTheDocument();
+ expect(handleChange).toHaveBeenCalledWith(85);
+ });
+
+ it('renders CheckboxRow items and emits checked state', () => {
+ const handleChange = vi.fn();
+
+ render();
+
+ fireEvent.click(screen.getByRole('checkbox', { name: 'Area Aman' }));
+
+ expect(handleChange).toHaveBeenCalledWith('safe-area', true);
+ });
+
+ it('renders preset buttons and emits selected id', () => {
+ const handleChange = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /TURBO/i }));
+
+ expect(handleChange).toHaveBeenCalledWith('turbo');
+ });
+
+ it('renders StatRow values and labels', () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Total Job')).toBeInTheDocument();
+ expect(screen.getByText('4')).toBeInTheDocument();
+ expect(screen.getByText('Progress')).toBeInTheDocument();
+ expect(screen.getByText('50%')).toBeInTheDocument();
+ });
+
+ it('renders Card with title, optional description, and children', () => {
+ render(
+
+ Konten kartu
+ ,
+ );
+
+ expect(screen.getByText('Panduan Fitur')).toBeInTheDocument();
+ expect(screen.getByText('Ringkasan singkat')).toBeInTheDocument();
+ expect(screen.getByText('Konten kartu')).toBeInTheDocument();
+ });
+
+ it('renders Card without description when omitted', () => {
+ render();
+
+ expect(screen.getByText('Tanpa Deskripsi')).toBeInTheDocument();
+ });
+
+ it('renders Section with title and children', () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Pengaturan Lanjutan')).toBeInTheDocument();
+ expect(screen.getByText('Isi section')).toBeInTheDocument();
+ });
+
+ it('renders Button as primary by default and forwards click', () => {
+ const handleClick = vi.fn();
+
+ render();
+ fireEvent.click(screen.getByRole('button', { name: 'Render' }));
+
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders Button variants without crashing', () => {
+ render(
+ <>
+
+
+
+ >,
+ );
+
+ expect(screen.getByRole('button', { name: 'Batal' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Kecil' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Hapus' })).toBeInTheDocument();
+ });
+
+ it('renders WorkflowStepper current step and disables edge navigation', () => {
+ const onPrevious = vi.fn();
+ const onNext = vi.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Langkah 1 dari 3')).toBeInTheDocument();
+ expect(screen.getByText('Visual')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Previous' })).toBeDisabled();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }));
+ expect(onNext).toHaveBeenCalledTimes(1);
+ expect(onPrevious).not.toHaveBeenCalled();
+ });
+
+ it('renders WorkflowStepper with Next disabled on last step', () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Langkah 2 dari 2')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled();
+ expect(screen.getByRole('button', { name: 'Previous' })).not.toBeDisabled();
+ });
+
+ it('renders ActionButtonGroup actions and forwards click per item', () => {
+ const onClickA = vi.fn();
+ const onClickB = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Mulai' }));
+ expect(onClickA).toHaveBeenCalledTimes(1);
+ expect(screen.getByRole('button', { name: 'Berhenti' })).toBeDisabled();
+ });
+});
diff --git a/frontend/src/components/ui/design-system-components.tsx b/frontend/src/components/ui/design-system-components.tsx
new file mode 100644
index 0000000..7a2a0e4
--- /dev/null
+++ b/frontend/src/components/ui/design-system-components.tsx
@@ -0,0 +1,706 @@
+/**
+ * PidioForge Desktop - Reusable Design System Components
+ * FASE 2: Implementation of Design System Components
+ *
+ * All components follow specs from design-system.ts
+ */
+
+import React from 'react';
+import { designSystem } from '../../design-system';
+import { Button as BaseButton } from './button';
+
+function createFieldId(label: string): string {
+ return `ds-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
+}
+
+// ============================================
+// FieldRow Component
+// ONE field per row - NEVER 2-3 fields in one row
+// ============================================
+interface FieldRowProps {
+ label: string;
+ children: React.ReactNode;
+ required?: boolean;
+}
+
+export function FieldRow({ label, children, required }: FieldRowProps) {
+ const spec = designSystem.components.fieldRow;
+ const generatedId = createFieldId(label);
+ const fieldId = React.isValidElement<{ id?: string }>(children) ? children.props.id || generatedId : generatedId;
+ const child = React.isValidElement<{ id?: string }>(children)
+ ? React.cloneElement(children, { id: fieldId })
+ : children;
+
+ return (
+
+
+
{child}
+
+ );
+}
+
+// ============================================
+// SliderControl Component
+// Green solid fill + percentage label on right
+// ============================================
+interface SliderControlProps {
+ label: string;
+ value: number;
+ onChange: (value: number) => void;
+ min?: number;
+ max?: number;
+ step?: number;
+ unit?: string;
+}
+
+export function SliderControl({
+ label,
+ value,
+ onChange,
+ min = 0,
+ max = 100,
+ step = 1,
+ unit = '%',
+}: SliderControlProps) {
+ const spec = designSystem.components.slider;
+ const sliderId = createFieldId(label);
+ const percentage = ((value - min) / (max - min)) * 100;
+
+ return (
+
+
+
+ onChange(Number(e.target.value))}
+ style={{
+ ...spec.slider,
+ background: `linear-gradient(to right, ${designSystem.colors.status.success} 0%, ${designSystem.colors.status.success} ${percentage}%, ${designSystem.colors.base.tertiary} ${percentage}%, ${designSystem.colors.base.tertiary} 100%)`,
+ }}
+ />
+
+ {value}
+ {unit}
+
+
+
+ );
+}
+
+// ============================================
+// CheckboxRow Component
+// 8px vertical spacing between items
+// ============================================
+interface CheckboxItem {
+ id: string;
+ label: string;
+ checked: boolean;
+}
+
+interface CheckboxRowProps {
+ items: CheckboxItem[];
+ onChange: (id: string, checked: boolean) => void;
+}
+
+export function CheckboxRow({ items, onChange }: CheckboxRowProps) {
+ const spec = designSystem.components.checkbox;
+
+ return (
+
+ {items.map(item => (
+
+ onChange(item.id, e.target.checked)}
+ style={spec.checkbox}
+ />
+
+
+ ))}
+
+ );
+}
+
+// ============================================
+// Callout Component
+// Blue + lightbulb = tips, Orange + warning = warnings
+// ============================================
+interface CalloutProps {
+ type: 'tip' | 'warning';
+ children: React.ReactNode;
+}
+
+export function Callout({ type, children }: CalloutProps) {
+ const { icon, iconColor, ...containerStyle } = designSystem.components.callout[type];
+
+ return (
+
+ );
+}
+
+// ============================================
+// PresetButtonGroup Component
+// Uniform grid, icon + label
+// ============================================
+interface PresetButton {
+ id: string;
+ icon: string;
+ label: string;
+}
+
+interface PresetButtonGroupProps {
+ presets: PresetButton[];
+ activeId: string;
+ onChange: (id: string) => void;
+}
+
+export function PresetButtonGroup({ presets, activeId, onChange }: PresetButtonGroupProps) {
+ const spec = designSystem.components.presetButtonGroup;
+
+ return (
+
+ {presets.map(preset => {
+ const isActive = preset.id === activeId;
+ const buttonStyle = isActive ? { ...spec.button, ...spec.buttonActive } : spec.button;
+
+ return (
+
+ );
+ })}
+
+ );
+}
+
+// ============================================
+// StatRow Component
+// Auto-aligned columns, large bold number + small label
+// ============================================
+interface Stat {
+ label: string;
+ value: string | number;
+ color?: string;
+ accent?: boolean;
+}
+
+interface StatRowProps {
+ stats: Stat[];
+}
+
+export function StatRow({ stats }: StatRowProps) {
+ const spec = designSystem.components.statRow;
+
+ return (
+
+ {stats.map((stat, index) => (
+
+
+ {stat.value}
+
+ {stat.label}
+
+ ))}
+
+ );
+}
+
+// ============================================
+// Card Component
+// 16px padding, 8px radius, title + description
+// ============================================
+interface CardProps {
+ title: string;
+ description?: string;
+ action?: React.ReactNode;
+ children?: React.ReactNode;
+}
+
+export function Card({ title, description, action, children }: CardProps) {
+ const spec = designSystem.components.card;
+
+ return (
+
+
+
{title}
+ {action}
+
+ {description &&
{description}
}
+ {children}
+
+ );
+}
+
+// ============================================
+// WorkflowStepper Component
+// Compact stepper replacing horizontal breadcrumb overflow
+// ============================================
+interface WorkflowStep {
+ id: string;
+ label: string;
+ completed?: boolean;
+ active?: boolean;
+}
+
+interface WorkflowStepperProps {
+ steps: WorkflowStep[];
+ activeIndex: number;
+ onPrevious?: () => void;
+ onNext?: () => void;
+}
+
+export function WorkflowStepper({ steps, activeIndex, onPrevious, onNext }: WorkflowStepperProps) {
+ const current = steps[activeIndex] || steps[0];
+ const canPrev = activeIndex > 0;
+ const canNext = activeIndex < steps.length - 1;
+
+ return (
+
+
+
+ Langkah {Math.min(activeIndex + 1, steps.length)} dari {steps.length}
+
+
+ {current?.label || '-'}
+
+
+
+
+
+
+
+ );
+}
+
+// ============================================
+// ActionButtonGroup Component
+// Compact wrap-safe action groups
+// ============================================
+interface ActionButtonItem {
+ id: string;
+ label: string;
+ icon?: string;
+ variant?: 'primary' | 'secondary' | 'small' | 'danger';
+ disabled?: boolean;
+ onClick?: () => void;
+}
+
+interface ActionButtonGroupProps {
+ actions: ActionButtonItem[];
+ columns?: 1 | 2;
+}
+
+export function ActionButtonGroup({ actions, columns = 2 }: ActionButtonGroupProps) {
+ return (
+
+ {actions.map(action => (
+
+ ))}
+
+ );
+}
+
+// ============================================
+// Chip / ChipGroup
+// ============================================
+interface ChipProps {
+ active?: boolean;
+ children: React.ReactNode;
+ onClick?: () => void;
+}
+
+export function Chip({ active = false, children, onClick }: ChipProps) {
+ return (
+
+ );
+}
+
+export function ChipGroup({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+// ============================================
+// Collapsible
+// ============================================
+interface CollapsibleProps {
+ title: string;
+ open: boolean;
+ onToggle: () => void;
+ children: React.ReactNode;
+}
+
+export function Collapsible({ title, open, onToggle, children }: CollapsibleProps) {
+ return (
+
+
+ {open ?
{children}
: null}
+
+ );
+}
+
+// ============================================
+// Textarea
+// ============================================
+type TextareaProps = React.TextareaHTMLAttributes & { minRows?: number };
+
+export function Textarea({ style, minRows = 4, ...props }: TextareaProps) {
+ return (
+
+ );
+}
+
+// ============================================
+// LogPre
+// ============================================
+interface LogPreProps {
+ children: React.ReactNode;
+ maxHeight?: number;
+}
+
+export function LogPre({ children, maxHeight = 300 }: LogPreProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+// ============================================
+// MetaStrip
+// ============================================
+interface MetaItem {
+ label?: string;
+ value: React.ReactNode;
+ muted?: boolean;
+}
+
+export function MetaStrip({ items }: { items: MetaItem[] }) {
+ return (
+
+ {items.map((item, index) => (
+
+ {index > 0 ? • : null}
+ {item.label ? (
+ {item.label}
+ ) : null}
+
+ {item.value}
+
+
+ ))}
+
+ );
+}
+
+// ============================================
+// WarningList
+// ============================================
+interface WarningListProps {
+ tone: 'warning' | 'danger' | 'info' | 'success';
+ title: string;
+ items: string[];
+}
+
+export function WarningList({ tone, title, items }: WarningListProps) {
+ const toneColor =
+ tone === 'warning'
+ ? designSystem.colors.status.warning
+ : tone === 'danger'
+ ? designSystem.colors.status.error
+ : tone === 'success'
+ ? designSystem.colors.status.success
+ : designSystem.colors.status.info;
+
+ return (
+
+
+ {title}
+
+
+ {items.map((item, index) => (
+ -
+ {item}
+
+ ))}
+
+
+ );
+}
+
+// ============================================
+// ProgressBar
+// ============================================
+interface ProgressBarProps {
+ value: number;
+ max?: number;
+ tone?: 'primary' | 'success' | 'warning' | 'danger';
+}
+
+export function ProgressBar({ value, max = 100, tone = 'primary' }: ProgressBarProps) {
+ const ratio = Math.max(0, Math.min(100, Math.round((value / max) * 100)));
+ const color =
+ tone === 'success'
+ ? designSystem.colors.status.success
+ : tone === 'warning'
+ ? designSystem.colors.status.warning
+ : tone === 'danger'
+ ? designSystem.colors.status.error
+ : designSystem.colors.accent.primary;
+
+ return (
+
+ );
+}
+
+// ============================================
+// Button Components
+// Primary (ALL CTAs), Secondary, Small
+// ============================================
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'secondary' | 'small' | 'danger';
+ children: React.ReactNode;
+}
+
+export function Button({ variant = 'primary', children, className, ...props }: ButtonProps) {
+ let baseVariant: 'primary' | 'default' | 'danger' = 'primary';
+ let baseSize: 'default' | 'sm' = 'default';
+
+ if (variant === 'secondary') {
+ baseVariant = 'default';
+ } else if (variant === 'small') {
+ baseVariant = 'default';
+ baseSize = 'sm';
+ } else if (variant === 'danger') {
+ baseVariant = 'danger';
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+// ============================================
+// Input Component (follows FieldRow spec)
+// ============================================
+type InputProps = React.InputHTMLAttributes;
+
+export function Input({ style, ...props }: InputProps) {
+ const spec = designSystem.components.fieldRow.input;
+
+ return ;
+}
+
+// ============================================
+// Select Component (follows FieldRow spec)
+// ============================================
+interface SelectProps extends React.SelectHTMLAttributes {
+ children: React.ReactNode;
+}
+
+export function Select({ style, children, ...props }: SelectProps) {
+ const spec = designSystem.components.fieldRow.input;
+
+ return (
+
+ );
+}
+
+// ============================================
+// Section Component (for panel sections)
+// ============================================
+interface SectionProps {
+ title: string;
+ children: React.ReactNode;
+}
+
+export function Section({ title, children }: SectionProps) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/components/ui/form-controls.tsx b/frontend/src/components/ui/form-controls.tsx
index ddecc2e..2df7fe0 100644
--- a/frontend/src/components/ui/form-controls.tsx
+++ b/frontend/src/components/ui/form-controls.tsx
@@ -1,69 +1,96 @@
import React from 'react';
+import {
+ FieldRow,
+ Input,
+ Select,
+ SliderControl,
+} from './design-system-components';
export function Field({ label, children }: { label: string; children: React.ReactNode }) {
- return (
-
- );
+ return {children};
}
-export function Check({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
+export function Check({ label, checked, onChange }: { label: string; checked: boolean; onChange: (value: boolean) => void }) {
return (
-