torch-glare-mcp 1.0.3 → 1.0.4

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.
@@ -579,6 +579,59 @@ 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
+ ### `npx torch-glare add DatePicker` does not install `utils/dateFormat.ts`
619
+
620
+ The CLI ships `DatePicker.tsx` without copying the `dateFormat.ts` utility it imports, so the component fails to build immediately after install:
621
+
622
+ ```
623
+ [plugin:vite:import-analysis] Failed to resolve import "../utils/dateFormat"
624
+ from "DatePicker.tsx".
625
+ ```
626
+
627
+ **Workaround until the CLI is fixed:** create `utils/dateFormat.ts` manually with the three exports the component needs:
628
+
629
+ - `TimePickerValue` (`{ hour: string; minute: string; time: "AM" | "PM" }`)
630
+ - `applyTimeToDateValue(value, timePickerValue)` — applies the time picker value to the date value
631
+ - `formatDateValueToString(value, timePickerValue, dateFormat)` — formats the date value to a display string
632
+
633
+ Reverse-engineer the implementation from the call sites in `DatePicker.tsx` until the CLI copies it automatically.
634
+
582
635
  ## Accessibility
583
636
 
584
637
  - **Keyboard Navigation**:
@@ -482,6 +482,69 @@ export const DialogCloseButton: React.ForwardRefExoticComponent<
482
482
  >
483
483
  ```
484
484
 
485
+ ## Known Limitations & Frontend Patterns
486
+
487
+ ### `DialogContent` ships without background, padding, border, or sizing
488
+
489
+ The base classes on the shipped `DialogContent` are missing every property a usable dialog panel needs — no background color (transparent), no padding (form fields flush against the edge), no border / rounded corners, `w-fit` (so the panel sizes to content), and no `max-height` / overflow handling. Out of the box, dialogs are visually broken and tall forms get cut off.
490
+
491
+ **Override pattern** — wrap or replace the shipped `DialogContent` with these defaults so you don't have to repeat them on every consumer:
492
+
493
+ ```tsx
494
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
495
+ import { cn } from "@/utils/cn";
496
+
497
+ const DialogContent = React.forwardRef<
498
+ React.ElementRef<typeof DialogPrimitive.Content>,
499
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
500
+ >(({ className, children, ...props }, ref) => (
501
+ <DialogPrimitive.Portal>
502
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
503
+ <DialogPrimitive.Content
504
+ ref={ref}
505
+ className={cn(
506
+ "fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
507
+ "flex flex-col gap-4 p-6 rounded-2xl",
508
+ "border border-border-presentation-global-primary",
509
+ "bg-background-presentation-form-base",
510
+ "shadow-2xl",
511
+ "max-h-[90vh] overflow-y-auto",
512
+ "duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out",
513
+ className,
514
+ )}
515
+ {...props}
516
+ >
517
+ {children}
518
+ </DialogPrimitive.Content>
519
+ </DialogPrimitive.Portal>
520
+ ));
521
+ ```
522
+
523
+ Critical defaults:
524
+
525
+ - `bg-background-presentation-form-base` — solid background using the design token
526
+ - `border border-border-presentation-global-primary rounded-2xl` — visible panel
527
+ - `p-6` — sensible padding
528
+ - `w-full max-w-lg` — consistent 512 px width; override per-dialog with `className="max-w-2xl"` etc.
529
+ - `max-h-[90vh] overflow-y-auto` — long forms scroll instead of getting clipped
530
+
531
+ ### `DialogTitle` with icon
532
+
533
+ Plain `<DialogTitle>Create Account</DialogTitle>` lacks visual hierarchy. Standard pattern across production dialogs:
534
+
535
+ ```tsx
536
+ <DialogHeader>
537
+ <DialogTitle>
538
+ <div className="flex items-center gap-2">
539
+ <i className="ri-bank-line text-content-presentation-state-information" />
540
+ <span>Create Account</span>
541
+ </div>
542
+ </DialogTitle>
543
+ </DialogHeader>
544
+ ```
545
+
546
+ Pick an icon that matches the entity (`ri-bank-line` for accounts, `ri-calendar-line` for fiscal periods, `ri-receipt-line` for vouchers, etc.).
547
+
485
548
  ## Common Patterns
486
549
 
487
550
  ### 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
@@ -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
@@ -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",
3
+ "version": "1.0.4",
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",