torch-glare 2.4.4 → 2.4.5

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.
@@ -37,12 +37,37 @@ function splitPhone(value: unknown, defaultDial: string): { dial: string; number
37
37
  * `FormBuilder.Phone` — country dial-code (every country) + number. The picker is a
38
38
  * `SearchableSelect` keyed by the unique ISO code (dial codes repeat) — type to filter by
39
39
  * country name or dial. The value is stored as a `"+<dial> <number>"` string.
40
+ *
41
+ * **Inside a `FormBuilder.Table` cell** this collapses to a single plain number input: the
42
+ * fixed-width picker imposed a ~155px floor on the column and left the number truncated,
43
+ * and two controls in one cell is not a table column. Add a separate column (e.g. a
44
+ * `FormBuilder.Select` of dial codes) when a table needs the country code.
40
45
  */
41
46
  export function PhoneField(props: PhoneFieldProps) {
42
47
  const loading = useLoading();
43
48
  const cell = useCell();
44
49
  const defaultDial = props.defaultCountry ?? "+964"; // Iraq
45
50
 
51
+ if (cell) {
52
+ return (
53
+ <FieldShell {...props}>
54
+ {(field) => (
55
+ <InputField
56
+ type="tel"
57
+ inputMode="tel"
58
+ value={typeof field.value === "string" ? field.value : ""}
59
+ onChange={(e) => field.onChange(e.target.value.replace(/[^\d\s+-]/g, ""))}
60
+ onBlur={field.onBlur}
61
+ placeholder={props.placeholder ?? "Phone number"}
62
+ disabled={props.disabled || loading}
63
+ onTable
64
+ className="w-full"
65
+ />
66
+ )}
67
+ </FieldShell>
68
+ );
69
+ }
70
+
46
71
  return (
47
72
  <FieldShell {...props}>
48
73
  {(field) => {
@@ -21,7 +21,16 @@ import {
21
21
 
22
22
  import { cn } from "../../../utils/cn";
23
23
  import { SectionBlock } from "../../SectionBlock";
24
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../Table";
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,11 +156,14 @@ export function TableField(props: TableFieldProps) {
126
156
  {columns.map((col, ci) => (
127
157
  <TableCell
128
158
  key={ci}
129
- style={col.width ? { width: col.width } : undefined}
130
- childrenClassName={cn(
131
- "min-w-0 w-full [&:has(input)]:bg-transparent",
132
- col.align && ALIGN[col.align],
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
168
  <CellContext.Provider value={true}>{col.cell(rowName, index)}</CellContext.Provider>
136
169
  </TableCell>
@@ -139,12 +172,19 @@ export function TableField(props: TableFieldProps) {
139
172
  );
140
173
 
141
174
  const table = (
142
- <Table>
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
- {showHandle && <TableHead isDummy />}
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="S"
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="S"
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
- <div className="flex w-full flex-col gap-2">
255
- {props.description && (
256
- <p className="typography-body-small-regular text-content-presentation-global-secondary">
257
- {props.description}
258
- </p>
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
- <div className="w-full overflow-x-auto">
262
- {showHandle ? (
263
- <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
264
- {table}
265
- </DndContext>
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 { 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
11
  import { Header } from "./header";
12
12
  import type { FormBuilderRootProps } from "./types";
@@ -57,13 +57,21 @@ export interface SectionProps {
57
57
  title?: ReactNode;
58
58
  color?: SectionColor;
59
59
  icon?: ReactNode;
60
+ /**
61
+ * `"Table"` switches to the full-bleed table shell — no body padding, a rule under
62
+ * the header, and the card clipped to its radius. `FormBuilder.Table` uses it; pass
63
+ * it here only when hand-composing a table inside a section.
64
+ */
65
+ variant?: SectionVariant;
66
+ /** Right-aligned content on the title row — e.g. action buttons. */
67
+ action?: ReactNode;
60
68
  children: ReactNode;
61
69
  }
62
70
 
63
71
  /** `FormBuilder.Section` — a titled Glare SectionBlock grouping fields. */
64
- function Section({ title, color, icon, children }: SectionProps) {
72
+ function Section({ title, color, icon, variant, action, children }: SectionProps) {
65
73
  return (
66
- <SectionBlock title={title} color={color} icon={icon}>
74
+ <SectionBlock title={title} color={color} icon={icon} variant={variant} action={action}>
67
75
  {children}
68
76
  </SectionBlock>
69
77
  );
@@ -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 typography-body-medium-medium text-white"
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 extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
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("flex px-[6px] items-center justify-between gap-3", headerClassName)}>
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("flex px-[42px] flex-col gap-[2px] px-[42px]", bodyClassName)}>
79
- <div className="flex w-full divide-y divide-gray-300 min-w-[300px] flex-col items-start ">
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
  },