torch-glare 2.4.2 → 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.
- package/apps/lib/components/FormBuilder/context.ts +2 -5
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +2 -7
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +1 -18
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +1 -6
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +1 -3
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +10 -15
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +6 -32
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +2 -9
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +24 -23
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +1 -7
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +3 -13
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +1 -19
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +1 -6
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +1 -2
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +22 -26
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +5 -14
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +1 -7
- package/apps/lib/components/FormBuilder/form-builder.tsx +22 -36
- package/apps/lib/components/FormBuilder/header.tsx +2 -6
- package/apps/lib/components/FormBuilder/index.ts +1 -5
- package/apps/lib/components/FormBuilder/stepper.tsx +88 -15
- package/apps/lib/components/FormBuilder/submit.tsx +1 -4
- package/apps/lib/components/FormBuilder/types.ts +2 -5
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +9 -2
- package/apps/lib/components/FormRenderer/detail.tsx +207 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +55 -14
- package/apps/lib/components/FormRenderer/index.ts +7 -5
- package/apps/lib/components/FormRenderer/types.ts +2 -3
- package/apps/lib/components/Popover.tsx +6 -2
- package/apps/lib/components/SearchableSelect.tsx +7 -2
- package/apps/lib/layouts/FieldSection.tsx +25 -22
- package/apps/lib/registry.json +1 -1
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/dist/src/shared/tailwindInit.d.ts.map +1 -1
- package/dist/src/shared/tailwindInit.js +3 -0
- package/dist/src/shared/tailwindInit.js.map +1 -1
- package/docs/components/form-builder.md +22 -29
- package/docs/components/form-renderer.md +72 -11
- package/docs/components/searchable-select.md +50 -46
- package/docs/how-to/forms-with-form-builder.md +68 -11
- package/docs/reference/tailwind-plugins.md +11 -1
- package/docs/tutorials/getting-started.md +9 -0
- package/package.json +1 -1
- package/apps/lib/components/FormBuilder/DisplayField.tsx +0 -40
- package/apps/lib/components/FormBuilder/viewFormat.tsx +0 -137
|
@@ -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,10 +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
|
-
import { FormIdContext, LoadingContext
|
|
7
|
+
import { FormIdContext, LoadingContext } from "../FormBuilder/context";
|
|
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";
|
|
8
18
|
import { FormDrawer } from "./FormDrawer";
|
|
9
19
|
import type { FormRendererProps } from "./types";
|
|
10
20
|
|
|
@@ -16,8 +26,12 @@ import type { FormRendererProps } from "./types";
|
|
|
16
26
|
* FormRenderer never manufactures a Submit — you compose it and hand it to `actions`
|
|
17
27
|
* (`actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}`). It renders in the form's
|
|
18
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.
|
|
19
33
|
*/
|
|
20
|
-
|
|
34
|
+
function FormRendererRoot<T extends FieldValues = FieldValues>({
|
|
21
35
|
children,
|
|
22
36
|
id,
|
|
23
37
|
onSubmit,
|
|
@@ -25,7 +39,6 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
25
39
|
resolver,
|
|
26
40
|
defaultValues,
|
|
27
41
|
values,
|
|
28
|
-
mode = "edit",
|
|
29
42
|
loading,
|
|
30
43
|
resetOnSuccess,
|
|
31
44
|
fieldDirection,
|
|
@@ -52,16 +65,33 @@ 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}
|
|
63
94
|
values={values}
|
|
64
|
-
mode={mode}
|
|
65
95
|
loading={loading}
|
|
66
96
|
fieldDirection={effectiveDirection}
|
|
67
97
|
resetOnSuccess={resetOnSuccess}
|
|
@@ -73,7 +103,8 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
73
103
|
>
|
|
74
104
|
{useHeader && (
|
|
75
105
|
<FormBuilder.Header title={header!.title} label={header!.label} variant={header!.variant}>
|
|
76
|
-
{
|
|
106
|
+
{/* A stepper form prepends chevron Back/Next + a divider before the Submit. */}
|
|
107
|
+
{actions && <StepperActions>{actions}</StepperActions>}
|
|
77
108
|
</FormBuilder.Header>
|
|
78
109
|
)}
|
|
79
110
|
|
|
@@ -90,15 +121,13 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
90
121
|
badge={badge ?? header?.label}
|
|
91
122
|
variant={header?.variant}
|
|
92
123
|
summary={summary}
|
|
93
|
-
// The drawer header sits outside the `<form>`, so re-supply the form
|
|
94
|
-
// a bare `FormBuilder.Submit` reads (on the page it gets these from the
|
|
124
|
+
// The drawer header sits outside the `<form>`, so re-supply the form-id and loading
|
|
125
|
+
// context a bare `FormBuilder.Submit` reads (on the page it gets these from the tree).
|
|
95
126
|
actions={
|
|
96
127
|
actions ? (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
</LoadingContext.Provider>
|
|
101
|
-
</ModeContext.Provider>
|
|
128
|
+
<LoadingContext.Provider value={!!loading}>
|
|
129
|
+
<FormIdContext.Provider value={formId}>{actions}</FormIdContext.Provider>
|
|
130
|
+
</LoadingContext.Provider>
|
|
102
131
|
) : undefined
|
|
103
132
|
}
|
|
104
133
|
onOpenInNewTab={onOpenInNewTab}
|
|
@@ -111,3 +140,15 @@ export function FormRenderer<T extends FieldValues = FieldValues>({
|
|
|
111
140
|
// Page display: FormBuilder already placed `summary` as the grid's conclusion column.
|
|
112
141
|
return inner;
|
|
113
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
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { FormRenderer } from "./form-renderer";
|
|
2
2
|
export { FormDrawer } from "./FormDrawer";
|
|
3
3
|
export type { FormDrawerProps } from "./FormDrawer";
|
|
4
|
+
export type { FormRendererProps, FormRendererDisplay, FieldDirection } from "./types";
|
|
4
5
|
export type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
DetailSidebarProps,
|
|
7
|
+
DetailSidebarItemProps,
|
|
8
|
+
DetailTabProps,
|
|
9
|
+
DetailRowProps,
|
|
10
|
+
DetailGridProps,
|
|
11
|
+
} from "./detail";
|
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
* form's header / drawer action bar) where you place the Save.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
export type FormRendererMode = "edit" | "view";
|
|
19
18
|
export type FormRendererDisplay = "page" | "drawer";
|
|
20
19
|
export type FieldDirection = "horizontal" | "vertical";
|
|
21
20
|
|
|
@@ -24,12 +23,12 @@ export interface FormRendererProps<T extends FieldValues = FieldValues> {
|
|
|
24
23
|
children: ReactNode;
|
|
25
24
|
|
|
26
25
|
// --- react-hook-form root (forwarded to FormBuilder) ---
|
|
27
|
-
|
|
26
|
+
/** Submit handler. Optional — a display-only detail-tabs view (`FormRenderer.Sidebar`) has no form. */
|
|
27
|
+
onSubmit?: (values: T) => void | Promise<void>;
|
|
28
28
|
onInvalid?: (errors: FieldErrors<T>) => void;
|
|
29
29
|
resolver?: Resolver<T>;
|
|
30
30
|
defaultValues?: DefaultValues<T>;
|
|
31
31
|
values?: T;
|
|
32
|
-
mode?: FormRendererMode;
|
|
33
32
|
loading?: boolean;
|
|
34
33
|
resetOnSuccess?: boolean;
|
|
35
34
|
/** Row layout; defaults to vertical inside a drawer. */
|
|
@@ -239,10 +239,14 @@ const popoverStyles = cva(
|
|
|
239
239
|
"bg-background-system-body-primary",
|
|
240
240
|
"shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
|
|
241
241
|
],
|
|
242
|
+
// Adopts the DropdownMenu surface (`menuContentStyles`): backdrop-blurred, borderless
|
|
243
|
+
// rounded-14 panel with a soft ambient shadow — keeping the original background color.
|
|
242
244
|
PresentationStyle: [
|
|
243
|
-
"border-
|
|
245
|
+
"border-transparent",
|
|
246
|
+
"rounded-[14px]",
|
|
247
|
+
"backdrop-blur-[21px]",
|
|
244
248
|
"bg-background-presentation-form-base",
|
|
245
|
-
"shadow-[
|
|
249
|
+
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
246
250
|
],
|
|
247
251
|
},
|
|
248
252
|
overlayBlur: {
|
|
@@ -24,6 +24,9 @@ export interface SearchableSelectOption {
|
|
|
24
24
|
value: string;
|
|
25
25
|
label: string;
|
|
26
26
|
icon?: ReactNode;
|
|
27
|
+
/** Text used for client-side filtering. Defaults to `label` — set it to match on a subset
|
|
28
|
+
* (e.g. a country name only, when the label also shows a dial code). */
|
|
29
|
+
searchText?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
interface Props {
|
|
@@ -38,6 +41,7 @@ interface Props {
|
|
|
38
41
|
theme?: Themes;
|
|
39
42
|
dir?: string;
|
|
40
43
|
className?: string;
|
|
44
|
+
inputClassName?: string;
|
|
41
45
|
/** Transparent border/background so the trigger blends into a table cell. */
|
|
42
46
|
onTable?: boolean;
|
|
43
47
|
|
|
@@ -107,6 +111,7 @@ export function SearchableSelect({
|
|
|
107
111
|
theme,
|
|
108
112
|
dir,
|
|
109
113
|
className,
|
|
114
|
+
inputClassName,
|
|
110
115
|
onTable,
|
|
111
116
|
filterClientSide = true,
|
|
112
117
|
onSearchChange,
|
|
@@ -155,7 +160,7 @@ export function SearchableSelect({
|
|
|
155
160
|
if (!filterClientSide) return options;
|
|
156
161
|
const q = search.trim().toLowerCase();
|
|
157
162
|
if (!q) return options;
|
|
158
|
-
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
163
|
+
return options.filter((o) => (o.searchText ?? o.label).toLowerCase().includes(q));
|
|
159
164
|
}, [options, search, filterClientSide]);
|
|
160
165
|
|
|
161
166
|
const selectedOption = options.find((o) => o.value === value) ?? null;
|
|
@@ -201,7 +206,7 @@ export function SearchableSelect({
|
|
|
201
206
|
setOpen(true);
|
|
202
207
|
}}
|
|
203
208
|
onBlur={() => setSearching(false)}
|
|
204
|
-
className={cn("min-w-[100px] flex-1", {
|
|
209
|
+
className={cn("min-w-[100px] flex-1", inputClassName, {
|
|
205
210
|
"!h-[18px]": size === "XS",
|
|
206
211
|
"!h-[22px]": size === "S",
|
|
207
212
|
"!h-[24px]": size === "M",
|
|
@@ -31,32 +31,35 @@ export function FieldSection({
|
|
|
31
31
|
<section
|
|
32
32
|
{...props}
|
|
33
33
|
data-theme={theme}
|
|
34
|
-
className={cn(
|
|
35
|
-
"grid py-[16px] px-[12px] w-full max-w-[1200px] min-w-[0px] ",
|
|
36
|
-
direction === "vertical" && "grid-rows-[auto_1fr] gap-[12px]",
|
|
37
|
-
direction === "horizontal" && "grid-cols-[350px_1fr] gap-[24px]",
|
|
38
|
-
direction === "flexible" &&
|
|
39
|
-
"grid-rows-[auto_1fr] gap-[12px] lg:grid-cols-[350px_1fr] lg:grid-rows-[1fr] lg:gap-[24px]",
|
|
40
|
-
className,
|
|
41
|
-
)}
|
|
34
|
+
className={cn("w-full max-w-[1200px] min-w-[0px] @container", className)}
|
|
42
35
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
labelDirections={"horizontal"}
|
|
51
|
-
/>
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
"grid py-[16px] px-[12px] w-full min-w-[0px]",
|
|
39
|
+
direction === "vertical" && "grid-rows-[auto_1fr] gap-[12px]",
|
|
40
|
+
direction === "horizontal" && "grid-cols-[350px_1fr] gap-[24px]",
|
|
41
|
+
direction === "flexible" &&
|
|
42
|
+
"grid-rows-[auto_1fr] gap-[12px] @md:grid-cols-[350px_1fr] @md:grid-rows-[1fr] @md:gap-[24px]",
|
|
52
43
|
)}
|
|
44
|
+
>
|
|
45
|
+
{/* Fixed width section for labels */}
|
|
46
|
+
<div className="flex flex-col gap-[12px]">
|
|
47
|
+
{label && (
|
|
48
|
+
<Label
|
|
49
|
+
size={size}
|
|
50
|
+
label={label}
|
|
51
|
+
requiredLabel={requiredLabel}
|
|
52
|
+
labelDirections={"horizontal"}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
{secondaryLabel && <Label size={size} secondaryLabel={secondaryLabel} />}
|
|
57
|
+
{childrenUnderLabel}
|
|
58
|
+
</div>
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
{/* Flexible section that takes up the remaining space */}
|
|
61
|
+
<div className="grid grid-cols-1 place-items-end gap-[12px]">{children}</div>
|
|
62
|
+
</div>
|
|
60
63
|
</section>
|
|
61
64
|
);
|
|
62
65
|
}
|
package/apps/lib/registry.json
CHANGED