torch-glare 2.5.4 → 2.5.6
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/BadgeField.tsx +138 -69
- package/apps/lib/components/Button.tsx +10 -2
- package/apps/lib/components/Card.tsx +2 -1
- package/apps/lib/components/ContextMenu.tsx +65 -22
- package/apps/lib/components/DataViews/context.ts +2 -2
- package/apps/lib/components/DataViews/data-views.tsx +20 -8
- package/apps/lib/components/DataViews/filters/filters.tsx +0 -2
- package/apps/lib/components/DataViews/index.ts +8 -4
- package/apps/lib/components/DataViews/slots.ts +9 -0
- package/apps/lib/components/DataViews/states.tsx +43 -8
- package/apps/lib/components/DataViews/views/table-view.tsx +184 -176
- package/apps/lib/components/Drawer.tsx +70 -39
- package/apps/lib/components/DropdownMenu.tsx +79 -22
- package/apps/lib/components/FormBuilder/context.ts +12 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +38 -19
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +31 -8
- package/apps/lib/components/FormBuilder/submit.tsx +21 -1
- package/apps/lib/components/FormBuilder/types.ts +21 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +139 -17
- package/apps/lib/components/FormRenderer/detail.tsx +57 -8
- package/apps/lib/components/FormRenderer/form-renderer.tsx +82 -10
- package/apps/lib/components/FormRenderer/index.ts +2 -0
- package/apps/lib/components/FormRenderer/notch-action.tsx +64 -0
- package/apps/lib/components/FormRenderer/stepper.tsx +56 -2
- package/apps/lib/components/FormRenderer/types.ts +37 -0
- package/apps/lib/components/HeaderBar.tsx +51 -53
- package/apps/lib/components/InputField.tsx +46 -47
- package/apps/lib/components/Popover.tsx +23 -9
- package/apps/lib/components/SearchableSelect.tsx +10 -6
- package/apps/lib/components/SearchableTree.tsx +23 -6
- package/apps/lib/components/SearchableTreeDialog.tsx +11 -1
- package/apps/lib/components/SectionBlock.tsx +24 -3
- package/apps/lib/components/Select.tsx +64 -56
- package/apps/lib/components/SlideDatePicker.tsx +5 -5
- package/apps/lib/components/TabSwitch.tsx +18 -12
- package/apps/lib/components/Table.tsx +15 -28
- package/apps/lib/hooks/useActiveTreeItem.ts +4 -1
- package/apps/lib/hooks/useHtmlDir.ts +31 -0
- package/apps/lib/hooks/useTagSelection.ts +95 -9
- package/apps/lib/layouts/FieldSection.tsx +28 -2
- package/apps/lib/registry.json +20 -5
- package/apps/lib/utils/scroller.ts +26 -0
- package/docs/components/badge-field.md +30 -4
- package/docs/components/context-menu.md +3 -1
- package/docs/components/data-views/examples/filters.md +0 -1
- package/docs/components/data-views/index.md +32 -22
- package/docs/components/data-views/migration.md +7 -5
- package/docs/components/drawer.md +5 -5
- package/docs/components/dropdown-menu.md +3 -0
- package/docs/components/form-builder.md +36 -2
- package/docs/components/form-renderer.md +71 -1
- package/docs/components/header-bar.md +3 -2
- package/docs/components/input-field.md +3 -3
- package/docs/components/section-block.md +6 -0
- package/docs/components/select.md +1 -1
- package/docs/migration/changelog.md +19 -0
- package/docs/reference/hooks.md +23 -0
- package/docs/reference/utilities.md +22 -0
- package/package.json +1 -1
- package/apps/lib/components/DataViews/filters/summary.tsx +0 -65
|
@@ -125,192 +125,200 @@ function TableViewImpl({
|
|
|
125
125
|
)}
|
|
126
126
|
>
|
|
127
127
|
<div className="flex flex-1 flex-col gap-4 overflow-hidden">
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
{/* The scroller and the end action are stacked here with NO gap, so the bar sits flush
|
|
129
|
+
under the table the way it does on the card. `min-h-0` lets the scroller actually
|
|
130
|
+
shrink — a flex item will not go below its content without it. */}
|
|
131
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
132
|
+
<div
|
|
133
|
+
ref={scrollRef}
|
|
134
|
+
// `min-w-0` so the scroller can be narrower than the table inside it. Without it a flex
|
|
135
|
+
// item refuses to shrink below its content, so pinning the table to its natural width
|
|
136
|
+
// pushes the whole component wide instead of scrolling within it.
|
|
137
|
+
className="min-w-0 flex-1 overflow-auto rounded-lg"
|
|
138
|
+
>
|
|
139
|
+
{/* Sizes the table, not the footer — the end action now lives outside this scroller.
|
|
140
|
+
Inside an `overflow-auto` scroller a child's `w-full` resolves against the *visible*
|
|
141
|
+
width, so the table needs `w-max` to reach its natural width and scroll; `min-w-full`
|
|
142
|
+
keeps it filling the scroller when the table is narrower. */}
|
|
143
|
+
<div className="w-max min-w-full">
|
|
144
|
+
{/* `overflow-visible` opts out of the primitive's `overflow-hidden` (tailwind-merge
|
|
145
|
+
lets ours win). It has to: a clipping table is its own scrollport, and the sticky
|
|
146
|
+
header would then resolve against a box that never scrolls. Safe here because the
|
|
147
|
+
scroller above is `min-w-0 overflow-auto`, so a wide table scrolls inside it rather
|
|
148
|
+
than pushing the layout. */}
|
|
149
|
+
<Table ref={tableRef} className="w-full overflow-visible">
|
|
150
|
+
{/* The header sticks to the scroller above by default — that lives on `TableHeader`
|
|
151
|
+
in the primitive. Two things this view adds:
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
Opacity. The primitive's header token is translucent, so scrolling rows read
|
|
154
|
+
straight through it; the primitive can't fix that without naming a surface colour
|
|
155
|
+
it doesn't know. Here the surface *is* known — the view root above sets
|
|
156
|
+
`form-base` — so paint that as the background-color (tailwind-merge drops the
|
|
157
|
+
primitive's translucent one, same `bg-color` group) and re-apply the tint as a
|
|
158
|
+
background-image, which stacks above background-color. Composited that is the
|
|
159
|
+
exact colour the header already had, just no longer see-through.
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
161
|
+
And `shadow-none`, because the drop shadow reads as a seam now that the view
|
|
162
|
+
carries its own border. */}
|
|
163
|
+
<TableHeader className="bg-background-presentation-form-base bg-[image:linear-gradient(var(--background-presentation-form-header),var(--background-presentation-form-header))] shadow-none">
|
|
164
|
+
<TableRow>
|
|
165
|
+
{onRowMove && <TableHead isDummy className="w-8" />}
|
|
166
|
+
{selectable && (
|
|
167
|
+
<TableHead isDummy className="w-12">
|
|
168
|
+
{/* Size is stated on both this and the per-row checkbox rather than left to
|
|
169
|
+
`Checkbox`'s default — they have to agree, and relying on the default on
|
|
170
|
+
one side only is what previously made the select-all bigger than the
|
|
171
|
+
column it heads. */}
|
|
172
|
+
<Checkbox
|
|
173
|
+
size="M"
|
|
174
|
+
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
175
|
+
onCheckedChange={toggleAll}
|
|
176
|
+
aria-label="Select all rows"
|
|
177
|
+
/>
|
|
178
|
+
</TableHead>
|
|
179
|
+
)}
|
|
180
|
+
{/* Keyed by position: two fields may share a `path` — the same value shown two
|
|
181
|
+
ways — and keying on it would collide. */}
|
|
182
|
+
{visibleFields.map((field, i) => (
|
|
183
|
+
<TableHead
|
|
184
|
+
key={`${field.path}-${i}`}
|
|
185
|
+
size="M"
|
|
186
|
+
sortType={sort?.path === field.path ? sort.direction : undefined}
|
|
187
|
+
onSort={() => setSort(cycle(field.path))}
|
|
188
|
+
// Without this every sort control announces itself as a bare "Sort ascending",
|
|
189
|
+
// so a screen reader hears one identical button per column.
|
|
190
|
+
sortLabel={field.label ?? formatPathLabel(field.path)}
|
|
191
|
+
>
|
|
192
|
+
{/* Wrapped rather than handed to `Table` as bare text: `truncate` needs a box
|
|
193
|
+
to clip, and the label's parent in the primitive is already `flex min-w-0`
|
|
194
|
+
so this span shrinks and ellipsises instead of wrapping the header row
|
|
195
|
+
onto a second line. */}
|
|
196
|
+
<span className="truncate">{field.label ?? field.path}</span>
|
|
197
|
+
</TableHead>
|
|
198
|
+
))}
|
|
199
|
+
</TableRow>
|
|
200
|
+
</TableHeader>
|
|
198
201
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
202
|
+
<TableBody>
|
|
203
|
+
{loading && <SkeletonRows fields={visibleFields} grip={Boolean(onRowMove)} selectable={selectable} />}
|
|
204
|
+
{virtualize && padTop > 0 && (
|
|
205
|
+
<tr aria-hidden style={{ height: padTop }} />
|
|
206
|
+
)}
|
|
207
|
+
<DragList ids={ids}>
|
|
208
|
+
{renderIndexes.map((index) => {
|
|
209
|
+
const row = rows[index];
|
|
210
|
+
const id = ids[index];
|
|
211
|
+
const selected = selection.includes(id);
|
|
212
|
+
return (
|
|
213
|
+
<DraggableRow
|
|
214
|
+
key={id}
|
|
215
|
+
id={id}
|
|
216
|
+
draggable={Boolean(onRowMove)}
|
|
217
|
+
state={selected ? "selected" : undefined}
|
|
218
|
+
onClick={onRowClick ? () => onRowClick(row, id) : undefined}
|
|
219
|
+
// A clickable row has to be reachable without a mouse. `<tr>` carries no
|
|
220
|
+
// implicit role, so a bare `onClick` is invisible to the keyboard and to a
|
|
221
|
+
// screen reader; these only appear when the row is actually interactive.
|
|
222
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
223
|
+
role={onRowClick ? "button" : undefined}
|
|
224
|
+
onKeyDown={
|
|
225
|
+
onRowClick
|
|
226
|
+
? (e) => {
|
|
227
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
228
|
+
if (e.target !== e.currentTarget) return;
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
onRowClick(row, id);
|
|
231
|
+
}
|
|
232
|
+
: undefined
|
|
228
233
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
</
|
|
306
|
-
</
|
|
234
|
+
className={cn(
|
|
235
|
+
// 40px per Figma's `Table-RowBackgroand-1.1`. `Table`'s body cells default
|
|
236
|
+
// to 50, and the two live on different elements, so the cells below must be
|
|
237
|
+
// told as well or CSS keeps the larger.
|
|
238
|
+
"h-[40px]",
|
|
239
|
+
"focus-visible:ring-border-presentation-state-focus outline-none focus-visible:ring-2",
|
|
240
|
+
onRowClick && "cursor-pointer",
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{onRowMove && <GripCell />}
|
|
244
|
+
{selectable && (
|
|
245
|
+
<TableCell isDummy className="min-h-[40px] w-12" onClick={(e) => e.stopPropagation()}>
|
|
246
|
+
{/* What `TableCheckbox` renders, inlined: its props are typed as button
|
|
247
|
+
attributes, so it cannot express a controlled checkbox. */}
|
|
248
|
+
<div className="flex items-center justify-center">
|
|
249
|
+
<Checkbox
|
|
250
|
+
size="M"
|
|
251
|
+
checked={selected}
|
|
252
|
+
onCheckedChange={() => toggleRow(id)}
|
|
253
|
+
aria-label="Select row"
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
</TableCell>
|
|
257
|
+
)}
|
|
258
|
+
{visibleFields.map((field, i) => {
|
|
259
|
+
// `undefined` means "you paint it" — distinct from `null`, which is a
|
|
260
|
+
// deliberate blank.
|
|
261
|
+
const custom = renderCell?.({ field, row, id, index, fields: visibleFields });
|
|
262
|
+
// `fade={false}` and clip instead. The fade is a `mask-image` on every
|
|
263
|
+
// cell, close to free on its own but very expensive once a sticky header
|
|
264
|
+
// repaints the region each scroll frame: measured on this page,
|
|
265
|
+
// mask+sticky costs ~61ms/frame against ~29ms with the mask off and
|
|
266
|
+
// ~17ms with neither. `overflow-hidden` is passed back because
|
|
267
|
+
// `fade={false}` otherwise switches the cell to `overflow-visible`, and
|
|
268
|
+
// the text would spill into the next column.
|
|
269
|
+
return (
|
|
270
|
+
<TableCell
|
|
271
|
+
key={`${field.path}-${i}`}
|
|
272
|
+
className="min-h-[40px]"
|
|
273
|
+
fade={false}
|
|
274
|
+
childrenClassName="overflow-hidden"
|
|
275
|
+
>
|
|
276
|
+
{/* `isolate` confines the Badge's mix-blend-luminosity to a local
|
|
277
|
+
stacking context and `transform-gpu` promotes it to its own layer, so
|
|
278
|
+
the table's post-mount column reflow repaints cleanly instead of
|
|
279
|
+
leaving a ghosted badge frame. */}
|
|
280
|
+
<span className="isolate inline-flex transform-gpu">
|
|
281
|
+
{custom === undefined ? <Cell field={field} row={row} /> : custom}
|
|
282
|
+
</span>
|
|
283
|
+
</TableCell>
|
|
284
|
+
);
|
|
285
|
+
})}
|
|
286
|
+
</DraggableRow>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</DragList>
|
|
290
|
+
{virtualize && padBottom > 0 && (
|
|
291
|
+
<tr aria-hidden style={{ height: padBottom }} />
|
|
292
|
+
)}
|
|
293
|
+
{/* The trigger. A row of its own rather than an element after the table, so it sits
|
|
294
|
+
inside the same scroller the rows do. */}
|
|
295
|
+
{hasMore && !loading && (
|
|
296
|
+
<tr ref={sentinelRef as React.Ref<HTMLTableRowElement>} aria-hidden>
|
|
297
|
+
<td style={{ height: 1, padding: 0, border: 0 }} colSpan={100} />
|
|
298
|
+
</tr>
|
|
299
|
+
)}
|
|
300
|
+
{loadingMore && (
|
|
301
|
+
<SkeletonRows
|
|
302
|
+
rows={2}
|
|
303
|
+
fields={visibleFields}
|
|
304
|
+
grip={Boolean(onRowMove)}
|
|
305
|
+
selectable={selectable}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
</TableBody>
|
|
309
|
+
</Table>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
{/* Outside the scroller, deliberately: as a sibling its width is the component's visible
|
|
313
|
+
width, so it spans the full outer container and stays put while the columns scroll
|
|
314
|
+
sideways under it — instead of stretching to the table's scrollable width and sliding
|
|
315
|
+
away like a normal row. Same placement the form Table field uses. */}
|
|
307
316
|
{onAddRow && (
|
|
308
317
|
<TableEndAction onClick={onAddRow}>
|
|
309
318
|
<Plus className="h-4 w-4" aria-hidden />
|
|
310
319
|
{addRowLabel}
|
|
311
320
|
</TableEndAction>
|
|
312
321
|
)}
|
|
313
|
-
</div>
|
|
314
322
|
</div>
|
|
315
323
|
</div>
|
|
316
324
|
</div>
|
|
@@ -40,7 +40,17 @@ interface DrawerContentProps extends React.ComponentPropsWithoutRef<
|
|
|
40
40
|
/** @deprecated No effect here — the drag handle is `DrawerPanel`'s `showHandle`. */
|
|
41
41
|
showHandle?: boolean;
|
|
42
42
|
notch?: React.ReactNode;
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Which INLINE edge the notch attaches to — `"start"` follows the document direction
|
|
45
|
+
* (left under LTR, right under RTL), so callers never compute `dir` themselves.
|
|
46
|
+
*
|
|
47
|
+
* LOCAL PATCH (Contact Center): these were physical `"left" | "right"` and every consumer
|
|
48
|
+
* passed `isRtl ? "right" : "left"`, duplicating a decision CSS already knows. The
|
|
49
|
+
* alignment (`self-start`) and the corner radii below are logical properties, so the
|
|
50
|
+
* browser mirrors them; only the wedge's SVG path needs a flip, which it does itself with
|
|
51
|
+
* `rtl:-scale-x-100`.
|
|
52
|
+
*/
|
|
53
|
+
notchSide?: "start" | "end";
|
|
44
54
|
/**
|
|
45
55
|
* Show the dark "tray" frame (and panel border + inset shadow) around the
|
|
46
56
|
* drawer panel. Defaults to `true`. Set to `false` for bottom-anchored
|
|
@@ -64,7 +74,8 @@ interface DrawerPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/**
|
|
67
|
-
* The
|
|
77
|
+
* The body surface inside a `DrawerContent` — the same surface a page draws, anchored to an
|
|
78
|
+
* edge, so it follows the app theme.
|
|
68
79
|
*
|
|
69
80
|
* It is an ordinary child, not something the tray paints — so a drawer can hold a panel
|
|
70
81
|
* and something else beside it (e.g. a `FormSummary`), each bringing its own background.
|
|
@@ -81,12 +92,6 @@ const DrawerPanel = React.forwardRef<HTMLDivElement, DrawerPanelProps>(
|
|
|
81
92
|
({ className, framed = true, showHandle = false, children, ...props }, ref) => (
|
|
82
93
|
<div
|
|
83
94
|
ref={ref}
|
|
84
|
-
// The content surface is always light (#F0F0F0) regardless of the page
|
|
85
|
-
// theme, so pin a light theme here. This makes theme-aware content tokens
|
|
86
|
-
// (DrawerTitle/Description and any consumer content) resolve to their
|
|
87
|
-
// dark-on-light values instead of following a dark page theme (which
|
|
88
|
-
// would render white-on-light = invisible).
|
|
89
|
-
data-theme="light"
|
|
90
95
|
className={cn(
|
|
91
96
|
// `flex-col` is load-bearing: this panel stacks header / body / footer, and its own
|
|
92
97
|
// `showHandle` centres the drag handle with `mx-auto`, which only centres in a column.
|
|
@@ -98,13 +103,26 @@ const DrawerPanel = React.forwardRef<HTMLDivElement, DrawerPanelProps>(
|
|
|
98
103
|
// main axis, and a flex item's default `min-width: auto` pins it to its content — a wide
|
|
99
104
|
// form pushed the panel straight out past the tray. `min-h-0` alone covered the old
|
|
100
105
|
// column tray; the row needs both.
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
//
|
|
107
|
+
// LOCAL PATCH (Contact Center): the surface was `bg-[#F0F0F0]` with a `#D4D4D4` border —
|
|
108
|
+
// frozen copies of the LIGHT values of the two tokens below — and the panel pinned
|
|
109
|
+
// `data-theme="light"` so its content didn't render white-on-light against them.
|
|
110
|
+
// mapping-color-system-v4 selects on a BARE `[data-theme="light"]`, so that attribute
|
|
111
|
+
// re-declared every colour variable on this element and inherited to the whole subtree:
|
|
112
|
+
// a drawer was immune to the app's theme, and its content was correctly themed to the
|
|
113
|
+
// WRONG theme. A drawer is the same surface as a page, only anchored to an edge, so it
|
|
114
|
+
// reads the same tokens and follows `<html>` like everything else. Light and default
|
|
115
|
+
// resolve to the exact literals removed here (#F0F0F0 / #D4D4D4), so only dark changes.
|
|
116
|
+
"flex flex-1 flex-col gap-2 rounded-t-[16px] p-1 bg-background-presentation-body-primary min-h-0 min-w-0",
|
|
117
|
+
framed &&
|
|
118
|
+
"border border-border-presentation-global-primary shadow-[inset_0_-4px_16px_rgba(0,0,0,0.1)]",
|
|
103
119
|
className,
|
|
104
120
|
)}
|
|
105
121
|
{...props}
|
|
106
122
|
>
|
|
107
|
-
{showHandle &&
|
|
123
|
+
{showHandle && (
|
|
124
|
+
<div className="mx-auto h-2 w-[100px] rounded-full bg-border-presentation-global-primary" />
|
|
125
|
+
)}
|
|
108
126
|
{children}
|
|
109
127
|
</div>
|
|
110
128
|
),
|
|
@@ -120,7 +138,7 @@ const DrawerContent = React.forwardRef<
|
|
|
120
138
|
className,
|
|
121
139
|
children,
|
|
122
140
|
notch,
|
|
123
|
-
notchSide = "
|
|
141
|
+
notchSide = "start",
|
|
124
142
|
framed: framedProp,
|
|
125
143
|
wrapperClassName,
|
|
126
144
|
trayClassName,
|
|
@@ -146,10 +164,12 @@ const DrawerContent = React.forwardRef<
|
|
|
146
164
|
)}
|
|
147
165
|
{...props}
|
|
148
166
|
>
|
|
167
|
+
{/* `self-start` / `self-end` are LOGICAL — the browser flips them under
|
|
168
|
+
`dir="rtl"`, so this needs no direction check. */}
|
|
149
169
|
{notch && (
|
|
150
|
-
<div className={notchSide === "
|
|
170
|
+
<div className={notchSide === "end" ? "self-end" : "self-start"}>
|
|
151
171
|
{React.isValidElement(notch)
|
|
152
|
-
? React.cloneElement(notch as React.ReactElement<{ side?: "
|
|
172
|
+
? React.cloneElement(notch as React.ReactElement<{ side?: "start" | "end" }>, {
|
|
153
173
|
side: notchSide,
|
|
154
174
|
})
|
|
155
175
|
: notch}
|
|
@@ -164,10 +184,12 @@ const DrawerContent = React.forwardRef<
|
|
|
164
184
|
framed
|
|
165
185
|
? "p-1.5 bg-black-400 shadow-[0_0_4px_rgba(0,0,0,0.2),0_0_30px_rgba(0,0,0,0.4)]"
|
|
166
186
|
: "p-0",
|
|
187
|
+
// Logical corner radii (`ss` = start-start, `se` = start-end): the squared
|
|
188
|
+
// corner follows the notch under either direction with no JS.
|
|
167
189
|
framed && notch
|
|
168
|
-
? notchSide === "
|
|
169
|
-
? "rounded-
|
|
170
|
-
: "rounded-
|
|
190
|
+
? notchSide === "end"
|
|
191
|
+
? "rounded-se-none rounded-ss-[22px] rounded-b-[22px]"
|
|
192
|
+
: "rounded-ss-none rounded-se-[22px] rounded-b-[22px]"
|
|
171
193
|
: framed
|
|
172
194
|
? "rounded-t-[22px]"
|
|
173
195
|
: "",
|
|
@@ -193,14 +215,15 @@ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
|
|
193
215
|
DrawerHeader.displayName = "DrawerHeader";
|
|
194
216
|
|
|
195
217
|
const drawerHeaderPane = cva(
|
|
196
|
-
//
|
|
197
|
-
// (
|
|
218
|
+
// A deliberately dark pill in every theme — the same slab the page-mode FormHeaderBar draws
|
|
219
|
+
// (FormRenderer/header.tsx). Its fill is a literal on purpose, not a frozen token: it must not
|
|
220
|
+
// follow the panel. So the title/description are forced to light text against it.
|
|
198
221
|
"flex items-center gap-2 rounded-[14px] border p-2 bg-[#131415] border-[#2C2D2E] shadow-[0_0_32px_2px_rgba(0,0,0,0.05)] [&_[data-slot=drawer-title]]:text-white [&_[data-slot=drawer-description]]:text-[#9FA0A1]",
|
|
199
222
|
);
|
|
200
223
|
|
|
201
224
|
const DrawerHeaderTitle = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
202
|
-
//
|
|
203
|
-
//
|
|
225
|
+
// The pane is dark in every theme, so pin dark here: the Buttons and content inside must
|
|
226
|
+
// resolve against THIS slab, not against the panel (which now follows the app theme).
|
|
204
227
|
<div data-theme="dark" className={cn(drawerHeaderPane(), className)} {...props} />
|
|
205
228
|
);
|
|
206
229
|
DrawerHeaderTitle.displayName = "DrawerHeaderTitle";
|
|
@@ -243,40 +266,48 @@ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
|
|
243
266
|
DrawerFooter.displayName = "DrawerFooter";
|
|
244
267
|
|
|
245
268
|
interface DrawerNotchProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
246
|
-
|
|
269
|
+
/** Logical edge, mirrored by the browser under `dir="rtl"`. See `notchSide`. */
|
|
270
|
+
side?: "start" | "end";
|
|
247
271
|
}
|
|
248
272
|
|
|
249
|
-
const DrawerNotch = ({ className, children, side = "
|
|
250
|
-
// Wedge bridges the notch's bottom-
|
|
251
|
-
//
|
|
252
|
-
//
|
|
273
|
+
const DrawerNotch = ({ className, children, side = "start", ...props }: DrawerNotchProps) => {
|
|
274
|
+
// Wedge bridges the notch's bottom inline-end corner into the tray's top edge.
|
|
275
|
+
//
|
|
276
|
+
// LOCAL PATCH (Contact Center): both the DOM order and the SVG path used to be picked from a
|
|
277
|
+
// physical left/right. Neither needs to be: the row is `flex-row`, which is direction-aware, so
|
|
278
|
+
// writing {pill}{wedge} already renders wedge-on-the-left under RTL. Only the path's curve is
|
|
279
|
+
// physical, and `rtl:-scale-x-100` mirrors it.
|
|
280
|
+
//
|
|
281
|
+
// That class, not a rule in some stylesheet: an earlier draft of this comment pointed at an
|
|
282
|
+
// `rtl.css` that does not exist in this repo — so under `dir="rtl"` the wedge pointed the wrong
|
|
283
|
+
// way and nothing failed loudly. Keeping the mirror on the element means it travels with the
|
|
284
|
+
// component when a consumer copies it in, which a global stylesheet would not.
|
|
253
285
|
const wedge = (
|
|
254
|
-
<svg
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
/>
|
|
286
|
+
<svg
|
|
287
|
+
aria-hidden
|
|
288
|
+
data-drawer-wedge
|
|
289
|
+
width="12"
|
|
290
|
+
height="12"
|
|
291
|
+
viewBox="0 0 12 12"
|
|
292
|
+
className="block shrink-0 self-end rtl:-scale-x-100"
|
|
293
|
+
>
|
|
294
|
+
<path d="M 0 0 L 0 12 L 12 12 A 12 12 0 0 1 0 0 Z" fill="#434446" />
|
|
263
295
|
</svg>
|
|
264
296
|
);
|
|
265
297
|
|
|
266
298
|
return (
|
|
267
299
|
<div className="relative flex flex-row items-end">
|
|
268
|
-
{side === "
|
|
300
|
+
{side === "end" && wedge}
|
|
269
301
|
<div
|
|
270
302
|
className={cn(
|
|
271
|
-
"flex items-center gap-1 rounded-t-[18px] bg-black-400 px-1.5 pt-1.5 pb-1.5",
|
|
272
|
-
side === "right" ? "flex-row-reverse" : "flex-row",
|
|
303
|
+
"flex flex-row items-center gap-1 rounded-t-[18px] bg-black-400 px-1.5 pt-1.5 pb-1.5",
|
|
273
304
|
className,
|
|
274
305
|
)}
|
|
275
306
|
{...props}
|
|
276
307
|
>
|
|
277
308
|
{children}
|
|
278
309
|
</div>
|
|
279
|
-
{side === "
|
|
310
|
+
{side === "start" && wedge}
|
|
280
311
|
</div>
|
|
281
312
|
);
|
|
282
313
|
};
|