torch-glare 2.4.4 → 2.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/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
- package/apps/lib/components/DataViews/cell.tsx +324 -0
- package/apps/lib/components/DataViews/context.ts +144 -0
- package/apps/lib/components/DataViews/data-views.tsx +383 -0
- package/apps/lib/components/DataViews/filters/children.tsx +98 -0
- package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
- package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
- package/apps/lib/components/DataViews/filters/index.ts +4 -0
- package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
- package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
- package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
- package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
- package/apps/lib/components/DataViews/filters/values.ts +173 -0
- package/apps/lib/components/DataViews/header.tsx +217 -0
- package/apps/lib/components/DataViews/hooks/index.ts +5 -0
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
- package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
- package/apps/lib/components/DataViews/index.ts +74 -26
- package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
- package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
- package/apps/lib/components/DataViews/panel/index.ts +3 -0
- package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
- package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
- package/apps/lib/components/DataViews/panel/section.tsx +79 -0
- package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
- package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
- package/apps/lib/components/DataViews/slots.ts +63 -0
- package/apps/lib/components/DataViews/states.tsx +38 -0
- package/apps/lib/components/DataViews/types.ts +485 -178
- package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
- package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
- package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
- package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
- package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
- package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
- package/apps/lib/components/FormBuilder/context.ts +20 -6
- package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
- package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
- package/apps/lib/components/FormBuilder/index.ts +3 -1
- package/apps/lib/components/FormBuilder/types.ts +40 -0
- package/apps/lib/components/Input.tsx +3 -0
- package/apps/lib/components/SearchableTable.tsx +5 -4
- package/apps/lib/components/SectionBlock.tsx +58 -11
- package/apps/lib/components/Select.tsx +3 -1
- package/apps/lib/components/TabSwitch.tsx +16 -4
- package/apps/lib/components/Table.tsx +265 -67
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
- package/apps/lib/components/TreeFolder/index.ts +1 -1
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
- package/apps/lib/hooks/useDragDrop.tsx +365 -0
- package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
- package/apps/lib/registry.json +159 -4
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/apps/lib/utils/dataViews/path.ts +67 -0
- package/apps/lib/utils/dataViews/query.ts +73 -0
- package/apps/lib/utils/dataViews/types.ts +187 -0
- package/docs/components/breadcrumb.md +1 -1
- package/docs/components/button-group.md +1 -1
- package/docs/components/button.md +1 -1
- package/docs/components/card.md +1 -1
- package/docs/components/checkbox.md +1 -1
- package/docs/components/data-views/backend-response.md +324 -0
- package/docs/components/data-views/examples/a11y-rtl.md +250 -0
- package/docs/components/data-views/examples/api-orders-route.md +130 -0
- package/docs/components/data-views/examples/fields.md +362 -0
- package/docs/components/data-views/examples/filters.md +308 -0
- package/docs/components/data-views/examples/inbox-routing.md +218 -0
- package/docs/components/data-views/examples/index.md +29 -0
- package/docs/components/data-views/examples/overview.md +244 -0
- package/docs/components/data-views/examples/panel.md +212 -0
- package/docs/components/data-views/examples/scale.md +231 -0
- package/docs/components/data-views/examples/server-side.md +210 -0
- package/docs/components/data-views/examples/state.md +250 -0
- package/docs/components/data-views/examples/tree-custom.md +388 -0
- package/docs/components/data-views/examples/view-registry.md +313 -0
- package/docs/components/data-views/examples/views.md +534 -0
- package/docs/components/data-views/guide.md +405 -0
- package/docs/components/data-views/index.md +1504 -0
- package/docs/components/data-views/migration.md +79 -0
- package/docs/components/date-picker.md +0 -1
- package/docs/components/form-builder.md +19 -8
- package/docs/components/form-renderer.md +2 -1
- package/docs/components/form.md +1 -1
- package/docs/components/input-field.md +1 -1
- package/docs/components/input-otp.md +1 -1
- package/docs/components/input.md +1 -1
- package/docs/components/labeled-check-box.md +1 -1
- package/docs/components/labeled-radio.md +1 -1
- package/docs/components/radio-card.md +1 -1
- package/docs/components/radio.md +1 -1
- package/docs/components/search-field.md +1 -1
- package/docs/components/section-block.md +79 -3
- package/docs/components/select.md +1 -1
- package/docs/components/simple-select.md +1 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/tab-switch.md +1 -1
- package/docs/components/table.md +45 -8
- package/docs/components/text-editor.md +1 -1
- package/docs/components/textarea.md +1 -1
- package/docs/components/toggle-button.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/components/tree-folder.md +110 -0
- package/docs/how-to/forms-with-form-builder.md +6 -4
- package/docs/reference/components.md +16 -6
- package/docs/tutorials/component-composition.md +11 -13
- package/package.json +3 -2
- package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
- package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
- package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
- package/apps/lib/components/DataViews/InboxView.tsx +0 -463
- package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
- package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
- package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
- package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
- package/apps/lib/components/DataViews/TableView.tsx +0 -212
- package/apps/lib/components/DataViews/TreeView.tsx +0 -364
- package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
- package/apps/lib/hooks/useDataViewsState.ts +0 -175
- package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
- package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
- package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
- package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
- package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
- package/docs/components/data-views-config-panel.md +0 -208
- package/docs/components/data-views-layout.md +0 -291
- package/docs/components/inbox-view.md +0 -170
- package/docs/components/kanban-view.md +0 -135
- package/docs/components/table-view.md +0 -141
- package/docs/components/tree-view.md +0 -147
- package/docs/how-to/data-views-from-backend-response.md +0 -194
|
@@ -21,7 +21,16 @@ import {
|
|
|
21
21
|
|
|
22
22
|
import { cn } from "../../../utils/cn";
|
|
23
23
|
import { SectionBlock } from "../../SectionBlock";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
Table,
|
|
26
|
+
TableBody,
|
|
27
|
+
TableCell,
|
|
28
|
+
TableEndAction,
|
|
29
|
+
TableHead,
|
|
30
|
+
TableHeader,
|
|
31
|
+
TableRow,
|
|
32
|
+
TableScroller,
|
|
33
|
+
} from "../../Table";
|
|
25
34
|
import { Checkbox } from "../../Checkbox";
|
|
26
35
|
import { Button } from "../../Button";
|
|
27
36
|
import { CellContext, useLoading } from "../context";
|
|
@@ -50,6 +59,8 @@ export function TableField(props: TableFieldProps) {
|
|
|
50
59
|
|
|
51
60
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
52
61
|
const [sort, setSort] = useState<{ key: string; dir: "asc" | "desc" } | null>(null);
|
|
62
|
+
// Drag-resized column widths, keyed by column index; falls back to `column.width`.
|
|
63
|
+
const [resized, setResized] = useState<Record<number, number>>({});
|
|
53
64
|
|
|
54
65
|
const sensors = useSensors(
|
|
55
66
|
useSensor(PointerSensor),
|
|
@@ -112,6 +123,25 @@ export function TableField(props: TableFieldProps) {
|
|
|
112
123
|
|
|
113
124
|
const totalCols = columns.length + (showHandle ? 1 : 0) + (showSelect ? 1 : 0);
|
|
114
125
|
|
|
126
|
+
// Under the browser's default `table-layout: auto`, a column's `width` is only a hint —
|
|
127
|
+
// content (an input's intrinsic size, a currency prefix) silently widens it, which is why
|
|
128
|
+
// a `width: 110` column used to render at 200-odd px. When every column declares a width
|
|
129
|
+
// we can switch to fixed layout and have them honoured exactly. Mixed/absent widths keep
|
|
130
|
+
// auto layout, since under fixed layout an unsized column would collapse to nothing.
|
|
131
|
+
//
|
|
132
|
+
// Fixed layout also needs a *definite* table width — with `width: auto` the browser sizes
|
|
133
|
+
// the table from content and then redistributes, throwing the declared widths (and any
|
|
134
|
+
// drag-resize) away. So the widths are owned here and totalled onto the table, which is
|
|
135
|
+
// what lets a drag grow the table instead of stealing space from the next column.
|
|
136
|
+
const DUMMY_COL_WIDTH = 40; // the design's onset / selector columns
|
|
137
|
+
const colWidth = (col: TableColumn, ci: number) => resized[ci] ?? col.width;
|
|
138
|
+
const fixedLayout = columns.length > 0 && columns.every((col, ci) => !!colWidth(col, ci));
|
|
139
|
+
const tableWidth = fixedLayout
|
|
140
|
+
? columns.reduce((sum, col, ci) => sum + (colWidth(col, ci) ?? 0), 0) +
|
|
141
|
+
(showHandle ? DUMMY_COL_WIDTH : 0) +
|
|
142
|
+
(showSelect ? DUMMY_COL_WIDTH : 0)
|
|
143
|
+
: undefined;
|
|
144
|
+
|
|
115
145
|
const rowCells = (rowName: string, index: number) => (
|
|
116
146
|
<>
|
|
117
147
|
{showSelect && (
|
|
@@ -126,25 +156,35 @@ export function TableField(props: TableFieldProps) {
|
|
|
126
156
|
{columns.map((col, ci) => (
|
|
127
157
|
<TableCell
|
|
128
158
|
key={ci}
|
|
129
|
-
style={col
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
style={colWidth(col, ci) ? { width: colWidth(col, ci) } : undefined}
|
|
160
|
+
// Column widths come from `col.width`, so opt out of TableCell's 200px floor —
|
|
161
|
+
// otherwise a narrow column (e.g. `width: 110`) silently renders at 200px.
|
|
162
|
+
minWidth={0}
|
|
163
|
+
// Every cell here holds a form control, and the right-edge fade washes out whatever
|
|
164
|
+
// sits at its end — a Select's chevron, a date picker's button.
|
|
165
|
+
fade={false}
|
|
166
|
+
childrenClassName={cn("min-w-0 w-full", col.align && ALIGN[col.align])}
|
|
134
167
|
>
|
|
135
|
-
<CellContext.Provider value=
|
|
168
|
+
<CellContext.Provider value="table">{col.cell(rowName, index)}</CellContext.Provider>
|
|
136
169
|
</TableCell>
|
|
137
170
|
))}
|
|
138
171
|
</>
|
|
139
172
|
);
|
|
140
173
|
|
|
141
174
|
const table = (
|
|
142
|
-
|
|
175
|
+
// `min-w-full` so the grid still spans the card when the columns are narrower than it,
|
|
176
|
+
// while staying free to overflow (and scroll) when they aren't.
|
|
177
|
+
<Table
|
|
178
|
+
className={cn("min-w-full", fixedLayout && "[table-layout:fixed]")}
|
|
179
|
+
style={tableWidth ? { width: tableWidth } : undefined}
|
|
180
|
+
>
|
|
143
181
|
<TableHeader>
|
|
144
182
|
<TableRow>
|
|
145
|
-
{
|
|
183
|
+
{/* Fixed 40px, per the design — and so these unsized columns can't absorb the
|
|
184
|
+
table's slack width and push the data columns off-screen. */}
|
|
185
|
+
{showHandle && <TableHead isDummy style={{ width: DUMMY_COL_WIDTH }} />}
|
|
146
186
|
{showSelect && (
|
|
147
|
-
<TableHead isDummy>
|
|
187
|
+
<TableHead isDummy style={{ width: DUMMY_COL_WIDTH }}>
|
|
148
188
|
<Checkbox
|
|
149
189
|
size="S"
|
|
150
190
|
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
@@ -155,7 +195,11 @@ export function TableField(props: TableFieldProps) {
|
|
|
155
195
|
{columns.map((col, ci) => (
|
|
156
196
|
<TableHead
|
|
157
197
|
key={ci}
|
|
198
|
+
// Width goes on the <th> too, so header and body columns agree.
|
|
199
|
+
style={colWidth(col, ci) ? { width: colWidth(col, ci) } : undefined}
|
|
200
|
+
onResize={(w) => setResized((prev) => ({ ...prev, [ci]: w }))}
|
|
158
201
|
sortType={col.sortKey && sort?.key === col.sortKey ? sort.dir : undefined}
|
|
202
|
+
sortLabel={typeof col.header === "string" ? col.header : undefined}
|
|
159
203
|
onSort={col.sortKey ? () => applySort(col.sortKey as string) : undefined}
|
|
160
204
|
>
|
|
161
205
|
{col.header}
|
|
@@ -183,58 +227,40 @@ export function TableField(props: TableFieldProps) {
|
|
|
183
227
|
|
|
184
228
|
{fields.length === 0 && (
|
|
185
229
|
<TableRow>
|
|
186
|
-
<TableCell colSpan={totalCols}>
|
|
230
|
+
<TableCell colSpan={totalCols} minWidth={0}>
|
|
187
231
|
<span className="typography-body-small-regular text-content-presentation-global-secondary">
|
|
188
232
|
No rows yet.
|
|
189
233
|
</span>
|
|
190
234
|
</TableCell>
|
|
191
235
|
</TableRow>
|
|
192
236
|
)}
|
|
193
|
-
|
|
194
|
-
<TableRow className="h-[40px]">
|
|
195
|
-
<TableCell
|
|
196
|
-
colSpan={totalCols}
|
|
197
|
-
className="border-t-2 border-transparent hover:border-border-presentation-table-action-hover hover:bg-background-presentation-table-acton-hover"
|
|
198
|
-
>
|
|
199
|
-
<button
|
|
200
|
-
type="button"
|
|
201
|
-
disabled={loading}
|
|
202
|
-
onClick={() => append((props.defaultItem ?? {}) as never)}
|
|
203
|
-
className="flex w-full items-center justify-start gap-2 typography-body-medium-semibold text-content-presentation-action-light-primary [&_i]:text-[20px]"
|
|
204
|
-
>
|
|
205
|
-
<i className="ri-add-line" />
|
|
206
|
-
{props.addLabel ?? "Add New"}
|
|
207
|
-
</button>
|
|
208
|
-
</TableCell>
|
|
209
|
-
</TableRow>
|
|
210
237
|
</TableBody>
|
|
211
238
|
</Table>
|
|
212
239
|
);
|
|
213
240
|
|
|
214
241
|
// Header actions on the right of the SectionBlock title row: a "Delete Row" that's disabled
|
|
215
|
-
// until rows are selected, and a primary "Add New".
|
|
242
|
+
// until rows are selected, and a primary "Add New". They live outside the scroll container,
|
|
243
|
+
// so they stay put while the grid scrolls sideways.
|
|
216
244
|
const headerActions = (
|
|
217
245
|
<>
|
|
218
246
|
{selectable && (
|
|
219
247
|
<Button
|
|
220
248
|
type="button"
|
|
221
|
-
size="
|
|
249
|
+
size="M"
|
|
222
250
|
variant="BorderStyle"
|
|
223
251
|
disabled={loading || selected.size === 0}
|
|
224
252
|
onClick={deleteSelected}
|
|
225
253
|
>
|
|
226
|
-
<i className="ri-delete-bin-line" />
|
|
227
254
|
Delete Row
|
|
228
255
|
</Button>
|
|
229
256
|
)}
|
|
230
257
|
<Button
|
|
231
258
|
type="button"
|
|
232
|
-
size="
|
|
259
|
+
size="M"
|
|
233
260
|
variant="BluColStyle"
|
|
234
261
|
disabled={loading}
|
|
235
262
|
onClick={() => append((props.defaultItem ?? {}) as never)}
|
|
236
263
|
>
|
|
237
|
-
<i className="ri-add-line" />
|
|
238
264
|
{props.addLabel ?? "Add New"}
|
|
239
265
|
</Button>
|
|
240
266
|
</>
|
|
@@ -242,32 +268,34 @@ export function TableField(props: TableFieldProps) {
|
|
|
242
268
|
|
|
243
269
|
return (
|
|
244
270
|
<SectionBlock
|
|
271
|
+
variant="Table"
|
|
245
272
|
title={props.title}
|
|
246
273
|
color={props.color}
|
|
247
274
|
icon={props.icon}
|
|
248
275
|
action={headerActions}
|
|
249
|
-
bodyClassName="px-[16px]"
|
|
250
|
-
// The bottom "Add New" footer is the last element, so drop SectionBlock's default
|
|
251
|
-
// bottom padding to keep the table flush with the card edge (matches the Figma).
|
|
252
|
-
containerClassName="pb-0"
|
|
253
276
|
>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
277
|
+
{props.description && (
|
|
278
|
+
<p className="px-[16px] pt-[12px] typography-body-small-regular text-content-presentation-global-secondary">
|
|
279
|
+
{props.description}
|
|
280
|
+
</p>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* The table is the only horizontally scrolling element … */}
|
|
284
|
+
<TableScroller>
|
|
285
|
+
{showHandle ? (
|
|
286
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
287
|
+
{table}
|
|
288
|
+
</DndContext>
|
|
289
|
+
) : (
|
|
290
|
+
table
|
|
259
291
|
)}
|
|
292
|
+
</TableScroller>
|
|
260
293
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
) : (
|
|
267
|
-
table
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
294
|
+
{/* … so this stays visible no matter how far right the columns are scrolled. */}
|
|
295
|
+
<TableEndAction disabled={loading} onClick={() => append((props.defaultItem ?? {}) as never)}>
|
|
296
|
+
<i className="ri-add-line" />
|
|
297
|
+
{props.addLabel ?? "Add New"}
|
|
298
|
+
</TableEndAction>
|
|
271
299
|
</SectionBlock>
|
|
272
300
|
);
|
|
273
301
|
}
|
|
@@ -6,7 +6,7 @@ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
|
|
|
6
6
|
import { InputField } from "../../InputField";
|
|
7
7
|
import { Textarea } from "../../Textarea";
|
|
8
8
|
import { PasswordLevel } from "../../PasswordLevel";
|
|
9
|
-
import { useLoading,
|
|
9
|
+
import { useLoading, useOnTable } from "../context";
|
|
10
10
|
import { formatNumber } from "../numberFormat";
|
|
11
11
|
import type { BaseFieldProps, CurrencyFieldProps, PasswordFieldProps } from "../types";
|
|
12
12
|
import { FieldShell } from "./FieldShell";
|
|
@@ -107,7 +107,7 @@ function NumberInput({
|
|
|
107
107
|
|
|
108
108
|
function TextLike({ props, type }: { props: BaseFieldProps; type: "text" | "email" | "password" }) {
|
|
109
109
|
const loading = useLoading();
|
|
110
|
-
const
|
|
110
|
+
const onTable = useOnTable();
|
|
111
111
|
return (
|
|
112
112
|
<FieldShell {...props}>
|
|
113
113
|
{(field) => (
|
|
@@ -117,7 +117,7 @@ function TextLike({ props, type }: { props: BaseFieldProps; type: "text" | "emai
|
|
|
117
117
|
value={field.value ?? ""}
|
|
118
118
|
placeholder={props.placeholder}
|
|
119
119
|
disabled={props.disabled || loading}
|
|
120
|
-
onTable={
|
|
120
|
+
onTable={onTable}
|
|
121
121
|
className="w-full"
|
|
122
122
|
/>
|
|
123
123
|
)}
|
|
@@ -133,7 +133,7 @@ export function EmailField(props: BaseFieldProps) {
|
|
|
133
133
|
}
|
|
134
134
|
export function PasswordField(props: PasswordFieldProps) {
|
|
135
135
|
const loading = useLoading();
|
|
136
|
-
const
|
|
136
|
+
const onTable = useOnTable();
|
|
137
137
|
return (
|
|
138
138
|
<FieldShell {...props}>
|
|
139
139
|
{(field) => (
|
|
@@ -144,7 +144,7 @@ export function PasswordField(props: PasswordFieldProps) {
|
|
|
144
144
|
value={field.value ?? ""}
|
|
145
145
|
placeholder={props.placeholder}
|
|
146
146
|
disabled={props.disabled || loading}
|
|
147
|
-
onTable={
|
|
147
|
+
onTable={onTable}
|
|
148
148
|
className="w-full"
|
|
149
149
|
/>
|
|
150
150
|
{props.strengthMeter && <PasswordLevel value={field.value ?? ""} />}
|
|
@@ -156,7 +156,7 @@ export function PasswordField(props: PasswordFieldProps) {
|
|
|
156
156
|
|
|
157
157
|
export function NumberField(props: BaseFieldProps) {
|
|
158
158
|
const loading = useLoading();
|
|
159
|
-
const
|
|
159
|
+
const onTable = useOnTable();
|
|
160
160
|
return (
|
|
161
161
|
<FieldShell {...props}>
|
|
162
162
|
{(field) => (
|
|
@@ -164,7 +164,7 @@ export function NumberField(props: BaseFieldProps) {
|
|
|
164
164
|
field={field}
|
|
165
165
|
placeholder={props.placeholder}
|
|
166
166
|
disabled={props.disabled || loading}
|
|
167
|
-
onTable={
|
|
167
|
+
onTable={onTable}
|
|
168
168
|
/>
|
|
169
169
|
)}
|
|
170
170
|
</FieldShell>
|
|
@@ -173,7 +173,7 @@ export function NumberField(props: BaseFieldProps) {
|
|
|
173
173
|
|
|
174
174
|
export function CurrencyField(props: CurrencyFieldProps) {
|
|
175
175
|
const loading = useLoading();
|
|
176
|
-
const
|
|
176
|
+
const onTable = useOnTable();
|
|
177
177
|
return (
|
|
178
178
|
<FieldShell {...props}>
|
|
179
179
|
{(field) => (
|
|
@@ -186,7 +186,7 @@ export function CurrencyField(props: CurrencyFieldProps) {
|
|
|
186
186
|
}
|
|
187
187
|
placeholder={props.placeholder}
|
|
188
188
|
disabled={props.disabled || loading}
|
|
189
|
-
onTable={
|
|
189
|
+
onTable={onTable}
|
|
190
190
|
/>
|
|
191
191
|
)}
|
|
192
192
|
</FieldShell>
|
|
@@ -6,8 +6,9 @@ import { useForm, type FieldValues } from "react-hook-form";
|
|
|
6
6
|
|
|
7
7
|
import { cn } from "../../utils/cn";
|
|
8
8
|
import { Form } from "../Form";
|
|
9
|
-
import { SectionBlock, type SectionColor } from "../SectionBlock";
|
|
9
|
+
import { SectionBlock, type SectionColor, type SectionVariant } from "../SectionBlock";
|
|
10
10
|
import { LoadingContext, DirectionContext, StepperContext, FormIdContext } from "./context";
|
|
11
|
+
import { markFieldKind } from "./field-kind";
|
|
11
12
|
import { Header } from "./header";
|
|
12
13
|
import type { FormBuilderRootProps } from "./types";
|
|
13
14
|
import {
|
|
@@ -57,13 +58,21 @@ export interface SectionProps {
|
|
|
57
58
|
title?: ReactNode;
|
|
58
59
|
color?: SectionColor;
|
|
59
60
|
icon?: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* `"Table"` switches to the full-bleed table shell — no body padding, a rule under
|
|
63
|
+
* the header, and the card clipped to its radius. `FormBuilder.Table` uses it; pass
|
|
64
|
+
* it here only when hand-composing a table inside a section.
|
|
65
|
+
*/
|
|
66
|
+
variant?: SectionVariant;
|
|
67
|
+
/** Right-aligned content on the title row — e.g. action buttons. */
|
|
68
|
+
action?: ReactNode;
|
|
60
69
|
children: ReactNode;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/** `FormBuilder.Section` — a titled Glare SectionBlock grouping fields. */
|
|
64
|
-
function Section({ title, color, icon, children }: SectionProps) {
|
|
73
|
+
function Section({ title, color, icon, variant, action, children }: SectionProps) {
|
|
65
74
|
return (
|
|
66
|
-
<SectionBlock title={title} color={color} icon={icon}>
|
|
75
|
+
<SectionBlock title={title} color={color} icon={icon} variant={variant} action={action}>
|
|
67
76
|
{children}
|
|
68
77
|
</SectionBlock>
|
|
69
78
|
);
|
|
@@ -89,6 +98,7 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
89
98
|
values,
|
|
90
99
|
loading = false,
|
|
91
100
|
fieldDirection,
|
|
101
|
+
layout = "page",
|
|
92
102
|
resetOnSuccess,
|
|
93
103
|
conclusion,
|
|
94
104
|
className,
|
|
@@ -140,10 +150,18 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
|
|
|
140
150
|
rest
|
|
141
151
|
);
|
|
142
152
|
|
|
143
|
-
// The fields column caps at
|
|
144
|
-
// standalone.
|
|
153
|
+
// The fields column caps at 1100px and centers — as the middle column of the grid, and
|
|
154
|
+
// standalone. `layout="bare"` drops the cap and the gutters so an embedded form fills its
|
|
155
|
+
// container instead: 48px of padding a side is most of a settings rail.
|
|
145
156
|
const fieldsInner = (
|
|
146
|
-
<div
|
|
157
|
+
<div
|
|
158
|
+
className={cn(
|
|
159
|
+
"flex w-full flex-col gap-4",
|
|
160
|
+
layout === "page" && "mx-auto max-w-[1100px] px-[48px]",
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{fields}
|
|
164
|
+
</div>
|
|
147
165
|
);
|
|
148
166
|
|
|
149
167
|
const formEl = (
|
|
@@ -276,3 +294,45 @@ export const FormBuilder = Object.assign(FormBuilderRoot, {
|
|
|
276
294
|
Back,
|
|
277
295
|
Next,
|
|
278
296
|
});
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* What each field produces, for consumers that read a form they did not author.
|
|
300
|
+
*
|
|
301
|
+
* Stamped here rather than on the field definitions because the aliases — `DateRange`, `Tags`,
|
|
302
|
+
* `Image` — only exist in the table above, and a reader checking "is this list complete?" should
|
|
303
|
+
* have one place to look. `Tags` and `MultiSelect` are the same component, so the repeat is
|
|
304
|
+
* harmless.
|
|
305
|
+
*/
|
|
306
|
+
(
|
|
307
|
+
[
|
|
308
|
+
[FormBuilder.Text, "text"],
|
|
309
|
+
[FormBuilder.Email, "text"],
|
|
310
|
+
[FormBuilder.Password, "text"],
|
|
311
|
+
[FormBuilder.Textarea, "text"],
|
|
312
|
+
[FormBuilder.Otp, "text"],
|
|
313
|
+
[FormBuilder.Color, "text"],
|
|
314
|
+
[FormBuilder.Phone, "text"],
|
|
315
|
+
[FormBuilder.Number, "number"],
|
|
316
|
+
[FormBuilder.Currency, "number"],
|
|
317
|
+
[FormBuilder.Select, "choice"],
|
|
318
|
+
[FormBuilder.SearchableSelect, "choice"],
|
|
319
|
+
[FormBuilder.RadioList, "choice"],
|
|
320
|
+
[FormBuilder.RadioCards, "choice"],
|
|
321
|
+
[FormBuilder.MultiSelect, "multiChoice"],
|
|
322
|
+
[FormBuilder.Tags, "multiChoice"],
|
|
323
|
+
[FormBuilder.CheckboxGroup, "multiChoice"],
|
|
324
|
+
[FormBuilder.TreeSelect, "multiChoice"],
|
|
325
|
+
[FormBuilder.Checkbox, "boolean"],
|
|
326
|
+
[FormBuilder.SwitchBox, "boolean"],
|
|
327
|
+
[FormBuilder.Date, "date"],
|
|
328
|
+
[FormBuilder.DateRange, "date"],
|
|
329
|
+
[FormBuilder.DateTime, "date"],
|
|
330
|
+
[FormBuilder.DateMultiple, "date"],
|
|
331
|
+
[FormBuilder.Slider, "slider"],
|
|
332
|
+
[FormBuilder.Custom, "custom"],
|
|
333
|
+
[FormBuilder.File, "custom"],
|
|
334
|
+
[FormBuilder.Image, "custom"],
|
|
335
|
+
[FormBuilder.Signature, "custom"],
|
|
336
|
+
[FormBuilder.RichText, "custom"],
|
|
337
|
+
] as const
|
|
338
|
+
).forEach(([component, kind]) => markFieldKind(component, kind));
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { FormBuilder } from "./form-builder";
|
|
2
2
|
export type { SectionProps } from "./form-builder";
|
|
3
|
+
export { fieldKindOf, markFieldKind } from "./field-kind";
|
|
3
4
|
export { FormHeaderBar } from "./header";
|
|
4
5
|
export type { HeaderProps, HeaderVariant, FormHeaderBarProps } from "./header";
|
|
5
6
|
export { RichTextField } from "../TextEditor/RichTextField";
|
|
6
7
|
export type { StepProps } from "./stepper";
|
|
7
|
-
export type { FieldDirection } from "./context";
|
|
8
|
+
export type { FieldDirection, CellMode } from "./context";
|
|
8
9
|
export type {
|
|
9
10
|
BaseFieldProps,
|
|
11
|
+
FieldKind,
|
|
10
12
|
OptionItem,
|
|
11
13
|
SelectFieldProps,
|
|
12
14
|
SearchableSelectFieldProps,
|
|
@@ -11,6 +11,35 @@ import type {
|
|
|
11
11
|
import type { FieldDirection } from "./context";
|
|
12
12
|
import type { SectionColor } from "../SectionBlock";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* What shape of value a field produces, stamped on each `FormBuilder.*` field component.
|
|
16
|
+
*
|
|
17
|
+
* This is for consumers that have to interpret a form they did not author — `DataViews.Filters`
|
|
18
|
+
* reads it to decide whether a child's value becomes a list, a numeric range or a date range. It
|
|
19
|
+
* describes the field, not any one consumer's use of it.
|
|
20
|
+
*
|
|
21
|
+
* Read it with `fieldKindOf(element)` rather than comparing `element.type` against an imported
|
|
22
|
+
* component: identity comparison breaks across duplicated module instances and stops matching
|
|
23
|
+
* after a hot reload.
|
|
24
|
+
*/
|
|
25
|
+
export type FieldKind =
|
|
26
|
+
/** A string value — `Text`, `Textarea`, `Email`, `Password`, `Color`, `Phone`, `Otp`. */
|
|
27
|
+
| "text"
|
|
28
|
+
/** A number — `Number`, `Currency`. */
|
|
29
|
+
| "number"
|
|
30
|
+
/** One of a supplied list — `Select`, `SearchableSelect`, `RadioList`, `RadioCards`. */
|
|
31
|
+
| "choice"
|
|
32
|
+
/** Several of a supplied list — `MultiSelect`, `Tags`, `CheckboxGroup`, `TreeSelect`. */
|
|
33
|
+
| "multiChoice"
|
|
34
|
+
/** A boolean — `Checkbox`, `SwitchBox`. */
|
|
35
|
+
| "boolean"
|
|
36
|
+
/** A date or date range — `Date`, `DateRange`, `DateTime`, `DateMultiple`. */
|
|
37
|
+
| "date"
|
|
38
|
+
/** Two numbers when `range`, otherwise one — `Slider`. */
|
|
39
|
+
| "slider"
|
|
40
|
+
/** Whatever the caller renders — `Custom`, `File`, `Image`, `Signature`, `RichText`. */
|
|
41
|
+
| "custom";
|
|
42
|
+
|
|
14
43
|
/** Props shared by every `FormBuilder.*` field. `name` is the RHF path. */
|
|
15
44
|
export interface BaseFieldProps {
|
|
16
45
|
name: string;
|
|
@@ -249,6 +278,17 @@ export interface FormBuilderRootProps<T extends FieldValues = FieldValues> {
|
|
|
249
278
|
loading?: boolean;
|
|
250
279
|
/** Field row direction. Defaults to horizontal (vertical inside a drawer). */
|
|
251
280
|
fieldDirection?: FieldDirection;
|
|
281
|
+
/**
|
|
282
|
+
* How the fields column is framed.
|
|
283
|
+
*
|
|
284
|
+
* `"page"` (the default) is the full-page form: centred, capped at 1100px, and held off the
|
|
285
|
+
* edges by 48px gutters.
|
|
286
|
+
*
|
|
287
|
+
* `"bare"` drops all three so the fields fill whatever they are placed inside. Use it when the
|
|
288
|
+
* form is embedded in something narrow — a 260px settings rail has no room to give up 96px to
|
|
289
|
+
* gutters, and its controls would overflow the panel rather than fill it.
|
|
290
|
+
*/
|
|
291
|
+
layout?: "page" | "bare";
|
|
252
292
|
/** Reset to defaults after a successful submit. */
|
|
253
293
|
resetOnSuccess?: boolean;
|
|
254
294
|
/**
|
|
@@ -198,6 +198,9 @@ export const GroupStyles = cva(
|
|
|
198
198
|
"hover:caret-border-presentation-state-negative",
|
|
199
199
|
],
|
|
200
200
|
},
|
|
201
|
+
// Transparent border/background so the control blends into a table cell. The hover and
|
|
202
|
+
// focus shadows are deliberately left alone — a cell control lifts exactly like any
|
|
203
|
+
// other field.
|
|
201
204
|
onTable: {
|
|
202
205
|
true: ["border-transparent", "bg-transparent"],
|
|
203
206
|
},
|
|
@@ -224,12 +224,13 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
224
224
|
data-theme="dark"
|
|
225
225
|
className="w-full rounded-[10px] overflow-hidden"
|
|
226
226
|
>
|
|
227
|
-
<TableHeader className="bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
|
|
228
|
-
<TableRow className="h-[44px] [&_button]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
|
|
227
|
+
<TableHeader className="backdrop-blur-none bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
|
|
228
|
+
<TableRow className="h-[44px] [&_button:not([data-slot=resize-handle])]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
|
|
229
229
|
{columns.map((col) => (
|
|
230
230
|
<TableHead
|
|
231
231
|
key={col.key}
|
|
232
|
-
className="cursor-default
|
|
232
|
+
className="cursor-default"
|
|
233
|
+
contentClassName="typography-body-medium-medium text-white"
|
|
233
234
|
>
|
|
234
235
|
{col.header}
|
|
235
236
|
</TableHead>
|
|
@@ -252,7 +253,7 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
252
253
|
{columns.map((col) => (
|
|
253
254
|
<TableCell
|
|
254
255
|
key={col.key}
|
|
255
|
-
className="border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
|
|
256
|
+
className="h-[40px] border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
|
|
256
257
|
>
|
|
257
258
|
{col.render ? col.render(row) : String(row[col.key] ?? "")}
|
|
258
259
|
</TableCell>
|
|
@@ -21,9 +21,61 @@ const titleBadge = cva(
|
|
|
21
21
|
},
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
const container = cva(
|
|
25
|
+
"flex w-full px-0 flex-col rounded-[16px] bg-background-presentation-form-base shadow-[0_0_32px_2px_rgba(0,0,0,0.05)]",
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
Default: "pt-[6px] pb-[24px]",
|
|
30
|
+
// No bottom padding — the table's end-action row is the last element and meets
|
|
31
|
+
// the card edge; `overflow-hidden` clips it to the 16px radius.
|
|
32
|
+
Table: "pt-[6px] pb-0 overflow-hidden",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { variant: "Default" },
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const header = cva("flex px-[6px] justify-between gap-3", {
|
|
40
|
+
variants: {
|
|
41
|
+
variant: {
|
|
42
|
+
Default: "items-center",
|
|
43
|
+
Table: "items-start",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: { variant: "Default" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const body = cva("flex w-full flex-col", {
|
|
50
|
+
variants: {
|
|
51
|
+
variant: {
|
|
52
|
+
Default: "px-[42px] gap-[2px]",
|
|
53
|
+
// Full bleed, with the rule that separates the header from the table.
|
|
54
|
+
Table: "mt-[6px] border-t border-border-presentation-global-primary overflow-hidden",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: { variant: "Default" },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const rail = cva("flex w-full min-w-[300px] flex-col items-start", {
|
|
61
|
+
variants: {
|
|
62
|
+
variant: {
|
|
63
|
+
// Hairline between each direct child (form rows).
|
|
64
|
+
Default: "divide-y divide-gray-300",
|
|
65
|
+
// The table draws its own row borders — a divide rule would double up on the
|
|
66
|
+
// table / scroller / end-action siblings.
|
|
67
|
+
Table: "",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
defaultVariants: { variant: "Default" },
|
|
71
|
+
});
|
|
72
|
+
|
|
24
73
|
export type SectionColor = NonNullable<VariantProps<typeof titleBadge>["color"]>;
|
|
74
|
+
export type SectionVariant = NonNullable<VariantProps<typeof container>["variant"]>;
|
|
25
75
|
|
|
26
|
-
export interface SectionBlockProps
|
|
76
|
+
export interface SectionBlockProps
|
|
77
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "title">,
|
|
78
|
+
VariantProps<typeof container> {
|
|
27
79
|
color?: SectionColor;
|
|
28
80
|
title?: ReactNode;
|
|
29
81
|
icon?: ReactNode;
|
|
@@ -39,6 +91,7 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
|
|
|
39
91
|
{
|
|
40
92
|
children,
|
|
41
93
|
color,
|
|
94
|
+
variant,
|
|
42
95
|
title,
|
|
43
96
|
action,
|
|
44
97
|
className,
|
|
@@ -53,15 +106,11 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
|
|
|
53
106
|
return (
|
|
54
107
|
<div
|
|
55
108
|
ref={ref}
|
|
56
|
-
className={cn(
|
|
57
|
-
"flex w-full pt-[6px] pb-[24px] px-0 flex-col rounded-[16px] bg-background-presentation-form-base shadow-[0_0_32px_2px_rgba(0,0,0,0.05)]",
|
|
58
|
-
className,
|
|
59
|
-
containerClassName,
|
|
60
|
-
)}
|
|
109
|
+
className={cn(container({ variant }), className, containerClassName)}
|
|
61
110
|
{...props}
|
|
62
111
|
>
|
|
63
112
|
{(title || action) && (
|
|
64
|
-
<div className={cn(
|
|
113
|
+
<div className={cn(header({ variant }), headerClassName)}>
|
|
65
114
|
{title ? (
|
|
66
115
|
<div className={cn(titleBadge({ color }))}>
|
|
67
116
|
<span className="flex items-center gap-1.5">
|
|
@@ -75,10 +124,8 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
|
|
|
75
124
|
{action && <div className="flex shrink-0 items-center gap-2">{action}</div>}
|
|
76
125
|
</div>
|
|
77
126
|
)}
|
|
78
|
-
<div className={cn(
|
|
79
|
-
<div className=
|
|
80
|
-
{children}
|
|
81
|
-
</div>
|
|
127
|
+
<div className={cn(body({ variant }), bodyClassName)}>
|
|
128
|
+
<div className={cn(rail({ variant }))}>{children}</div>
|
|
82
129
|
</div>
|
|
83
130
|
</div>
|
|
84
131
|
);
|
|
@@ -293,7 +293,9 @@ const PopoverTriggerStyles = cva(
|
|
|
293
293
|
"hover:caret-border-presentation-state-negative",
|
|
294
294
|
],
|
|
295
295
|
},
|
|
296
|
-
// Transparent border/background so the trigger blends into a table cell.
|
|
296
|
+
// Transparent border/background so the trigger blends into a table cell. The hover and
|
|
297
|
+
// focus shadows are deliberately left alone — a cell control lifts exactly like any
|
|
298
|
+
// other field.
|
|
297
299
|
onTable: {
|
|
298
300
|
true: ["border-transparent", "bg-transparent"],
|
|
299
301
|
},
|