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