torch-glare 2.4.1 → 2.4.3
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/Button.tsx +1 -1
- package/apps/lib/components/Card.tsx +47 -20
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/DatePicker.tsx +2 -0
- package/apps/lib/components/Drawer.tsx +66 -24
- package/apps/lib/components/FormBuilder/context.ts +68 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +60 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +59 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +11 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +74 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +148 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +39 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +132 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +31 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +83 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +31 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +91 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +157 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +67 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +41 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +308 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +213 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +43 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +279 -0
- package/apps/lib/components/FormBuilder/header.tsx +101 -0
- package/apps/lib/components/FormBuilder/index.ts +33 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +290 -0
- package/apps/lib/components/FormBuilder/submit.tsx +38 -0
- package/apps/lib/components/FormBuilder/types.ts +261 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +128 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +111 -0
- package/apps/lib/components/FormRenderer/index.ts +4 -0
- package/apps/lib/components/FormRenderer/types.ts +77 -0
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/ImageAttachment.tsx +36 -61
- package/apps/lib/components/Label.tsx +49 -42
- package/apps/lib/components/Popover.tsx +6 -2
- package/apps/lib/components/RadioCard.tsx +2 -0
- package/apps/lib/components/SearchableSelect.tsx +16 -5
- package/apps/lib/components/SectionBlock.tsx +16 -8
- package/apps/lib/components/Select.tsx +41 -120
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/components/Textarea.tsx +1 -1
- package/apps/lib/layouts/FieldSection.tsx +25 -22
- package/apps/lib/registry.json +51 -58
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/color.ts +175 -0
- 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/card.md +4 -2
- package/docs/components/chart-block-tool.md +5 -4
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/drawer.md +153 -102
- package/docs/components/form-builder.md +260 -0
- package/docs/components/form-renderer.md +234 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/image-attachment.md +10 -4
- package/docs/components/searchable-select.md +50 -46
- package/docs/components/table-dnd-wrapper.md +5 -3
- package/docs/components/text-editor.md +18 -0
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +411 -0
- package/docs/how-to/guides.md +153 -179
- package/docs/reference/tailwind-plugins.md +11 -1
- package/docs/tutorials/building-first-form.md +150 -159
- package/docs/tutorials/getting-started.md +9 -0
- package/package.json +1 -1
- /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
- /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
- /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
|
@@ -1,33 +1,60 @@
|
|
|
1
1
|
import { cn } from "../utils/cn";
|
|
2
2
|
import React, { HTMLAttributes } from "react";
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export const cardStyles = cva(
|
|
7
|
+
[
|
|
8
|
+
"flex flex-col justify-start",
|
|
9
|
+
"gap-2 rounded-[12px] border",
|
|
10
|
+
"transition-all ease-in-out duration-200",
|
|
11
|
+
"p-[16px]",
|
|
12
|
+
"border-border-presentation-global-primary",
|
|
13
|
+
"bg-background-presentation-form-radiocard-base",
|
|
14
|
+
"focus:border-border-presentation-state-focus",
|
|
15
|
+
],
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
/** Static container — no hover affordance. */
|
|
20
|
+
default: [],
|
|
21
|
+
/** Interactive card (a link/label/button surface) — highlights on hover. */
|
|
22
|
+
clickable: ["cursor-pointer", "hover:border-border-presentation-state-focus"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: "default",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
interface Props
|
|
32
|
+
extends
|
|
33
|
+
Omit<
|
|
34
|
+
HTMLAttributes<HTMLDivElement | HTMLHeadingElement | HTMLParagraphElement | HTMLLabelElement>,
|
|
35
|
+
"htmlFor"
|
|
36
|
+
>,
|
|
37
|
+
VariantProps<typeof cardStyles> {
|
|
9
38
|
as?: React.ElementType;
|
|
10
39
|
asChild?: boolean;
|
|
11
40
|
htmlFor?: string;
|
|
12
41
|
}
|
|
13
|
-
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A surface container. Defaults to a **static** card; pass `variant="clickable"` when the whole
|
|
45
|
+
* card is an interactive target so it gets the hover highlight + pointer cursor.
|
|
46
|
+
*/
|
|
47
|
+
export const Card = ({
|
|
48
|
+
className,
|
|
49
|
+
htmlFor,
|
|
50
|
+
asChild,
|
|
51
|
+
variant,
|
|
52
|
+
as: Tag = "section",
|
|
53
|
+
...props
|
|
54
|
+
}: Props) => {
|
|
14
55
|
const Component = asChild ? Slot : Tag;
|
|
15
56
|
return (
|
|
16
|
-
<Component
|
|
17
|
-
htmlFor={htmlFor}
|
|
18
|
-
{...props}
|
|
19
|
-
className={cn(
|
|
20
|
-
"flex flex-col justify-start",
|
|
21
|
-
"gap-2 rounded-[12px] border",
|
|
22
|
-
"transition-all ease-in-out duration-200",
|
|
23
|
-
"p-[16px]",
|
|
24
|
-
"border-border-presentation-global-primary",
|
|
25
|
-
"bg-background-presentation-form-radiocard-base",
|
|
26
|
-
"hover:border-border-presentation-state-focus",
|
|
27
|
-
"focus:border-border-presentation-state-focus",
|
|
28
|
-
className,
|
|
29
|
-
)}
|
|
30
|
-
/>
|
|
57
|
+
<Component htmlFor={htmlFor} {...props} className={cn(cardStyles({ variant }), className)} />
|
|
31
58
|
);
|
|
32
59
|
};
|
|
33
60
|
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
cloneElement,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactElement,
|
|
12
|
+
} from "react";
|
|
13
|
+
import * as Slider from "@radix-ui/react-slider";
|
|
14
|
+
|
|
15
|
+
import { cn } from "../utils/cn";
|
|
16
|
+
import { Themes } from "../utils/types";
|
|
17
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
|
|
18
|
+
import { Group, Input } from "./Input";
|
|
19
|
+
import {
|
|
20
|
+
clamp,
|
|
21
|
+
formatHex,
|
|
22
|
+
hslToRgb,
|
|
23
|
+
hsvToRgb,
|
|
24
|
+
parseHex,
|
|
25
|
+
rgbToHsl,
|
|
26
|
+
rgbToHsv,
|
|
27
|
+
round,
|
|
28
|
+
type HSV,
|
|
29
|
+
} from "../utils/color";
|
|
30
|
+
|
|
31
|
+
export interface ColorPickerProps {
|
|
32
|
+
/**
|
|
33
|
+
* The trigger — a single element that opens the picker (rendered via `asChild`, so it must
|
|
34
|
+
* forward its ref/props to a DOM node). ColorPicker renders **no trigger of its own**; it
|
|
35
|
+
* hands the current color to whatever you pass:
|
|
36
|
+
*
|
|
37
|
+
* - an **input** (native `<input>`/`<textarea>`, or any element already given a `value` prop)
|
|
38
|
+
* receives the hex as its `value`;
|
|
39
|
+
* - **anything else** receives the hex as its `children` — unless it already has children of
|
|
40
|
+
* its own, which are left untouched.
|
|
41
|
+
*/
|
|
42
|
+
children: ReactElement;
|
|
43
|
+
/** Current color as a hex string (`#rrggbb` or `#rrggbbaa`). Defaults to `#000000`. */
|
|
44
|
+
value?: string;
|
|
45
|
+
/** Called with the new hex string on every change. */
|
|
46
|
+
onChange?: (hex: string) => void;
|
|
47
|
+
/** Quick-pick swatches shown at the bottom of the panel. */
|
|
48
|
+
presets?: string[];
|
|
49
|
+
/** Show the opacity slider + opacity input and emit `#rrggbbaa` when < 100%. Default true. */
|
|
50
|
+
alpha?: boolean;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
theme?: Themes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Native form controls, or any element already driven by a `value` prop, take the hex as `value`. */
|
|
56
|
+
function isInputLike(el: ReactElement): boolean {
|
|
57
|
+
if (typeof el.type === "string") return el.type === "input" || el.type === "textarea";
|
|
58
|
+
return "value" in ((el.props ?? {}) as Record<string, unknown>);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Mode = "hex" | "rgb" | "hsl";
|
|
62
|
+
|
|
63
|
+
const HUE_GRADIENT =
|
|
64
|
+
"linear-gradient(to right,#f00 0%,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,#f00 100%)";
|
|
65
|
+
|
|
66
|
+
/** Small 8px checkerboard so transparency reads clearly behind swatches / the alpha track. */
|
|
67
|
+
export const CHECKERBOARD: React.CSSProperties = {
|
|
68
|
+
backgroundColor: "#fff",
|
|
69
|
+
backgroundImage:
|
|
70
|
+
"linear-gradient(45deg,#c8c8c8 25%,transparent 25%),linear-gradient(-45deg,#c8c8c8 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#c8c8c8 75%),linear-gradient(-45deg,transparent 75%,#c8c8c8 75%)",
|
|
71
|
+
backgroundSize: "8px 8px",
|
|
72
|
+
backgroundPosition: "0 0,0 4px,4px -4px,-4px 0",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const THUMB_CLS =
|
|
76
|
+
"block h-[16px] w-[16px] rounded-full border-2 border-white bg-transparent shadow-[0_1px_4px_0_rgba(0,0,0,0.45)] outline-none transition-transform hover:scale-110 focus:ring-2 focus:ring-white/70";
|
|
77
|
+
|
|
78
|
+
// Compact numeric inputs: allow shrinking below the base `min-w-[30px]` and drop the native
|
|
79
|
+
// number spinners so a full row (3 channels + opacity) never overflows the fixed-width panel.
|
|
80
|
+
const NUM_INPUT_CLS =
|
|
81
|
+
"min-w-0 px-0 text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A full Figma-style color picker: a draggable saturation/value area, hue slider,
|
|
85
|
+
* optional opacity slider, an eyedropper (where supported), switchable HEX/RGB/HSL
|
|
86
|
+
* inputs, and preset swatches — all opened from a field-styled swatch trigger.
|
|
87
|
+
*
|
|
88
|
+
* The value is always a hex string, kept `#rrggbb` while fully opaque and promoted to
|
|
89
|
+
* `#rrggbbaa` only when opacity drops below 100%.
|
|
90
|
+
*/
|
|
91
|
+
export const ColorPicker = forwardRef<HTMLElement, ColorPickerProps>(
|
|
92
|
+
({ children, value, onChange, presets, alpha = true, disabled, theme }, ref) => {
|
|
93
|
+
// Internal source of truth is HSV + alpha: keeping HSV (not RGB/hex) means dragging in the
|
|
94
|
+
// grey column or the black row doesn't discard the chosen hue.
|
|
95
|
+
const [hsv, setHsv] = useState<HSV>({ h: 0, s: 0, v: 0 });
|
|
96
|
+
const [a, setA] = useState(1);
|
|
97
|
+
const [mode, setMode] = useState<Mode>("hex");
|
|
98
|
+
const lastHexRef = useRef<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const rgb = useMemo(() => hsvToRgb(hsv), [hsv]);
|
|
101
|
+
const hsl = useMemo(() => rgbToHsl(rgb), [rgb]);
|
|
102
|
+
const hex = useMemo(() => formatHex({ ...rgb, a }), [rgb, a]);
|
|
103
|
+
const hex6 = useMemo(() => formatHex({ ...rgb, a: 1 }), [rgb]);
|
|
104
|
+
|
|
105
|
+
// Sync FROM the controlled value — but skip the echo of our own last emit to avoid loops.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!value) return;
|
|
108
|
+
if (value === lastHexRef.current) return;
|
|
109
|
+
const parsed = parseHex(value);
|
|
110
|
+
if (!parsed) return;
|
|
111
|
+
setHsv(rgbToHsv(parsed));
|
|
112
|
+
setA(parsed.a);
|
|
113
|
+
lastHexRef.current = value;
|
|
114
|
+
}, [value]);
|
|
115
|
+
|
|
116
|
+
const emit = useCallback(
|
|
117
|
+
(nextHsv: HSV, nextA: number) => {
|
|
118
|
+
setHsv(nextHsv);
|
|
119
|
+
setA(nextA);
|
|
120
|
+
const out = formatHex({ ...hsvToRgb(nextHsv), a: nextA });
|
|
121
|
+
lastHexRef.current = out;
|
|
122
|
+
onChange?.(out);
|
|
123
|
+
},
|
|
124
|
+
[onChange],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const applyHsv = useCallback((next: HSV, nextA = a) => emit(next, nextA), [emit, a]);
|
|
128
|
+
const applyRgba = useCallback(
|
|
129
|
+
(r: number, g: number, b: number, nextA = a) =>
|
|
130
|
+
emit(rgbToHsv({ r, g, b }), clamp(nextA, 0, 1)),
|
|
131
|
+
[emit, a],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// ── Saturation / value area ─────────────────────────────────────────────
|
|
135
|
+
const svRef = useRef<HTMLDivElement>(null);
|
|
136
|
+
const readSV = useCallback(
|
|
137
|
+
(clientX: number, clientY: number) => {
|
|
138
|
+
const el = svRef.current;
|
|
139
|
+
if (!el) return;
|
|
140
|
+
const rect = el.getBoundingClientRect();
|
|
141
|
+
const x = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
142
|
+
const y = clamp((clientY - rect.top) / rect.height, 0, 1);
|
|
143
|
+
applyHsv({ h: hsv.h, s: round(x * 100), v: round((1 - y) * 100) });
|
|
144
|
+
},
|
|
145
|
+
[applyHsv, hsv.h],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const onSVPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
149
|
+
if (disabled) return;
|
|
150
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
151
|
+
readSV(e.clientX, e.clientY);
|
|
152
|
+
};
|
|
153
|
+
const onSVPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
154
|
+
if (disabled || e.buttons !== 1) return;
|
|
155
|
+
readSV(e.clientX, e.clientY);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ── Eyedropper (Chromium/Edge only) ─────────────────────────────────────
|
|
159
|
+
const hasEyeDropper = typeof window !== "undefined" && "EyeDropper" in window;
|
|
160
|
+
const pickWithEyeDropper = async () => {
|
|
161
|
+
try {
|
|
162
|
+
const ED = (
|
|
163
|
+
window as unknown as {
|
|
164
|
+
EyeDropper: new () => { open: () => Promise<{ sRGBHex: string }> };
|
|
165
|
+
}
|
|
166
|
+
).EyeDropper;
|
|
167
|
+
const res = await new ED().open();
|
|
168
|
+
const parsed = parseHex(res.sRGBHex);
|
|
169
|
+
if (parsed) applyRgba(parsed.r, parsed.g, parsed.b, parsed.a);
|
|
170
|
+
} catch {
|
|
171
|
+
/* user cancelled — ignore */
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ── HEX input draft (free typing, commit on blur / Enter) ────────────────
|
|
176
|
+
const [hexDraft, setHexDraft] = useState(hex6);
|
|
177
|
+
const hexFocused = useRef(false);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!hexFocused.current) setHexDraft(hex6);
|
|
180
|
+
}, [hex6]);
|
|
181
|
+
const commitHex = () => {
|
|
182
|
+
const parsed = parseHex(hexDraft);
|
|
183
|
+
if (parsed) applyRgba(parsed.r, parsed.g, parsed.b, a);
|
|
184
|
+
else setHexDraft(hex6);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const opacityPct = round(a * 100);
|
|
188
|
+
|
|
189
|
+
// The trigger is the caller's child — ColorPicker renders none of its own. An input gets the
|
|
190
|
+
// hex as `value`; anything else gets it as `children` (unless it already has its own).
|
|
191
|
+
const trigger = useMemo(() => {
|
|
192
|
+
const childProps = (children.props ?? {}) as Record<string, unknown>;
|
|
193
|
+
|
|
194
|
+
if (isInputLike(children)) {
|
|
195
|
+
return cloneElement(children as ReactElement<Record<string, unknown>>, {
|
|
196
|
+
value: hex,
|
|
197
|
+
// A controlled input with no `onChange` warns in React — commit typed hex instead.
|
|
198
|
+
...(childProps.onChange
|
|
199
|
+
? {}
|
|
200
|
+
: {
|
|
201
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
202
|
+
const parsed = parseHex(e.target.value);
|
|
203
|
+
if (parsed) applyRgba(parsed.r, parsed.g, parsed.b, parsed.a);
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (childProps.children == null) {
|
|
210
|
+
return cloneElement(children as ReactElement<Record<string, unknown>>, {}, hex);
|
|
211
|
+
}
|
|
212
|
+
return children;
|
|
213
|
+
}, [children, hex, applyRgba]);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Popover>
|
|
217
|
+
<PopoverTrigger asChild disabled={disabled} ref={ref as React.Ref<HTMLButtonElement>}>
|
|
218
|
+
{trigger}
|
|
219
|
+
</PopoverTrigger>
|
|
220
|
+
|
|
221
|
+
<PopoverContent
|
|
222
|
+
variant="PresentationStyle"
|
|
223
|
+
theme={theme}
|
|
224
|
+
align="start"
|
|
225
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
226
|
+
className="w-[300px] max-h-fit overflow-visible p-0"
|
|
227
|
+
>
|
|
228
|
+
<div className="flex flex-col gap-3 p-3">
|
|
229
|
+
{/* Saturation / value */}
|
|
230
|
+
<div
|
|
231
|
+
ref={svRef}
|
|
232
|
+
onPointerDown={onSVPointerDown}
|
|
233
|
+
onPointerMove={onSVPointerMove}
|
|
234
|
+
className="relative h-[150px] w-full cursor-crosshair touch-none rounded-[6px]"
|
|
235
|
+
style={{ backgroundColor: `hsl(${hsv.h},100%,50%)` }}
|
|
236
|
+
>
|
|
237
|
+
<div
|
|
238
|
+
className="absolute inset-0 rounded-[6px]"
|
|
239
|
+
style={{ backgroundImage: "linear-gradient(to right,#fff,rgba(255,255,255,0))" }}
|
|
240
|
+
/>
|
|
241
|
+
<div
|
|
242
|
+
className="absolute inset-0 rounded-[6px]"
|
|
243
|
+
style={{ backgroundImage: "linear-gradient(to top,#000,rgba(0,0,0,0))" }}
|
|
244
|
+
/>
|
|
245
|
+
<span
|
|
246
|
+
className="absolute h-[14px] w-[14px] -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_1px_4px_0_rgba(0,0,0,0.45)]"
|
|
247
|
+
style={{
|
|
248
|
+
left: `${hsv.s}%`,
|
|
249
|
+
top: `${100 - hsv.v}%`,
|
|
250
|
+
backgroundColor: hex6,
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Eyedropper + hue (+ alpha) sliders */}
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
{hasEyeDropper && (
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
aria-label="Pick color from screen"
|
|
261
|
+
onClick={pickWithEyeDropper}
|
|
262
|
+
disabled={disabled}
|
|
263
|
+
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[6px] border border-border-presentation-action-primary text-content-presentation-action-light-secondary transition-colors hover:bg-background-presentation-form-field-hover"
|
|
264
|
+
>
|
|
265
|
+
<i className="ri-sip-line text-[16px]" />
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
270
|
+
<Slider.Root
|
|
271
|
+
className="relative flex h-[14px] w-full touch-none select-none items-center"
|
|
272
|
+
min={0}
|
|
273
|
+
max={360}
|
|
274
|
+
step={1}
|
|
275
|
+
value={[hsv.h]}
|
|
276
|
+
onValueChange={([h]) => applyHsv({ ...hsv, h })}
|
|
277
|
+
disabled={disabled}
|
|
278
|
+
>
|
|
279
|
+
<Slider.Track
|
|
280
|
+
className="relative h-[10px] grow rounded-full"
|
|
281
|
+
style={{ backgroundImage: HUE_GRADIENT }}
|
|
282
|
+
>
|
|
283
|
+
<Slider.Range className="absolute h-full rounded-full bg-transparent" />
|
|
284
|
+
</Slider.Track>
|
|
285
|
+
<Slider.Thumb
|
|
286
|
+
className={THUMB_CLS}
|
|
287
|
+
style={{ backgroundColor: `hsl(${hsv.h},100%,50%)` }}
|
|
288
|
+
/>
|
|
289
|
+
</Slider.Root>
|
|
290
|
+
|
|
291
|
+
{alpha && (
|
|
292
|
+
<Slider.Root
|
|
293
|
+
className="relative flex h-[14px] w-full touch-none select-none items-center"
|
|
294
|
+
min={0}
|
|
295
|
+
max={1}
|
|
296
|
+
step={0.01}
|
|
297
|
+
value={[a]}
|
|
298
|
+
onValueChange={([na]) => applyHsv(hsv, na)}
|
|
299
|
+
disabled={disabled}
|
|
300
|
+
>
|
|
301
|
+
<Slider.Track
|
|
302
|
+
className="relative h-[10px] grow overflow-hidden rounded-full"
|
|
303
|
+
style={CHECKERBOARD}
|
|
304
|
+
>
|
|
305
|
+
<div
|
|
306
|
+
className="absolute inset-0"
|
|
307
|
+
style={{
|
|
308
|
+
backgroundImage: `linear-gradient(to right,rgba(${rgb.r},${rgb.g},${rgb.b},0),rgb(${rgb.r},${rgb.g},${rgb.b}))`,
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
<Slider.Range className="absolute h-full rounded-full bg-transparent" />
|
|
312
|
+
</Slider.Track>
|
|
313
|
+
<Slider.Thumb className={THUMB_CLS} style={{ backgroundColor: hex }} />
|
|
314
|
+
</Slider.Root>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Model switch + numeric inputs */}
|
|
320
|
+
<div className="flex items-start gap-2">
|
|
321
|
+
<select
|
|
322
|
+
value={mode}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
onChange={(e) => setMode(e.target.value as Mode)}
|
|
325
|
+
className="h-[30px] w-[54px] shrink-0 rounded-[6px] border border-border-presentation-action-primary bg-background-presentation-form-field-primary px-1 typography-body-small-regular text-content-presentation-action-light-primary outline-none"
|
|
326
|
+
>
|
|
327
|
+
<option value="hex">HEX</option>
|
|
328
|
+
<option value="rgb">RGB</option>
|
|
329
|
+
<option value="hsl">HSL</option>
|
|
330
|
+
</select>
|
|
331
|
+
|
|
332
|
+
<div className="flex min-w-0 flex-1 items-center gap-1">
|
|
333
|
+
{mode === "hex" && (
|
|
334
|
+
<Group size="S" className="min-w-0 flex-1">
|
|
335
|
+
<Input
|
|
336
|
+
value={hexDraft}
|
|
337
|
+
disabled={disabled}
|
|
338
|
+
onFocus={() => (hexFocused.current = true)}
|
|
339
|
+
onChange={(e) => setHexDraft(e.target.value)}
|
|
340
|
+
onBlur={() => {
|
|
341
|
+
hexFocused.current = false;
|
|
342
|
+
commitHex();
|
|
343
|
+
}}
|
|
344
|
+
onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()}
|
|
345
|
+
className="min-w-0 text-center"
|
|
346
|
+
/>
|
|
347
|
+
</Group>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{mode === "rgb" &&
|
|
351
|
+
(["r", "g", "b"] as const).map((ch) => (
|
|
352
|
+
<Group key={ch} size="S" className="min-w-0 flex-1 px-0">
|
|
353
|
+
<Input
|
|
354
|
+
type="number"
|
|
355
|
+
min={0}
|
|
356
|
+
max={255}
|
|
357
|
+
disabled={disabled}
|
|
358
|
+
value={rgb[ch]}
|
|
359
|
+
onChange={(e) => {
|
|
360
|
+
const n = clamp(Number(e.target.value) || 0, 0, 255);
|
|
361
|
+
applyRgba(
|
|
362
|
+
ch === "r" ? n : rgb.r,
|
|
363
|
+
ch === "g" ? n : rgb.g,
|
|
364
|
+
ch === "b" ? n : rgb.b,
|
|
365
|
+
a,
|
|
366
|
+
);
|
|
367
|
+
}}
|
|
368
|
+
className={NUM_INPUT_CLS}
|
|
369
|
+
/>
|
|
370
|
+
</Group>
|
|
371
|
+
))}
|
|
372
|
+
|
|
373
|
+
{mode === "hsl" &&
|
|
374
|
+
(["h", "s", "l"] as const).map((ch) => (
|
|
375
|
+
<Group key={ch} size="S" className="min-w-0 flex-1 px-0">
|
|
376
|
+
<Input
|
|
377
|
+
type="number"
|
|
378
|
+
min={0}
|
|
379
|
+
max={ch === "h" ? 360 : 100}
|
|
380
|
+
disabled={disabled}
|
|
381
|
+
value={hsl[ch]}
|
|
382
|
+
onChange={(e) => {
|
|
383
|
+
const n = clamp(Number(e.target.value) || 0, 0, ch === "h" ? 360 : 100);
|
|
384
|
+
const next = { ...hsl, [ch]: n };
|
|
385
|
+
const r = hslToRgb(next);
|
|
386
|
+
applyRgba(r.r, r.g, r.b, a);
|
|
387
|
+
}}
|
|
388
|
+
className={NUM_INPUT_CLS}
|
|
389
|
+
/>
|
|
390
|
+
</Group>
|
|
391
|
+
))}
|
|
392
|
+
|
|
393
|
+
{alpha && (
|
|
394
|
+
<Group size="S" className="w-[48px] shrink-0 gap-0 px-0">
|
|
395
|
+
<Input
|
|
396
|
+
type="number"
|
|
397
|
+
min={0}
|
|
398
|
+
max={100}
|
|
399
|
+
disabled={disabled}
|
|
400
|
+
value={opacityPct}
|
|
401
|
+
onChange={(e) => {
|
|
402
|
+
const n = clamp(Number(e.target.value) || 0, 0, 100);
|
|
403
|
+
applyHsv(hsv, n / 100);
|
|
404
|
+
}}
|
|
405
|
+
className={NUM_INPUT_CLS}
|
|
406
|
+
/>
|
|
407
|
+
<span className="pr-1 typography-body-small-regular text-content-presentation-action-light-secondary">
|
|
408
|
+
%
|
|
409
|
+
</span>
|
|
410
|
+
</Group>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{/* Presets */}
|
|
416
|
+
{presets && presets.length > 0 && (
|
|
417
|
+
<div className="flex flex-wrap items-center gap-1.5 border-t border-border-presentation-global-primary pt-3">
|
|
418
|
+
{presets.map((c) => (
|
|
419
|
+
<button
|
|
420
|
+
key={c}
|
|
421
|
+
type="button"
|
|
422
|
+
aria-label={c}
|
|
423
|
+
disabled={disabled}
|
|
424
|
+
onClick={() => {
|
|
425
|
+
const parsed = parseHex(c);
|
|
426
|
+
if (parsed) applyRgba(parsed.r, parsed.g, parsed.b, parsed.a);
|
|
427
|
+
}}
|
|
428
|
+
style={{ backgroundColor: c }}
|
|
429
|
+
className="h-[20px] w-[20px] rounded-[4px] border border-border-presentation-action-primary transition-transform hover:scale-110"
|
|
430
|
+
/>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
</PopoverContent>
|
|
436
|
+
</Popover>
|
|
437
|
+
);
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
ColorPicker.displayName = "ColorPicker";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef, useState, ButtonHTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { Badge } from "./Badge";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
import { Themes } from "../utils/types";
|
|
7
|
+
|
|
8
|
+
export interface ConclusionHeaderProps extends Omit<
|
|
9
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
10
|
+
"onClick"
|
|
11
|
+
> {
|
|
12
|
+
/** The header text. */
|
|
13
|
+
label: ReactNode;
|
|
14
|
+
/** Controlled open state. Omit to let the header manage its own (uncontrolled). */
|
|
15
|
+
open?: boolean;
|
|
16
|
+
/** Initial open state when uncontrolled (default `true`). */
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
/** Called with the next open state when the header is clicked. */
|
|
19
|
+
onOpenChange?: (open: boolean) => void;
|
|
20
|
+
/** Disabled — greys the label and shows a "Disabled" badge instead of the chevron. */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Badge text shown when `disabled` (default `"Disabled"`). */
|
|
23
|
+
disabledLabel?: string;
|
|
24
|
+
theme?: Themes;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `ConclusionHeader` — a clickable section header, per the "Conclusion-Header-2.0" design.
|
|
30
|
+
*
|
|
31
|
+
* The surfaces are **button overlays**, not an input field: at rest the label stands alone;
|
|
32
|
+
* on hover the row gains a `Button/ContStyle-Hover` pill and the chevron a `Button/Secondary`
|
|
33
|
+
* box. The chevron is hidden at rest while open, shows sideways when closed (left in LTR,
|
|
34
|
+
* right in RTL), and flips to down on hover. When `disabled` the label greys out, truncates
|
|
35
|
+
* at 120px, and a gray-subtle `Badge` replaces the chevron.
|
|
36
|
+
*
|
|
37
|
+
* Direction is inherited from the ambient `dir` — the RTL design variants are the LTR ones
|
|
38
|
+
* mirrored, which the logical properties here produce automatically.
|
|
39
|
+
*
|
|
40
|
+
* Works controlled (`open` + `onOpenChange`) or uncontrolled.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <ConclusionHeader label="Total" defaultOpen onOpenChange={setOpen} />
|
|
44
|
+
* {open && <section>…</section>}
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const ConclusionHeader = forwardRef<HTMLButtonElement, ConclusionHeaderProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
label,
|
|
51
|
+
open: openProp,
|
|
52
|
+
defaultOpen = true,
|
|
53
|
+
onOpenChange,
|
|
54
|
+
disabled = false,
|
|
55
|
+
disabledLabel = "Disabled",
|
|
56
|
+
theme,
|
|
57
|
+
className,
|
|
58
|
+
...props
|
|
59
|
+
},
|
|
60
|
+
ref,
|
|
61
|
+
) => {
|
|
62
|
+
// Controlled when `open` is passed; otherwise manage state internally.
|
|
63
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
64
|
+
const open = openProp ?? internalOpen;
|
|
65
|
+
|
|
66
|
+
const toggle = () => {
|
|
67
|
+
if (disabled) return;
|
|
68
|
+
if (openProp === undefined) setInternalOpen((o) => !o);
|
|
69
|
+
onOpenChange?.(!open);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
{...props}
|
|
75
|
+
ref={ref}
|
|
76
|
+
type="button"
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
aria-expanded={disabled ? undefined : open}
|
|
79
|
+
data-theme={theme}
|
|
80
|
+
data-state={open ? "open" : "closed"}
|
|
81
|
+
onClick={toggle}
|
|
82
|
+
className={cn(
|
|
83
|
+
// The hover surface is ONE full-width pill (design: 216×28, r6); the chevron box
|
|
84
|
+
// sits flush inside its trailing end, 6px off the label.
|
|
85
|
+
"group/collapsible flex h-[28px] w-full items-center gap-[6px] overflow-hidden rounded-[6px]",
|
|
86
|
+
"transition-all duration-200 ease-in-out",
|
|
87
|
+
"outline-none focus-visible:ring-1 focus-visible:ring-border-presentation-state-focus",
|
|
88
|
+
disabled
|
|
89
|
+
? "cursor-not-allowed"
|
|
90
|
+
: // Design insets the label by 4px once the pill appears.
|
|
91
|
+
"cursor-pointer hover:bg-background-presentation-button-contstyle-hover hover:ps-[4px]",
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{disabled ? (
|
|
96
|
+
<>
|
|
97
|
+
{/* Design: label shrinks to 120px and the badge sits right beside it, not at the end. */}
|
|
98
|
+
<span
|
|
99
|
+
className={cn(
|
|
100
|
+
"max-w-[120px] shrink-0 truncate text-start",
|
|
101
|
+
"typography-headers-medium-medium",
|
|
102
|
+
"text-content-presentation-global-secondary",
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{label}
|
|
106
|
+
</span>
|
|
107
|
+
<Badge label={disabledLabel} badgeStyle="subtle" color="gray" size="S" />
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<span
|
|
112
|
+
className={cn(
|
|
113
|
+
"min-w-0 flex-1 truncate text-start",
|
|
114
|
+
// Design: En/Header/Medium/Medium — 18px / 510 / 132% / -0.18px.
|
|
115
|
+
"typography-headers-medium-medium",
|
|
116
|
+
"text-content-presentation-global-primary",
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{label}
|
|
120
|
+
</span>
|
|
121
|
+
|
|
122
|
+
{/* Chevron — bare when closed at rest; gains a Button/Secondary box on hover. */}
|
|
123
|
+
<span
|
|
124
|
+
className={cn(
|
|
125
|
+
"flex size-[28px] shrink-0 items-center justify-center rounded-[6px] p-[5px]",
|
|
126
|
+
"text-content-presentation-global-primary",
|
|
127
|
+
"transition-all duration-200 ease-in-out",
|
|
128
|
+
"group-hover/collapsible:bg-background-presentation-button-secondary",
|
|
129
|
+
// Hidden at rest while open; always visible when closed; hover reveals.
|
|
130
|
+
open ? "opacity-0 group-hover/collapsible:opacity-100" : "opacity-100",
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
<i
|
|
134
|
+
className={cn(
|
|
135
|
+
"ri-arrow-down-s-line text-[18px] transition-transform duration-200 ease-in-out",
|
|
136
|
+
// Down when open; sideways when closed (left LTR / right RTL); hover → down.
|
|
137
|
+
!open && "ltr:rotate-90 rtl:-rotate-90 group-hover/collapsible:!rotate-0",
|
|
138
|
+
)}
|
|
139
|
+
/>
|
|
140
|
+
</span>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</button>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
ConclusionHeader.displayName = "ConclusionHeader";
|
|
@@ -35,6 +35,8 @@ interface DatePickerProps extends HTMLAttributes<HTMLInputElement> {
|
|
|
35
35
|
calendarProps?: CalendarProps;
|
|
36
36
|
timePicker?: boolean;
|
|
37
37
|
value?: Date | Date[] | DateRange;
|
|
38
|
+
/** Transparent border/background so the picker blends into a table cell. */
|
|
39
|
+
onTable?: boolean;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export const DatePicker = forwardRef(
|