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.
@@ -0,0 +1,275 @@
1
+ ---
2
+ name: SectionBlock
3
+ title: SectionBlock
4
+ description: Sectioned card container with a colored title badge for grouping related content like forms, tables, or lists.
5
+ category: layout
6
+ group: Layout & Containers
7
+ tags: [layout, card, section, container, form, group]
8
+ status: stable
9
+ version: 1.1.22
10
+ dependencies:
11
+ - "class-variance-authority": "^0.7.0"
12
+ ---
13
+
14
+ # SectionBlock
15
+
16
+ > A card container with an optional colored title badge. Use it to group related content — custom field forms, tables, settings groups — under a clear, color-coded heading.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npx torch-glare add SectionBlock
22
+ ```
23
+
24
+ ## Import
25
+
26
+ ```typescript
27
+ import { SectionBlock, type SectionColor } from "@/components/SectionBlock";
28
+ ```
29
+
30
+ ## Basic Usage
31
+
32
+ ```tsx
33
+ import { SectionBlock } from "@/components/SectionBlock";
34
+
35
+ export function Example() {
36
+ return (
37
+ <SectionBlock color="Blue" title="Project details">
38
+ <p className="text-content-presentation-action-light-primary py-4">
39
+ Card body content goes here.
40
+ </p>
41
+ </SectionBlock>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## API Reference
47
+
48
+ ### Props
49
+
50
+ | Prop | Type | Default | Description |
51
+ |------|------|---------|-------------|
52
+ | `color` | `SectionColor` | `"Blue"` | Color of the title badge. One of `Blue`, `Yellow`, `Green`, `Red`, `Orange`, `Purple`, `Pink`, `Gray`. |
53
+ | `title` | `ReactNode` | — | Title rendered inside the colored badge. Optional — when omitted, the header is hidden entirely. Accepts any ReactNode (string, JSX with icons, links, etc.). |
54
+ | `containerClassName` | `string` | — | Class name applied to the outer container (alongside `className`). |
55
+ | `headerClassName` | `string` | — | Class name applied to the header wrapper around the title badge. |
56
+ | `bodyClassName` | `string` | — | Class name applied to the body wrapper holding `children`. |
57
+ | `className` | `string` | — | Standard React class name on the outer container. |
58
+ | `children` | `ReactNode` | — | Body content. |
59
+ | `...rest` | `HTMLAttributes<HTMLDivElement>` | — | All standard div props except `title` (which is overridden). |
60
+
61
+ ### TypeScript
62
+
63
+ ```typescript
64
+ export type SectionColor =
65
+ | "Blue"
66
+ | "Yellow"
67
+ | "Green"
68
+ | "Red"
69
+ | "Orange"
70
+ | "Purple"
71
+ | "Pink"
72
+ | "Gray";
73
+
74
+ export interface SectionBlockProps
75
+ extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
76
+ color?: SectionColor;
77
+ title?: ReactNode;
78
+ containerClassName?: string;
79
+ headerClassName?: string;
80
+ bodyClassName?: string;
81
+ }
82
+ ```
83
+
84
+ The component is a `forwardRef<HTMLDivElement, SectionBlockProps>`.
85
+
86
+ ## Examples
87
+
88
+ ### All Colors
89
+
90
+ ```tsx
91
+ import { SectionBlock, type SectionColor } from "@/components/SectionBlock";
92
+
93
+ const COLORS: SectionColor[] = [
94
+ "Blue",
95
+ "Yellow",
96
+ "Green",
97
+ "Red",
98
+ "Orange",
99
+ "Purple",
100
+ "Pink",
101
+ "Gray",
102
+ ];
103
+
104
+ export function AllColors() {
105
+ return (
106
+ <div className="flex flex-col gap-[24px]">
107
+ {COLORS.map((color) => (
108
+ <SectionBlock key={color} color={color} title={`${color} section`}>
109
+ <p className="text-content-presentation-action-light-primary py-4">
110
+ This is a {color.toLowerCase()} sectioned card.
111
+ </p>
112
+ </SectionBlock>
113
+ ))}
114
+ </div>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ### No Title
120
+
121
+ When `title` is omitted, the header is hidden but the body padding remains.
122
+
123
+ ```tsx
124
+ <SectionBlock>
125
+ <p className="text-content-presentation-action-light-primary py-4">
126
+ A SectionBlock without a title — header is hidden, body still padded.
127
+ </p>
128
+ </SectionBlock>
129
+ ```
130
+
131
+ ### Rich Title with Icon
132
+
133
+ `title` accepts any ReactNode, so you can pass JSX with icons, counts, links, etc.
134
+
135
+ ```tsx
136
+ <SectionBlock
137
+ color="Purple"
138
+ title={
139
+ <span className="flex items-center gap-[6px]">
140
+ <i className="ri-magic-line" />
141
+ Rich title with icon
142
+ </span>
143
+ }
144
+ >
145
+ <p className="text-content-presentation-action-light-primary py-4">
146
+ The title prop is fully customizable.
147
+ </p>
148
+ </SectionBlock>
149
+ ```
150
+
151
+ ### Custom Fields Form
152
+
153
+ A common pattern: pair `SectionBlock` with a row layout to build labeled-field forms.
154
+
155
+ ```tsx
156
+ import { type ReactNode } from "react";
157
+ import { SectionBlock } from "@/components/SectionBlock";
158
+ import { InputField } from "@/components/InputField";
159
+ import { ActionButton } from "@/components/ActionButton";
160
+
161
+ export function CustomFieldsForm() {
162
+ return (
163
+ <SectionBlock
164
+ color="Blue"
165
+ title={
166
+ <span className="flex items-center gap-[6px]">
167
+ <i className="ri-edit-box-line" />
168
+ Custom fields
169
+ </span>
170
+ }
171
+ >
172
+ <FieldRow
173
+ label="Name"
174
+ required
175
+ right={
176
+ <div className="flex flex-1 items-center gap-[12px]">
177
+ <InputField placeholder="First Name*" className="flex-1" />
178
+ <InputField placeholder="Last Name*" className="flex-1" />
179
+ </div>
180
+ }
181
+ />
182
+
183
+ <RowDivider />
184
+
185
+ <FieldRow
186
+ label="Department"
187
+ right={<InputField placeholder="Write Hint Here" className="flex-1" />}
188
+ />
189
+
190
+ <RowDivider />
191
+
192
+ <FieldRow
193
+ label="Alias names"
194
+ right={
195
+ <InputField
196
+ placeholder="Write Hint Here"
197
+ className="flex-1"
198
+ childrenSide={
199
+ <ActionButton aria-label="Add alias name">
200
+ <i className="ri-add-line" />
201
+ </ActionButton>
202
+ }
203
+ />
204
+ }
205
+ />
206
+ </SectionBlock>
207
+ );
208
+ }
209
+
210
+ interface FieldRowProps {
211
+ label: string;
212
+ required?: boolean;
213
+ right: ReactNode;
214
+ }
215
+
216
+ function FieldRow({ label, required, right }: FieldRowProps) {
217
+ return (
218
+ <div className="flex items-center gap-[24px] py-[18px]">
219
+ <div className="flex w-[220px] shrink-0 items-center gap-[6px]">
220
+ <span className="typography-body-medium-regular text-content-presentation-action-light-primary">
221
+ {label}
222
+ </span>
223
+ {required && (
224
+ <span className="typography-body-small-medium text-content-presentation-state-negative">
225
+ (Required)
226
+ </span>
227
+ )}
228
+ </div>
229
+ <div className="flex flex-1 items-center">{right}</div>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function RowDivider() {
235
+ return <div className="h-px w-full bg-border-presentation-global-primary" />;
236
+ }
237
+ ```
238
+
239
+ ### Custom Layout (override defaults)
240
+
241
+ Use `containerClassName`, `headerClassName`, and `bodyClassName` to override the built-in spacing and width without losing the title/body structure.
242
+
243
+ ```tsx
244
+ <SectionBlock
245
+ color="Green"
246
+ title="Compact card"
247
+ containerClassName="w-[600px] pt-[4px] pb-[16px]"
248
+ bodyClassName="px-[24px]"
249
+ >
250
+ <p className="text-content-presentation-action-light-primary py-2">
251
+ Tighter, narrower variant.
252
+ </p>
253
+ </SectionBlock>
254
+ ```
255
+
256
+ ## Patterns
257
+
258
+ - **Color coding**: Use distinct colors to help users scan a page of multiple sections (e.g., Blue for primary forms, Yellow for warnings, Red for destructive zones).
259
+ - **No-title sections**: Drop the `title` prop entirely when the section's purpose is obvious from context — keeps the body padding without the visual weight of a header.
260
+ - **Composing with form fields**: `SectionBlock` does not impose any inner layout — pair it with helpers like the `FieldRow` pattern above, or with `InputField`, `Form`, or `FieldSection` for more structured forms.
261
+ - **Default width**: The component ships with `w-[1100px]`. Override via `containerClassName="w-full"` (or any specific width) for narrower containers.
262
+
263
+ ## Accessibility
264
+
265
+ - The container is a plain `<div>`. If the section represents a distinct landmark, add `role="region"` and an `aria-label` (or use `aria-labelledby` pointing at the title).
266
+ - The title is rendered as styled text inside a badge `<div>`, not as a heading element. If the section needs a heading semantically, wrap your `title` content in an appropriate `<h2>`/`<h3>` and reset its margin via Tailwind classes.
267
+
268
+ ## Troubleshooting
269
+
270
+ | Issue | Fix |
271
+ |-------|-----|
272
+ | Card overflows on small screens | Default width is `w-[1100px]`. Override with `containerClassName="w-full"` or a smaller fixed width. |
273
+ | Title not showing | The `title` prop is optional — omitting it hides the entire header. Pass any non-null `ReactNode` to render it. |
274
+ | Badge color looks wrong | `color` only accepts the predefined `SectionColor` values. For custom badge colors, override via `headerClassName` and a custom child node. |
275
+ | Need a different background | The body uses `bg-background-presentation-form-base`. Override via `containerClassName` (your class wins via `cn()` merging). |
@@ -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
@@ -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