@timeax/form-palette 0.0.1
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/.scaffold-cache.json +537 -0
- package/package.json +42 -0
- package/src/.scaffold-cache.json +544 -0
- package/src/adapters/axios.ts +117 -0
- package/src/adapters/index.ts +91 -0
- package/src/adapters/inertia.ts +187 -0
- package/src/core/adapter-registry.ts +87 -0
- package/src/core/bound/bind-host.ts +14 -0
- package/src/core/bound/observe-bound-field.ts +172 -0
- package/src/core/bound/wait-for-bound-field.ts +57 -0
- package/src/core/context.ts +23 -0
- package/src/core/core-provider.tsx +818 -0
- package/src/core/core-root.tsx +72 -0
- package/src/core/core-shell.tsx +44 -0
- package/src/core/errors/error-strip.tsx +71 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/map-error-bag.ts +51 -0
- package/src/core/errors/map-zod.ts +39 -0
- package/src/core/hooks/use-button.ts +220 -0
- package/src/core/hooks/use-core-context.ts +20 -0
- package/src/core/hooks/use-core-utility.ts +0 -0
- package/src/core/hooks/use-core.ts +13 -0
- package/src/core/hooks/use-field.ts +497 -0
- package/src/core/hooks/use-optional-field.ts +28 -0
- package/src/core/index.ts +0 -0
- package/src/core/registry/binder-registry.ts +82 -0
- package/src/core/registry/field-registry.ts +187 -0
- package/src/core/test.tsx +17 -0
- package/src/global.d.ts +14 -0
- package/src/index.ts +68 -0
- package/src/input/index.ts +4 -0
- package/src/input/input-field.tsx +854 -0
- package/src/input/input-layout-graph.ts +230 -0
- package/src/input/input-props.ts +190 -0
- package/src/lib/get-global-countries.ts +87 -0
- package/src/lib/utils.ts +6 -0
- package/src/presets/index.ts +0 -0
- package/src/presets/shadcn-preset.ts +0 -0
- package/src/presets/shadcn-variants/checkbox.tsx +849 -0
- package/src/presets/shadcn-variants/chips.tsx +756 -0
- package/src/presets/shadcn-variants/color.tsx +284 -0
- package/src/presets/shadcn-variants/custom.tsx +227 -0
- package/src/presets/shadcn-variants/date.tsx +796 -0
- package/src/presets/shadcn-variants/file.tsx +764 -0
- package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
- package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
- package/src/presets/shadcn-variants/number.tsx +176 -0
- package/src/presets/shadcn-variants/password.tsx +737 -0
- package/src/presets/shadcn-variants/phone.tsx +628 -0
- package/src/presets/shadcn-variants/radio.tsx +578 -0
- package/src/presets/shadcn-variants/select.tsx +956 -0
- package/src/presets/shadcn-variants/slider.tsx +622 -0
- package/src/presets/shadcn-variants/text.tsx +343 -0
- package/src/presets/shadcn-variants/textarea.tsx +66 -0
- package/src/presets/shadcn-variants/toggle.tsx +218 -0
- package/src/presets/shadcn-variants/treeselect.tsx +784 -0
- package/src/presets/ui/badge.tsx +46 -0
- package/src/presets/ui/button.tsx +60 -0
- package/src/presets/ui/calendar.tsx +214 -0
- package/src/presets/ui/checkbox.tsx +115 -0
- package/src/presets/ui/custom.tsx +0 -0
- package/src/presets/ui/dialog.tsx +141 -0
- package/src/presets/ui/field.tsx +246 -0
- package/src/presets/ui/input-mask.tsx +739 -0
- package/src/presets/ui/input-otp.tsx +77 -0
- package/src/presets/ui/input.tsx +1011 -0
- package/src/presets/ui/label.tsx +22 -0
- package/src/presets/ui/number.tsx +1370 -0
- package/src/presets/ui/popover.tsx +46 -0
- package/src/presets/ui/radio-group.tsx +43 -0
- package/src/presets/ui/scroll-area.tsx +56 -0
- package/src/presets/ui/select.tsx +190 -0
- package/src/presets/ui/separator.tsx +28 -0
- package/src/presets/ui/slider.tsx +61 -0
- package/src/presets/ui/switch.tsx +32 -0
- package/src/presets/ui/textarea.tsx +634 -0
- package/src/presets/ui/time-dropdowns.tsx +350 -0
- package/src/schema/adapter.ts +217 -0
- package/src/schema/core.ts +429 -0
- package/src/schema/field-map.ts +0 -0
- package/src/schema/field.ts +224 -0
- package/src/schema/index.ts +0 -0
- package/src/schema/input-field.ts +260 -0
- package/src/schema/presets.ts +0 -0
- package/src/schema/variant.ts +216 -0
- package/src/variants/core/checkbox.tsx +54 -0
- package/src/variants/core/chips.tsx +22 -0
- package/src/variants/core/color.tsx +16 -0
- package/src/variants/core/custom.tsx +18 -0
- package/src/variants/core/date.tsx +25 -0
- package/src/variants/core/file.tsx +9 -0
- package/src/variants/core/keyvalue.tsx +12 -0
- package/src/variants/core/multiselect.tsx +28 -0
- package/src/variants/core/number.tsx +115 -0
- package/src/variants/core/password.tsx +35 -0
- package/src/variants/core/phone.tsx +16 -0
- package/src/variants/core/radio.tsx +38 -0
- package/src/variants/core/select.tsx +15 -0
- package/src/variants/core/slider.tsx +55 -0
- package/src/variants/core/text.tsx +114 -0
- package/src/variants/core/textarea.tsx +22 -0
- package/src/variants/core/toggle.tsx +50 -0
- package/src/variants/core/treeselect.tsx +11 -0
- package/src/variants/helpers/selection-summary.tsx +236 -0
- package/src/variants/index.ts +75 -0
- package/src/variants/registry.ts +38 -0
- package/src/variants/select-shared.ts +0 -0
- package/src/variants/shared.ts +126 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// src/presets/shadcn-variants/slider.tsx
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Slider } from "@/presets/ui/slider";
|
|
7
|
+
|
|
8
|
+
type SliderValue = number | undefined;
|
|
9
|
+
|
|
10
|
+
type Size = "sm" | "md" | "lg";
|
|
11
|
+
type Density = "compact" | "comfortable" | "loose";
|
|
12
|
+
|
|
13
|
+
export interface ShadcnSliderVariantProps
|
|
14
|
+
extends Pick<
|
|
15
|
+
VariantBaseProps<SliderValue>,
|
|
16
|
+
| "value"
|
|
17
|
+
| "onValue"
|
|
18
|
+
| "error"
|
|
19
|
+
| "disabled"
|
|
20
|
+
| "readOnly"
|
|
21
|
+
| "size"
|
|
22
|
+
| "density"
|
|
23
|
+
> {
|
|
24
|
+
/**
|
|
25
|
+
* Minimum value for the slider.
|
|
26
|
+
* Default: 0
|
|
27
|
+
*/
|
|
28
|
+
min?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maximum value for the slider.
|
|
32
|
+
* Default: 100
|
|
33
|
+
*/
|
|
34
|
+
max?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Step between values.
|
|
38
|
+
* Default: 1
|
|
39
|
+
*/
|
|
40
|
+
step?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Show the current value as text next to the slider.
|
|
44
|
+
* Default: true
|
|
45
|
+
*/
|
|
46
|
+
showValue?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Where to place the value label, relative to the slider.
|
|
50
|
+
* - "end" → right of the slider (horizontal)
|
|
51
|
+
* - "start" → left of the slider
|
|
52
|
+
*
|
|
53
|
+
* Default: "end"
|
|
54
|
+
*/
|
|
55
|
+
valuePlacement?: "start" | "end";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Custom formatter for the numeric value.
|
|
59
|
+
* If omitted, uses the raw number.
|
|
60
|
+
*/
|
|
61
|
+
formatValue?: (value: SliderValue) => React.ReactNode;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wrapper class for the entire slider field.
|
|
65
|
+
*/
|
|
66
|
+
className?: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extra classes for the Slider root.
|
|
70
|
+
*/
|
|
71
|
+
sliderClassName?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extra classes for the value label.
|
|
75
|
+
*/
|
|
76
|
+
valueClassName?: string;
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────
|
|
79
|
+
// Icons & controls (mirror text / select variants)
|
|
80
|
+
// ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One or more icons displayed inside the slider region, on the left.
|
|
84
|
+
*
|
|
85
|
+
* If not provided and `icon` is set, that single icon
|
|
86
|
+
* is treated as `leadingIcons[0]`.
|
|
87
|
+
*/
|
|
88
|
+
leadingIcons?: React.ReactNode[];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Icons displayed on the right side of the slider region
|
|
92
|
+
* (before/after the value label depending on placement).
|
|
93
|
+
*/
|
|
94
|
+
trailingIcons?: React.ReactNode[];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convenience single-icon prop for the left side.
|
|
98
|
+
*/
|
|
99
|
+
icon?: React.ReactNode;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Base gap between icons and slider/value.
|
|
103
|
+
* Defaults to 4px-ish via `gap-1`.
|
|
104
|
+
*/
|
|
105
|
+
iconGap?: number;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extra spacing to apply between leading icons and the slider track.
|
|
109
|
+
*/
|
|
110
|
+
leadingIconSpacing?: number;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extra spacing to apply between trailing icons and the value label.
|
|
114
|
+
*/
|
|
115
|
+
trailingIconSpacing?: number;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Arbitrary React node rendered before the slider (e.g. a button).
|
|
119
|
+
*/
|
|
120
|
+
leadingControl?: React.ReactNode;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Arbitrary React node rendered after the slider (e.g. a button).
|
|
124
|
+
*/
|
|
125
|
+
trailingControl?: React.ReactNode;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extra classes for the leading control wrapper.
|
|
129
|
+
*/
|
|
130
|
+
leadingControlClassName?: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extra classes for the trailing control wrapper.
|
|
134
|
+
*/
|
|
135
|
+
trailingControlClassName?: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* If true and there are controls, the slider + controls share
|
|
139
|
+
* a single visual box (borders, radius, focus states).
|
|
140
|
+
* Default: true (to match text/select behaviour).
|
|
141
|
+
*/
|
|
142
|
+
joinControls?: boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* When joinControls is true, whether the box styling extends over controls
|
|
146
|
+
* (true) or controls are visually separate (false).
|
|
147
|
+
* Default: true.
|
|
148
|
+
*/
|
|
149
|
+
extendBoxToControls?: boolean;
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────
|
|
152
|
+
// Built-in +/- control variants
|
|
153
|
+
// ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Built-in +/- controls around the slider.
|
|
157
|
+
*
|
|
158
|
+
* - "none" → no built-in step buttons (default)
|
|
159
|
+
* - "boxed" → +/- inside the same frame as the slider
|
|
160
|
+
* - "edge" → loose layout: "- [ slider ] +"
|
|
161
|
+
*/
|
|
162
|
+
controlVariant?: "none" | "boxed" | "edge";
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Step used when clicking the +/- controls.
|
|
166
|
+
* Defaults to `step`.
|
|
167
|
+
*/
|
|
168
|
+
controlStep?: number;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Custom node for the decrement control. Default: "−".
|
|
172
|
+
*/
|
|
173
|
+
controlDecrementIcon?: React.ReactNode;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Custom node for the increment control. Default: "+".
|
|
177
|
+
*/
|
|
178
|
+
controlIncrementIcon?: React.ReactNode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─────────────────────────────────────────────
|
|
182
|
+
// Helpers
|
|
183
|
+
// ─────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function sliderHeight(size?: Size): string {
|
|
186
|
+
switch (size) {
|
|
187
|
+
case "sm":
|
|
188
|
+
return "h-7 text-xs";
|
|
189
|
+
case "lg":
|
|
190
|
+
return "h-10 text-base";
|
|
191
|
+
case "md":
|
|
192
|
+
default:
|
|
193
|
+
return "h-9 text-sm";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sliderPadding(density?: Density): string {
|
|
198
|
+
switch (density) {
|
|
199
|
+
case "compact":
|
|
200
|
+
return "py-1";
|
|
201
|
+
case "loose":
|
|
202
|
+
return "py-3";
|
|
203
|
+
case "comfortable":
|
|
204
|
+
default:
|
|
205
|
+
return "py-2";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function defaultFormatValue(value: SliderValue): React.ReactNode {
|
|
210
|
+
if (value == null) return "—";
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function clampToRange(v: number, min: number, max: number): number {
|
|
215
|
+
if (v < min) return min;
|
|
216
|
+
if (v > max) return max;
|
|
217
|
+
return v;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─────────────────────────────────────────────
|
|
221
|
+
// Component
|
|
222
|
+
// ─────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export const ShadcnSliderVariant = React.forwardRef<
|
|
225
|
+
HTMLDivElement,
|
|
226
|
+
ShadcnSliderVariantProps
|
|
227
|
+
>(function ShadcnSliderVariant(props, _ref) {
|
|
228
|
+
const {
|
|
229
|
+
value,
|
|
230
|
+
onValue,
|
|
231
|
+
error,
|
|
232
|
+
disabled,
|
|
233
|
+
readOnly,
|
|
234
|
+
size,
|
|
235
|
+
density,
|
|
236
|
+
|
|
237
|
+
min = 0,
|
|
238
|
+
max = 100,
|
|
239
|
+
step = 1,
|
|
240
|
+
|
|
241
|
+
showValue = true,
|
|
242
|
+
valuePlacement = "end",
|
|
243
|
+
formatValue,
|
|
244
|
+
|
|
245
|
+
className,
|
|
246
|
+
sliderClassName,
|
|
247
|
+
valueClassName,
|
|
248
|
+
|
|
249
|
+
// Icons & controls
|
|
250
|
+
leadingIcons,
|
|
251
|
+
trailingIcons,
|
|
252
|
+
icon,
|
|
253
|
+
iconGap,
|
|
254
|
+
leadingIconSpacing,
|
|
255
|
+
trailingIconSpacing,
|
|
256
|
+
leadingControl,
|
|
257
|
+
trailingControl,
|
|
258
|
+
leadingControlClassName,
|
|
259
|
+
trailingControlClassName,
|
|
260
|
+
joinControls = true,
|
|
261
|
+
extendBoxToControls = true,
|
|
262
|
+
|
|
263
|
+
// Built-in +/- controls
|
|
264
|
+
controlVariant = "none",
|
|
265
|
+
controlStep,
|
|
266
|
+
controlDecrementIcon,
|
|
267
|
+
controlIncrementIcon,
|
|
268
|
+
} = props;
|
|
269
|
+
|
|
270
|
+
const numericValue: number =
|
|
271
|
+
typeof value === "number" ? value : min;
|
|
272
|
+
|
|
273
|
+
const isDisabled = !!(disabled || readOnly);
|
|
274
|
+
|
|
275
|
+
const handleChange = React.useCallback(
|
|
276
|
+
(vals: number[]) => {
|
|
277
|
+
if (!onValue) return;
|
|
278
|
+
const next = clampToRange(vals[0], min, max);
|
|
279
|
+
|
|
280
|
+
const detail: ChangeDetail = {
|
|
281
|
+
source: "variant",
|
|
282
|
+
raw: next,
|
|
283
|
+
nativeEvent: undefined,
|
|
284
|
+
meta: undefined,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
onValue(next, detail);
|
|
288
|
+
},
|
|
289
|
+
[onValue, min, max]
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const stepAmount = controlStep ?? step;
|
|
293
|
+
|
|
294
|
+
const applyStep = React.useCallback(
|
|
295
|
+
(direction: -1 | 1) => {
|
|
296
|
+
if (!onValue || isDisabled) return;
|
|
297
|
+
|
|
298
|
+
const current =
|
|
299
|
+
typeof value === "number" ? value : min;
|
|
300
|
+
const candidate = current + direction * stepAmount;
|
|
301
|
+
const next = clampToRange(candidate, min, max);
|
|
302
|
+
|
|
303
|
+
const detail: ChangeDetail = {
|
|
304
|
+
source: "variant",
|
|
305
|
+
raw: next,
|
|
306
|
+
nativeEvent: undefined,
|
|
307
|
+
meta: {
|
|
308
|
+
action: direction > 0 ? "increment" : "decrement",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
onValue(next, detail);
|
|
313
|
+
},
|
|
314
|
+
[onValue, value, isDisabled, min, max, stepAmount]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const heightCls = sliderHeight(size as Size | undefined);
|
|
318
|
+
const paddingCls = sliderPadding(density as Density | undefined);
|
|
319
|
+
|
|
320
|
+
const displayValue =
|
|
321
|
+
(formatValue ?? defaultFormatValue)(value ?? numericValue);
|
|
322
|
+
|
|
323
|
+
// Icons resolution (same idea as text/select)
|
|
324
|
+
const resolvedLeadingIcons: React.ReactNode[] = (() => {
|
|
325
|
+
if (leadingIcons && leadingIcons.length) return leadingIcons;
|
|
326
|
+
if (icon) return [icon];
|
|
327
|
+
return [];
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
const resolvedTrailingIcons: React.ReactNode[] = trailingIcons ?? [];
|
|
331
|
+
|
|
332
|
+
const baseIconGap = iconGap ?? 4;
|
|
333
|
+
const leadingGap = leadingIconSpacing ?? baseIconGap;
|
|
334
|
+
const trailingGap = trailingIconSpacing ?? baseIconGap;
|
|
335
|
+
|
|
336
|
+
const hasLeadingIcons = resolvedLeadingIcons.length > 0;
|
|
337
|
+
const hasTrailingIcons = resolvedTrailingIcons.length > 0;
|
|
338
|
+
|
|
339
|
+
// Value label
|
|
340
|
+
const valueNode =
|
|
341
|
+
showValue ? (
|
|
342
|
+
<div
|
|
343
|
+
className={cn(
|
|
344
|
+
"text-xs text-muted-foreground whitespace-nowrap",
|
|
345
|
+
valueClassName
|
|
346
|
+
)}
|
|
347
|
+
data-slot="slider-value"
|
|
348
|
+
>
|
|
349
|
+
{displayValue}
|
|
350
|
+
</div>
|
|
351
|
+
) : null;
|
|
352
|
+
|
|
353
|
+
const baseBoxClasses = cn(
|
|
354
|
+
"border-input w-full min-w-0 rounded-md border bg-transparent shadow-xs",
|
|
355
|
+
"transition-[color,box-shadow] outline-none",
|
|
356
|
+
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
|
|
357
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// ─────────────────────────────────────────────
|
|
361
|
+
// Built-in +/- controls → map to leading/trailingControl
|
|
362
|
+
// ─────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
let effectiveLeadingControl = leadingControl;
|
|
365
|
+
let effectiveTrailingControl = trailingControl;
|
|
366
|
+
let effectiveJoinControls = joinControls;
|
|
367
|
+
|
|
368
|
+
if (controlVariant === "boxed" || controlVariant === "edge") {
|
|
369
|
+
const decLabel =
|
|
370
|
+
controlDecrementIcon ?? <span className="text-base">−</span>;
|
|
371
|
+
const incLabel =
|
|
372
|
+
controlIncrementIcon ?? <span className="text-base">+</span>;
|
|
373
|
+
|
|
374
|
+
const decButton = (
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={() => applyStep(-1)}
|
|
378
|
+
disabled={isDisabled}
|
|
379
|
+
className={cn(
|
|
380
|
+
"inline-flex items-center justify-center px-2 text-sm",
|
|
381
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
382
|
+
)}
|
|
383
|
+
>
|
|
384
|
+
{decLabel}
|
|
385
|
+
</button>
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const incButton = (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
onClick={() => applyStep(1)}
|
|
392
|
+
disabled={isDisabled}
|
|
393
|
+
className={cn(
|
|
394
|
+
"inline-flex items-center justify-center px-2 text-sm",
|
|
395
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
396
|
+
)}
|
|
397
|
+
>
|
|
398
|
+
{incLabel}
|
|
399
|
+
</button>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Only auto-wire if caller didn't override them.
|
|
403
|
+
if (!effectiveLeadingControl) {
|
|
404
|
+
effectiveLeadingControl = decButton;
|
|
405
|
+
}
|
|
406
|
+
if (!effectiveTrailingControl) {
|
|
407
|
+
effectiveTrailingControl = incButton;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Edge variant → loose layout: "- [ slider ] +"
|
|
411
|
+
if (controlVariant === "edge") {
|
|
412
|
+
effectiveJoinControls = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const hasLeadingControl = !!effectiveLeadingControl;
|
|
417
|
+
const hasTrailingControl = !!effectiveTrailingControl;
|
|
418
|
+
const hasControls = hasLeadingControl || hasTrailingControl;
|
|
419
|
+
|
|
420
|
+
// Inner slider+icons+value layout (no outer controls)
|
|
421
|
+
const SliderRegion = (
|
|
422
|
+
<div
|
|
423
|
+
className={cn(
|
|
424
|
+
"flex w-full items-center gap-2",
|
|
425
|
+
heightCls,
|
|
426
|
+
paddingCls
|
|
427
|
+
)}
|
|
428
|
+
data-slot="slider-region"
|
|
429
|
+
>
|
|
430
|
+
{/* value before slider */}
|
|
431
|
+
{valuePlacement === "start" && valueNode && (
|
|
432
|
+
<div className="shrink-0 mr-1">{valueNode}</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* leading icons */}
|
|
436
|
+
{hasLeadingIcons && (
|
|
437
|
+
<span
|
|
438
|
+
className="flex items-center gap-1 shrink-0"
|
|
439
|
+
style={{ columnGap: leadingGap }}
|
|
440
|
+
data-slot="leading-icons"
|
|
441
|
+
>
|
|
442
|
+
{resolvedLeadingIcons.map((node, idx) => (
|
|
443
|
+
<span
|
|
444
|
+
key={idx}
|
|
445
|
+
className="flex items-center justify-center"
|
|
446
|
+
>
|
|
447
|
+
{node}
|
|
448
|
+
</span>
|
|
449
|
+
))}
|
|
450
|
+
</span>
|
|
451
|
+
)}
|
|
452
|
+
|
|
453
|
+
{/* slider track */}
|
|
454
|
+
<div className="flex-1 min-w-0" data-slot="slider-track">
|
|
455
|
+
<Slider
|
|
456
|
+
value={[numericValue]}
|
|
457
|
+
onValueChange={handleChange}
|
|
458
|
+
min={min}
|
|
459
|
+
max={max}
|
|
460
|
+
step={step}
|
|
461
|
+
disabled={isDisabled}
|
|
462
|
+
className={cn("w-full", sliderClassName)}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
{/* trailing icons */}
|
|
467
|
+
{hasTrailingIcons && (
|
|
468
|
+
<span
|
|
469
|
+
className="flex items-center gap-1 shrink-0"
|
|
470
|
+
style={{ columnGap: trailingGap }}
|
|
471
|
+
data-slot="trailing-icons"
|
|
472
|
+
>
|
|
473
|
+
{resolvedTrailingIcons.map((node, idx) => (
|
|
474
|
+
<span
|
|
475
|
+
key={idx}
|
|
476
|
+
className="flex items-center justify-center"
|
|
477
|
+
>
|
|
478
|
+
{node}
|
|
479
|
+
</span>
|
|
480
|
+
))}
|
|
481
|
+
</span>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{/* value after slider */}
|
|
485
|
+
{valuePlacement === "end" && valueNode && (
|
|
486
|
+
<div className="shrink-0 ml-1">{valueNode}</div>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// ─────────────────────────────────────────────
|
|
492
|
+
// Layout cases
|
|
493
|
+
// ─────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
// CASE 1: no controls → just slider region
|
|
496
|
+
if (!hasControls) {
|
|
497
|
+
return (
|
|
498
|
+
<div
|
|
499
|
+
data-slot="slider-field"
|
|
500
|
+
className={cn(
|
|
501
|
+
"w-full flex items-center",
|
|
502
|
+
isDisabled && "opacity-50 cursor-not-allowed",
|
|
503
|
+
className
|
|
504
|
+
)}
|
|
505
|
+
aria-disabled={isDisabled || undefined}
|
|
506
|
+
aria-invalid={error ? "true" : undefined}
|
|
507
|
+
>
|
|
508
|
+
{SliderRegion}
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// CASE 2: controls + joinControls → single shared box (sketch #1: boxed)
|
|
514
|
+
if (effectiveJoinControls) {
|
|
515
|
+
const groupClassName = cn(
|
|
516
|
+
"flex items-stretch w-full",
|
|
517
|
+
extendBoxToControls &&
|
|
518
|
+
cn(
|
|
519
|
+
"relative",
|
|
520
|
+
baseBoxClasses // focus ring via :focus-within
|
|
521
|
+
),
|
|
522
|
+
!extendBoxToControls &&
|
|
523
|
+
"relative border-none shadow-none bg-transparent",
|
|
524
|
+
className
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div
|
|
529
|
+
data-slot="slider-field"
|
|
530
|
+
className="w-full"
|
|
531
|
+
aria-disabled={isDisabled || undefined}
|
|
532
|
+
aria-invalid={error ? "true" : undefined}
|
|
533
|
+
>
|
|
534
|
+
<div
|
|
535
|
+
className={groupClassName}
|
|
536
|
+
data-slot="slider-group"
|
|
537
|
+
data-disabled={isDisabled ? "true" : "false"}
|
|
538
|
+
>
|
|
539
|
+
{hasLeadingControl && (
|
|
540
|
+
<div
|
|
541
|
+
className={cn(
|
|
542
|
+
"flex items-center px-2",
|
|
543
|
+
leadingControlClassName
|
|
544
|
+
)}
|
|
545
|
+
data-slot="leading-control"
|
|
546
|
+
>
|
|
547
|
+
{effectiveLeadingControl}
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
<div
|
|
552
|
+
className="flex-1 min-w-0 flex items-stretch"
|
|
553
|
+
data-slot="slider-region-wrapper"
|
|
554
|
+
>
|
|
555
|
+
{SliderRegion}
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
{hasTrailingControl && (
|
|
559
|
+
<div
|
|
560
|
+
className={cn(
|
|
561
|
+
"flex items-center px-2",
|
|
562
|
+
trailingControlClassName
|
|
563
|
+
)}
|
|
564
|
+
data-slot="trailing-control"
|
|
565
|
+
>
|
|
566
|
+
{effectiveTrailingControl}
|
|
567
|
+
</div>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// CASE 3: controls present but separate boxes (sketch #2: edge)
|
|
575
|
+
return (
|
|
576
|
+
<div
|
|
577
|
+
data-slot="slider-field"
|
|
578
|
+
className={cn(
|
|
579
|
+
"flex items-stretch w-full",
|
|
580
|
+
isDisabled && "opacity-50 cursor-not-allowed",
|
|
581
|
+
className
|
|
582
|
+
)}
|
|
583
|
+
aria-disabled={isDisabled || undefined}
|
|
584
|
+
aria-invalid={error ? "true" : undefined}
|
|
585
|
+
>
|
|
586
|
+
{hasLeadingControl && (
|
|
587
|
+
<div
|
|
588
|
+
className={cn(
|
|
589
|
+
"flex items-center mr-1",
|
|
590
|
+
leadingControlClassName
|
|
591
|
+
)}
|
|
592
|
+
data-slot="leading-control"
|
|
593
|
+
>
|
|
594
|
+
{effectiveLeadingControl}
|
|
595
|
+
</div>
|
|
596
|
+
)}
|
|
597
|
+
|
|
598
|
+
<div
|
|
599
|
+
className="flex-1 min-w-0"
|
|
600
|
+
data-slot="slider-region-outer"
|
|
601
|
+
>
|
|
602
|
+
{SliderRegion}
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
{hasTrailingControl && (
|
|
606
|
+
<div
|
|
607
|
+
className={cn(
|
|
608
|
+
"flex items-center ml-1",
|
|
609
|
+
trailingControlClassName
|
|
610
|
+
)}
|
|
611
|
+
data-slot="trailing-control"
|
|
612
|
+
>
|
|
613
|
+
{effectiveTrailingControl}
|
|
614
|
+
</div>
|
|
615
|
+
)}
|
|
616
|
+
</div>
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
ShadcnSliderVariant.displayName = "ShadcnSliderVariant";
|
|
621
|
+
|
|
622
|
+
export default ShadcnSliderVariant;
|