torch-glare 2.5.2 → 2.5.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/ActionButton.tsx +39 -6
- package/apps/lib/components/Badge.tsx +5 -2
- package/apps/lib/components/BadgeField.tsx +1 -1
- package/apps/lib/components/Button.tsx +4 -4
- package/apps/lib/components/Calendar.tsx +7 -17
- package/apps/lib/components/ColorPicker.tsx +1 -1
- package/apps/lib/components/DataViews/data-views.tsx +1 -1
- package/apps/lib/components/DataViews/filters/filters.tsx +1 -1
- package/apps/lib/components/DataViews/header.tsx +5 -2
- package/apps/lib/components/DataViews/panel/section.tsx +3 -1
- package/apps/lib/components/DataViews/views/board-view.tsx +3 -1
- package/apps/lib/components/DataViews/views/inbox-view.tsx +4 -1
- package/apps/lib/components/DataViews/views/pane-views.tsx +5 -1
- package/apps/lib/components/DataViews/views/table-view.tsx +67 -8
- package/apps/lib/components/DataViews/views/tree-view.tsx +5 -3
- package/apps/lib/components/Drawer.tsx +18 -1
- package/apps/lib/components/FormBuilder/context.ts +9 -1
- package/apps/lib/components/FormBuilder/submit.tsx +11 -2
- package/apps/lib/components/FormBuilder/types.ts +5 -1
- package/apps/lib/components/FormRenderer/form-renderer.tsx +15 -3
- package/apps/lib/components/FormRenderer/stepper.tsx +35 -14
- package/apps/lib/components/FormRenderer/types.ts +1 -1
- package/apps/lib/components/Input.tsx +19 -4
- package/apps/lib/components/Popover.tsx +93 -55
- package/apps/lib/components/SearchableSelect.tsx +12 -11
- package/apps/lib/components/SearchableTree.tsx +10 -11
- package/apps/lib/components/SearchableTreeDialog.tsx +10 -11
- package/apps/lib/components/SectionBlock.tsx +14 -2
- package/apps/lib/components/Select.tsx +35 -11
- package/apps/lib/components/SimpleSelect.tsx +2 -2
- package/apps/lib/components/SlideDatePicker.tsx +17 -4
- package/apps/lib/components/Stepper.tsx +329 -180
- package/apps/lib/components/Switch.tsx +2 -2
- package/apps/lib/components/Table.tsx +55 -30
- package/apps/lib/components/Textarea.tsx +29 -4
- package/apps/lib/components/TreeDropDown.tsx +18 -6
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +61 -61
- package/apps/lib/registry.json +7 -16
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/docs/components/action-button.md +3 -1
- package/docs/components/data-views/index.md +7 -0
- package/docs/components/form-builder.md +4 -2
- package/docs/components/form-renderer.md +1 -1
- package/docs/components/stepper.md +119 -24
- package/docs/components/table.md +33 -0
- package/docs/how-to/forms-with-form-builder.md +3 -1
- package/docs/reference/tailwind-plugins.md +42 -0
- package/docs/tutorials/getting-started.md +32 -11
- package/package.json +1 -1
- package/apps/lib/components/FormStepper.tsx +0 -272
- package/docs/components/form-stepper.md +0 -250
|
@@ -4,12 +4,24 @@ import { Button } from "./Button";
|
|
|
4
4
|
import { cn } from "../utils/cn";
|
|
5
5
|
import { ButtonVariant, Themes } from "../utils/types";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The button that sits at the end of an input field — Figma `ActionButton` (Type=DropDown).
|
|
9
|
+
*
|
|
10
|
+
* Sizes are Figma's exactly: Xs 18, S 22, M 32, with `radius/sm` on the two small ones and
|
|
11
|
+
* `radius/lg` on M.
|
|
12
|
+
*
|
|
13
|
+
* The `!` on the radius is load-bearing. `twMerge` does not dedupe `rounded-radius-*` — they are
|
|
14
|
+
* custom scale keys, not values it recognises — so this radius and the one coming from `Button`'s
|
|
15
|
+
* own `size` variant both survive, and CSS source order picks the winner. Tailwind emits the scale
|
|
16
|
+
* in config order (sm before md before lg), so without `!` size S would render Button M's 6px
|
|
17
|
+
* instead of its own 4px.
|
|
18
|
+
*/
|
|
19
|
+
const buttonVariants = cva("", {
|
|
8
20
|
variants: {
|
|
9
21
|
size: {
|
|
10
|
-
XS: "h-[18px] w-[18px] text-[12px]",
|
|
11
|
-
S: "h-[22px] w-[22px] text-[12px]",
|
|
12
|
-
M: "h-[32px] w-[32px] text-[18px]",
|
|
22
|
+
XS: "h-[18px] w-[18px] text-[12px] !rounded-radius-sm",
|
|
23
|
+
S: "h-[22px] w-[22px] text-[12px] !rounded-radius-sm",
|
|
24
|
+
M: "h-[32px] w-[32px] text-[18px] !rounded-radius-lg",
|
|
13
25
|
},
|
|
14
26
|
},
|
|
15
27
|
defaultVariants: {
|
|
@@ -17,6 +29,23 @@ const buttonVariants = cva("!rounded-[4px]", {
|
|
|
17
29
|
},
|
|
18
30
|
});
|
|
19
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Default/hover/disabled colours as Figma binds them. Applied only when the caller passes no
|
|
34
|
+
* `variant`, so anything asking for a specific Button variant still gets it.
|
|
35
|
+
*
|
|
36
|
+
* `action-secondary` and `action-disabled` already match `Button`'s `PrimeStyle` equivalents; the
|
|
37
|
+
* real difference is hover, where `PrimeStyle` uses `button-hover` and the design uses
|
|
38
|
+
* `action-hover`.
|
|
39
|
+
*/
|
|
40
|
+
const actionTokens = [
|
|
41
|
+
"bg-background-presentation-action-secondary",
|
|
42
|
+
"text-content-presentation-action-light-primary",
|
|
43
|
+
"hover:bg-background-presentation-action-hover",
|
|
44
|
+
"hover:text-content-presentation-global-hover",
|
|
45
|
+
"disabled:bg-background-presentation-action-disabled",
|
|
46
|
+
"disabled:text-content-presentation-state-disabled",
|
|
47
|
+
];
|
|
48
|
+
|
|
20
49
|
interface Props
|
|
21
50
|
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
22
51
|
is_loading?: boolean;
|
|
@@ -29,7 +58,6 @@ interface Props
|
|
|
29
58
|
export const ActionButton = function ({
|
|
30
59
|
size,
|
|
31
60
|
asChild,
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from {...props} spread
|
|
33
61
|
as: Tag = "button",
|
|
34
62
|
className,
|
|
35
63
|
variant,
|
|
@@ -42,14 +70,19 @@ export const ActionButton = function ({
|
|
|
42
70
|
<Button
|
|
43
71
|
theme={theme}
|
|
44
72
|
asChild={asChild}
|
|
73
|
+
// Forwarded, not swallowed: the chevron inside a field renders `as="span"` because it is
|
|
74
|
+
// nested in a clickable trigger, and a <button> cannot contain a <button>.
|
|
75
|
+
as={Tag}
|
|
45
76
|
buttonType="icon"
|
|
46
|
-
type
|
|
77
|
+
// `type` is only meaningful on a real button; on a span it would be an invalid attribute.
|
|
78
|
+
type={Tag === "button" ? type : undefined}
|
|
47
79
|
size={size == "XS" ? "S" : size == "S" ? "M" : size == "M" ? "L" : "S"}
|
|
48
80
|
variant={variant}
|
|
49
81
|
className={cn(
|
|
50
82
|
buttonVariants({
|
|
51
83
|
size,
|
|
52
84
|
}),
|
|
85
|
+
!variant && actionTokens,
|
|
53
86
|
className,
|
|
54
87
|
)}
|
|
55
88
|
{...props}
|
|
@@ -118,7 +118,10 @@ const subtleCompoundVariants = [
|
|
|
118
118
|
export const badgeStyles = cva(
|
|
119
119
|
[
|
|
120
120
|
"inline-flex items-center justify-center w-fit",
|
|
121
|
-
|
|
121
|
+
// `radius/md` (6px), read off the Badge master component in Figma. Note the badge instances
|
|
122
|
+
// nested inside InnerLabelField render at `radius/lg` — that is an instance override, not the
|
|
123
|
+
// component's own value, and reading it as the spec is how this briefly shipped as 8px.
|
|
124
|
+
"rounded-radius-md",
|
|
122
125
|
"transition-all duration-200 ease-in-out",
|
|
123
126
|
"whitespace-nowrap",
|
|
124
127
|
"[&_i]:!leading-none",
|
|
@@ -215,7 +218,7 @@ export const Badge: React.FC<BadgeProps> = ({
|
|
|
215
218
|
}
|
|
216
219
|
}}
|
|
217
220
|
className={cn(
|
|
218
|
-
"rounded-
|
|
221
|
+
"rounded-radius-sm",
|
|
219
222
|
"flex items-center justify-center cursor-pointer",
|
|
220
223
|
"hover:bg-background-presentation-action-secondary",
|
|
221
224
|
"transition-colors duration-150",
|
|
@@ -222,7 +222,7 @@ export const BadgeField = forwardRef<HTMLInputElement, Props>(
|
|
|
222
222
|
fill="black"
|
|
223
223
|
/>
|
|
224
224
|
</svg>
|
|
225
|
-
<p className="text-black-1000 text-
|
|
225
|
+
<p className="text-black-1000 text-end text-[12px] font-[510] leading-[148%]">
|
|
226
226
|
{addLabel}
|
|
227
227
|
</p>
|
|
228
228
|
</div>
|
|
@@ -248,10 +248,10 @@ export const buttonVariants = cva(
|
|
|
248
248
|
],
|
|
249
249
|
},
|
|
250
250
|
size: {
|
|
251
|
-
S: "h-[22px] py-[2px] px-[6px] typography-body-small-medium rounded-
|
|
252
|
-
M: "h-[28px] py-[2px] px-[14px] typography-body-large-medium rounded-
|
|
253
|
-
L: "h-[34px] py-[5px] px-[22px] typography-body-large-medium rounded-
|
|
254
|
-
XL: "h-[40px] py-[8px] px-[30px] typography-headers-medium-medium rounded-
|
|
251
|
+
S: "h-[22px] py-[2px] px-[6px] typography-body-small-medium rounded-radius-sm [&_i]:text-[12px]",
|
|
252
|
+
M: "h-[28px] py-[2px] px-[14px] typography-body-large-medium rounded-radius-md [&_i]:text-[18px]",
|
|
253
|
+
L: "h-[34px] py-[5px] px-[22px] typography-body-large-medium rounded-radius-lg [&_i]:text-[20px]",
|
|
254
|
+
XL: "h-[40px] py-[8px] px-[30px] typography-headers-medium-medium rounded-radius-lg [&_i]:text-[22px]",
|
|
255
255
|
},
|
|
256
256
|
is_loading: {
|
|
257
257
|
true: "[&_i]:hidden",
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { DayPicker } from "react-day-picker";
|
|
5
5
|
import "react-day-picker/style.css";
|
|
6
6
|
import { cn } from "../utils/cn";
|
|
7
|
-
import {
|
|
7
|
+
import { ActionButton } from "./ActionButton";
|
|
8
8
|
import { SimpleSelectValue, SimpleSelectItem } from "./SimpleSelect";
|
|
9
9
|
|
|
10
10
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
|
@@ -30,24 +30,14 @@ const Calendar = ({
|
|
|
30
30
|
Nav: (e) => {
|
|
31
31
|
return (
|
|
32
32
|
<div className="w-full flex items-center justify-between absolute top-0 left-0 p-[6px]">
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
variant="PrimeStyle"
|
|
37
|
-
size="M"
|
|
38
|
-
className="w-[26px] h-[26px]"
|
|
39
|
-
>
|
|
33
|
+
{/* Popover navigation rather than an in-field button, so it keeps its own 26px box
|
|
34
|
+
and only adopts the shared action colours and radius. */}
|
|
35
|
+
<ActionButton onClick={e.onPreviousClick} size="S" className="w-[26px] h-[26px]">
|
|
40
36
|
<i className="ri-arrow-left-s-line"></i>
|
|
41
|
-
</
|
|
42
|
-
<
|
|
43
|
-
onClick={e.onNextClick}
|
|
44
|
-
buttonType="icon"
|
|
45
|
-
variant="PrimeStyle"
|
|
46
|
-
size="M"
|
|
47
|
-
className="w-[26px] h-[26px]"
|
|
48
|
-
>
|
|
37
|
+
</ActionButton>
|
|
38
|
+
<ActionButton onClick={e.onNextClick} size="S" className="w-[26px] h-[26px]">
|
|
49
39
|
<i className="ri-arrow-right-s-line"></i>
|
|
50
|
-
</
|
|
40
|
+
</ActionButton>
|
|
51
41
|
</div>
|
|
52
42
|
);
|
|
53
43
|
},
|
|
@@ -404,7 +404,7 @@ export const ColorPicker = forwardRef<HTMLElement, ColorPickerProps>(
|
|
|
404
404
|
}}
|
|
405
405
|
className={NUM_INPUT_CLS}
|
|
406
406
|
/>
|
|
407
|
-
<span className="
|
|
407
|
+
<span className="pe-1 typography-body-small-regular text-content-presentation-action-light-secondary">
|
|
408
408
|
%
|
|
409
409
|
</span>
|
|
410
410
|
</Group>
|
|
@@ -311,7 +311,7 @@ function DataViewsRoot({
|
|
|
311
311
|
inherits it rather than landing on the black shell.
|
|
312
312
|
|
|
313
313
|
No border: Figma draws one, but every view already brings its own edge — the
|
|
314
|
-
table
|
|
314
|
+
table and the split views each draw their own panel border — so it only ever
|
|
315
315
|
read as a second outline around the first. */}
|
|
316
316
|
<div className="bg-background-presentation-form-base flex flex-1 overflow-hidden rounded-[16px]">
|
|
317
317
|
{/* Clip the scrollable surface to the parent radius minus its 1px border
|
|
@@ -95,7 +95,7 @@ function FiltersRoot({
|
|
|
95
95
|
) : (
|
|
96
96
|
// Reading the colour from a token rather than a `text-white` literal is what lets
|
|
97
97
|
// the same controls sit in the dark rail and in the light content area.
|
|
98
|
-
<h3 className="text-content-presentation-global-primary text-[18px] font-[510] leading-[1.32] tracking-[-0.01em]">
|
|
98
|
+
<h3 className="text-content-presentation-global-primary min-w-0 truncate text-[18px] font-[510] leading-[1.32] tracking-[-0.01em]">
|
|
99
99
|
{title}
|
|
100
100
|
</h3>
|
|
101
101
|
))}
|
|
@@ -44,8 +44,11 @@ export function Header({ title, children, className }: HeaderProps) {
|
|
|
44
44
|
>
|
|
45
45
|
{title !== undefined && (
|
|
46
46
|
<>
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
{/* `min-w-0`, not `shrink-0`: the pill has to be able to give up width, or a long title
|
|
48
|
+
pushes the view switch, actions and panel toggle off the end of the bar. It yields
|
|
49
|
+
first and the title ellipsises instead. */}
|
|
50
|
+
<div className="flex h-9 min-w-0 items-center gap-2 rounded-[12px] border border-[#434446] bg-[#252729] px-[10px]">
|
|
51
|
+
<span className="truncate text-[28px] font-[510] uppercase leading-[1.19] text-white">
|
|
49
52
|
{title}
|
|
50
53
|
</span>
|
|
51
54
|
</div>
|
|
@@ -40,7 +40,9 @@ export function Section({
|
|
|
40
40
|
aria-controls={bodyId}
|
|
41
41
|
/>
|
|
42
42
|
) : (
|
|
43
|
-
|
|
43
|
+
// No `min-w-0` needed here, unlike the other headings: the parent is `flex flex-col`,
|
|
44
|
+
// so this already stretches to the column's width rather than sizing to its text.
|
|
45
|
+
<h3 className="text-content-presentation-global-primary truncate text-[18px] font-[510] leading-[1.32] tracking-[-0.01em]">
|
|
44
46
|
{title}
|
|
45
47
|
</h3>
|
|
46
48
|
))}
|
|
@@ -356,7 +356,9 @@ function ColumnHeader({
|
|
|
356
356
|
COLUMN_BG[group.color ?? "gray"],
|
|
357
357
|
)}
|
|
358
358
|
>
|
|
359
|
-
|
|
359
|
+
{/* `min-w-0` because this is a flex item: without it the heading refuses to shrink below its
|
|
360
|
+
text and a long group name pushes the action button out of the column. */}
|
|
361
|
+
<h3 className="typography-headers-small-medium text-content-presentation-global-primary-light min-w-0 truncate">
|
|
360
362
|
{group.label}
|
|
361
363
|
</h3>
|
|
362
364
|
{onAction && (
|
|
@@ -67,9 +67,12 @@ function InboxViewImpl({
|
|
|
67
67
|
)}
|
|
68
68
|
>
|
|
69
69
|
<li className="border-border-presentation-global-primary border-b px-3 py-2">
|
|
70
|
+
{/* `block` before `truncate`: the parent is a plain `<li>`, so this span would otherwise
|
|
71
|
+
be inline — and `overflow` has no effect on an inline box, which would leave the
|
|
72
|
+
ellipsis silently doing nothing. */}
|
|
70
73
|
<span
|
|
71
74
|
style={{ fontFeatureSettings: "'cv05' on" }}
|
|
72
|
-
className="typography-display-medium-medium text-content-presentation-global-primary uppercase"
|
|
75
|
+
className="typography-display-medium-medium text-content-presentation-global-primary block truncate uppercase"
|
|
73
76
|
>
|
|
74
77
|
{titleField?.label ?? "inbox"}
|
|
75
78
|
</span>
|
|
@@ -134,7 +134,11 @@ function useIsActive(element: { value?: string }, defaultValue: string) {
|
|
|
134
134
|
export const PaneTable = markPaneView(function PaneTable(props: TreePaneTableProps) {
|
|
135
135
|
const active = useIsActive({ value: props.value }, "table");
|
|
136
136
|
if (!active) return null;
|
|
137
|
-
|
|
137
|
+
// Standalone, `TableView` draws its own border and radius so it reads as a separated surface.
|
|
138
|
+
// In here it is not standalone: the pane around it already draws exactly that border and radius
|
|
139
|
+
// (`tree-view.tsx`), so leaving the table's on nests one inside the other a pixel apart.
|
|
140
|
+
// `TreePaneTableProps` omits `className`, so there is no caller value to merge with.
|
|
141
|
+
return <TableView {...withoutTabProps(props)} className="rounded-none border-0" />;
|
|
138
142
|
}, { defaultValue: "table", defaultLabel: "List", defaultIcon: <Table2 /> });
|
|
139
143
|
|
|
140
144
|
/**
|
|
@@ -115,7 +115,15 @@ function TableViewImpl({
|
|
|
115
115
|
|
|
116
116
|
return (
|
|
117
117
|
<DndContext {...contextProps}>
|
|
118
|
-
|
|
118
|
+
{/* Its own edge, matching the inbox and tree panels: this view is a surface on the shell,
|
|
119
|
+
not a fill of it. `overflow-hidden` keeps the rows clipped to the radius. */}
|
|
120
|
+
<div
|
|
121
|
+
className={cn(
|
|
122
|
+
"bg-background-presentation-form-base flex h-full overflow-hidden",
|
|
123
|
+
"border-border-presentation-global-primary rounded-[16px] border",
|
|
124
|
+
className,
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
119
127
|
<div className="flex flex-1 flex-col gap-4 overflow-hidden">
|
|
120
128
|
<div
|
|
121
129
|
ref={scrollRef}
|
|
@@ -124,13 +132,42 @@ function TableViewImpl({
|
|
|
124
132
|
// pushes the whole component wide instead of scrolling within it.
|
|
125
133
|
className="min-w-0 flex-1 overflow-auto rounded-lg"
|
|
126
134
|
>
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
{/* One width for the table and the footer bar below it. Inside an `overflow-auto`
|
|
136
|
+
scroller a child's `w-full` resolves against the *visible* width, not the scrollable
|
|
137
|
+
width — so without this box the footer stopped at the fold whenever the columns
|
|
138
|
+
overflowed. `w-max` sizes to the table (the widest child); `min-w-full` keeps it
|
|
139
|
+
filling the scroller when the table is narrower. */}
|
|
140
|
+
<div className="w-max min-w-full">
|
|
141
|
+
{/* `overflow-visible` opts out of the primitive's `overflow-hidden` (tailwind-merge
|
|
142
|
+
lets ours win). It has to: a clipping table is its own scrollport, and the sticky
|
|
143
|
+
header would then resolve against a box that never scrolls. Safe here because the
|
|
144
|
+
scroller above is `min-w-0 overflow-auto`, so a wide table scrolls inside it rather
|
|
145
|
+
than pushing the layout. */}
|
|
146
|
+
<Table ref={tableRef} className="w-full overflow-visible">
|
|
147
|
+
{/* The header sticks to the scroller above by default — that lives on `TableHeader`
|
|
148
|
+
in the primitive. Two things this view adds:
|
|
149
|
+
|
|
150
|
+
Opacity. The primitive's header token is translucent, so scrolling rows read
|
|
151
|
+
straight through it; the primitive can't fix that without naming a surface colour
|
|
152
|
+
it doesn't know. Here the surface *is* known — the view root above sets
|
|
153
|
+
`form-base` — so paint that as the background-color (tailwind-merge drops the
|
|
154
|
+
primitive's translucent one, same `bg-color` group) and re-apply the tint as a
|
|
155
|
+
background-image, which stacks above background-color. Composited that is the
|
|
156
|
+
exact colour the header already had, just no longer see-through.
|
|
157
|
+
|
|
158
|
+
And `shadow-none`, because the drop shadow reads as a seam now that the view
|
|
159
|
+
carries its own border. */}
|
|
160
|
+
<TableHeader className="bg-background-presentation-form-base bg-[image:linear-gradient(var(--background-presentation-form-header),var(--background-presentation-form-header))] shadow-none">
|
|
129
161
|
<TableRow>
|
|
130
162
|
{onRowMove && <TableHead isDummy className="w-8" />}
|
|
131
163
|
{selectable && (
|
|
132
164
|
<TableHead isDummy className="w-12">
|
|
165
|
+
{/* Size is stated on both this and the per-row checkbox rather than left to
|
|
166
|
+
`Checkbox`'s default — they have to agree, and relying on the default on
|
|
167
|
+
one side only is what previously made the select-all bigger than the
|
|
168
|
+
column it heads. */}
|
|
133
169
|
<Checkbox
|
|
170
|
+
size="M"
|
|
134
171
|
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
|
135
172
|
onCheckedChange={toggleAll}
|
|
136
173
|
aria-label="Select all rows"
|
|
@@ -149,7 +186,11 @@ function TableViewImpl({
|
|
|
149
186
|
// so a screen reader hears one identical button per column.
|
|
150
187
|
sortLabel={field.label ?? formatPathLabel(field.path)}
|
|
151
188
|
>
|
|
152
|
-
{
|
|
189
|
+
{/* Wrapped rather than handed to `Table` as bare text: `truncate` needs a box
|
|
190
|
+
to clip, and the label's parent in the primitive is already `flex min-w-0`
|
|
191
|
+
so this span shrinks and ellipsises instead of wrapping the header row
|
|
192
|
+
onto a second line. */}
|
|
193
|
+
<span className="truncate">{field.label ?? field.path}</span>
|
|
153
194
|
</TableHead>
|
|
154
195
|
))}
|
|
155
196
|
</TableRow>
|
|
@@ -198,12 +239,12 @@ function TableViewImpl({
|
|
|
198
239
|
>
|
|
199
240
|
{onRowMove && <GripCell />}
|
|
200
241
|
{selectable && (
|
|
201
|
-
<TableCell isDummy className="h-[40px] w-12" onClick={(e) => e.stopPropagation()}>
|
|
242
|
+
<TableCell isDummy className="min-h-[40px] w-12" onClick={(e) => e.stopPropagation()}>
|
|
202
243
|
{/* What `TableCheckbox` renders, inlined: its props are typed as button
|
|
203
244
|
attributes, so it cannot express a controlled checkbox. */}
|
|
204
245
|
<div className="flex items-center justify-center">
|
|
205
246
|
<Checkbox
|
|
206
|
-
size="
|
|
247
|
+
size="M"
|
|
207
248
|
checked={selected}
|
|
208
249
|
onCheckedChange={() => toggleRow(id)}
|
|
209
250
|
aria-label="Select row"
|
|
@@ -215,8 +256,20 @@ function TableViewImpl({
|
|
|
215
256
|
// `undefined` means "you paint it" — distinct from `null`, which is a
|
|
216
257
|
// deliberate blank.
|
|
217
258
|
const custom = renderCell?.({ field, row, id, index, fields: visibleFields });
|
|
259
|
+
// `fade={false}` and clip instead. The fade is a `mask-image` on every
|
|
260
|
+
// cell, close to free on its own but very expensive once a sticky header
|
|
261
|
+
// repaints the region each scroll frame: measured on this page,
|
|
262
|
+
// mask+sticky costs ~61ms/frame against ~29ms with the mask off and
|
|
263
|
+
// ~17ms with neither. `overflow-hidden` is passed back because
|
|
264
|
+
// `fade={false}` otherwise switches the cell to `overflow-visible`, and
|
|
265
|
+
// the text would spill into the next column.
|
|
218
266
|
return (
|
|
219
|
-
<TableCell
|
|
267
|
+
<TableCell
|
|
268
|
+
key={`${field.path}-${i}`}
|
|
269
|
+
className="min-h-[40px]"
|
|
270
|
+
fade={false}
|
|
271
|
+
childrenClassName="overflow-hidden"
|
|
272
|
+
>
|
|
220
273
|
{/* `isolate` confines the Badge's mix-blend-luminosity to a local
|
|
221
274
|
stacking context and `transform-gpu` promotes it to its own layer, so
|
|
222
275
|
the table's post-mount column reflow repaints cleanly instead of
|
|
@@ -257,6 +310,7 @@ function TableViewImpl({
|
|
|
257
310
|
{addRowLabel}
|
|
258
311
|
</TableEndAction>
|
|
259
312
|
)}
|
|
313
|
+
</div>
|
|
260
314
|
</div>
|
|
261
315
|
</div>
|
|
262
316
|
</div>
|
|
@@ -389,7 +443,12 @@ function SkeletonRows({
|
|
|
389
443
|
{grip && <TableCell isDummy className="h-[40px] w-8" />}
|
|
390
444
|
{selectable && <TableCell isDummy className="h-[40px] w-12" />}
|
|
391
445
|
{fields.map((field, i) => (
|
|
392
|
-
<TableCell
|
|
446
|
+
<TableCell
|
|
447
|
+
key={`${field.path}-${i}`}
|
|
448
|
+
className="min-h-[40px]"
|
|
449
|
+
fade={false}
|
|
450
|
+
childrenClassName="overflow-hidden"
|
|
451
|
+
>
|
|
393
452
|
{/* Widths cycle rather than repeat, so a column of bars reads as text that has not
|
|
394
453
|
arrived instead of as a grid. */}
|
|
395
454
|
<SkeletonBar className={["w-[70%]", "w-[45%]", "w-[85%]", "w-[60%]"][(row + i) % 4]} />
|
|
@@ -253,8 +253,8 @@ function TreeViewImpl({
|
|
|
253
253
|
|
|
254
254
|
return (
|
|
255
255
|
// A split view, so the gap between the rail and the pane is part of the design: it paints the
|
|
256
|
-
// shell's black over the Master Container's surface, and the two cards float on it. The
|
|
257
|
-
//
|
|
256
|
+
// shell's black over the Master Container's surface, and the two cards float on it. The board
|
|
257
|
+
// fills its container instead — the table now carries its own border, like these panels.
|
|
258
258
|
<div className={cn("flex h-full gap-2 bg-black", className)}>
|
|
259
259
|
<div
|
|
260
260
|
className={cn(
|
|
@@ -265,9 +265,11 @@ function TreeViewImpl({
|
|
|
265
265
|
)}
|
|
266
266
|
>
|
|
267
267
|
<div className="border-border-presentation-global-primary border-b px-3 py-2">
|
|
268
|
+
{/* `block` before `truncate` — the parent is a plain div, so an inline span would clip
|
|
269
|
+
nothing. Same reason as the inbox's panel header. */}
|
|
268
270
|
<span
|
|
269
271
|
style={{ fontFeatureSettings: "'cv05' on" }}
|
|
270
|
-
className="typography-display-medium-medium text-content-presentation-global-primary uppercase"
|
|
272
|
+
className="typography-display-medium-medium text-content-presentation-global-primary block truncate uppercase"
|
|
271
273
|
>
|
|
272
274
|
{labelField?.label ?? "categories"}
|
|
273
275
|
</span>
|
|
@@ -37,6 +37,7 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
|
|
37
37
|
interface DrawerContentProps extends React.ComponentPropsWithoutRef<
|
|
38
38
|
typeof DrawerPrimitive.Content
|
|
39
39
|
> {
|
|
40
|
+
/** @deprecated No effect here — the drag handle is `DrawerPanel`'s `showHandle`. */
|
|
40
41
|
showHandle?: boolean;
|
|
41
42
|
notch?: React.ReactNode;
|
|
42
43
|
notchSide?: "left" | "right";
|
|
@@ -87,7 +88,17 @@ const DrawerPanel = React.forwardRef<HTMLDivElement, DrawerPanelProps>(
|
|
|
87
88
|
// would render white-on-light = invisible).
|
|
88
89
|
data-theme="light"
|
|
89
90
|
className={cn(
|
|
90
|
-
|
|
91
|
+
// `flex-col` is load-bearing: this panel stacks header / body / footer, and its own
|
|
92
|
+
// `showHandle` centres the drag handle with `mx-auto`, which only centres in a column.
|
|
93
|
+
// It was dropped in 37fddd5 alongside the tray's — on the tray that was deliberate (FormDrawer
|
|
94
|
+
// puts the panel and its summary side by side), here it was not, and every consumer that puts
|
|
95
|
+
// a header/body/footer straight into a panel rendered them in a row.
|
|
96
|
+
//
|
|
97
|
+
// `min-w-0` for the same reason, one axis over: the tray is a row now, so width is its
|
|
98
|
+
// main axis, and a flex item's default `min-width: auto` pins it to its content — a wide
|
|
99
|
+
// form pushed the panel straight out past the tray. `min-h-0` alone covered the old
|
|
100
|
+
// column tray; the row needs both.
|
|
101
|
+
"flex flex-1 flex-col gap-2 rounded-t-[16px] p-1 bg-[#F0F0F0] min-h-0 min-w-0",
|
|
91
102
|
framed && "border border-[#D4D4D4] shadow-[inset_0_-4px_16px_rgba(0,0,0,0.1)]",
|
|
92
103
|
className,
|
|
93
104
|
)}
|
|
@@ -113,6 +124,12 @@ const DrawerContent = React.forwardRef<
|
|
|
113
124
|
framed: framedProp,
|
|
114
125
|
wrapperClassName,
|
|
115
126
|
trayClassName,
|
|
127
|
+
// Destructured only to keep it out of `...props`. The handle belongs to `DrawerPanel` now, so
|
|
128
|
+
// this does nothing here — but it was never pulled out either, so it was being spread onto the
|
|
129
|
+
// DOM node and React warned "does not recognize the `showHandle` prop on a DOM element" every
|
|
130
|
+
// time a drawer opened. Kept in the props type rather than removed: consumers still pass it.
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
132
|
+
showHandle,
|
|
116
133
|
...props
|
|
117
134
|
},
|
|
118
135
|
ref,
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* How a field row lays out its label against its control.
|
|
7
|
+
*
|
|
8
|
+
* `"flexible"` is the responsive one: stacked, then label-beside-control once the field row itself
|
|
9
|
+
* is past the container `md` breakpoint. It is what you get by leaving `fieldDirection` unset — and
|
|
10
|
+
* it is assignable so you can ask for it back where something else defaults you away from it, most
|
|
11
|
+
* notably a `FormRenderer` drawer, which pins `"vertical"`.
|
|
12
|
+
*/
|
|
13
|
+
export type FieldDirection = "horizontal" | "vertical" | "flexible";
|
|
6
14
|
|
|
7
15
|
/** Loading flag — drives the Submit spinner and disables inputs. */
|
|
8
16
|
export const LoadingContext = createContext<boolean>(false);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
5
6
|
import { Button } from "../Button";
|
|
6
7
|
import { useFormId, useLoading } from "./context";
|
|
7
8
|
|
|
@@ -28,9 +29,17 @@ export function SubmitButton({ children, className, loadingText, form }: SubmitB
|
|
|
28
29
|
// Defaults to the enclosing form's id, so a Save in the header (outside the `<form>`)
|
|
29
30
|
// still submits it via native form-association.
|
|
30
31
|
form={form ?? ctxFormId}
|
|
31
|
-
|
|
32
|
+
// The blue fill, because Save is the form's primary action and a primary action is blue.
|
|
33
|
+
// Deliberately not a prop: a form has one primary action, and letting each caller pick a
|
|
34
|
+
// variant is how the rule stops being a rule.
|
|
35
|
+
variant="BluColStyle"
|
|
32
36
|
is_loading={loading}
|
|
33
|
-
|
|
37
|
+
// `w-fit` because the FormBuilder root is a flex COLUMN: a direct child with `width: auto`
|
|
38
|
+
// inherits `align-items: stretch` and spans the whole form. Sections want that (SectionBlock
|
|
39
|
+
// sets its own `w-full`); a Save button does not. `w-fit` rather than `self-start` so the
|
|
40
|
+
// header action bar — a row with `items-center` — is untouched at every button size.
|
|
41
|
+
// Merged through `cn` so a caller can still opt into `className="w-full"` in a narrow drawer.
|
|
42
|
+
className={cn("w-fit", className)}
|
|
34
43
|
>
|
|
35
44
|
{loading ? (loadingText ?? children ?? "Saving…") : (children ?? "Save")}
|
|
36
45
|
</Button>
|
|
@@ -276,7 +276,11 @@ export interface FormBuilderRootProps<T extends FieldValues = FieldValues> {
|
|
|
276
276
|
values?: T;
|
|
277
277
|
/** Loading flag — Submit shows a spinner and inputs disable. */
|
|
278
278
|
loading?: boolean;
|
|
279
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* Field row direction. Unset means `"flexible"` — stacked, then label-beside-control once the
|
|
281
|
+
* field row passes the container `md` breakpoint. `FormRenderer` pins `"vertical"` in a drawer;
|
|
282
|
+
* pass `"flexible"` there to get the responsive layout back.
|
|
283
|
+
*/
|
|
280
284
|
fieldDirection?: FieldDirection;
|
|
281
285
|
/** Reset to defaults after a successful submit. */
|
|
282
286
|
resetOnSuccess?: boolean;
|
|
@@ -159,10 +159,22 @@ function FormRendererRoot<T extends FieldValues = FieldValues>({
|
|
|
159
159
|
// inside it: a form embedded in a 260px rail has no room to give up 96px to gutters.
|
|
160
160
|
const fieldsColumn = <div className="mx-auto w-full max-w-[1100px] px-[48px]">{formEl}</div>;
|
|
161
161
|
|
|
162
|
-
// Inside the form surface: the stepper rail beside the fields. Columns never wrap — the
|
|
163
|
-
//
|
|
162
|
+
// Inside the form surface: the stepper rail beside the fields. Columns never wrap — the layout
|
|
163
|
+
// stays side-by-side at every screen size; when space runs short the fields column shrinks and
|
|
164
|
+
// the rail's labels truncate.
|
|
165
|
+
//
|
|
166
|
+
// `minmax(180px,1fr)` and not a bare `1fr` on the side tracks. `1fr` means `minmax(auto,1fr)`,
|
|
167
|
+
// whose *auto* minimum stops a track shrinking below its content — so a long step label made the
|
|
168
|
+
// rail's track wider than the empty track opposite it, and the two side tracks being equal is the
|
|
169
|
+
// only thing centring the middle column. Hovering a step made it visibly worse: the label's
|
|
170
|
+
// padding grows 6px→9px on hover (FormStepper), so the centre jumped 3px every time.
|
|
171
|
+
//
|
|
172
|
+
// The minimum is a fixed 180px rather than `auto` precisely so it cannot depend on the labels:
|
|
173
|
+
// both side tracks size identically whatever the rail holds, so the middle column stays centred,
|
|
174
|
+
// and the rail still has room to be legible (a plain `minmax(0,…)` let it collapse to 0 in a
|
|
175
|
+
// narrow preview frame). Past that floor the labels truncate instead of pushing.
|
|
164
176
|
const bodyInner = isStepper ? (
|
|
165
|
-
<div className="grid w-full grid-cols-[
|
|
177
|
+
<div className="grid w-full grid-cols-[minmax(180px,1fr)_minmax(0,1100px)_minmax(180px,1fr)] gap-8">
|
|
166
178
|
<StepperNav control={formInstance.control as Control<FieldValues>} />
|
|
167
179
|
{fieldsColumn}
|
|
168
180
|
{/* Empty third column — balances the rail's gutter so the middle column is centred. */}
|
|
@@ -10,7 +10,16 @@ import { Button } from "../Button";
|
|
|
10
10
|
// stepper — the rail, the state, Back/Next — is chrome and lives here. The dependency only ever
|
|
11
11
|
// points this way: FormRenderer → FormBuilder, never back.
|
|
12
12
|
import { StepContext, type StepRegistry } from "../FormBuilder/context";
|
|
13
|
-
|
|
13
|
+
// Aliased: this module exports its own `Stepper` and `Step` (the wizard behaviour —
|
|
14
|
+
// `FormRenderer.Stepper` / `.Step`), which would otherwise collide with the presentational
|
|
15
|
+
// component of the same name.
|
|
16
|
+
import {
|
|
17
|
+
Stepper as StepperRail,
|
|
18
|
+
Step as StepPill,
|
|
19
|
+
StepIndicator,
|
|
20
|
+
StepLabel,
|
|
21
|
+
StepConnector,
|
|
22
|
+
} from "../Stepper";
|
|
14
23
|
|
|
15
24
|
// ─── Stepper state context ───────────────────────────────────────────────────
|
|
16
25
|
|
|
@@ -113,7 +122,10 @@ function StepperNav({ control }: { control: Control<FieldValues> }) {
|
|
|
113
122
|
[...(stepFields[index] ?? [])].some((name) => name in errors);
|
|
114
123
|
|
|
115
124
|
return (
|
|
116
|
-
|
|
125
|
+
// `min-w-0` so the rail can be squeezed: its grid track no longer grows to fit a long label
|
|
126
|
+
// (see form-renderer.tsx), so the column has to be allowed to shrink and let the labels
|
|
127
|
+
// truncate instead of spilling over the fields column.
|
|
128
|
+
<StepperRail activeStep={currentStep} orientation="vertical" className="min-w-0 shrink-0">
|
|
117
129
|
{titles.map((title, index) => {
|
|
118
130
|
// The step buttons ARE the navigation: click to move. Backward is free;
|
|
119
131
|
// clicking forward validates the steps in between (goToStep) and stops at
|
|
@@ -126,21 +138,30 @@ function StepperNav({ control }: { control: Control<FieldValues> }) {
|
|
|
126
138
|
: "default";
|
|
127
139
|
return (
|
|
128
140
|
<React.Fragment key={title}>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
{/* The shrink/truncate pair is passed in rather than changed in `Stepper`, so its
|
|
142
|
+
other consumers keep sizing to their labels. `shrink` beats the pill's own
|
|
143
|
+
`shrink-0` and `truncate` beats the label's `whitespace-nowrap` — both via
|
|
144
|
+
tailwind-merge, which `Stepper` runs the caller's className through. */}
|
|
145
|
+
<StepPill
|
|
146
|
+
index={index}
|
|
147
|
+
type={type}
|
|
148
|
+
onClick={() => void goToStep(index)}
|
|
149
|
+
// `max-w-full` is what actually makes the label truncate: the rail is
|
|
150
|
+
// `items-start`, so without a cap the pill shrink-wraps its label and grows straight
|
|
151
|
+
// past the track instead of clipping inside it.
|
|
152
|
+
className="min-w-0 max-w-full shrink"
|
|
153
|
+
>
|
|
154
|
+
<StepIndicator />
|
|
155
|
+
{/* `max-w-none` opts out of the component's 106px cap: the rail sizes to its own
|
|
156
|
+
grid track, so the label should truncate there rather than 106px early. */}
|
|
157
|
+
<StepLabel className="max-w-none truncate">{title}</StepLabel>
|
|
158
|
+
</StepPill>
|
|
159
|
+
{/* Connector between steps — the component centres it under the indicator per size. */}
|
|
160
|
+
{index < titles.length - 1 && <StepConnector />}
|
|
140
161
|
</React.Fragment>
|
|
141
162
|
);
|
|
142
163
|
})}
|
|
143
|
-
</
|
|
164
|
+
</StepperRail>
|
|
144
165
|
);
|
|
145
166
|
}
|
|
146
167
|
|