torch-glare 2.4.3 → 2.4.4

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.
@@ -122,11 +122,11 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
122
122
  // `<form>`. Called unconditionally (inert when there are no steps) to keep hooks order stable.
123
123
  const stepper = useStepperState(steps, form.trigger as Parameters<typeof useStepperState>[1]);
124
124
 
125
- // The stepper nav is its own column beside the fields, inside the form surface.
125
+ // The stepper nav is its own grid column beside the fields, inside the scrolling body.
126
126
  const nav = isStepper ? <StepperNav /> : null;
127
127
 
128
- // The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like
129
- // Back/Next), or the plain children. The Submit itself lives outside the form (see FormRenderer).
128
+ // The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like Back/Next),
129
+ // or the plain children. The Submit itself lives outside the form (see FormRenderer).
130
130
  const fields = isStepper ? (
131
131
  <>
132
132
  {steps.map((step, i) => (
@@ -179,13 +179,12 @@ function FormBuilderRoot<T extends FieldValues = FieldValues>({
179
179
  bodyInner
180
180
  );
181
181
 
182
- // The conclusion lives OUTSIDE the form surface — its own panel beside it, exactly like the
183
- // drawer's tray (`FormDrawer` puts the conclusion next to the form panel with a 6px gutter).
184
- // It never wraps below the form: the two stay side-by-side at every screen size.
182
+ // The conclusion (right) lives OUTSIDE the scroll surface — its own panel beside it (mirroring the
183
+ // drawer's tray, a 6px gutter). Only the surface's body scrolls; the conclusion stays put.
185
184
  const body = conclusion ? (
186
- <div className="flex h-full flex-row items-stretch gap-[6px]">
185
+ <div className="flex h-full flex-row items-stretch">
187
186
  <div className="min-h-0 min-w-0 flex-1">{surface}</div>
188
- <div className="flex min-h-0">{conclusion}</div>
187
+ <div className="ml-[6px] flex min-h-0">{conclusion}</div>
189
188
  </div>
190
189
  ) : (
191
190
  surface
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+
6
+ import { cn } from "../../utils/cn";
7
+ import { formBarItemStyles } from "../TabFormItem";
8
+ import { FormHeaderBar, type HeaderVariant } from "../FormBuilder/header";
9
+
10
+ /**
11
+ * Detail-tabs — a display-only view where a left **sidebar** switches the main area between
12
+ * **`FormBuilder.Section` panels** (a detail page, not a form). It lives on `FormRenderer`
13
+ * (`FormRenderer.Sidebar` / `.Sidebar.Item` / `.Tab`) so `FormBuilder` stays form-only. Built on
14
+ * the same Radix Tabs primitive shadcn uses: the sidebar is the `Tabs.List`, each `Sidebar.Item` a
15
+ * `Tabs.Trigger`, each `Tab` a `Tabs.Content` — so the fixed rail and the panels share tab state.
16
+ */
17
+
18
+ export interface DetailSidebarProps {
19
+ children: React.ReactNode;
20
+ }
21
+
22
+ /**
23
+ * `FormRenderer.Sidebar` — the tab rail, sitting where a `FormBuilder.Stepper`'s nav would.
24
+ * Renders **nothing itself**: the FormRenderer root detects it and places its `Sidebar.Item`
25
+ * children (the tab triggers) into the fixed rail.
26
+ */
27
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- children are read by the FormRenderer root
28
+ function DetailSidebarRoot(_props: DetailSidebarProps) {
29
+ return null;
30
+ }
31
+ (DetailSidebarRoot as unknown as { __isDetailSidebar: boolean }).__isDetailSidebar = true;
32
+
33
+ export interface DetailSidebarItemProps {
34
+ /** Ties this row to the `FormRenderer.Tab` of the same `value` — clicking it shows that panel. */
35
+ value: string;
36
+ /** Leading icon (e.g. a Remix `<i className="ri-…" />`). */
37
+ icon?: React.ReactNode;
38
+ children: React.ReactNode;
39
+ }
40
+
41
+ /**
42
+ * `FormRenderer.Sidebar.Item` — a rail nav row, rendered as a Radix `Tabs.Trigger` styled with the
43
+ * `TabFormItem` `tree` look. Radix drives the active state (`data-state="active"` → black pill).
44
+ */
45
+ function DetailSidebarItem({ value, icon, children }: DetailSidebarItemProps) {
46
+ return (
47
+ <TabsPrimitive.Trigger
48
+ value={value}
49
+ className={cn(
50
+ formBarItemStyles({ componentType: "tree" }),
51
+ "h-[32px] w-full justify-start gap-2 rounded-[10px] px-[6px] [&_i]:text-[16px]",
52
+ // Radix sets `data-state` on the trigger; the active tab is the filled (black) pill.
53
+ "data-[state=active]:bg-background-presentation-tab-topbar-selected data-[state=active]:text-content-presentation-tab-action-selected",
54
+ "data-[state=active]:hover:bg-background-presentation-tab-topbar-selected data-[state=active]:hover:px-[6px]",
55
+ )}
56
+ >
57
+ {icon}
58
+ <span className="flex-1 truncate text-left typography-body-medium-medium">{children}</span>
59
+ </TabsPrimitive.Trigger>
60
+ );
61
+ }
62
+
63
+ export const DetailSidebar = Object.assign(DetailSidebarRoot, {
64
+ Item: DetailSidebarItem,
65
+ });
66
+
67
+ export function isDetailSidebarElement(
68
+ node: React.ReactNode,
69
+ ): node is React.ReactElement<DetailSidebarProps> {
70
+ return (
71
+ React.isValidElement(node) &&
72
+ (node.type as { __isDetailSidebar?: boolean })?.__isDetailSidebar === true
73
+ );
74
+ }
75
+
76
+ export interface DetailRowProps {
77
+ label: React.ReactNode;
78
+ value: React.ReactNode;
79
+ }
80
+
81
+ /**
82
+ * `FormRenderer.Row` — a read-only label/value cell for detail content (the display counterpart of a
83
+ * form field). Drop several inside a `FormRenderer.Grid` within a `FormBuilder.Section`.
84
+ */
85
+ export function DetailRow({ label, value }: DetailRowProps) {
86
+ return (
87
+ <div className="flex flex-col gap-1">
88
+ <span className="typography-body-small-regular text-content-presentation-global-secondary">
89
+ {label}
90
+ </span>
91
+ <span className="typography-body-medium-medium text-content-presentation-global-primary">
92
+ {value}
93
+ </span>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export interface DetailGridProps {
99
+ /** Column count (default 2). */
100
+ columns?: 1 | 2 | 3;
101
+ children: React.ReactNode;
102
+ }
103
+
104
+ const GRID_COLS: Record<NonNullable<DetailGridProps["columns"]>, string> = {
105
+ 1: "grid-cols-1",
106
+ 2: "grid-cols-2",
107
+ 3: "grid-cols-3",
108
+ };
109
+
110
+ /**
111
+ * `FormRenderer.Grid` — arranges `FormRenderer.Row`s in an equal-column grid spanning the **full
112
+ * section width** (default 2 columns, so cells split in half). Padded so the rows breathe inside a
113
+ * `FormBuilder.Section` (clear of the title badge, roomy row + column spacing).
114
+ */
115
+ export function DetailGrid({ columns = 2, children }: DetailGridProps) {
116
+ return (
117
+ <div className={cn("grid w-full gap-x-12 gap-y-5 py-3", GRID_COLS[columns])}>{children}</div>
118
+ );
119
+ }
120
+
121
+ export interface DetailTabProps {
122
+ /** Ties this panel to the `Sidebar.Item` of the same `value`. */
123
+ value: string;
124
+ /** The panel body — typically `FormBuilder.Section` display blocks. */
125
+ children: React.ReactNode;
126
+ }
127
+
128
+ /**
129
+ * `FormRenderer.Tab` — a content panel shown when its `value` is the active tab. Renders
130
+ * **nothing itself**: the FormRenderer root detects it and mounts it as a Radix `Tabs.Content`
131
+ * (kept mounted, inactive ones hidden).
132
+ */
133
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- read by the FormRenderer root
134
+ export function DetailTab(_props: DetailTabProps) {
135
+ return null;
136
+ }
137
+ (DetailTab as unknown as { __isDetailTab: boolean }).__isDetailTab = true;
138
+
139
+ export function isDetailTabElement(
140
+ node: React.ReactNode,
141
+ ): node is React.ReactElement<DetailTabProps> {
142
+ return (
143
+ React.isValidElement(node) && (node.type as { __isDetailTab?: boolean })?.__isDetailTab === true
144
+ );
145
+ }
146
+
147
+ export interface DetailTabsViewProps {
148
+ header?: { title: string; label?: string; variant?: HeaderVariant };
149
+ /** Header action buttons (Print / Approve / …). */
150
+ actions?: React.ReactNode;
151
+ /** The `FormRenderer.Sidebar` element — its children are the tab triggers. */
152
+ sidebar: React.ReactElement<DetailSidebarProps>;
153
+ /** The `FormRenderer.Tab` elements — the content panels. */
154
+ tabs: React.ReactElement<DetailTabProps>[];
155
+ className?: string;
156
+ }
157
+
158
+ /**
159
+ * The detail-tabs surface: the floating header over a fixed left rail (the sidebar) + a scrolling
160
+ * content column showing the active tab. Radix `Tabs.Root` owns the state (uncontrolled, defaults to
161
+ * the first tab). The rail matches the stepper's rail position; only the content column scrolls.
162
+ */
163
+ export function DetailTabsView({ header, actions, sidebar, tabs, className }: DetailTabsViewProps) {
164
+ const defaultValue = tabs[0]?.props.value;
165
+
166
+ return (
167
+ <TabsPrimitive.Root
168
+ orientation="vertical"
169
+ defaultValue={defaultValue}
170
+ className={cn("h-full w-full @container", className)}
171
+ >
172
+ {/* Scroll shell — mirrors FormBuilder's: the absolute header floats over the body. */}
173
+ <div className="relative isolate flex h-full w-full flex-col overflow-hidden rounded-2xl bg-background-presentation-body-primary">
174
+ {header && (
175
+ <FormHeaderBar title={header.title} label={header.label} variant={header.variant}>
176
+ {actions}
177
+ </FormHeaderBar>
178
+ )}
179
+
180
+ <div className="relative z-[1] flex min-h-0 w-full flex-1 flex-row">
181
+ {/* The fixed rail — the tab list. `pt-[72px]` clears the floating header. */}
182
+ <TabsPrimitive.List asChild>
183
+ <aside className="flex h-full w-[216px] shrink-0 flex-col gap-1 overflow-y-auto border-r border-border-presentation-global-primary bg-black-alpha-5 px-2 pb-6 pt-[72px] scrollbar-hide">
184
+ {sidebar.props.children}
185
+ </aside>
186
+ </TabsPrimitive.List>
187
+
188
+ {/* The only scrolling region — the active tab's Sections. */}
189
+ <div className="flex min-h-0 w-full flex-1 flex-col overflow-y-auto px-6 py-6 pt-[72px] scrollbar-hide">
190
+ <div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4">
191
+ {tabs.map((panel) => (
192
+ <TabsPrimitive.Content
193
+ key={panel.props.value}
194
+ value={panel.props.value}
195
+ forceMount
196
+ className="flex flex-col gap-4 outline-none data-[state=inactive]:hidden"
197
+ >
198
+ {panel.props.children}
199
+ </TabsPrimitive.Content>
200
+ ))}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </TabsPrimitive.Root>
206
+ );
207
+ }
@@ -1,11 +1,20 @@
1
1
  "use client";
2
2
 
3
- import { useId } from "react";
3
+ import { Children, useId } from "react";
4
4
  import { FieldValues } from "react-hook-form";
5
5
 
6
6
  import { FormBuilder } from "../FormBuilder";
7
7
  import { FormIdContext, LoadingContext } from "../FormBuilder/context";
8
8
  import { StepperActions } from "../FormBuilder/stepper";
9
+ import {
10
+ DetailSidebar,
11
+ DetailTab,
12
+ DetailGrid,
13
+ DetailRow,
14
+ DetailTabsView,
15
+ isDetailSidebarElement,
16
+ isDetailTabElement,
17
+ } from "./detail";
9
18
  import { FormDrawer } from "./FormDrawer";
10
19
  import type { FormRendererProps } from "./types";
11
20
 
@@ -17,8 +26,12 @@ import type { FormRendererProps } from "./types";
17
26
  * FormRenderer never manufactures a Submit — you compose it and hand it to `actions`
18
27
  * (`actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}`). It renders in the form's
19
28
  * header action pill (page) or the drawer header (drawer), and auto-targets this form.
29
+ *
30
+ * Give it `FormRenderer.Sidebar` + `FormRenderer.Tab` children instead of fields and it switches to
31
+ * a **detail-tabs** view: a display-only page (no `<form>`) whose sidebar swaps `FormBuilder.Section`
32
+ * panels — the sidebar sits where a stepper's rail would.
20
33
  */
21
- export function FormRenderer<T extends FieldValues = FieldValues>({
34
+ function FormRendererRoot<T extends FieldValues = FieldValues>({
22
35
  children,
23
36
  id,
24
37
  onSubmit,
@@ -52,11 +65,29 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
52
65
  const autoId = useId();
53
66
  const formId = id ?? autoId;
54
67
 
68
+ // Detail-tabs mode: a `FormRenderer.Sidebar` + `FormRenderer.Tab` children mean a display-only
69
+ // detail page (no `<form>`) — the sidebar swaps Section panels via Radix Tabs. Detected here so
70
+ // the form props below are simply unused.
71
+ const childArray = Children.toArray(children);
72
+ const detailSidebar = childArray.find(isDetailSidebarElement);
73
+ const detailTabs = childArray.filter(isDetailTabElement);
74
+ if (detailSidebar && detailTabs.length > 0) {
75
+ return (
76
+ <DetailTabsView
77
+ header={header}
78
+ actions={actions}
79
+ sidebar={detailSidebar}
80
+ tabs={detailTabs}
81
+ className={className}
82
+ />
83
+ );
84
+ }
85
+
55
86
  const inner = (
56
87
  <FormBuilder
57
88
  id={formId}
58
89
  form={form}
59
- onSubmit={onSubmit}
90
+ onSubmit={onSubmit ?? (() => {})}
60
91
  onInvalid={onInvalid}
61
92
  resolver={resolver}
62
93
  defaultValues={defaultValues}
@@ -109,3 +140,15 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
109
140
  // Page display: FormBuilder already placed `summary` as the grid's conclusion column.
110
141
  return inner;
111
142
  }
143
+
144
+ /**
145
+ * FormRenderer — see {@link FormRendererRoot}. The compound statics drive the display-only
146
+ * **detail-tabs** view: `FormRenderer.Sidebar` (the rail) + `FormRenderer.Sidebar.Item` (a tab) +
147
+ * `FormRenderer.Tab` (a `FormBuilder.Section` panel).
148
+ */
149
+ export const FormRenderer = Object.assign(FormRendererRoot, {
150
+ Sidebar: DetailSidebar,
151
+ Tab: DetailTab,
152
+ Grid: DetailGrid,
153
+ Row: DetailRow,
154
+ });
@@ -2,3 +2,10 @@ export { FormRenderer } from "./form-renderer";
2
2
  export { FormDrawer } from "./FormDrawer";
3
3
  export type { FormDrawerProps } from "./FormDrawer";
4
4
  export type { FormRendererProps, FormRendererDisplay, FieldDirection } from "./types";
5
+ export type {
6
+ DetailSidebarProps,
7
+ DetailSidebarItemProps,
8
+ DetailTabProps,
9
+ DetailRowProps,
10
+ DetailGridProps,
11
+ } from "./detail";
@@ -23,7 +23,8 @@ export interface FormRendererProps<T extends FieldValues = FieldValues> {
23
23
  children: ReactNode;
24
24
 
25
25
  // --- react-hook-form root (forwarded to FormBuilder) ---
26
- onSubmit: (values: T) => void | Promise<void>;
26
+ /** Submit handler. Optional — a display-only detail-tabs view (`FormRenderer.Sidebar`) has no form. */
27
+ onSubmit?: (values: T) => void | Promise<void>;
27
28
  onInvalid?: (errors: FieldErrors<T>) => void;
28
29
  resolver?: Resolver<T>;
29
30
  defaultValues?: DefaultValues<T>;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.4.3",
2
+ "version": "2.4.4",
3
3
  "generatedBy": "scripts/bin/generateRegistry",
4
4
  "items": [
5
5
  {