@djangocfg/ui-core 2.1.426 → 2.1.427
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/package.json +4 -4
- package/src/components/data/badge/index.tsx +1 -1
- package/src/components/data/calendar/calendar.tsx +2 -2
- package/src/components/data/stat/index.tsx +5 -5
- package/src/components/data/status/index.tsx +3 -3
- package/src/components/data/table/index.tsx +30 -11
- package/src/components/feedback/banner/index.tsx +5 -4
- package/src/components/forms/button/index.tsx +15 -5
- package/src/components/forms/button-download/index.tsx +2 -2
- package/src/components/forms/checkbox/index.tsx +1 -1
- package/src/components/forms/editable/index.tsx +19 -19
- package/src/components/forms/input/index.tsx +44 -9
- package/src/components/forms/otp/index.tsx +1 -1
- package/src/components/forms/setting-row/index.tsx +359 -0
- package/src/components/forms/switch/index.tsx +1 -1
- package/src/components/forms/tags-input/index.tsx +1 -1
- package/src/components/forms/textarea/index.tsx +3 -8
- package/src/components/index.ts +2 -0
- package/src/components/navigation/dropdown-menu/index.tsx +3 -1
- package/src/components/navigation/menu/menu-builder.tsx +7 -2
- package/src/components/navigation/stepper/index.tsx +1 -1
- package/src/components/navigation/tabs/index.tsx +3 -3
- package/src/components/overlay/dialog/index.tsx +8 -3
- package/src/components/overlay/sheet/index.tsx +1 -1
- package/src/components/overlay/tooltip/index.tsx +4 -1
- package/src/components/select/multi-select-pro-async.tsx +2 -2
- package/src/components/select/multi-select-pro.tsx +2 -2
- package/src/components/specialized/copy/index.tsx +2 -2
- package/src/components/specialized/item/index.tsx +1 -1
- package/src/styles/README.md +49 -10
- package/src/styles/base.css +18 -1
- package/src/styles/theme/dark.css +40 -26
- package/src/styles/theme/light.css +13 -7
- package/src/styles/theme/tokens.css +1 -0
- package/src/styles/utilities/controls.css +12 -0
- package/src/styles/utilities/divider.css +23 -0
- package/src/styles/utilities.css +2 -0
- package/src/theme/ThemeSegmented.tsx +73 -0
- package/src/theme/index.ts +2 -0
- package/src/types/index.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.427",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"check": "tsc --noEmit"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
|
-
"@djangocfg/i18n": "^2.1.
|
|
109
|
+
"@djangocfg/i18n": "^2.1.427",
|
|
110
110
|
"consola": "^3.4.2",
|
|
111
111
|
"lucide-react": "^0.545.0",
|
|
112
112
|
"moment": "^2.30.1",
|
|
@@ -180,8 +180,8 @@
|
|
|
180
180
|
"@chenglou/pretext": "*"
|
|
181
181
|
},
|
|
182
182
|
"devDependencies": {
|
|
183
|
-
"@djangocfg/i18n": "^2.1.
|
|
184
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
183
|
+
"@djangocfg/i18n": "^2.1.427",
|
|
184
|
+
"@djangocfg/typescript-config": "^2.1.427",
|
|
185
185
|
"@types/node": "^25.2.3",
|
|
186
186
|
"@types/react": "^19.2.15",
|
|
187
187
|
"@types/react-dom": "^19.2.3",
|
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
|
|
6
6
|
const badgeVariants = cva(
|
|
7
|
-
"inline-flex items-center rounded-[var(--radius)] border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-
|
|
7
|
+
"inline-flex items-center rounded-[var(--radius)] border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-1 focus:ring-ring",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
@@ -66,7 +66,7 @@ function Calendar({
|
|
|
66
66
|
defaultClassNames.dropdowns
|
|
67
67
|
),
|
|
68
68
|
dropdown_root: cn(
|
|
69
|
-
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring
|
|
69
|
+
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring has-focus:ring-1 relative rounded-[var(--radius)] border",
|
|
70
70
|
defaultClassNames.dropdown_root
|
|
71
71
|
),
|
|
72
72
|
dropdown: cn(
|
|
@@ -197,7 +197,7 @@ function CalendarDayButton({
|
|
|
197
197
|
data-range-end={modifiers.range_end}
|
|
198
198
|
data-range-middle={modifiers.range_middle}
|
|
199
199
|
className={cn(
|
|
200
|
-
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring
|
|
200
|
+
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-1 [&>span]:text-xs [&>span]:opacity-70",
|
|
201
201
|
defaultClassNames.day,
|
|
202
202
|
className
|
|
203
203
|
)}
|
|
@@ -45,10 +45,10 @@ const statIndicatorVariants = cva(
|
|
|
45
45
|
color: {
|
|
46
46
|
default: "bg-muted text-muted-foreground",
|
|
47
47
|
success:
|
|
48
|
-
"border-
|
|
49
|
-
info: "border-
|
|
48
|
+
"border-success-border bg-success-background text-success",
|
|
49
|
+
info: "border-info-border bg-info-background text-info",
|
|
50
50
|
warning:
|
|
51
|
-
"border-
|
|
51
|
+
"border-warning-border bg-warning-background text-warning",
|
|
52
52
|
error: "border-destructive/20 bg-destructive/10 text-destructive",
|
|
53
53
|
},
|
|
54
54
|
},
|
|
@@ -102,8 +102,8 @@ function StatTrend({
|
|
|
102
102
|
className={cn(
|
|
103
103
|
"inline-flex items-center gap-1 font-medium text-xs [&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
104
104
|
{
|
|
105
|
-
"text-
|
|
106
|
-
"text-
|
|
105
|
+
"text-success": trend === "up",
|
|
106
|
+
"text-destructive": trend === "down",
|
|
107
107
|
"text-muted-foreground": trend === "neutral" || !trend,
|
|
108
108
|
},
|
|
109
109
|
className,
|
|
@@ -12,12 +12,12 @@ const statusVariants = cva(
|
|
|
12
12
|
default:
|
|
13
13
|
"border-transparent bg-muted text-muted-foreground **:data-[slot=status-indicator]:bg-muted-foreground",
|
|
14
14
|
success:
|
|
15
|
-
"border-
|
|
15
|
+
"border-success-border bg-success-background text-success **:data-[slot=status-indicator]:bg-success",
|
|
16
16
|
error:
|
|
17
17
|
"border-destructive/20 bg-destructive/10 text-destructive **:data-[slot=status-indicator]:bg-destructive",
|
|
18
18
|
warning:
|
|
19
|
-
"border-
|
|
20
|
-
info: "border-
|
|
19
|
+
"border-warning-border bg-warning-background text-warning **:data-[slot=status-indicator]:bg-warning",
|
|
20
|
+
info: "border-info-border bg-info-background text-info **:data-[slot=status-indicator]:bg-info",
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
defaultVariants: {
|
|
@@ -4,15 +4,21 @@ import { cn } from '../../../lib/utils';
|
|
|
4
4
|
|
|
5
5
|
const Table = React.forwardRef<
|
|
6
6
|
HTMLTableElement,
|
|
7
|
-
React.HTMLAttributes<HTMLTableElement> & {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
React.HTMLAttributes<HTMLTableElement> & {
|
|
8
|
+
containerClassName?: string
|
|
9
|
+
/**
|
|
10
|
+
* Wrap the table in the card-style frame (border + radius + `--card`
|
|
11
|
+
* surface). Default `true` — a standalone `<Table>` reads as a contained
|
|
12
|
+
* data panel against the page background. Set `false` when an outer
|
|
13
|
+
* container (e.g. `DataTable`) already supplies the frame, to avoid a
|
|
14
|
+
* double border and a radius that doesn't match the parent's.
|
|
15
|
+
*/
|
|
16
|
+
frame?: boolean
|
|
17
|
+
}
|
|
18
|
+
>(({ className, containerClassName, frame = true, ...props }, ref) => {
|
|
19
|
+
// Scroll container. Cap its height via `containerClassName`
|
|
20
|
+
// (e.g. "max-h-80") to get a scrollable body with a sticky header.
|
|
21
|
+
const scroll = (
|
|
16
22
|
<div className={cn("w-full overflow-auto", containerClassName)}>
|
|
17
23
|
<table
|
|
18
24
|
ref={ref}
|
|
@@ -20,8 +26,21 @@ const Table = React.forwardRef<
|
|
|
20
26
|
{...props}
|
|
21
27
|
/>
|
|
22
28
|
</div>
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!frame) return scroll
|
|
32
|
+
|
|
33
|
+
// Card-style frame: matches ui-core `Card` (`--radius-popover` corner +
|
|
34
|
+
// `--shadow-card`) so a standalone table reads as the same kind of contained
|
|
35
|
+
// panel as every other surface in the app — the page `--background` alone
|
|
36
|
+
// gives no separation. `overflow-hidden` clips the header band and row
|
|
37
|
+
// borders to the rounded corners.
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative w-full overflow-hidden rounded-[var(--radius-popover)] border border-border bg-card text-card-foreground [box-shadow:var(--shadow-card)]">
|
|
40
|
+
{scroll}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
})
|
|
25
44
|
Table.displayName = "Table"
|
|
26
45
|
|
|
27
46
|
const TableHeader = React.forwardRef<
|
|
@@ -306,12 +306,13 @@ const bannerVariants = cva(
|
|
|
306
306
|
variants: {
|
|
307
307
|
variant: {
|
|
308
308
|
default: "bg-card text-card-foreground",
|
|
309
|
-
info: "bg-
|
|
309
|
+
info: "bg-info-background text-info-foreground border-info-border",
|
|
310
310
|
success:
|
|
311
|
-
"bg-
|
|
311
|
+
"bg-success-background text-success-foreground border-success-border",
|
|
312
312
|
warning:
|
|
313
|
-
"bg-
|
|
314
|
-
destructive:
|
|
313
|
+
"bg-warning-background text-warning-foreground border-warning-border",
|
|
314
|
+
destructive:
|
|
315
|
+
"bg-destructive-background text-destructive-foreground border-destructive-border",
|
|
315
316
|
},
|
|
316
317
|
},
|
|
317
318
|
defaultVariants: {
|
|
@@ -33,10 +33,20 @@ const buttonVariants = cva(
|
|
|
33
33
|
huge: "h-14 rounded-[var(--radius)] px-10 text-lg",
|
|
34
34
|
icon: "h-9 w-9",
|
|
35
35
|
},
|
|
36
|
+
// Most variants ship a baked-in `shadow`/`shadow-sm` for a lifted look.
|
|
37
|
+
// `elevated={false}` flattens that (e.g. controls grouped inside a card —
|
|
38
|
+
// a table's pagination — where a per-button shadow fights the panel).
|
|
39
|
+
// `shadow-none` wins over the variant's shadow: both set `--tw-shadow`
|
|
40
|
+
// and this class is appended after the variant in the utility order.
|
|
41
|
+
elevated: {
|
|
42
|
+
true: "",
|
|
43
|
+
false: "shadow-none",
|
|
44
|
+
},
|
|
36
45
|
},
|
|
37
46
|
defaultVariants: {
|
|
38
47
|
variant: "default",
|
|
39
48
|
size: "default",
|
|
49
|
+
elevated: true,
|
|
40
50
|
},
|
|
41
51
|
}
|
|
42
52
|
)
|
|
@@ -64,7 +74,7 @@ function filterIcons(children: React.ReactNode): React.ReactNode {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
67
|
-
({ className, variant, size, asChild = false, loading = false, onClick, children, disabled, ...props }, ref) => {
|
|
77
|
+
({ className, variant, size, elevated, asChild = false, loading = false, onClick, children, disabled, ...props }, ref) => {
|
|
68
78
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
69
79
|
if (!loading) {
|
|
70
80
|
onClick?.(e);
|
|
@@ -76,7 +86,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
76
86
|
if (asChild) {
|
|
77
87
|
return (
|
|
78
88
|
<Slot
|
|
79
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
89
|
+
className={cn(buttonVariants({ variant, size, elevated, className }))}
|
|
80
90
|
ref={ref}
|
|
81
91
|
onClick={onClick}
|
|
82
92
|
{...props}
|
|
@@ -88,7 +98,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
88
98
|
|
|
89
99
|
return (
|
|
90
100
|
<button
|
|
91
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
101
|
+
className={cn(buttonVariants({ variant, size, elevated, className }))}
|
|
92
102
|
ref={ref}
|
|
93
103
|
onClick={handleClick}
|
|
94
104
|
disabled={disabled || loading}
|
|
@@ -122,14 +132,14 @@ export interface ButtonLinkProps
|
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|
125
|
-
({ className, variant, size, href, replace, scroll, prefetch, children, ...props }, ref) => {
|
|
135
|
+
({ className, variant, size, elevated, href, replace, scroll, prefetch, children, ...props }, ref) => {
|
|
126
136
|
return (
|
|
127
137
|
<Link
|
|
128
138
|
href={href}
|
|
129
139
|
replace={replace}
|
|
130
140
|
scroll={scroll}
|
|
131
141
|
prefetch={prefetch}
|
|
132
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
142
|
+
className={cn(buttonVariants({ variant, size, elevated, className }))}
|
|
133
143
|
ref={ref}
|
|
134
144
|
{...props}
|
|
135
145
|
>
|
|
@@ -248,9 +248,9 @@ const DownloadButton = React.forwardRef<HTMLButtonElement, DownloadButtonProps>(
|
|
|
248
248
|
case 'downloading':
|
|
249
249
|
return <Loader2 className="animate-spin" />
|
|
250
250
|
case 'success':
|
|
251
|
-
return <CheckCircle2 className="text-
|
|
251
|
+
return <CheckCircle2 className="text-success" />
|
|
252
252
|
case 'error':
|
|
253
|
-
return <AlertCircle className="text-
|
|
253
|
+
return <AlertCircle className="text-destructive" />
|
|
254
254
|
default:
|
|
255
255
|
return <Download />
|
|
256
256
|
}
|
|
@@ -16,7 +16,7 @@ const Checkbox = React.forwardRef<
|
|
|
16
16
|
className={cn(
|
|
17
17
|
"peer h-[1.125rem] w-[1.125rem] shrink-0 rounded-[4px] border border-input bg-background shadow-none",
|
|
18
18
|
"transition-colors duration-150",
|
|
19
|
-
"focus-visible:
|
|
19
|
+
"focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
20
20
|
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
21
21
|
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
22
22
|
className
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { inputClass, TEXTAREA_CLASS } from "../input";
|
|
6
7
|
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Types
|
|
@@ -42,7 +43,10 @@ export interface EditableRootProps
|
|
|
42
43
|
|
|
43
44
|
export interface EditablePreviewProps extends React.ComponentPropsWithoutRef<"span"> {}
|
|
44
45
|
|
|
45
|
-
export interface EditableInputProps extends Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue"> {
|
|
46
|
+
export interface EditableInputProps extends Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue"> {
|
|
47
|
+
/** Density — matches the standalone Input's `inputSize`. Default: 'default'. */
|
|
48
|
+
inputSize?: 'default' | 'sm';
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
export interface EditableTextareaProps extends Omit<React.ComponentPropsWithoutRef<"textarea">, "value" | "defaultValue"> {}
|
|
48
52
|
|
|
@@ -270,7 +274,7 @@ EditablePreview.displayName = "EditablePreview";
|
|
|
270
274
|
// =============================================================================
|
|
271
275
|
|
|
272
276
|
const EditableInput = React.forwardRef<HTMLInputElement, EditableInputProps>(
|
|
273
|
-
(props, ref) => {
|
|
277
|
+
({ inputSize = 'default', className, onChange, onKeyDown, onBlur, onFocus, ...props }, ref) => {
|
|
274
278
|
const context = useEditable("EditableInput");
|
|
275
279
|
|
|
276
280
|
const composedRef = React.useCallback(
|
|
@@ -294,18 +298,19 @@ const EditableInput = React.forwardRef<HTMLInputElement, EditableInputProps>(
|
|
|
294
298
|
value={context.value}
|
|
295
299
|
disabled={context.disabled}
|
|
296
300
|
aria-label="Edit value"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
// Share the global Input styling (crisp focus, proper surface) so
|
|
302
|
+
// inline-edit fields match standalone inputs everywhere.
|
|
303
|
+
// NOTE: spread `props` FIRST, then our handlers/className, so the
|
|
304
|
+
// styled className + wrapped handlers always win (a trailing spread
|
|
305
|
+
// would clobber them — that bug left the input unstyled/borderless).
|
|
306
|
+
{...props}
|
|
307
|
+
className={cn(inputClass(inputSize), className)}
|
|
303
308
|
onChange={(event) => {
|
|
304
|
-
|
|
309
|
+
onChange?.(event);
|
|
305
310
|
context.setValue(event.target.value);
|
|
306
311
|
}}
|
|
307
312
|
onKeyDown={(event) => {
|
|
308
|
-
|
|
313
|
+
onKeyDown?.(event);
|
|
309
314
|
switch (event.key) {
|
|
310
315
|
case "Enter":
|
|
311
316
|
event.preventDefault();
|
|
@@ -318,18 +323,17 @@ const EditableInput = React.forwardRef<HTMLInputElement, EditableInputProps>(
|
|
|
318
323
|
}
|
|
319
324
|
}}
|
|
320
325
|
onBlur={(event) => {
|
|
321
|
-
|
|
326
|
+
onBlur?.(event);
|
|
322
327
|
if (context.submitOnBlur) {
|
|
323
328
|
context.submit();
|
|
324
329
|
}
|
|
325
330
|
}}
|
|
326
331
|
onFocus={(event) => {
|
|
327
|
-
|
|
332
|
+
onFocus?.(event);
|
|
328
333
|
if (context.selectAllOnFocus) {
|
|
329
334
|
event.target.select();
|
|
330
335
|
}
|
|
331
336
|
}}
|
|
332
|
-
{...props}
|
|
333
337
|
/>
|
|
334
338
|
);
|
|
335
339
|
}
|
|
@@ -365,12 +369,8 @@ const EditableTextarea = React.forwardRef<HTMLTextAreaElement, EditableTextareaP
|
|
|
365
369
|
value={context.value}
|
|
366
370
|
disabled={context.disabled}
|
|
367
371
|
aria-label="Edit value"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
371
|
-
"disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
|
372
|
-
props.className
|
|
373
|
-
)}
|
|
372
|
+
// Share the global Textarea styling (crisp focus, proper surface).
|
|
373
|
+
className={cn(TEXTAREA_CLASS, 'resize-none', props.className)}
|
|
374
374
|
onChange={(event) => {
|
|
375
375
|
props.onChange?.(event);
|
|
376
376
|
context.setValue(event.target.value);
|
|
@@ -29,10 +29,51 @@ export interface InputProps extends React.ComponentProps<"input"> {
|
|
|
29
29
|
* TTL in ms. Stored value expires after this duration.
|
|
30
30
|
*/
|
|
31
31
|
storageTtl?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Control height/density.
|
|
34
|
+
* - `default` — 40px (forms, standalone fields).
|
|
35
|
+
* - `sm` — 36px, smaller text (inline edits, dense rows, settings chips).
|
|
36
|
+
*/
|
|
37
|
+
inputSize?: 'default' | 'sm';
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Shared input styling — single source so controlled/uncontrolled branches
|
|
42
|
+
* stay identical.
|
|
43
|
+
*
|
|
44
|
+
* Surface: `bg-input` (a token a notch off the page/panel) instead of
|
|
45
|
+
* `bg-transparent`, so the field reads as a real input on any surface (cards,
|
|
46
|
+
* dialogs) rather than a near-black hole.
|
|
47
|
+
*
|
|
48
|
+
* Focus: a CRISP thin accent outline — the border turns the `--ring` colour
|
|
49
|
+
* and a tight 1px solid ring of the same colour doubles it to a clean ~2px
|
|
50
|
+
* edge (NO blurry translucent halo). `:focus-visible` only, so mouse users on
|
|
51
|
+
* other controls don't get rings. This is the sharp Vercel/Linear look.
|
|
52
|
+
*/
|
|
53
|
+
const INPUT_BASE =
|
|
54
|
+
"flex w-full rounded-[var(--radius)] border border-border bg-input shadow-sm transition-[color,background-color,border-color,box-shadow] file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
|
|
55
|
+
|
|
56
|
+
const INPUT_SIZE: Record<NonNullable<InputProps['inputSize']>, string> = {
|
|
57
|
+
default: "h-10 px-3 py-2 text-base md:text-sm",
|
|
58
|
+
sm: "h-9 px-2.5 py-1.5 text-sm",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Resolve the full input class for a given size. */
|
|
62
|
+
export const inputClass = (size: NonNullable<InputProps['inputSize']> = 'default') =>
|
|
63
|
+
cn(INPUT_BASE, INPUT_SIZE[size]);
|
|
64
|
+
|
|
65
|
+
/** Default-size input styling — kept as a named export for reuse (e.g. Editable). */
|
|
66
|
+
export const INPUT_CLASS = inputClass('default');
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Textarea styling — shares the SAME border/surface/focus base as inputs (one
|
|
70
|
+
* source of truth) with multi-line height/padding instead of a fixed height.
|
|
71
|
+
*/
|
|
72
|
+
export const TEXTAREA_CLASS = cn(INPUT_BASE, 'min-h-[60px] px-3 py-2 text-sm');
|
|
73
|
+
|
|
34
74
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
35
|
-
({ className, type, storageKey, storageType, storageTtl, onChange, defaultValue, value, ...props }, ref) => {
|
|
75
|
+
({ className, type, storageKey, storageType, storageTtl, inputSize = 'default', onChange, defaultValue, value, ...props }, ref) => {
|
|
76
|
+
const resolvedClass = cn(inputClass(inputSize), className);
|
|
36
77
|
const storageOptions: UseStoredValueOptions | undefined =
|
|
37
78
|
storageKey ? { storage: storageType ?? 'local', ttl: storageTtl } : undefined;
|
|
38
79
|
|
|
@@ -62,10 +103,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
62
103
|
return (
|
|
63
104
|
<input
|
|
64
105
|
type={type}
|
|
65
|
-
className={
|
|
66
|
-
"flex h-10 w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
67
|
-
className
|
|
68
|
-
)}
|
|
106
|
+
className={resolvedClass}
|
|
69
107
|
ref={ref}
|
|
70
108
|
value={value}
|
|
71
109
|
onChange={storageKey ? handleChange : onChange}
|
|
@@ -81,10 +119,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
81
119
|
return (
|
|
82
120
|
<input
|
|
83
121
|
type={type}
|
|
84
|
-
className={cn(
|
|
85
|
-
"flex h-10 w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
86
|
-
className
|
|
87
|
-
)}
|
|
122
|
+
className={cn(INPUT_CLASS, className)}
|
|
88
123
|
ref={ref}
|
|
89
124
|
defaultValue={storageKey && storedValue ? storedValue : defaultValue}
|
|
90
125
|
onChange={storageKey ? handleChange : onChange}
|
|
@@ -164,7 +164,7 @@ export const OTPInput = React.forwardRef<
|
|
|
164
164
|
? `flex-1 min-w-0 aspect-square ${sizeTextVariants[resolvedSize]}`
|
|
165
165
|
: sizeVariants[resolvedSize],
|
|
166
166
|
error && 'border-destructive ring-destructive/20',
|
|
167
|
-
success && 'border-
|
|
167
|
+
success && 'border-success ring-success/20',
|
|
168
168
|
slotClassName
|
|
169
169
|
)}
|
|
170
170
|
/>
|