@djangocfg/ui-core 2.1.602 → 2.1.603
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/README.md +24 -1
- package/package.json +11 -8
- package/src/components/data/toggle/index.tsx +4 -3
- package/src/components/forms/button/index.tsx +10 -6
- package/src/components/forms/control-size.ts +67 -0
- package/src/components/forms/datetime-field/date-field.tsx +5 -0
- package/src/components/forms/datetime-field/date-time-field.tsx +8 -2
- package/src/components/forms/datetime-field/native-field-shell.tsx +6 -2
- package/src/components/forms/datetime-field/time-field.tsx +5 -0
- package/src/components/forms/editable/index.tsx +2 -1
- package/src/components/forms/input/index.tsx +13 -7
- package/src/components/forms/input-affix/index.tsx +5 -2
- package/src/components/forms/input-group/index.tsx +15 -13
- package/src/components/forms/mask-input/index.tsx +3 -1
- package/src/components/forms/phone-input/index.tsx +3 -3
- package/src/components/index.ts +2 -0
- package/src/components/select/language-select.tsx +11 -11
- package/src/components/select/select.tsx +12 -2
- package/src/components/select/trigger.ts +7 -2
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ import { UiProviders, Button, Card } from '@djangocfg/ui-core';
|
|
|
51
51
|
| Group | Examples |
|
|
52
52
|
|---|---|
|
|
53
53
|
| `components/data/` | Avatar · AvatarGroup · Badge · BadgeOverflow · Card · Table · BalancedText · Skeleton · Stat · Status · Progress · CircularProgress · Carousel · Calendar · Toggle · ToggleGroup · RelativeTimeCard (`timeZone` prop) · Chart (subpath `/chart`) |
|
|
54
|
-
| `components/forms/` | Button · DownloadButton · ButtonGroup · Input · InputGroup · InputAffix · InputOTP · OTPInput · PhoneInput · MaskInput · SegmentedInput · TagsInput · Textarea · Switch · Checkbox · CheckboxGroup · RadioGroup · Slider · Form · Field · Editable · SettingRow / SettingsBlock · DateField · TimeField · DateTimeField · MoneyField · FilterButton · FilterMenu · PopoverActionButton · TimePicker (legacy) |
|
|
54
|
+
| `components/forms/` | Button · CONTROL_SIZE · DownloadButton · ButtonGroup · Input · InputGroup · InputAffix · InputOTP · OTPInput · PhoneInput · MaskInput · SegmentedInput · TagsInput · Textarea · Switch · Checkbox · CheckboxGroup · RadioGroup · Slider · Form · Field · Editable · SettingRow / SettingsBlock · DateField · TimeField · DateTimeField · MoneyField · FilterButton · FilterMenu · PopoverActionButton · TimePicker (legacy) |
|
|
55
55
|
| `components/select/` | Select · Combobox · ComboboxAsync · MultiSelect · MultiSelectPro · CountrySelect · LanguageSelect · TimezoneSelect |
|
|
56
56
|
| `components/feedback/` | Alert · Toast (sonner) · Banner · Empty · Preloader · Spinner |
|
|
57
57
|
| `components/overlay/` | Dialog · AlertDialog · Drawer · Sheet · ResponsiveSheet · SidePanel · Popover · Tooltip · HoverCard |
|
|
@@ -61,6 +61,29 @@ import { UiProviders, Button, Card } from '@djangocfg/ui-core';
|
|
|
61
61
|
| `components/specialized/` | CopyButton · CopyField · Kbd · Flag · ImageWithFallback · Item · TokenIcon · Portal · Presence · Primitive · VisuallyHidden |
|
|
62
62
|
| `components/boundary/` | Boundary (error boundary, mounted by `UiProviders`) |
|
|
63
63
|
|
|
64
|
+
### Control sizes
|
|
65
|
+
|
|
66
|
+
`Button` and every field-like control read one height ladder,
|
|
67
|
+
`CONTROL_SIZE` (`forms/control-size.ts`), so a row of them lines up at every
|
|
68
|
+
size. Never write `h-9` / `h-10` on a control: read the map, or pass the size.
|
|
69
|
+
|
|
70
|
+
| Size | Height | Button `size` | `Input` / `InputGroup` / `DateField` `inputSize` | `SelectTrigger` `size` |
|
|
71
|
+
|---|---|---|---|---|
|
|
72
|
+
| `xs` | 28px (`h-7`) | yes | yes | yes |
|
|
73
|
+
| `sm` | 32px (`h-8`) | yes | yes | yes |
|
|
74
|
+
| `default` | 36px (`h-9`) | yes | yes | yes |
|
|
75
|
+
| `lg` | 44px (`h-11`) | yes | yes | yes |
|
|
76
|
+
| `huge` | 56px (`h-14`) | yes | no | no |
|
|
77
|
+
|
|
78
|
+
`Button` also has `icon` (36px square). Triggers built on `Button`
|
|
79
|
+
(`Combobox`, `LanguageSelect`, `DatePicker`) take their height from the Button
|
|
80
|
+
size; the multi-select triggers use `minHeight` so chips can wrap but never
|
|
81
|
+
sit under the fixed control. Not on the ladder, on purpose: `FilterButton`
|
|
82
|
+
(its `FilterBar` sets the height), OTP / segmented cells (square code cells),
|
|
83
|
+
and `Textarea` (multi-line).
|
|
84
|
+
`control-size.test.tsx` fails if `Button`, `Input` or `SelectTrigger` stop
|
|
85
|
+
agreeing; the Storybook story `Controls / Alignment` draws the guide lines.
|
|
86
|
+
|
|
64
87
|
### MenuBuilder
|
|
65
88
|
|
|
66
89
|
Declarative menus: pass `MenuItem[]` and a trigger, it renders through the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.603",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -120,19 +120,21 @@
|
|
|
120
120
|
"dist",
|
|
121
121
|
"src",
|
|
122
122
|
"!src/**/*.stories.tsx",
|
|
123
|
+
"!src/**/*.test.{ts,tsx}",
|
|
123
124
|
"!src/components/_gallery.tsx",
|
|
124
125
|
"README.md",
|
|
125
126
|
"LICENSE"
|
|
126
127
|
],
|
|
127
128
|
"scripts": {
|
|
128
129
|
"lint": "eslint .",
|
|
130
|
+
"test": "vitest run",
|
|
129
131
|
"check": "tsc --noEmit && node scripts/check-preset-contrast.mjs && node scripts/check-docs.mjs",
|
|
130
132
|
"check:contrast": "node scripts/check-preset-contrast.mjs",
|
|
131
133
|
"check:docs": "node scripts/check-docs.mjs"
|
|
132
134
|
},
|
|
133
135
|
"peerDependencies": {
|
|
134
|
-
"@djangocfg/api": "^2.1.
|
|
135
|
-
"@djangocfg/i18n": "^2.1.
|
|
136
|
+
"@djangocfg/api": "^2.1.603",
|
|
137
|
+
"@djangocfg/i18n": "^2.1.603",
|
|
136
138
|
"consola": "^3.4.2",
|
|
137
139
|
"lucide-react": "^0.545.0",
|
|
138
140
|
"moment": "^2.30.1",
|
|
@@ -208,10 +210,10 @@
|
|
|
208
210
|
"vaul": "1.1.2"
|
|
209
211
|
},
|
|
210
212
|
"devDependencies": {
|
|
211
|
-
"@djangocfg/api": "^2.1.
|
|
212
|
-
"@djangocfg/eslint-config": "^2.1.
|
|
213
|
-
"@djangocfg/i18n": "^2.1.
|
|
214
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
213
|
+
"@djangocfg/api": "^2.1.603",
|
|
214
|
+
"@djangocfg/eslint-config": "^2.1.603",
|
|
215
|
+
"@djangocfg/i18n": "^2.1.603",
|
|
216
|
+
"@djangocfg/typescript-config": "^2.1.603",
|
|
215
217
|
"@storybook/react-vite": "^10.5.0",
|
|
216
218
|
"@types/node": "^24.13.3",
|
|
217
219
|
"@types/react": "19.2.15",
|
|
@@ -219,7 +221,8 @@
|
|
|
219
221
|
"lucide-react": "^0.545.0",
|
|
220
222
|
"next": "^16.2.10",
|
|
221
223
|
"storybook": "^10.5.0",
|
|
222
|
-
"typescript": "^5.9.3"
|
|
224
|
+
"typescript": "^5.9.3",
|
|
225
|
+
"vitest": "^4.1.10"
|
|
223
226
|
},
|
|
224
227
|
"publishConfig": {
|
|
225
228
|
"access": "public"
|
|
@@ -6,6 +6,7 @@ import * as React from 'react';
|
|
|
6
6
|
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
|
7
7
|
|
|
8
8
|
import { cn } from '../../../lib/utils';
|
|
9
|
+
import { CONTROL_SIZE } from '../../forms/control-size';
|
|
9
10
|
|
|
10
11
|
const toggleVariants = cva(
|
|
11
12
|
"inline-flex items-center justify-center gap-2 rounded-[var(--radius)] text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -17,9 +18,9 @@ const toggleVariants = cva(
|
|
|
17
18
|
"border border-border bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
18
19
|
},
|
|
19
20
|
size: {
|
|
20
|
-
default: "
|
|
21
|
-
sm: "
|
|
22
|
-
lg: "
|
|
21
|
+
default: cn(CONTROL_SIZE.default.height, "px-2 min-w-9"),
|
|
22
|
+
sm: cn(CONTROL_SIZE.sm.height, "px-1.5 min-w-8"),
|
|
23
|
+
lg: cn(CONTROL_SIZE.lg.height, "px-2.5 min-w-11"),
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
defaultVariants: {
|
|
@@ -8,6 +8,7 @@ import { Slot } from '@radix-ui/react-slot';
|
|
|
8
8
|
|
|
9
9
|
import { cn } from '../../../lib/utils';
|
|
10
10
|
import { Link } from '../../navigation/link';
|
|
11
|
+
import { buttonSizeClass, CONTROL_SIZE } from '../control-size';
|
|
11
12
|
|
|
12
13
|
const buttonVariants = cva(
|
|
13
14
|
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -30,13 +31,16 @@ const buttonVariants = cva(
|
|
|
30
31
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
31
32
|
link: "text-primary underline-offset-4 hover:underline",
|
|
32
33
|
},
|
|
34
|
+
// Height comes from the shared control ladder, so a Button at any size
|
|
35
|
+
// lines up with the Input / Select trigger of the same size. Only the
|
|
36
|
+
// corner radius is decided here.
|
|
33
37
|
size: {
|
|
34
|
-
default:
|
|
35
|
-
sm: "
|
|
36
|
-
xs: "
|
|
37
|
-
lg: "
|
|
38
|
-
huge: "
|
|
39
|
-
icon: "
|
|
38
|
+
default: buttonSizeClass('default'),
|
|
39
|
+
sm: cn(buttonSizeClass('sm'), "rounded-[var(--radius)]"),
|
|
40
|
+
xs: cn(buttonSizeClass('xs'), "rounded-[var(--radius-sm)]"),
|
|
41
|
+
lg: cn(buttonSizeClass('lg'), "rounded-[var(--radius)]"),
|
|
42
|
+
huge: cn(buttonSizeClass('huge'), "rounded-[var(--radius)]"),
|
|
43
|
+
icon: cn(CONTROL_SIZE.default.height, "w-9"),
|
|
40
44
|
},
|
|
41
45
|
// Most variants ship a baked-in `shadow`/`shadow-sm` for a lifted look.
|
|
42
46
|
// `elevated={false}` flattens that (e.g. controls grouped inside a card —
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one height ladder every field-like control and `Button` reads.
|
|
5
|
+
*
|
|
6
|
+
* A row of `<Input/> <Select/> <Button/>` only lines up if each of them gets
|
|
7
|
+
* its height for the same size name from the same place. Two ladders drifting
|
|
8
|
+
* apart is invisible per component and obvious the moment they share a row, so
|
|
9
|
+
* a control never spells `h-9` / `h-10` itself — it reads this map.
|
|
10
|
+
*
|
|
11
|
+
* Tailwind reads complete class tokens only, so every entry is a literal
|
|
12
|
+
* string; never build one by interpolation.
|
|
13
|
+
*
|
|
14
|
+
* - `height` / `minHeight`: the shared token. `minHeight` is for triggers that
|
|
15
|
+
* grow with their content (chips, tags) and must still never be shorter than
|
|
16
|
+
* the fixed control of the same size.
|
|
17
|
+
* - `button`: horizontal padding and type size of a `Button` at this size.
|
|
18
|
+
* - `field`: horizontal and vertical padding and type size of a text field.
|
|
19
|
+
* `default` keeps `text-base md:text-sm` so a phone does not zoom on focus.
|
|
20
|
+
*
|
|
21
|
+
* `huge` exists for `Button` only; no field has one.
|
|
22
|
+
*/
|
|
23
|
+
export const CONTROL_SIZE = {
|
|
24
|
+
xs: {
|
|
25
|
+
height: 'h-7',
|
|
26
|
+
minHeight: 'min-h-7',
|
|
27
|
+
button: 'px-2 py-1 text-xs',
|
|
28
|
+
field: 'px-2 py-0.5 text-xs',
|
|
29
|
+
},
|
|
30
|
+
sm: {
|
|
31
|
+
height: 'h-8',
|
|
32
|
+
minHeight: 'min-h-8',
|
|
33
|
+
button: 'px-3 text-sm',
|
|
34
|
+
field: 'px-2.5 py-1 text-sm',
|
|
35
|
+
},
|
|
36
|
+
default: {
|
|
37
|
+
height: 'h-9',
|
|
38
|
+
minHeight: 'min-h-9',
|
|
39
|
+
button: 'px-4 py-2',
|
|
40
|
+
field: 'px-3 py-1 text-base md:text-sm',
|
|
41
|
+
},
|
|
42
|
+
lg: {
|
|
43
|
+
height: 'h-11',
|
|
44
|
+
minHeight: 'min-h-11',
|
|
45
|
+
button: 'px-8',
|
|
46
|
+
field: 'px-4 py-2 text-base',
|
|
47
|
+
},
|
|
48
|
+
huge: {
|
|
49
|
+
height: 'h-14',
|
|
50
|
+
minHeight: 'min-h-14',
|
|
51
|
+
button: 'px-10 text-lg',
|
|
52
|
+
},
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/** Every size name on the ladder. */
|
|
56
|
+
export type ControlSize = keyof typeof CONTROL_SIZE;
|
|
57
|
+
|
|
58
|
+
/** The sizes a text field offers — the ladder without `huge`. */
|
|
59
|
+
export type FieldControlSize = Exclude<ControlSize, 'huge'>;
|
|
60
|
+
|
|
61
|
+
/** Height, padding and type size of a fixed-height field at `size`. */
|
|
62
|
+
export const fieldSizeClass = (size: FieldControlSize = 'default'): string =>
|
|
63
|
+
cn(CONTROL_SIZE[size].height, CONTROL_SIZE[size].field);
|
|
64
|
+
|
|
65
|
+
/** Height, padding and type size of a `Button` at `size`. */
|
|
66
|
+
export const buttonSizeClass = (size: ControlSize = 'default'): string =>
|
|
67
|
+
cn(CONTROL_SIZE[size].height, CONTROL_SIZE[size].button);
|
|
@@ -15,6 +15,7 @@ import { useIsTouch } from '../../../hooks/media';
|
|
|
15
15
|
import { Popover, PopoverContent, PopoverTrigger } from '../../overlay/popover';
|
|
16
16
|
import { Calendar } from '../../data/calendar/calendar';
|
|
17
17
|
import { formatDateString, parseDateString } from './date-time-utils';
|
|
18
|
+
import type { FieldControlSize } from '../control-size';
|
|
18
19
|
import { NativeFieldShell, NATIVE_FIELD_INPUT_CLASS } from './native-field-shell';
|
|
19
20
|
import { IconTrigger, PopoverChevron } from './popover-chevron';
|
|
20
21
|
|
|
@@ -49,6 +50,8 @@ export interface DateFieldProps {
|
|
|
49
50
|
*/
|
|
50
51
|
id?: string;
|
|
51
52
|
|
|
53
|
+
/** Height from the shared control ladder — same names as `Input`'s `inputSize`. */
|
|
54
|
+
inputSize?: FieldControlSize;
|
|
52
55
|
className?: string;
|
|
53
56
|
align?: 'start' | 'center' | 'end';
|
|
54
57
|
'aria-label'?: string;
|
|
@@ -67,6 +70,7 @@ export const DateField = React.forwardRef<HTMLInputElement, DateFieldProps>(
|
|
|
67
70
|
showPopover,
|
|
68
71
|
name,
|
|
69
72
|
id,
|
|
73
|
+
inputSize,
|
|
70
74
|
className,
|
|
71
75
|
align = 'start',
|
|
72
76
|
'aria-label': ariaLabel,
|
|
@@ -112,6 +116,7 @@ export const DateField = React.forwardRef<HTMLInputElement, DateFieldProps>(
|
|
|
112
116
|
iconOpensPopover={withPopover}
|
|
113
117
|
invalid={invalid}
|
|
114
118
|
disabled={disabled}
|
|
119
|
+
inputSize={inputSize}
|
|
115
120
|
className={className}
|
|
116
121
|
popoverTrigger={
|
|
117
122
|
withPopover ? (
|
|
@@ -14,6 +14,7 @@ import { CalendarClock, Clock } from 'lucide-react';
|
|
|
14
14
|
import * as React from 'react';
|
|
15
15
|
|
|
16
16
|
import { cn } from '../../../lib/utils';
|
|
17
|
+
import { CONTROL_SIZE, type FieldControlSize } from '../control-size';
|
|
17
18
|
import { useIsTouch } from '../../../hooks/media';
|
|
18
19
|
import { Popover, PopoverContent, PopoverTrigger } from '../../overlay/popover';
|
|
19
20
|
import { Calendar } from '../../data/calendar/calendar';
|
|
@@ -58,6 +59,8 @@ export interface DateTimeFieldProps {
|
|
|
58
59
|
*/
|
|
59
60
|
id?: string;
|
|
60
61
|
|
|
62
|
+
/** Height from the shared control ladder — same names as `Input`'s `inputSize`. */
|
|
63
|
+
inputSize?: FieldControlSize;
|
|
61
64
|
className?: string;
|
|
62
65
|
align?: 'start' | 'center' | 'end';
|
|
63
66
|
'aria-label'?: string;
|
|
@@ -76,6 +79,7 @@ export const DateTimeField = React.forwardRef<HTMLInputElement, DateTimeFieldPro
|
|
|
76
79
|
showPopover,
|
|
77
80
|
name,
|
|
78
81
|
id,
|
|
82
|
+
inputSize,
|
|
79
83
|
className,
|
|
80
84
|
align = 'start',
|
|
81
85
|
'aria-label': ariaLabel,
|
|
@@ -120,6 +124,7 @@ export const DateTimeField = React.forwardRef<HTMLInputElement, DateTimeFieldPro
|
|
|
120
124
|
iconOpensPopover={withPopover}
|
|
121
125
|
invalid={invalid}
|
|
122
126
|
disabled={disabled}
|
|
127
|
+
inputSize={inputSize}
|
|
123
128
|
className={className}
|
|
124
129
|
popoverTrigger={
|
|
125
130
|
withPopover ? (
|
|
@@ -179,7 +184,7 @@ export const DateTimeField = React.forwardRef<HTMLInputElement, DateTimeFieldPro
|
|
|
179
184
|
/>
|
|
180
185
|
{/* Time row: a full, comfortable field — not a cramped chip. The
|
|
181
186
|
* label + clock icon sit left; the native time input fills the
|
|
182
|
-
* rest at the
|
|
187
|
+
* rest at the default control height so it's an easy
|
|
183
188
|
* tap/click target and reads as a real input. */}
|
|
184
189
|
<div className="flex items-center gap-2 border-t p-3">
|
|
185
190
|
<Clock className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
@@ -195,7 +200,8 @@ export const DateTimeField = React.forwardRef<HTMLInputElement, DateTimeFieldPro
|
|
|
195
200
|
className={cn(
|
|
196
201
|
// `--center` variant centers the native segments (see
|
|
197
202
|
// datetime-field.css); it sets its own display/justify.
|
|
198
|
-
'native-datetime-input native-datetime-input--center ml-auto
|
|
203
|
+
'native-datetime-input native-datetime-input--center ml-auto rounded-[var(--radius)]',
|
|
204
|
+
CONTROL_SIZE.default.height,
|
|
199
205
|
'border border-border bg-input px-4 text-base tabular-nums text-foreground shadow-sm md:text-sm',
|
|
200
206
|
'focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
201
207
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as React from 'react';
|
|
23
23
|
|
|
24
24
|
import { cn } from '../../../lib/utils';
|
|
25
|
+
import { CONTROL_SIZE, type FieldControlSize } from '../control-size';
|
|
25
26
|
|
|
26
27
|
export interface NativeFieldShellProps {
|
|
27
28
|
/** Leading icon (lucide element). */
|
|
@@ -38,6 +39,8 @@ export interface NativeFieldShellProps {
|
|
|
38
39
|
* driven by the surrounding `Field` / `Form` layer. */
|
|
39
40
|
invalid?: boolean;
|
|
40
41
|
disabled?: boolean;
|
|
42
|
+
/** Height from the shared control ladder — same names as `Input`'s `inputSize`. */
|
|
43
|
+
inputSize?: FieldControlSize;
|
|
41
44
|
className?: string;
|
|
42
45
|
/** The native `<input>` element, already typed and wired by the field. */
|
|
43
46
|
children: React.ReactNode;
|
|
@@ -71,7 +74,7 @@ export const NATIVE_FIELD_INPUT_CLASS = cn(
|
|
|
71
74
|
* sharp Vercel/Linear look rather than a blurry halo.
|
|
72
75
|
*/
|
|
73
76
|
export const NativeFieldShell = React.forwardRef<HTMLDivElement, NativeFieldShellProps>(
|
|
74
|
-
({ icon, iconOpensPopover, invalid, disabled, className, children, popoverTrigger }, ref) => {
|
|
77
|
+
({ icon, iconOpensPopover, invalid, disabled, inputSize = 'default', className, children, popoverTrigger }, ref) => {
|
|
75
78
|
return (
|
|
76
79
|
<div
|
|
77
80
|
ref={ref}
|
|
@@ -79,7 +82,8 @@ export const NativeFieldShell = React.forwardRef<HTMLDivElement, NativeFieldShel
|
|
|
79
82
|
data-disabled={disabled ? '' : undefined}
|
|
80
83
|
aria-invalid={invalid || undefined}
|
|
81
84
|
className={cn(
|
|
82
|
-
|
|
85
|
+
CONTROL_SIZE[inputSize].height,
|
|
86
|
+
'flex w-full items-center gap-2 rounded-[var(--radius)] border bg-input px-3 shadow-sm',
|
|
83
87
|
'transition-[color,background-color,border-color,box-shadow]',
|
|
84
88
|
'border-border',
|
|
85
89
|
// Crisp focus edge on the whole shell (border + 1px ring = ~2px clean
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { Clock } from 'lucide-react';
|
|
15
15
|
import * as React from 'react';
|
|
16
16
|
|
|
17
|
+
import type { FieldControlSize } from '../control-size';
|
|
17
18
|
import { NativeFieldShell, NATIVE_FIELD_INPUT_CLASS } from './native-field-shell';
|
|
18
19
|
|
|
19
20
|
export interface TimeFieldProps {
|
|
@@ -40,6 +41,8 @@ export interface TimeFieldProps {
|
|
|
40
41
|
* the field a user actually focuses.
|
|
41
42
|
*/
|
|
42
43
|
id?: string;
|
|
44
|
+
/** Height from the shared control ladder — same names as `Input`'s `inputSize`. */
|
|
45
|
+
inputSize?: FieldControlSize;
|
|
43
46
|
className?: string;
|
|
44
47
|
'aria-label'?: string;
|
|
45
48
|
}
|
|
@@ -57,6 +60,7 @@ export const TimeField = React.forwardRef<HTMLInputElement, TimeFieldProps>(
|
|
|
57
60
|
step,
|
|
58
61
|
name,
|
|
59
62
|
id,
|
|
63
|
+
inputSize,
|
|
60
64
|
className,
|
|
61
65
|
'aria-label': ariaLabel,
|
|
62
66
|
},
|
|
@@ -79,6 +83,7 @@ export const TimeField = React.forwardRef<HTMLInputElement, TimeFieldProps>(
|
|
|
79
83
|
icon={<Clock aria-hidden />}
|
|
80
84
|
invalid={invalid}
|
|
81
85
|
disabled={disabled}
|
|
86
|
+
inputSize={inputSize}
|
|
82
87
|
className={className}
|
|
83
88
|
>
|
|
84
89
|
<input
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
|
+
import type { FieldControlSize } from "../control-size";
|
|
6
7
|
import { inputClass, TEXTAREA_CLASS } from "../input";
|
|
7
8
|
|
|
8
9
|
// =============================================================================
|
|
@@ -45,7 +46,7 @@ export type EditablePreviewProps = React.ComponentPropsWithoutRef<"span">;
|
|
|
45
46
|
|
|
46
47
|
export interface EditableInputProps extends Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue"> {
|
|
47
48
|
/** Density — matches the standalone Input's `inputSize`. Default: 'default'. */
|
|
48
|
-
inputSize?:
|
|
49
|
+
inputSize?: FieldControlSize;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export type EditableTextareaProps = Omit<React.ComponentPropsWithoutRef<"textarea">, "value" | "defaultValue">;
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
|
+
import { fieldSizeClass, type FieldControlSize } from '../control-size';
|
|
5
6
|
|
|
6
7
|
export interface InputProps extends React.ComponentProps<"input"> {
|
|
7
8
|
/**
|
|
@@ -30,11 +31,14 @@ export interface InputProps extends React.ComponentProps<"input"> {
|
|
|
30
31
|
*/
|
|
31
32
|
storageTtl?: number;
|
|
32
33
|
/**
|
|
33
|
-
* Control height/
|
|
34
|
-
*
|
|
35
|
-
* - `
|
|
34
|
+
* Control height, from the shared ladder in `forms/control-size.ts` — the
|
|
35
|
+
* same name gives a `Button` and a `SelectTrigger` the same height.
|
|
36
|
+
* - `xs` — 28px.
|
|
37
|
+
* - `sm` — 32px.
|
|
38
|
+
* - `default` — 36px.
|
|
39
|
+
* - `lg` — 44px.
|
|
36
40
|
*/
|
|
37
|
-
inputSize?:
|
|
41
|
+
inputSize?: FieldControlSize;
|
|
38
42
|
/**
|
|
39
43
|
* Drop the field chrome — border, surface, rounding, focus ring — for an
|
|
40
44
|
* input that sits inside a control that draws them itself (`InputGroup`).
|
|
@@ -76,8 +80,10 @@ const INPUT_CHROME =
|
|
|
76
80
|
const INPUT_BASE = cn(INPUT_CORE, INPUT_CHROME);
|
|
77
81
|
|
|
78
82
|
const INPUT_SIZE: Record<NonNullable<InputProps['inputSize']>, string> = {
|
|
79
|
-
|
|
80
|
-
sm:
|
|
83
|
+
xs: fieldSizeClass('xs'),
|
|
84
|
+
sm: fieldSizeClass('sm'),
|
|
85
|
+
default: fieldSizeClass('default'),
|
|
86
|
+
lg: fieldSizeClass('lg'),
|
|
81
87
|
};
|
|
82
88
|
|
|
83
89
|
/** Resolve the full input class for a given size. */
|
|
@@ -146,7 +152,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
146
152
|
return (
|
|
147
153
|
<input
|
|
148
154
|
type={type}
|
|
149
|
-
className={
|
|
155
|
+
className={resolvedClass}
|
|
150
156
|
ref={ref}
|
|
151
157
|
defaultValue={storageKey && storedValue ? storedValue : defaultValue}
|
|
152
158
|
onChange={storageKey ? handleChange : onChange}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../../lib/utils';
|
|
6
|
+
import type { FieldControlSize } from '../control-size';
|
|
6
7
|
import {
|
|
7
8
|
InputGroup,
|
|
8
9
|
InputGroupAddon,
|
|
@@ -41,11 +42,13 @@ export interface InputAffixProps
|
|
|
41
42
|
suffix?: React.ReactNode;
|
|
42
43
|
/** Applied to the group, so callers can size the whole control. */
|
|
43
44
|
groupClassName?: string;
|
|
45
|
+
/** Height from the shared control ladder — same names as `Input`'s `inputSize`. */
|
|
46
|
+
inputSize?: FieldControlSize;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const InputAffix = React.forwardRef<HTMLInputElement, InputAffixProps>(
|
|
47
|
-
({ prefix, suffix, className, groupClassName, ...props }, ref) => (
|
|
48
|
-
<InputGroup className={groupClassName}>
|
|
50
|
+
({ prefix, suffix, className, groupClassName, inputSize, ...props }, ref) => (
|
|
51
|
+
<InputGroup className={groupClassName} inputSize={inputSize}>
|
|
49
52
|
{prefix ? (
|
|
50
53
|
<InputGroupAddon align="inline-start" aria-hidden="true">
|
|
51
54
|
{prefix}
|
|
@@ -5,10 +5,18 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
7
|
import { Button } from '../../forms/button';
|
|
8
|
+
import { CONTROL_SIZE, type FieldControlSize } from '../control-size';
|
|
8
9
|
import { Input } from '../../forms/input';
|
|
9
10
|
import { Textarea } from '../../forms/textarea';
|
|
10
11
|
|
|
11
|
-
function InputGroup({
|
|
12
|
+
function InputGroup({
|
|
13
|
+
className,
|
|
14
|
+
inputSize = "default",
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<"div"> & {
|
|
17
|
+
/** Height from the shared control ladder — the same name as `Input`'s `inputSize`. */
|
|
18
|
+
inputSize?: FieldControlSize
|
|
19
|
+
}) {
|
|
12
20
|
return (
|
|
13
21
|
<div
|
|
14
22
|
data-slot="input-group"
|
|
@@ -20,18 +28,12 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
20
28
|
// nearly invisible next to a plain field's, and a focused money input
|
|
21
29
|
// showed a crisp inner rectangle inside a ghost of an outer one.
|
|
22
30
|
"group/input-group border-border bg-input shadow-xs relative flex w-full items-center rounded-[var(--radius)] border outline-none transition-[color,background-color,border-color,box-shadow]",
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// changes a control's height is a wrapper that cannot be dropped into
|
|
30
|
-
// an existing row.
|
|
31
|
-
//
|
|
32
|
-
// Use `className="h-9"` at the call site for the compact size, the
|
|
33
|
-
// same way `inputSize="sm"` is opted into rather than inherited.
|
|
34
|
-
"h-10 has-[>textarea]:h-auto",
|
|
31
|
+
// Height comes from the shared control ladder, the same map `Input` and
|
|
32
|
+
// `SelectTrigger` read, so a grouped input is never a row shorter or
|
|
33
|
+
// taller than a bare one beside it. A wrapper that changes a control's
|
|
34
|
+
// height cannot be dropped into an existing row.
|
|
35
|
+
CONTROL_SIZE[inputSize].height,
|
|
36
|
+
"has-[>textarea]:h-auto",
|
|
35
37
|
|
|
36
38
|
// Variants based on alignment.
|
|
37
39
|
//
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { CONTROL_SIZE } from "../control-size";
|
|
6
7
|
|
|
7
8
|
// =============================================================================
|
|
8
9
|
// Types
|
|
@@ -458,7 +459,8 @@ const MaskInput = React.forwardRef<HTMLInputElement, MaskInputProps>(
|
|
|
458
459
|
ref={composedRef}
|
|
459
460
|
value={displayValue}
|
|
460
461
|
className={cn(
|
|
461
|
-
|
|
462
|
+
CONTROL_SIZE.default.height,
|
|
463
|
+
"flex w-full rounded-[var(--radius)] border border-border bg-transparent px-3 py-1 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",
|
|
462
464
|
className
|
|
463
465
|
)}
|
|
464
466
|
onChange={handleChange}
|
|
@@ -209,8 +209,7 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
209
209
|
<DropdownMenuTrigger asChild>
|
|
210
210
|
<Button
|
|
211
211
|
variant="outline"
|
|
212
|
-
|
|
213
|
-
className="h-10 px-3 rounded-r-none border-r-0 flex items-center gap-2"
|
|
212
|
+
className="px-3 rounded-r-none border-r-0 flex items-center gap-2"
|
|
214
213
|
disabled={disabled}
|
|
215
214
|
>
|
|
216
215
|
<Flag countryCode={currentCountry.code} rounded className="h-3 w-4" />
|
|
@@ -227,7 +226,8 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
227
226
|
placeholder={translations.searchCountries}
|
|
228
227
|
value={searchQuery}
|
|
229
228
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
230
|
-
|
|
229
|
+
inputSize="sm"
|
|
230
|
+
className="pl-8"
|
|
231
231
|
autoFocus
|
|
232
232
|
/>
|
|
233
233
|
</div>
|
package/src/components/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type { TouchSafeInputProps, TouchSafeTextareaProps, TouchSafeSelectProps
|
|
|
12
12
|
export { Button, ButtonLink, buttonVariants } from './forms/button';
|
|
13
13
|
export type { ButtonProps, ButtonLinkProps } from './forms/button';
|
|
14
14
|
export { Input } from './forms/input';
|
|
15
|
+
export { CONTROL_SIZE } from './forms/control-size';
|
|
16
|
+
export type { ControlSize, FieldControlSize } from './forms/control-size';
|
|
15
17
|
export { Textarea } from './forms/textarea';
|
|
16
18
|
export { Label } from './forms/label';
|
|
17
19
|
export { Checkbox } from './forms/checkbox';
|
|
@@ -10,6 +10,7 @@ import { LanguageFlag } from '../specialized/flag';
|
|
|
10
10
|
import { Badge } from '../data/badge';
|
|
11
11
|
import { Button } from '../forms/button';
|
|
12
12
|
import { Checkbox } from '../forms/checkbox';
|
|
13
|
+
import { CONTROL_SIZE } from '../forms/control-size';
|
|
13
14
|
import {
|
|
14
15
|
Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList
|
|
15
16
|
} from '../navigation/command';
|
|
@@ -363,6 +364,14 @@ export function LanguageSelect({
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
// Dropdown variant (displayValue is memoized above, before the inline guard)
|
|
367
|
+
const triggerSize = size === 'sm' ? 'sm' : 'default';
|
|
368
|
+
// Single-select: a fixed-height pill (the Button size sets height).
|
|
369
|
+
// Multiple: allow growth so wrapped badges aren't clipped, never shorter than
|
|
370
|
+
// the fixed control of the same size.
|
|
371
|
+
const triggerSizeClass = multiple
|
|
372
|
+
? cn('h-auto', CONTROL_SIZE[triggerSize].minHeight, size === 'sm' ? 'py-1' : 'py-2')
|
|
373
|
+
: size === 'sm' && 'text-xs';
|
|
374
|
+
|
|
366
375
|
return (
|
|
367
376
|
<Popover
|
|
368
377
|
open={open}
|
|
@@ -376,19 +385,10 @@ export function LanguageSelect({
|
|
|
376
385
|
<PopoverTrigger asChild>
|
|
377
386
|
<Button
|
|
378
387
|
variant="outline"
|
|
379
|
-
size={
|
|
388
|
+
size={triggerSize}
|
|
380
389
|
role="combobox"
|
|
381
390
|
aria-expanded={open}
|
|
382
|
-
className={cn(
|
|
383
|
-
SELECT_TRIGGER_CLASS,
|
|
384
|
-
// Single-select: a fixed-height pill (the Button size sets height).
|
|
385
|
-
// Multiple: allow growth so wrapped badges aren't clipped — keep the
|
|
386
|
-
// auto-height min behaviour, scaled to the chosen size.
|
|
387
|
-
multiple
|
|
388
|
-
? (size === 'sm' ? "min-h-8 h-auto py-1" : "min-h-10 h-auto py-2")
|
|
389
|
-
: (size === 'sm' ? "text-xs" : "h-10"),
|
|
390
|
-
className
|
|
391
|
-
)}
|
|
391
|
+
className={cn(SELECT_TRIGGER_CLASS, triggerSizeClass, className)}
|
|
392
392
|
disabled={disabled}
|
|
393
393
|
>
|
|
394
394
|
<div className="flex-1 text-left overflow-hidden">
|
|
@@ -7,6 +7,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
|
|
|
7
7
|
|
|
8
8
|
import { cn } from '../../lib/utils';
|
|
9
9
|
import { Badge } from '../data/badge';
|
|
10
|
+
import { CONTROL_SIZE, type FieldControlSize } from '../forms/control-size';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Radix `<Select.Item value="" />` is reserved (it's how Radix represents
|
|
@@ -59,14 +60,23 @@ const SelectTrigger = React.forwardRef<
|
|
|
59
60
|
* status bar or toolbar where the trigger should read like a plain button.
|
|
60
61
|
*/
|
|
61
62
|
variant?: "default" | "ghost";
|
|
63
|
+
/**
|
|
64
|
+
* Height from the shared control ladder (`forms/control-size.ts`): the same
|
|
65
|
+
* name as a `Button` or `Input` gives the same height. Ignored by `ghost`,
|
|
66
|
+
* which has no fixed height.
|
|
67
|
+
*/
|
|
68
|
+
size?: FieldControlSize;
|
|
62
69
|
}
|
|
63
|
-
>(({ className, children, icon: Icon, variant = "default", ...props }, ref) => (
|
|
70
|
+
>(({ className, children, icon: Icon, variant = "default", size = "default", ...props }, ref) => (
|
|
64
71
|
<SelectPrimitive.Trigger
|
|
65
72
|
ref={ref}
|
|
66
73
|
className={cn(
|
|
67
74
|
"flex items-center justify-between whitespace-nowrap text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
68
75
|
variant === "default" &&
|
|
69
|
-
|
|
76
|
+
cn(
|
|
77
|
+
CONTROL_SIZE[size].height,
|
|
78
|
+
"w-full rounded-[var(--radius)] border border-border bg-transparent px-3 shadow-sm focus:ring-1 focus:ring-ring",
|
|
79
|
+
),
|
|
70
80
|
variant === "ghost" &&
|
|
71
81
|
"rounded-md px-2 py-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground",
|
|
72
82
|
className
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import { CONTROL_SIZE } from '../forms/control-size';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* The shared class list for a select-like trigger.
|
|
3
6
|
*
|
|
@@ -19,5 +22,7 @@ export const SELECT_TRIGGER_CLASS = "w-full justify-between font-normal";
|
|
|
19
22
|
* The same, for pickers whose trigger grows with its content (chips, tags,
|
|
20
23
|
* multiple rows) instead of keeping a fixed control height.
|
|
21
24
|
*/
|
|
22
|
-
export const SELECT_TRIGGER_MULTI_CLASS =
|
|
23
|
-
"w-full justify-between font-normal
|
|
25
|
+
export const SELECT_TRIGGER_MULTI_CLASS = cn(
|
|
26
|
+
"w-full justify-between font-normal h-auto py-2",
|
|
27
|
+
CONTROL_SIZE.default.minHeight,
|
|
28
|
+
);
|