feature: Phase4 - Clinic two-step treatment UI

This commit is contained in:
2026-06-28 17:14:02 +03:30
parent 21f545ebdb
commit 7b19d6953c
17 changed files with 1042 additions and 615 deletions

View File

@@ -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

View File

@@ -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">

View File

@@ -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>;
}