torch-glare 1.3.0 → 1.5.0
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/apps/lib/components/Button.tsx +115 -64
- package/apps/lib/components/Drawer.tsx +368 -84
- package/apps/lib/components/SectionBlock.tsx +71 -0
- package/apps/lib/components/Stepper.tsx +374 -0
- package/apps/lib/components/Timeline.tsx +283 -0
- package/apps/lib/utils/types.ts +8 -4
- 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/section-block.md +275 -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
|