feature: Phase4 - Clinic two-step treatment UI
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useId } from 'react';
|
||||
import { useId, type KeyboardEvent } from 'react';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
type CheckboxProps = {
|
||||
@@ -14,6 +14,7 @@ type CheckboxProps = {
|
||||
|
||||
/**
|
||||
* App design-system checkbox: primary fill when checked, rounded, focus-visible ring.
|
||||
* Uses a button-like label toggle so mouse clicks do not focus a hidden input (scroll jumps).
|
||||
*/
|
||||
export function Checkbox({
|
||||
checked,
|
||||
@@ -26,25 +27,42 @@ export function Checkbox({
|
||||
const genId = useId();
|
||||
const inputId = id ?? genId;
|
||||
|
||||
const toggle = () => {
|
||||
if (!disabled) onChange(!checked);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLLabelElement>) => {
|
||||
if (disabled) return;
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
id={inputId}
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onKeyDown={onKeyDown}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
toggle();
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
// Avoid focus-driven scroll-into-view in overflow panels.
|
||||
if (event.button === 0) event.preventDefault();
|
||||
}}
|
||||
className={`
|
||||
inline-flex items-center gap-2.5 cursor-pointer select-none rounded-[var(--radius-sm)] -m-0.5 p-0.5
|
||||
has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/45 has-[:focus-visible]:ring-offset-2
|
||||
has-[:focus-visible]:ring-offset-background-secondary
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 focus-visible:ring-offset-2
|
||||
focus-visible:ring-offset-background-secondary
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
id={inputId}
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<span
|
||||
className={`
|
||||
flex h-5 w-5 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border-2 transition-all duration-200
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/components/ui/Input.tsx
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useId } from 'react';
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
@@ -9,8 +9,8 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, icon, className = '', id, ...props }, ref) => {
|
||||
const inputId =
|
||||
id || `input-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const generatedId = useId();
|
||||
const inputId = id || generatedId;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
@@ -8,6 +8,8 @@ interface SearchBarProps {
|
||||
placeholder: string;
|
||||
onSubmit?: () => void;
|
||||
actions?: ReactNode;
|
||||
/** When true, renders inline without the outer surface-card wrapper (for nested panels). */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
@@ -16,23 +18,28 @@ export function SearchBar({
|
||||
placeholder,
|
||||
onSubmit,
|
||||
actions,
|
||||
embedded = false,
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<div className="surface-card p-4">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSubmit?.();
|
||||
}}
|
||||
icon={<Search className="h-4 w-4 icon-flat" />}
|
||||
/>
|
||||
</div>
|
||||
{actions && <div className="flex gap-2">{actions}</div>}
|
||||
const field = (
|
||||
<div className={`flex gap-4 items-center ${embedded ? '' : 'flex-1'}`}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSubmit?.();
|
||||
}}
|
||||
icon={<Search className="h-4 w-4 icon-flat" />}
|
||||
/>
|
||||
</div>
|
||||
{actions && <div className="flex gap-2 shrink-0">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return <div className="surface-card p-4">{field}</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user