torch-glare-mcp 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/components/alert-dialog.md +160 -0
- package/docs/components/date-picker.md +78 -0
- package/docs/components/dialog.md +189 -0
- package/docs/components/input-field.md +36 -0
- package/docs/components/select.md +24 -0
- package/docs/components/simple-select.md +34 -0
- package/docs/components/table.md +75 -0
- package/docs/components/textarea.md +40 -0
- package/docs/components/toggle.md +59 -0
- package/docs/how-to/form-and-list-recipes.md +379 -0
- package/package.json +1 -1
|
@@ -456,6 +456,166 @@ function PermissionAlert() {
|
|
|
456
456
|
| `variant` | `ButtonVariant` | `'RedSecStyle'` | Button variant |
|
|
457
457
|
| `buttonType` | `'button' \| 'icon'` | `'icon'` | Button type |
|
|
458
458
|
|
|
459
|
+
## Known Limitations & Frontend Patterns
|
|
460
|
+
|
|
461
|
+
### Every AlertDialog subcomponent ships without panel/typography defaults — and worse than `Dialog`
|
|
462
|
+
|
|
463
|
+
`AlertDialogContent` ships with `p-[12px]` (too tight), `AlertDialogHeader` with `flex justify-between` (instead of `flex-col` for stacked title+description), `AlertDialogFooter` with `flex-col-reverse sm:flex-row` (vertical-on-mobile), and `AlertDialogDescription` with the bizarre `bg-background-presentation-form-base border ... rounded-[8px] p-[24px_48px_48px_48px]` — a 48 px-padded bordered box wrapped around what should be a one-line subtitle.
|
|
464
|
+
|
|
465
|
+
**Production-tested override** — patch `AlertDialog.tsx` once after `npx torch-glare add AlertDialog`. Same surface-token corrections as `Dialog`: use `bg-background-presentation-body-primary` and `text-content-presentation-global-*`, never `*-system-*` and never `form-base`.
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
"use client";
|
|
469
|
+
|
|
470
|
+
import * as React from "react";
|
|
471
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
472
|
+
import { cn } from "@/utils/cn";
|
|
473
|
+
|
|
474
|
+
const AlertDialog = AlertDialogPrimitive.Root;
|
|
475
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
|
476
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
477
|
+
|
|
478
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
479
|
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
480
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
481
|
+
>(({ className, ...props }, ref) => (
|
|
482
|
+
<AlertDialogPrimitive.Overlay
|
|
483
|
+
ref={ref}
|
|
484
|
+
className={cn(
|
|
485
|
+
"fixed inset-0 z-50 bg-black/60",
|
|
486
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
487
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
488
|
+
className,
|
|
489
|
+
)}
|
|
490
|
+
{...props}
|
|
491
|
+
/>
|
|
492
|
+
));
|
|
493
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
494
|
+
|
|
495
|
+
const AlertDialogContent = React.forwardRef<
|
|
496
|
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
497
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
498
|
+
>(({ className, ...props }, ref) => (
|
|
499
|
+
<AlertDialogPortal>
|
|
500
|
+
<AlertDialogOverlay />
|
|
501
|
+
<AlertDialogPrimitive.Content
|
|
502
|
+
ref={ref}
|
|
503
|
+
className={cn(
|
|
504
|
+
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
|
505
|
+
"flex flex-col items-stretch justify-start",
|
|
506
|
+
"w-[92vw] sm:max-w-md",
|
|
507
|
+
"bg-background-presentation-body-primary",
|
|
508
|
+
"border border-border-presentation-global-primary",
|
|
509
|
+
"rounded-xl shadow-lg",
|
|
510
|
+
"p-0 gap-0 overflow-hidden",
|
|
511
|
+
"duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
512
|
+
className,
|
|
513
|
+
)}
|
|
514
|
+
{...props}
|
|
515
|
+
/>
|
|
516
|
+
</AlertDialogPortal>
|
|
517
|
+
));
|
|
518
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
519
|
+
|
|
520
|
+
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
521
|
+
<div
|
|
522
|
+
className={cn("flex flex-col space-y-1.5 text-left px-6 pt-5 pb-2", className)}
|
|
523
|
+
{...props}
|
|
524
|
+
/>
|
|
525
|
+
);
|
|
526
|
+
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
527
|
+
|
|
528
|
+
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
529
|
+
<div
|
|
530
|
+
className={cn("flex flex-row justify-end gap-2 px-6 py-4", className)}
|
|
531
|
+
{...props}
|
|
532
|
+
/>
|
|
533
|
+
);
|
|
534
|
+
AlertDialogFooter.displayName = "AlertDialogFooter";
|
|
535
|
+
|
|
536
|
+
const AlertDialogTitle = React.forwardRef<
|
|
537
|
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
538
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
539
|
+
>(({ className, ...props }, ref) => (
|
|
540
|
+
<AlertDialogPrimitive.Title
|
|
541
|
+
ref={ref}
|
|
542
|
+
className={cn(
|
|
543
|
+
"typography-headers-small-semibold text-content-presentation-global-primary",
|
|
544
|
+
className,
|
|
545
|
+
)}
|
|
546
|
+
{...props}
|
|
547
|
+
/>
|
|
548
|
+
));
|
|
549
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
|
550
|
+
|
|
551
|
+
const AlertDialogDescription = React.forwardRef<
|
|
552
|
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
553
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
554
|
+
>(({ className, ...props }, ref) => (
|
|
555
|
+
<AlertDialogPrimitive.Description
|
|
556
|
+
ref={ref}
|
|
557
|
+
className={cn(
|
|
558
|
+
"typography-body-small-regular text-content-presentation-global-secondary px-6 pb-2",
|
|
559
|
+
className,
|
|
560
|
+
)}
|
|
561
|
+
{...props}
|
|
562
|
+
/>
|
|
563
|
+
));
|
|
564
|
+
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
|
565
|
+
|
|
566
|
+
const AlertDialogAction = AlertDialogPrimitive.Action;
|
|
567
|
+
const AlertDialogCancel = AlertDialogPrimitive.Cancel;
|
|
568
|
+
|
|
569
|
+
export {
|
|
570
|
+
AlertDialog,
|
|
571
|
+
AlertDialogTrigger,
|
|
572
|
+
AlertDialogPortal,
|
|
573
|
+
AlertDialogOverlay,
|
|
574
|
+
AlertDialogContent,
|
|
575
|
+
AlertDialogHeader,
|
|
576
|
+
AlertDialogFooter,
|
|
577
|
+
AlertDialogTitle,
|
|
578
|
+
AlertDialogDescription,
|
|
579
|
+
AlertDialogAction,
|
|
580
|
+
AlertDialogCancel,
|
|
581
|
+
};
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Critical default decisions:
|
|
585
|
+
|
|
586
|
+
- **Drop the bordered box on `AlertDialogDescription`.** The shipped default wraps the description in a 48 px-padded bordered box, which produces a visually heavy nested panel. Description is a one-line subtitle — same typography pattern as `DialogDescription`.
|
|
587
|
+
- **`AlertDialogHeader` → `flex flex-col` (NOT `flex justify-between`)** so title and description stack with proper rhythm.
|
|
588
|
+
- **`AlertDialogFooter` → `flex-row justify-end`** on all viewports, no `flex-col-reverse sm:flex-row` weirdness.
|
|
589
|
+
- **`AlertDialogDescription` carries `px-6 pb-2`** since alert dialogs typically don't have a separate body section between header and footer.
|
|
590
|
+
- Surface and text tokens are **identical to `Dialog`** — `bg-background-presentation-body-primary` + `text-content-presentation-global-primary` + `text-content-presentation-global-secondary`. Never `*-system-*`. Never `form-base`. Never `action-secondary`.
|
|
591
|
+
|
|
592
|
+
### Standard usage after the override
|
|
593
|
+
|
|
594
|
+
Once the patches above are in place, every consumer renders correctly with bare components:
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
598
|
+
<AlertDialogContent>
|
|
599
|
+
<AlertDialogHeader>
|
|
600
|
+
<AlertDialogTitle>Delete this template?</AlertDialogTitle>
|
|
601
|
+
<AlertDialogDescription>
|
|
602
|
+
This action cannot be undone. The template will be permanently removed.
|
|
603
|
+
</AlertDialogDescription>
|
|
604
|
+
</AlertDialogHeader>
|
|
605
|
+
<AlertDialogFooter>
|
|
606
|
+
<AlertDialogCancel asChild>
|
|
607
|
+
<Button type="button" variant="BorderStyle" size="M">Cancel</Button>
|
|
608
|
+
</AlertDialogCancel>
|
|
609
|
+
<AlertDialogAction asChild>
|
|
610
|
+
<Button type="button" variant="RedColStyle" size="M" onClick={handleDelete}>
|
|
611
|
+
Delete
|
|
612
|
+
</Button>
|
|
613
|
+
</AlertDialogAction>
|
|
614
|
+
</AlertDialogFooter>
|
|
615
|
+
</AlertDialogContent>
|
|
616
|
+
</AlertDialog>
|
|
617
|
+
```
|
|
618
|
+
|
|
459
619
|
## Variants
|
|
460
620
|
|
|
461
621
|
### Default Variant
|
|
@@ -579,6 +579,84 @@ describe('DatePicker', () => {
|
|
|
579
579
|
})
|
|
580
580
|
```
|
|
581
581
|
|
|
582
|
+
## Known Limitations & Frontend Patterns
|
|
583
|
+
|
|
584
|
+
### `onChange` payload type does not match what TypeScript thinks
|
|
585
|
+
|
|
586
|
+
`DatePicker` props extend `HTMLAttributes<HTMLInputElement>`, which makes `onChange` look like `(e: ChangeEvent<HTMLInputElement>) => void` where `e.target.value: string`. **At runtime the value is a `Date | Date[] | DateRange`**, not a string — the component dispatches a hand-rolled pseudo-event with the typed payload behind a `string` type assertion.
|
|
587
|
+
|
|
588
|
+
**Workaround — cast through `unknown` on every call site:**
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
<DatePicker
|
|
592
|
+
mode="single"
|
|
593
|
+
value={startDate}
|
|
594
|
+
onChange={(e) =>
|
|
595
|
+
setStartDate(e.target.value as unknown as Date | undefined)
|
|
596
|
+
}
|
|
597
|
+
/>
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
For `mode="multiple"`:
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
onChange={(e) => setDates(e.target.value as unknown as Date[] | undefined)}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
For `mode="range"`:
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
onChange={(e) => setRange(e.target.value as unknown as DateRange | undefined)}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
The `as unknown as Date` shape is required — TypeScript will reject `as Date` directly because `string` and `Date` don't overlap.
|
|
613
|
+
|
|
614
|
+
### `TimePickerValue` interface — `hour`/`minute` are strings, not numbers
|
|
615
|
+
|
|
616
|
+
The shipped runtime uses `string` for `hour`, `minute`, and `time` (`"AM" | "PM"`). If you derive your own state from `TimePickerValue`, type those fields as `string`, not `number`, regardless of what older docs claim.
|
|
617
|
+
|
|
618
|
+
### Internal `<Picker>` value/onChange types are incompatible with `TimePickerValue` — `tsc` fails out of the box
|
|
619
|
+
|
|
620
|
+
`DatePicker.tsx` wraps `torch-react-mobile-picker`'s `<Picker>` and passes a concrete `TimePickerValue` (`{ hour, minute, time }`) where the picker's generic `PickerValue` (`Record<string, string>`) is expected. Strict TypeScript builds fail immediately:
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
DatePicker.tsx: Type 'TimePickerValue' is not assignable to type 'PickerValue'.
|
|
624
|
+
Index signature for type 'string' is missing in type 'TimePickerValue'.
|
|
625
|
+
DatePicker.tsx: Type '(e: TimePickerValue) => void' is not assignable to type
|
|
626
|
+
'(value: PickerValue, key: string) => void'.
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Workaround — add `as any` casts at the `<Picker>` boundary** (and only there, so consumer types stay honest):
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
<Picker
|
|
633
|
+
value={value as any}
|
|
634
|
+
onChange={((e: TimePickerValue) => {
|
|
635
|
+
onChange(e);
|
|
636
|
+
}) as any}
|
|
637
|
+
wheelMode="normal"
|
|
638
|
+
>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Patch this manually after `npx torch-glare add DatePicker` — otherwise `pnpm build` (which runs `tsc -b`) will fail. The same `Ref` import in the file is also unused and trips `noUnusedLocals`; remove it while you're in there.
|
|
642
|
+
|
|
643
|
+
### `npx torch-glare add DatePicker` does not install `utils/dateFormat.ts`
|
|
644
|
+
|
|
645
|
+
The CLI ships `DatePicker.tsx` without copying the `dateFormat.ts` utility it imports, so the component fails to build immediately after install:
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
[plugin:vite:import-analysis] Failed to resolve import "../utils/dateFormat"
|
|
649
|
+
from "DatePicker.tsx".
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Workaround until the CLI is fixed:** create `utils/dateFormat.ts` manually with the three exports the component needs:
|
|
653
|
+
|
|
654
|
+
- `TimePickerValue` (`{ hour: string; minute: string; time: "AM" | "PM" }`)
|
|
655
|
+
- `applyTimeToDateValue(value, timePickerValue)` — applies the time picker value to the date value
|
|
656
|
+
- `formatDateValueToString(value, timePickerValue, dateFormat)` — formats the date value to a display string
|
|
657
|
+
|
|
658
|
+
Reverse-engineer the implementation from the call sites in `DatePicker.tsx` until the CLI copies it automatically.
|
|
659
|
+
|
|
582
660
|
## Accessibility
|
|
583
661
|
|
|
584
662
|
- **Keyboard Navigation**:
|
|
@@ -482,6 +482,195 @@ export const DialogCloseButton: React.ForwardRefExoticComponent<
|
|
|
482
482
|
>
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
+
## Known Limitations & Frontend Patterns
|
|
486
|
+
|
|
487
|
+
### Every Dialog subcomponent ships without panel/typography defaults
|
|
488
|
+
|
|
489
|
+
`DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, and `DialogDescription` all ship without surfaces, padding, rounded corners, or theme-correct typography. The official "bare" example renders an invisible/transparent panel, and the shipped `DialogTitle` defaults to `text-content-system-global-primary` (a light-on-dark token) which renders white-on-white on the correct light surface.
|
|
490
|
+
|
|
491
|
+
**Critical token corrections** (the previous override was wrong):
|
|
492
|
+
|
|
493
|
+
- The right surface is `bg-background-presentation-body-primary` — NOT `bg-background-presentation-form-base` (form-base is for input field surfaces) and NOT `bg-background-system-action-secondary` (that's a brand-dark inverted action surface).
|
|
494
|
+
- The right title token is `text-content-presentation-global-primary` — NOT `text-content-system-global-primary`.
|
|
495
|
+
- The right description token is `text-content-presentation-global-secondary` — NOT the shipped triple-stack `"text-sm text-muted-foreground text-content-system-global-primary"`.
|
|
496
|
+
|
|
497
|
+
**Production-tested override** — patch `Dialog.tsx` once after `npx torch-glare add Dialog`, then every consumer renders correctly with bare components:
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
"use client";
|
|
501
|
+
|
|
502
|
+
import * as React from "react";
|
|
503
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
504
|
+
import { cn } from "@/utils/cn";
|
|
505
|
+
|
|
506
|
+
const Dialog = DialogPrimitive.Root;
|
|
507
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
508
|
+
const DialogPortal = DialogPrimitive.Portal;
|
|
509
|
+
|
|
510
|
+
const DialogOverlay = React.forwardRef<
|
|
511
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
512
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
513
|
+
>(({ className, ...props }, ref) => (
|
|
514
|
+
<DialogPrimitive.Overlay
|
|
515
|
+
ref={ref}
|
|
516
|
+
className={cn(
|
|
517
|
+
"fixed inset-0 z-50 bg-black/60",
|
|
518
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
519
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
520
|
+
className,
|
|
521
|
+
)}
|
|
522
|
+
{...props}
|
|
523
|
+
/>
|
|
524
|
+
));
|
|
525
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
526
|
+
|
|
527
|
+
const DialogContent = React.forwardRef<
|
|
528
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
529
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
530
|
+
>(({ className, children, ...props }, ref) => (
|
|
531
|
+
<DialogPortal>
|
|
532
|
+
<DialogOverlay />
|
|
533
|
+
<DialogPrimitive.Content
|
|
534
|
+
ref={ref}
|
|
535
|
+
className={cn(
|
|
536
|
+
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
|
537
|
+
"flex flex-col items-stretch justify-start",
|
|
538
|
+
"w-[92vw] sm:max-w-lg",
|
|
539
|
+
"bg-background-presentation-body-primary",
|
|
540
|
+
"border border-border-presentation-global-primary",
|
|
541
|
+
"rounded-xl shadow-lg",
|
|
542
|
+
"p-0 gap-0 overflow-hidden",
|
|
543
|
+
"max-h-[90vh]",
|
|
544
|
+
"duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
545
|
+
className,
|
|
546
|
+
)}
|
|
547
|
+
{...props}
|
|
548
|
+
>
|
|
549
|
+
{children}
|
|
550
|
+
</DialogPrimitive.Content>
|
|
551
|
+
</DialogPortal>
|
|
552
|
+
));
|
|
553
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
554
|
+
|
|
555
|
+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
556
|
+
<div
|
|
557
|
+
className={cn("flex flex-col space-y-1.5 text-left px-6 pt-5 pb-4", className)}
|
|
558
|
+
{...props}
|
|
559
|
+
/>
|
|
560
|
+
);
|
|
561
|
+
DialogHeader.displayName = "DialogHeader";
|
|
562
|
+
|
|
563
|
+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
564
|
+
<div
|
|
565
|
+
className={cn("flex flex-row justify-end gap-2 px-6 py-4", className)}
|
|
566
|
+
{...props}
|
|
567
|
+
/>
|
|
568
|
+
);
|
|
569
|
+
DialogFooter.displayName = "DialogFooter";
|
|
570
|
+
|
|
571
|
+
const DialogTitle = React.forwardRef<
|
|
572
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
573
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
574
|
+
>(({ className, ...props }, ref) => (
|
|
575
|
+
<DialogPrimitive.Title
|
|
576
|
+
ref={ref}
|
|
577
|
+
className={cn(
|
|
578
|
+
"typography-headers-small-semibold text-content-presentation-global-primary",
|
|
579
|
+
className,
|
|
580
|
+
)}
|
|
581
|
+
{...props}
|
|
582
|
+
/>
|
|
583
|
+
));
|
|
584
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
585
|
+
|
|
586
|
+
const DialogDescription = React.forwardRef<
|
|
587
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
588
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
589
|
+
>(({ className, ...props }, ref) => (
|
|
590
|
+
<DialogPrimitive.Description
|
|
591
|
+
ref={ref}
|
|
592
|
+
className={cn(
|
|
593
|
+
"typography-body-small-regular text-content-presentation-global-secondary",
|
|
594
|
+
className,
|
|
595
|
+
)}
|
|
596
|
+
{...props}
|
|
597
|
+
/>
|
|
598
|
+
));
|
|
599
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
600
|
+
|
|
601
|
+
export {
|
|
602
|
+
Dialog,
|
|
603
|
+
DialogTrigger,
|
|
604
|
+
DialogPortal,
|
|
605
|
+
DialogOverlay,
|
|
606
|
+
DialogContent,
|
|
607
|
+
DialogHeader,
|
|
608
|
+
DialogFooter,
|
|
609
|
+
DialogTitle,
|
|
610
|
+
DialogDescription,
|
|
611
|
+
};
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Critical default decisions, and why:
|
|
615
|
+
|
|
616
|
+
- **`bg-background-presentation-body-primary`** for the panel surface — it's the body-surface token that pairs correctly with `text-content-presentation-global-primary` across all themes. `form-base` and `action-secondary` are wrong choices that the previous version of this doc recommended.
|
|
617
|
+
- **`p-0 gap-0 overflow-hidden`** on `DialogContent` — padding is delegated to `DialogHeader`/`DialogFooter` (and the form body). Keeping the content padding-free lets header/footer touch the rounded corners cleanly.
|
|
618
|
+
- **`DialogHeader` → `flex flex-col space-y-1.5 ... px-6 pt-5 pb-4`** — column layout (NOT `flex justify-between` like the shipped default) so title and description stack with a tight 6 px gap. Padding lives here, not on the content.
|
|
619
|
+
- **`DialogFooter` → `flex flex-row justify-end gap-2 px-6 py-4`** — horizontal on all viewports (NOT `flex-col-reverse sm:flex-row` like the shipped default). The shipped vertical-on-mobile pattern looks broken on narrow phones too.
|
|
620
|
+
- **`w-[92vw] sm:max-w-lg`** — 92 % of viewport on mobile, 512 px max from `sm` up. Override per-dialog with `className="sm:max-w-110"` (440 px) or `sm:max-w-2xl` (672 px) for wider forms.
|
|
621
|
+
|
|
622
|
+
### Form body lives outside the form-padded `DialogFooter`
|
|
623
|
+
|
|
624
|
+
Place the `<form>` between `DialogHeader` and `DialogFooter`, with the footer as a **sibling outside the form** so its padding doesn't compound with the form body's padding. Use `type="button"` + `onClick={handleSubmit(onSubmit)}` on the submit button rather than `type="submit"`:
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
628
|
+
<DialogContent className="sm:max-w-110">
|
|
629
|
+
<DialogHeader>
|
|
630
|
+
<DialogTitle>Create Folder</DialogTitle>
|
|
631
|
+
<DialogDescription>Add a new folder.</DialogDescription>
|
|
632
|
+
</DialogHeader>
|
|
633
|
+
|
|
634
|
+
<form className="flex flex-col gap-4 px-6 py-5">
|
|
635
|
+
<LabelField
|
|
636
|
+
label="Folder Name"
|
|
637
|
+
requiredLabel="*"
|
|
638
|
+
{...register("name")}
|
|
639
|
+
className="w-full *:w-full"
|
|
640
|
+
/>
|
|
641
|
+
</form>
|
|
642
|
+
|
|
643
|
+
<DialogFooter>
|
|
644
|
+
<Button type="button" variant="BorderStyle" size="M" onClick={() => onOpenChange(false)}>
|
|
645
|
+
Cancel
|
|
646
|
+
</Button>
|
|
647
|
+
<Button type="button" variant="PrimeStyle" size="M" onClick={handleSubmit(onSubmit)}>
|
|
648
|
+
Create
|
|
649
|
+
</Button>
|
|
650
|
+
</DialogFooter>
|
|
651
|
+
</DialogContent>
|
|
652
|
+
</Dialog>
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The `px-6 py-5` body padding matches the header/footer's horizontal `px-6` rhythm so the columns line up vertically across the panel.
|
|
656
|
+
|
|
657
|
+
### `DialogTitle` with icon
|
|
658
|
+
|
|
659
|
+
Plain `<DialogTitle>Create Account</DialogTitle>` is fine, but you can pass JSX with an icon for instant entity recognition:
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
<DialogHeader>
|
|
663
|
+
<DialogTitle>
|
|
664
|
+
<div className="flex items-center gap-2">
|
|
665
|
+
<i className="ri-bank-line text-content-presentation-state-information" />
|
|
666
|
+
<span>Create Account</span>
|
|
667
|
+
</div>
|
|
668
|
+
</DialogTitle>
|
|
669
|
+
</DialogHeader>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Pick an icon that matches the entity (`ri-bank-line` for accounts, `ri-calendar-line` for fiscal periods, `ri-receipt-line` for vouchers, etc.).
|
|
673
|
+
|
|
485
674
|
## Common Patterns
|
|
486
675
|
|
|
487
676
|
### Alert/Confirmation Pattern
|
|
@@ -426,6 +426,42 @@ test('InputField meets WCAG standards', async () => {
|
|
|
426
426
|
})
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
+
## Known Limitations & Frontend Patterns
|
|
430
|
+
|
|
431
|
+
### Form-row alignment (h-10)
|
|
432
|
+
|
|
433
|
+
When mixing `InputField` with `Select`, `<input type="date">`, or other form controls in a multi-column row, default heights don't always match. The shipped pattern that keeps everything on a 40 px baseline:
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
<InputField
|
|
437
|
+
className="h-10"
|
|
438
|
+
icon={<i className="ri-hashtag text-base" />}
|
|
439
|
+
placeholder="e.g. JV-001"
|
|
440
|
+
errorMessage={errors.code?.message}
|
|
441
|
+
toolTipSide="top"
|
|
442
|
+
{...register("code")}
|
|
443
|
+
/>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Recommendations:
|
|
447
|
+
|
|
448
|
+
- Pass `className="h-10"` on every form-context `InputField`. Pair with `Select` set to `className="w-full h-10"` so the row aligns.
|
|
449
|
+
- Use `errorMessage` + `toolTipSide` instead of rendering a separate `<p>` below the field — the built-in tooltip is less visually noisy and is what the component is designed for.
|
|
450
|
+
- Add an `icon` prop (`<i className="ri-* text-base" />`) for visual scannability; this is standard across most form fields in production apps.
|
|
451
|
+
|
|
452
|
+
> ⚠️ **Do not use `variant="SystemStyle"`** to "fix" the form look — `SystemStyle` is reserved for internal library system surfaces. Use the default `PresentationStyle` plus `className="h-10"`. See the rules banner at the top of every doc response.
|
|
453
|
+
|
|
454
|
+
### Field wrapper for forms
|
|
455
|
+
|
|
456
|
+
Wrap each labeled field in `flex flex-col gap-1.5` (not `space-y-1.5`) so labels align cleanly at the top of multi-column rows:
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
<div className="flex flex-col gap-1.5">
|
|
460
|
+
<Label>Voucher Number *</Label>
|
|
461
|
+
<InputField className="h-10" icon={...} {...register("number")} />
|
|
462
|
+
</div>
|
|
463
|
+
```
|
|
464
|
+
|
|
429
465
|
## Accessibility
|
|
430
466
|
|
|
431
467
|
### Keyboard Support
|
|
@@ -535,6 +535,30 @@ test('Select meets WCAG standards', async () => {
|
|
|
535
535
|
})
|
|
536
536
|
```
|
|
537
537
|
|
|
538
|
+
## Known Limitations & Frontend Patterns
|
|
539
|
+
|
|
540
|
+
### `Select` does not stretch to fill its container by default
|
|
541
|
+
|
|
542
|
+
The shipped `SelectTrigger` base styles do not include `w-full`, so when placed inside a CSS grid or flex column the trigger collapses to the width of its placeholder/selected text. This breaks row alignment in forms whenever `Select` is mixed with `InputField` or other full-width controls.
|
|
543
|
+
|
|
544
|
+
**Always pass `className="w-full h-10"` on form-context `Select` usages**:
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
<Select
|
|
548
|
+
className="w-full h-10"
|
|
549
|
+
value={type}
|
|
550
|
+
onValueChange={setType}
|
|
551
|
+
options={typeOptions}
|
|
552
|
+
placeholder="Select type"
|
|
553
|
+
/>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
For inline / compact / table-cell usages where you genuinely want the trigger to be content-sized, omit `w-full` (or pass `w-auto`).
|
|
557
|
+
|
|
558
|
+
### Numeric/financial selects
|
|
559
|
+
|
|
560
|
+
When `Select` is the source of a number (account code, fiscal period), keep the displayed `label` formatted (e.g. `"JV-001 — Journal Voucher"`) but the `value` as the raw key (`"JV"`). Do **not** put numeric formatting in the `value` itself.
|
|
561
|
+
|
|
538
562
|
## Accessibility
|
|
539
563
|
|
|
540
564
|
### Keyboard Support
|
|
@@ -547,6 +547,40 @@ test('SimpleSelect meets WCAG standards', async () => {
|
|
|
547
547
|
})
|
|
548
548
|
```
|
|
549
549
|
|
|
550
|
+
## Known Limitations & Frontend Patterns
|
|
551
|
+
|
|
552
|
+
### Empty-string option values crash at runtime
|
|
553
|
+
|
|
554
|
+
`SimpleSelect` is built on Radix UI's `Select` primitive, which **forbids `<Select.Item value="" />`**. Radix reserves the empty string for "clear selection / show placeholder" semantics. Passing an option with `value: ""` throws:
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
A <Select.Item /> must have a value prop that is not an empty string.
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
This is a common footgun for filter bars where the natural "All / Any / None" option looks like `{ value: "", label: "All Statuses" }`.
|
|
561
|
+
|
|
562
|
+
**Workaround — use a sentinel value and translate in both directions:**
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
<SimpleSelect
|
|
566
|
+
value={statusFilter || "ALL"}
|
|
567
|
+
onValueChange={(val) => setStatusFilter(val === "ALL" ? "" : val)}
|
|
568
|
+
options={[
|
|
569
|
+
{ value: "ALL", label: "All Statuses" },
|
|
570
|
+
{ value: "DRAFT", label: "Draft" },
|
|
571
|
+
{ value: "POSTED", label: "Posted" },
|
|
572
|
+
]}
|
|
573
|
+
className="w-48 h-10"
|
|
574
|
+
placeholder="All Statuses"
|
|
575
|
+
/>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Pick a sentinel that won't collide with real values (`"ALL"`, `"__NONE__"`, etc.). Map back to the empty string (or `null`/`undefined`, depending on your state shape) inside `onValueChange` so consumers downstream still see the cleared state.
|
|
579
|
+
|
|
580
|
+
### Width / height — same as `Select`
|
|
581
|
+
|
|
582
|
+
`SimpleSelect`'s trigger does not include `w-full` by default. For form rows and filter bars, always pass `className="w-full h-10"` (or `className="w-48 h-10"` for narrower filter selects).
|
|
583
|
+
|
|
550
584
|
## Accessibility
|
|
551
585
|
|
|
552
586
|
### Keyboard Support
|
package/docs/components/table.md
CHANGED
|
@@ -747,6 +747,81 @@ test('Table meets WCAG standards', async () => {
|
|
|
747
747
|
})
|
|
748
748
|
```
|
|
749
749
|
|
|
750
|
+
## Known Limitations & Frontend Patterns
|
|
751
|
+
|
|
752
|
+
### `Table` defaults to `w-auto` — always pass `className="w-full"` for data tables
|
|
753
|
+
|
|
754
|
+
The shipped `Table` base classes include `w-auto`, so the table sizes to its content. Inside a `Card`, `Dialog`, or any flex/grid container with short rows, the table collapses to a narrow column on the left with empty space on the right. This is rarely what you want for a data list.
|
|
755
|
+
|
|
756
|
+
**Always pass `className="w-full"`** for list/data tables:
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
<Card className="p-0 overflow-hidden">
|
|
760
|
+
<CardContent className="p-0 overflow-x-auto">
|
|
761
|
+
<Table className="w-full">
|
|
762
|
+
<TableHeader>
|
|
763
|
+
<TableRow>
|
|
764
|
+
<TableHead>Number</TableHead>
|
|
765
|
+
<TableHead>Status</TableHead>
|
|
766
|
+
<TableHead className="text-right">Total Debit</TableHead>
|
|
767
|
+
</TableRow>
|
|
768
|
+
</TableHeader>
|
|
769
|
+
<TableBody>...</TableBody>
|
|
770
|
+
</Table>
|
|
771
|
+
</CardContent>
|
|
772
|
+
</Card>
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Wrap with `overflow-x-auto`** on the parent so wide tables (10+ columns) scroll horizontally inside the card instead of overflowing it.
|
|
776
|
+
|
|
777
|
+
### Numeric columns: `text-right` on both head and cell
|
|
778
|
+
|
|
779
|
+
Right-align debit, credit, amount, count columns on **both** `TableHead` and the corresponding `TableCell`:
|
|
780
|
+
|
|
781
|
+
```tsx
|
|
782
|
+
<TableHead className="text-right">Total Debit</TableHead>
|
|
783
|
+
<TableCell className="text-right font-mono">{voucher.totalDebit}</TableCell>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Use `font-mono` on the cell so digits line up across rows.
|
|
787
|
+
|
|
788
|
+
### `whitespace-nowrap` on date and numeric cells
|
|
789
|
+
|
|
790
|
+
Dates and amounts wrap awkwardly when the column is narrow. Add `whitespace-nowrap` on those cells (not on heads):
|
|
791
|
+
|
|
792
|
+
```tsx
|
|
793
|
+
<TableCell className="whitespace-nowrap">{formatDate(voucher.date)}</TableCell>
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### `TableCell` force-wraps children — use `childrenClassName` to override
|
|
797
|
+
|
|
798
|
+
`TableCell` unconditionally wraps children in an inner `<div>` with `flex justify-start items-center gap-1 min-w-[200px] overflow-hidden` plus a fade-out gradient mask. Three common breakages:
|
|
799
|
+
|
|
800
|
+
1. **Empty-state rows don't center.** A `flex flex-col items-center` empty state ends up flush left because the outer wrapper's `justify-start` already decided alignment.
|
|
801
|
+
2. **Multi-line content is clipped** by `overflow-hidden` + the gradient mask.
|
|
802
|
+
3. **`flex-col` doesn't work** because the wrapper is `flex` (row) by default.
|
|
803
|
+
|
|
804
|
+
**Workaround — use the `childrenClassName` prop**, which merges into the inner wrapper, and skip your own outer `<div>`:
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
<TableRow>
|
|
808
|
+
<TableCell
|
|
809
|
+
colSpan={7}
|
|
810
|
+
childrenClassName="flex flex-col items-center justify-center gap-3 py-12 w-full min-w-0 text-content-presentation-global-secondary"
|
|
811
|
+
>
|
|
812
|
+
<i className="ri-inbox-line text-4xl opacity-60" />
|
|
813
|
+
<p className="typography-body-medium-regular">No data</p>
|
|
814
|
+
<p className="typography-body-small-regular opacity-80">Try adjusting filters</p>
|
|
815
|
+
</TableCell>
|
|
816
|
+
</TableRow>
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Key overrides on `childrenClassName`:
|
|
820
|
+
|
|
821
|
+
- `flex flex-col` overrides the default `flex-row`
|
|
822
|
+
- `items-center justify-center` overrides `justify-start`
|
|
823
|
+
- `w-full min-w-0` overrides the hardcoded `min-w-[200px]`
|
|
824
|
+
|
|
750
825
|
## Accessibility
|
|
751
826
|
|
|
752
827
|
### Keyboard Support
|
|
@@ -425,6 +425,46 @@ test('Textarea meets WCAG standards', async () => {
|
|
|
425
425
|
})
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
+
## Known Limitations & Frontend Patterns
|
|
429
|
+
|
|
430
|
+
### `Textarea` auto-sizes to its own content and cannot be made full-width via `className`
|
|
431
|
+
|
|
432
|
+
The shipped component has these hardcoded base classes:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
field-sizing-content w-full min-w-[100px] max-w-[100%]
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Two consequences:
|
|
439
|
+
|
|
440
|
+
1. **`field-sizing-content`** sizes the textarea to its content. An empty textarea collapses to ~100 × 36 px regardless of explicit `width`/`min-height` until the user types enough to expand it.
|
|
441
|
+
2. **`className` is forwarded to the wrapping `Label`**, not the inner `<textarea>` element — so passing `className="w-full min-h-24"` does not override the inner sizing.
|
|
442
|
+
|
|
443
|
+
Real-world impact: a description textarea inside a full-width card renders as a tiny ~100 × 40 px box even with `className="w-full min-h-24"`.
|
|
444
|
+
|
|
445
|
+
**Workaround used in production — drop the wrapper and use a plain `<textarea>` with TORCH design tokens:**
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
<textarea
|
|
449
|
+
{...register("notes")}
|
|
450
|
+
placeholder="Optional notes..."
|
|
451
|
+
rows={4}
|
|
452
|
+
className={[
|
|
453
|
+
"w-full min-h-24 px-3 py-2 rounded-lg",
|
|
454
|
+
"border border-border-presentation-global-primary",
|
|
455
|
+
"bg-background-presentation-form-field-primary",
|
|
456
|
+
"text-content-presentation-action-light-primary",
|
|
457
|
+
"typography-body-medium-regular",
|
|
458
|
+
"outline-none resize-y",
|
|
459
|
+
"hover:border-border-presentation-action-hover",
|
|
460
|
+
"focus:border-border-presentation-state-focus",
|
|
461
|
+
"transition-colors",
|
|
462
|
+
].join(" ")}
|
|
463
|
+
/>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
This gives you the TORCH look-and-feel without `field-sizing-content`. Switch back to the wrapped `Textarea` once the underlying component drops the auto-sizing default and forwards `className` correctly.
|
|
467
|
+
|
|
428
468
|
## Accessibility
|
|
429
469
|
|
|
430
470
|
### Keyboard Support
|
|
@@ -373,6 +373,65 @@ interface CustomToggleProps extends ToggleProps, VariantProps<typeof toggleVaria
|
|
|
373
373
|
export const Toggle: React.ForwardRefExoticComponent<CustomToggleProps>
|
|
374
374
|
```
|
|
375
375
|
|
|
376
|
+
## Known Limitations & Frontend Patterns
|
|
377
|
+
|
|
378
|
+
> ⚠️ **`Toggle` is icon-only.** Its dimensions are hardcoded squares — `S=22×22`, `M=28×28`, `L=34×34`, `XL=40×40` — so any text label wider than ~3 characters will visually clip. The "Filter Options", "Theme Switcher with text", and similar text-label examples below are misleading: they only render correctly in environments that override the dimensions.
|
|
379
|
+
|
|
380
|
+
### Use `Button` with variant swap for selectable text chips
|
|
381
|
+
|
|
382
|
+
For format selectors (DOCX, MD, RTF), tag pickers, filter pills, or any case where the chip carries text wider than ~3 characters, **do not use `Toggle`**. Use `Button` with a `variant` swap to express the selected state:
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
const FORMATS = ["DOCX", "MD", "RTF", "ODT", "EPUB", "TXT"];
|
|
386
|
+
|
|
387
|
+
export function FormatPicker({ selected, onSelect }: Props) {
|
|
388
|
+
return (
|
|
389
|
+
<div className="flex flex-wrap gap-2">
|
|
390
|
+
{FORMATS.map((format) => {
|
|
391
|
+
const isSelected = selected === format;
|
|
392
|
+
return (
|
|
393
|
+
<Button
|
|
394
|
+
key={format}
|
|
395
|
+
type="button"
|
|
396
|
+
variant={isSelected ? "PrimeStyle" : "BorderStyle"}
|
|
397
|
+
size="S"
|
|
398
|
+
onClick={() => onSelect(format)}
|
|
399
|
+
className="uppercase"
|
|
400
|
+
>
|
|
401
|
+
{format}
|
|
402
|
+
</Button>
|
|
403
|
+
);
|
|
404
|
+
})}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
This gives you:
|
|
411
|
+
|
|
412
|
+
- Auto-sizing horizontal padding via the button's `px-*` defaults
|
|
413
|
+
- Theme-correct surface and text tokens (button variants are pre-themed)
|
|
414
|
+
- Clear pressed/unpressed visual via `PrimeStyle` (filled) vs `BorderStyle` (outlined)
|
|
415
|
+
- All the focus/disabled/loading states from `Button` for free
|
|
416
|
+
|
|
417
|
+
### When `Toggle` IS the right component
|
|
418
|
+
|
|
419
|
+
Reserve `Toggle` for its actual designed use case — **icon-only toolbar actions** with a binary on/off state:
|
|
420
|
+
|
|
421
|
+
- Bold / Italic / Underline in a rich-text toolbar
|
|
422
|
+
- Play / Pause
|
|
423
|
+
- Grid / List view
|
|
424
|
+
- Mute / Unmute
|
|
425
|
+
- Eye / Eye-slash (show/hide password)
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
<Toggle pressed={isBold} onPressedChange={setBold} aria-label="Bold">
|
|
429
|
+
<i className="ri-bold" />
|
|
430
|
+
</Toggle>
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The icon must fit within the square dimension — `[&_i]:text-[14px]` for size M, etc.
|
|
434
|
+
|
|
376
435
|
## Common Patterns
|
|
377
436
|
|
|
378
437
|
### Toolbar Group
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Form & List Page Recipes
|
|
3
|
+
description: Production-tested patterns for building create dialogs, list pages, journal-line grids, and login layouts with TORCH Glare. Captures the "what actually ships" decisions for app-builders.
|
|
4
|
+
keywords: [recipes, patterns, form, list, dialog, journal, line-items, login, filter-bar, grid]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Form & List Page Recipes
|
|
8
|
+
|
|
9
|
+
Battle-tested layouts and component combinations from production frontend work.
|
|
10
|
+
Use these as the starting point for create dialogs, list/filter pages,
|
|
11
|
+
line-item grids, and login flows.
|
|
12
|
+
|
|
13
|
+
> All examples use the `presentation` color tokens. Never substitute
|
|
14
|
+
> `*-system-*` tokens or `variant="SystemStyle"` — see the rules banner at
|
|
15
|
+
> the top of every doc response.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Recipe 1 — Multi-column form row with consistent baselines
|
|
20
|
+
|
|
21
|
+
**Use case:** A row in a create dialog that mixes `InputField`, `Select`, and
|
|
22
|
+
date inputs. Default heights don't match across components, so you have to
|
|
23
|
+
opt in to alignment explicitly.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<div className="grid grid-cols-3 gap-4">
|
|
27
|
+
<div className="flex flex-col gap-1.5">
|
|
28
|
+
<Label>Voucher Type *</Label>
|
|
29
|
+
<Select
|
|
30
|
+
className="w-full h-10"
|
|
31
|
+
value={type}
|
|
32
|
+
onValueChange={setType}
|
|
33
|
+
options={typeOptions}
|
|
34
|
+
placeholder="Select type"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex flex-col gap-1.5">
|
|
39
|
+
<Label>Date *</Label>
|
|
40
|
+
<InputField
|
|
41
|
+
className="h-10"
|
|
42
|
+
type="date"
|
|
43
|
+
icon={<i className="ri-calendar-line text-base" />}
|
|
44
|
+
{...register("date")}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="flex flex-col gap-1.5">
|
|
49
|
+
<Label>Fiscal Period *</Label>
|
|
50
|
+
<Select
|
|
51
|
+
className="w-full h-10"
|
|
52
|
+
value={period}
|
|
53
|
+
onValueChange={setPeriod}
|
|
54
|
+
options={periodOptions}
|
|
55
|
+
placeholder="Select period"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Key rules:
|
|
62
|
+
|
|
63
|
+
- **Every form control gets `h-10`** — both `InputField` and `Select` (which
|
|
64
|
+
also needs `w-full`).
|
|
65
|
+
- **Wrap each field in `flex flex-col gap-1.5`**, not `space-y-1.5`, so labels
|
|
66
|
+
sit cleanly at the top of the row.
|
|
67
|
+
- **Use `<InputField type="date">`** instead of a native `<input type="date">`
|
|
68
|
+
so you keep the icon slot and consistent styling.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Recipe 2 — List page with filter bar + data table
|
|
73
|
+
|
|
74
|
+
**Use case:** Every list page (vouchers, accounts, bank accounts, etc.) needs
|
|
75
|
+
the same filter bar — a search input + one or more select filters above a
|
|
76
|
+
data table.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
{/* Filter bar */}
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
<div className="flex-1 max-w-sm">
|
|
82
|
+
<InputField
|
|
83
|
+
className="h-10"
|
|
84
|
+
icon={<i className="ri-search-line text-base" />}
|
|
85
|
+
placeholder="Search..."
|
|
86
|
+
value={search}
|
|
87
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<SimpleSelect
|
|
92
|
+
className="w-48 h-10"
|
|
93
|
+
value={statusFilter || "ALL"}
|
|
94
|
+
onValueChange={(val) => setStatusFilter(val === "ALL" ? "" : val)}
|
|
95
|
+
options={[
|
|
96
|
+
{ value: "ALL", label: "All Statuses" },
|
|
97
|
+
{ value: "DRAFT", label: "Draft" },
|
|
98
|
+
{ value: "POSTED", label: "Posted" },
|
|
99
|
+
]}
|
|
100
|
+
placeholder="All Statuses"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Data table */}
|
|
105
|
+
<Card className="p-0 overflow-hidden">
|
|
106
|
+
<CardContent className="p-0 overflow-x-auto">
|
|
107
|
+
<Table className="w-full">
|
|
108
|
+
<TableHeader>
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableHead>Number</TableHead>
|
|
111
|
+
<TableHead>Status</TableHead>
|
|
112
|
+
<TableHead className="text-right">Total Debit</TableHead>
|
|
113
|
+
<TableHead className="text-right">Total Credit</TableHead>
|
|
114
|
+
</TableRow>
|
|
115
|
+
</TableHeader>
|
|
116
|
+
<TableBody>
|
|
117
|
+
{rows.length === 0 ? (
|
|
118
|
+
<TableRow>
|
|
119
|
+
<TableCell
|
|
120
|
+
colSpan={4}
|
|
121
|
+
childrenClassName="flex flex-col items-center justify-center gap-3 py-12 w-full min-w-0 text-content-presentation-global-secondary"
|
|
122
|
+
>
|
|
123
|
+
<i className="ri-inbox-line text-4xl opacity-60" />
|
|
124
|
+
<p className="typography-body-medium-regular">No vouchers</p>
|
|
125
|
+
<p className="typography-body-small-regular opacity-80">
|
|
126
|
+
Try adjusting your filters
|
|
127
|
+
</p>
|
|
128
|
+
</TableCell>
|
|
129
|
+
</TableRow>
|
|
130
|
+
) : (
|
|
131
|
+
rows.map((v) => (
|
|
132
|
+
<TableRow key={v.id}>
|
|
133
|
+
<TableCell className="whitespace-nowrap">{v.number}</TableCell>
|
|
134
|
+
<TableCell>{v.status}</TableCell>
|
|
135
|
+
<TableCell className="text-right font-mono whitespace-nowrap">
|
|
136
|
+
{v.totalDebit}
|
|
137
|
+
</TableCell>
|
|
138
|
+
<TableCell className="text-right font-mono whitespace-nowrap">
|
|
139
|
+
{v.totalCredit}
|
|
140
|
+
</TableCell>
|
|
141
|
+
</TableRow>
|
|
142
|
+
))
|
|
143
|
+
)}
|
|
144
|
+
</TableBody>
|
|
145
|
+
</Table>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Key rules:
|
|
151
|
+
|
|
152
|
+
- **Never use a native `<select>`** — `SimpleSelect` keeps styling consistent
|
|
153
|
+
with the rest of your forms.
|
|
154
|
+
- **Never manually position a search icon with absolute positioning** —
|
|
155
|
+
`InputField`'s `icon` prop is built for this.
|
|
156
|
+
- **Numeric columns get `text-right` on both `TableHead` and `TableCell`**,
|
|
157
|
+
plus `font-mono` on the cell so digits line up.
|
|
158
|
+
- **Empty states use `childrenClassName`** on `TableCell` to bypass the
|
|
159
|
+
default `flex-row justify-start min-w-[200px]` wrapper. See `Table.md`
|
|
160
|
+
→ "TableCell force-wraps children" for why.
|
|
161
|
+
- **The "All / Any" filter option needs a sentinel value** (`"ALL"` here)
|
|
162
|
+
because Radix forbids `value=""`. See `SimpleSelect.md` for details.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Recipe 3 — Journal lines / line-item grid (display: contents)
|
|
167
|
+
|
|
168
|
+
**Use case:** A repeating-row layout (journal lines, invoice line items,
|
|
169
|
+
purchase order rows) with a header, N data rows, and a totals row — where
|
|
170
|
+
columns must align across all rows.
|
|
171
|
+
|
|
172
|
+
The naïve approach (each row is its own `display: grid`) breaks: short
|
|
173
|
+
footer content collapses to a different width than long input content,
|
|
174
|
+
and columns drift across rows. The fix is **one parent grid** with
|
|
175
|
+
`className="contents"` on each row wrapper so the row's children become
|
|
176
|
+
direct grid items of the parent.
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
<div className="grid w-full grid-cols-[32px_minmax(0,2.5fr)_minmax(0,3fr)_minmax(0,1.5fr)_minmax(0,1.5fr)_36px] gap-x-3 gap-y-2 px-2">
|
|
180
|
+
{/* Header cells (direct grid items) */}
|
|
181
|
+
<div>#</div>
|
|
182
|
+
<div>Account</div>
|
|
183
|
+
<div>Description</div>
|
|
184
|
+
<div>Debit</div>
|
|
185
|
+
<div>Credit</div>
|
|
186
|
+
<div />
|
|
187
|
+
|
|
188
|
+
{/* Data rows — wrapper uses className="contents" so its children become
|
|
189
|
+
direct grid items of the parent grid */}
|
|
190
|
+
{lines.map((line, idx) => (
|
|
191
|
+
<div key={line.key} className="contents">
|
|
192
|
+
<div>{idx + 1}</div>
|
|
193
|
+
<Select
|
|
194
|
+
className="w-full h-10"
|
|
195
|
+
value={line.accountId}
|
|
196
|
+
onValueChange={(v) => updateLine(line.key, { accountId: v })}
|
|
197
|
+
options={accountOptions}
|
|
198
|
+
/>
|
|
199
|
+
<InputField
|
|
200
|
+
className="h-10"
|
|
201
|
+
value={line.description}
|
|
202
|
+
onChange={(e) => updateLine(line.key, { description: e.target.value })}
|
|
203
|
+
/>
|
|
204
|
+
<InputField
|
|
205
|
+
className="h-10 text-right font-mono"
|
|
206
|
+
type="number"
|
|
207
|
+
value={line.debit}
|
|
208
|
+
onChange={(e) => updateLine(line.key, { debit: e.target.value })}
|
|
209
|
+
/>
|
|
210
|
+
<InputField
|
|
211
|
+
className="h-10 text-right font-mono"
|
|
212
|
+
type="number"
|
|
213
|
+
value={line.credit}
|
|
214
|
+
onChange={(e) => updateLine(line.key, { credit: e.target.value })}
|
|
215
|
+
/>
|
|
216
|
+
<button onClick={() => removeLine(line.key)} aria-label="Delete row">
|
|
217
|
+
<i className="ri-delete-bin-line" />
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
|
|
222
|
+
{/* Totals row — direct grid items, no wrapper */}
|
|
223
|
+
<div />
|
|
224
|
+
<div />
|
|
225
|
+
<div className="text-right">Totals</div>
|
|
226
|
+
<div className="text-right font-mono">{totalDebit}</div>
|
|
227
|
+
<div className="text-right font-mono">{totalCredit}</div>
|
|
228
|
+
<div />
|
|
229
|
+
</div>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Key rules:
|
|
233
|
+
|
|
234
|
+
- **`className="contents"`** on the row wrapper is the critical trick. The
|
|
235
|
+
wrapper renders nothing of its own, so its children become direct grid
|
|
236
|
+
items of the parent.
|
|
237
|
+
- **`minmax(0, Xfr)`** columns prevent content-driven blowout when a single
|
|
238
|
+
cell has a long value.
|
|
239
|
+
- **Fixed-width columns** (the `32px` row-number column, the `36px` delete
|
|
240
|
+
column) are absolute pixel widths; flexible columns use `fr`.
|
|
241
|
+
- **Header alignment**: Debit/Credit headers are `text-left` because the
|
|
242
|
+
column starts at the input's left edge — right-aligning the header would
|
|
243
|
+
visually disconnect it from the column. The values inside the input are
|
|
244
|
+
right-aligned via `text-right font-mono` on the input itself.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Recipe 4 — `DialogTitle` with icon
|
|
249
|
+
|
|
250
|
+
**Use case:** Plain `<DialogTitle>Create Account</DialogTitle>` lacks visual
|
|
251
|
+
hierarchy. Adding a relevant icon makes dialogs instantly recognizable.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
<DialogHeader>
|
|
255
|
+
<DialogTitle>
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<i className="ri-bank-line text-content-presentation-state-information" />
|
|
258
|
+
<span>Create Account</span>
|
|
259
|
+
</div>
|
|
260
|
+
</DialogTitle>
|
|
261
|
+
</DialogHeader>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Pick an icon that matches the entity:
|
|
265
|
+
|
|
266
|
+
| Entity | Icon |
|
|
267
|
+
|---|---|
|
|
268
|
+
| Account | `ri-bank-line` |
|
|
269
|
+
| Fiscal period / year | `ri-calendar-line` |
|
|
270
|
+
| Voucher / journal | `ri-receipt-line` |
|
|
271
|
+
| Number series | `ri-hashtag` |
|
|
272
|
+
| Posting rule | `ri-settings-3-line` |
|
|
273
|
+
| Exchange rate | `ri-exchange-line` |
|
|
274
|
+
| Bank / cash account | `ri-bank-card-line` |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Recipe 5 — Two-column login layout
|
|
279
|
+
|
|
280
|
+
**Use case:** A login page with brand panel on the left and form on the right.
|
|
281
|
+
Communicates product identity better than a single centered card.
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { Card, CardContent } from "@/components/Card";
|
|
285
|
+
import { InputField } from "@/components/InputField";
|
|
286
|
+
import { LoginButton } from "@/components/LoginButton";
|
|
287
|
+
|
|
288
|
+
export function LoginPage() {
|
|
289
|
+
return (
|
|
290
|
+
<div className="grid min-h-screen grid-cols-1 lg:grid-cols-2">
|
|
291
|
+
{/* Brand panel — hidden on mobile */}
|
|
292
|
+
<div
|
|
293
|
+
className={[
|
|
294
|
+
"hidden lg:flex flex-col justify-center gap-6 p-12",
|
|
295
|
+
"bg-linear-to-br from-background-presentation-action-primary/20",
|
|
296
|
+
"via-background-presentation-form-base",
|
|
297
|
+
"to-background-presentation-state-information/10",
|
|
298
|
+
].join(" ")}
|
|
299
|
+
>
|
|
300
|
+
<h1 className="typography-headers-large-medium">
|
|
301
|
+
Welcome to Torch Finance
|
|
302
|
+
</h1>
|
|
303
|
+
<ul className="flex flex-col gap-3">
|
|
304
|
+
{features.map((f) => (
|
|
305
|
+
<li key={f.title} className="flex items-start gap-3">
|
|
306
|
+
<span
|
|
307
|
+
className={[
|
|
308
|
+
"mt-0.5 inline-flex h-6 w-6 items-center justify-center",
|
|
309
|
+
"rounded-full bg-background-presentation-state-success-primary",
|
|
310
|
+
"text-content-presentation-action-dark-primary",
|
|
311
|
+
].join(" ")}
|
|
312
|
+
>
|
|
313
|
+
<i className="ri-check-line" />
|
|
314
|
+
</span>
|
|
315
|
+
<div>
|
|
316
|
+
<p className="typography-body-medium-medium">{f.title}</p>
|
|
317
|
+
<p className="typography-body-small-regular text-content-presentation-global-secondary">
|
|
318
|
+
{f.description}
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
</li>
|
|
322
|
+
))}
|
|
323
|
+
</ul>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{/* Form panel */}
|
|
327
|
+
<div className="flex items-center justify-center p-6">
|
|
328
|
+
<Card className="w-full max-w-md">
|
|
329
|
+
<CardContent className="flex flex-col gap-4 p-8">
|
|
330
|
+
<h2 className="typography-headers-medium-medium">Sign in</h2>
|
|
331
|
+
|
|
332
|
+
<InputField
|
|
333
|
+
className="h-10"
|
|
334
|
+
type="email"
|
|
335
|
+
icon={<i className="ri-mail-line text-base" />}
|
|
336
|
+
placeholder="Email"
|
|
337
|
+
errorMessage={errors.email?.message}
|
|
338
|
+
{...register("email")}
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<InputField
|
|
342
|
+
className="h-10"
|
|
343
|
+
type={showPassword ? "text" : "password"}
|
|
344
|
+
icon={<i className="ri-lock-line text-base" />}
|
|
345
|
+
childrenSide={
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={() => setShowPassword((v) => !v)}
|
|
349
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
350
|
+
>
|
|
351
|
+
<i className={showPassword ? "ri-eye-off-line" : "ri-eye-line"} />
|
|
352
|
+
</button>
|
|
353
|
+
}
|
|
354
|
+
placeholder="Password"
|
|
355
|
+
errorMessage={errors.password?.message}
|
|
356
|
+
{...register("password")}
|
|
357
|
+
/>
|
|
358
|
+
|
|
359
|
+
<LoginButton isLoading={isSubmitting} type="submit">
|
|
360
|
+
Sign in
|
|
361
|
+
</LoginButton>
|
|
362
|
+
</CardContent>
|
|
363
|
+
</Card>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Key rules:
|
|
371
|
+
|
|
372
|
+
- **`hidden lg:flex`** hides the brand panel on mobile; the form panel
|
|
373
|
+
stretches to full width below the breakpoint.
|
|
374
|
+
- **`bg-linear-to-br from-* via-* to-*`** is Tailwind v4 gradient syntax. For
|
|
375
|
+
v3 use `bg-gradient-to-br`.
|
|
376
|
+
- **`LoginButton` has a built-in `isLoading` prop** — wire it to your form's
|
|
377
|
+
submitting state instead of writing your own loading spinner.
|
|
378
|
+
- **Use `InputField`'s `childrenSide`** for the password show/hide toggle —
|
|
379
|
+
the slot is built for trailing actions like this.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torch-glare-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "MCP server for TORCH Glare component library — gives AI assistants full access to component docs, API references, code examples, and design system info",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|