torch-glare 2.4.5 → 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.
Files changed (142) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +7 -6
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +1 -1
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +55 -3
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/TabSwitch.tsx +16 -4
  48. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  49. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  50. package/apps/lib/components/TreeFolder/index.ts +1 -1
  51. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  52. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  53. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  54. package/apps/lib/registry.json +159 -4
  55. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  56. package/apps/lib/utils/dataViews/path.ts +67 -0
  57. package/apps/lib/utils/dataViews/query.ts +73 -0
  58. package/apps/lib/utils/dataViews/types.ts +187 -0
  59. package/docs/components/breadcrumb.md +1 -1
  60. package/docs/components/button-group.md +1 -1
  61. package/docs/components/button.md +1 -1
  62. package/docs/components/card.md +1 -1
  63. package/docs/components/checkbox.md +1 -1
  64. package/docs/components/data-views/backend-response.md +324 -0
  65. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  66. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  67. package/docs/components/data-views/examples/fields.md +362 -0
  68. package/docs/components/data-views/examples/filters.md +308 -0
  69. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  70. package/docs/components/data-views/examples/index.md +29 -0
  71. package/docs/components/data-views/examples/overview.md +244 -0
  72. package/docs/components/data-views/examples/panel.md +212 -0
  73. package/docs/components/data-views/examples/scale.md +231 -0
  74. package/docs/components/data-views/examples/server-side.md +210 -0
  75. package/docs/components/data-views/examples/state.md +250 -0
  76. package/docs/components/data-views/examples/tree-custom.md +388 -0
  77. package/docs/components/data-views/examples/view-registry.md +313 -0
  78. package/docs/components/data-views/examples/views.md +534 -0
  79. package/docs/components/data-views/guide.md +405 -0
  80. package/docs/components/data-views/index.md +1504 -0
  81. package/docs/components/data-views/migration.md +79 -0
  82. package/docs/components/date-picker.md +0 -1
  83. package/docs/components/form-builder.md +3 -2
  84. package/docs/components/form-renderer.md +2 -1
  85. package/docs/components/form.md +1 -1
  86. package/docs/components/input-field.md +1 -1
  87. package/docs/components/input-otp.md +1 -1
  88. package/docs/components/input.md +1 -1
  89. package/docs/components/labeled-check-box.md +1 -1
  90. package/docs/components/labeled-radio.md +1 -1
  91. package/docs/components/radio-card.md +1 -1
  92. package/docs/components/radio.md +1 -1
  93. package/docs/components/search-field.md +1 -1
  94. package/docs/components/section-block.md +1 -1
  95. package/docs/components/select.md +1 -1
  96. package/docs/components/simple-select.md +1 -1
  97. package/docs/components/switch.md +1 -1
  98. package/docs/components/tab-switch.md +1 -1
  99. package/docs/components/table.md +1 -1
  100. package/docs/components/text-editor.md +1 -1
  101. package/docs/components/textarea.md +1 -1
  102. package/docs/components/toggle-button.md +1 -1
  103. package/docs/components/toggle.md +1 -1
  104. package/docs/components/tree-folder.md +110 -0
  105. package/docs/how-to/forms-with-form-builder.md +2 -2
  106. package/docs/reference/components.md +16 -6
  107. package/docs/tutorials/component-composition.md +11 -13
  108. package/package.json +3 -2
  109. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  110. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  111. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  112. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  113. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  114. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  115. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  116. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  117. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  118. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  119. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  120. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  121. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  122. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  123. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  124. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  125. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  126. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  127. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  128. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  129. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  130. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  131. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  132. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  133. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  134. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  135. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  136. package/docs/components/data-views-config-panel.md +0 -208
  137. package/docs/components/data-views-layout.md +0 -291
  138. package/docs/components/inbox-view.md +0 -170
  139. package/docs/components/kanban-view.md +0 -135
  140. package/docs/components/table-view.md +0 -141
  141. package/docs/components/tree-view.md +0 -147
  142. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -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, useCell } from "../context";
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 cell = useCell();
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={cell}
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 cell = useCell();
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={cell}
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 cell = useCell();
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={cell}
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 cell = useCell();
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={cell}
189
+ onTable={onTable}
190
190
  />
191
191
  )}
192
192
  </FieldShell>
@@ -8,6 +8,7 @@ import { cn } from "../../utils/cn";
8
8
  import { Form } from "../Form";
9
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 {
@@ -97,6 +98,7 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
97
98
  values,
98
99
  loading = false,
99
100
  fieldDirection,
101
+ layout = "page",
100
102
  resetOnSuccess,
101
103
  conclusion,
102
104
  className,
@@ -148,10 +150,18 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
148
150
  rest
149
151
  );
150
152
 
151
- // The fields column caps at 1200px and centers — as the middle column of the grid, and
152
- // 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.
153
156
  const fieldsInner = (
154
- <div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4 px-[48px]">{fields}</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>
155
165
  );
156
166
 
157
167
  const formEl = (
@@ -284,3 +294,45 @@ export const FormBuilder = Object.assign(FormBuilderRoot, {
284
294
  Back,
285
295
  Next,
286
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
  /**
@@ -48,7 +48,7 @@ const trackStyles = cva(
48
48
  // transparent and lighten on hover.
49
49
  const optionStyles = cva(
50
50
  [
51
- "flex items-center justify-center gap-[6px]",
51
+ "flex items-center justify-center gap-[4px]",
52
52
  "rounded-[8px]",
53
53
  "font-[510] leading-none",
54
54
  "transition-all duration-200 ease-in-out",
@@ -74,8 +74,11 @@ const optionStyles = cva(
74
74
  true: [
75
75
  "bg-white",
76
76
  "text-[#1C1D1F]",
77
+ // The border is not in the design. It stays because its transparent twin below is what
78
+ // keeps both states the same width — without it, selecting a tab resizes the pill and
79
+ // shoves its neighbours. The design's own 4px gap absorbs the 1px.
77
80
  "border border-black/5",
78
- "shadow-[0_1px_3px_0_rgba(0,0,0,0.18)]",
81
+ "drop-shadow-[0px_0px_5px_rgba(0,0,0,0.25)]",
79
82
  ],
80
83
  false: [
81
84
  "border border-transparent",
@@ -121,8 +124,17 @@ function TabSwitchInner<T extends string = string>(
121
124
 
122
125
  return (
123
126
  <div key={option.value} className="flex items-center">
124
- {showDivider && (
125
- <div className="mx-[3px] h-3 w-px bg-border-presentation-action-disabled" />
127
+ {idx > 0 && (
128
+ <div
129
+ aria-hidden
130
+ className={cn(
131
+ // The slot is always here, and only its ink changes. Unmounting it instead cost
132
+ // the track 7px every time the rule below hid one, so picking a tab resized the
133
+ // track and shoved its neighbours sideways.
134
+ "mx-[3px] h-3 w-px",
135
+ showDivider ? "bg-border-presentation-action-disabled" : "bg-transparent",
136
+ )}
137
+ />
126
138
  )}
127
139
  <button
128
140
  type="button"
@@ -22,6 +22,7 @@ import type {
22
22
  TreeFolderVisibleRow,
23
23
  } from "./types";
24
24
  import { useTreeFolderDnD } from "./useTreeFolderDnD";
25
+ import { DndContext, DragList } from "../../hooks/useDragDrop";
25
26
 
26
27
  export type TreeFolderProps = {
27
28
  data: TreeFolderNode[];
@@ -230,10 +231,9 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
230
231
  }, []);
231
232
 
232
233
  // ---- DnD ----
233
- const { dragIds, dropTarget, getRowDragHandlers } = useTreeFolderDnD({
234
+ const { dragIds, dropTarget, contextProps, ids } = useTreeFolderDnD({
234
235
  data,
235
236
  rowsById,
236
- scrollContainerRef: scrollRef,
237
237
  enabled: dndEnabled,
238
238
  onMove: handleMoveInternal,
239
239
  canDrop: ({ parentId }) => {
@@ -310,6 +310,7 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
310
310
  )}
311
311
 
312
312
  <TreeFolderStyles />
313
+ <DndContext {...contextProps}>
313
314
  <div ref={scrollRef} role="tree" className="tf-scroll flex-1 min-h-0 overflow-auto">
314
315
  {isEmpty ? (
315
316
  (emptyState ?? (
@@ -319,6 +320,7 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
319
320
  ))
320
321
  ) : (
321
322
  <div className="min-w-max" style={stripStyle}>
323
+ <DragList ids={ids} makeRoom={false}>
322
324
  {visibleRows.map((row, idx) => {
323
325
  const prevRow = visibleRows[idx - 1];
324
326
  const nextRow = visibleRows[idx + 1];
@@ -364,13 +366,14 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
364
366
  dndEnabled={dndEnabled}
365
367
  onSelect={handleSelect}
366
368
  onToggle={handleToggle}
367
- dragHandlers={getRowDragHandlers(row.node.id)}
368
369
  />
369
370
  );
370
371
  })}
372
+ </DragList>
371
373
  </div>
372
374
  )}
373
375
  </div>
376
+ </DndContext>
374
377
  </div>
375
378
  );
376
379
  },
@@ -1,20 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import { ChevronRight, ChevronDown, GripVertical } from "lucide-react";
4
- import type { DragEvent } from "react";
4
+ import { useDragItem } from "../../hooks/useDragDrop";
5
5
  import { cn } from "../../utils/cn";
6
6
  import { resolveIcon } from "./icons";
7
7
  import type { TreeFolderIconResolver, TreeFolderNode, TreeFolderVisibleRow } from "./types";
8
8
 
9
- export type TreeFolderRowDragHandlers = {
10
- draggable: boolean;
11
- onDragStart: (e: DragEvent<HTMLElement>) => void;
12
- onDragEnd: (e: DragEvent<HTMLElement>) => void;
13
- onDragOver: (e: DragEvent<HTMLElement>) => void;
14
- onDragLeave: (e: DragEvent<HTMLElement>) => void;
15
- onDrop: (e: DragEvent<HTMLElement>) => void;
16
- };
17
-
18
9
  export type TreeFolderRowProps = {
19
10
  row: TreeFolderVisibleRow;
20
11
  rowHeight: number;
@@ -37,7 +28,6 @@ export type TreeFolderRowProps = {
37
28
  dndEnabled: boolean;
38
29
  onSelect: (id: string | null) => void;
39
30
  onToggle: (id: string) => void;
40
- dragHandlers: TreeFolderRowDragHandlers;
41
31
  };
42
32
 
43
33
  export function TreeFolderRow({
@@ -57,9 +47,14 @@ export function TreeFolderRow({
57
47
  dndEnabled,
58
48
  onSelect,
59
49
  onToggle,
60
- dragHandlers,
61
50
  }: TreeFolderRowProps) {
62
51
  const { node, level, isOpen, isInternal, ancestorHasMoreSiblings } = row;
52
+
53
+ const {
54
+ ref: dragRef,
55
+ handleProps: dragProps,
56
+ style: dragStyle,
57
+ } = useDragItem(node.id, !dndEnabled || Boolean(node.disabled), { follow: false });
63
58
  const data = node;
64
59
  const hasChildren = isInternal;
65
60
 
@@ -134,10 +129,13 @@ export function TreeFolderRow({
134
129
 
135
130
  return (
136
131
  <div
132
+ ref={dragRef}
137
133
  data-row-id={node.id}
138
134
  className={cn("select-none group/row", outerClassName)}
139
- style={{ height: rowHeight }}
140
- {...dragHandlers}
135
+ // The row is the drag surface, not just the grip: that is how this tree has always behaved,
136
+ // and the 8px threshold is what keeps a click on it still a click.
137
+ style={{ ...dragStyle, height: rowHeight }}
138
+ {...dragProps}
141
139
  >
142
140
  <span aria-hidden className={bandClassName} />
143
141
 
@@ -234,6 +232,10 @@ export function TreeFolderRow({
234
232
  {data.name}
235
233
  </span>
236
234
 
235
+ {/* `meta` has always been part of `TreeFolderNode` but was never painted. It sits after
236
+ the name, before the collapsed-child count, and only appears when a node supplies it. */}
237
+ {data.meta != null && <span className="shrink-0 pr-2">{data.meta}</span>}
238
+
237
239
  {hasChildren && !isOpen && (
238
240
  <span
239
241
  className={cn(
@@ -1,7 +1,7 @@
1
1
  export { TreeFolder } from "./TreeFolder";
2
2
  export type { TreeFolderHandle, TreeFolderProps } from "./TreeFolder";
3
3
  export { TreeFolderRow } from "./TreeFolderRow";
4
- export type { TreeFolderRowProps, TreeFolderRowDragHandlers } from "./TreeFolderRow";
4
+ export type { TreeFolderRowProps } from "./TreeFolderRow";
5
5
  export { TreeFolderBreadcrumb } from "./TreeFolderBreadcrumb";
6
6
  export { defaultIconRegistry, defaultIconFor } from "./icons";
7
7
  export {