@rebasepro/ui 0.12.1-canary.gf5f1d39 → 0.13.0
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/dist/components/Button.d.ts +2 -1
- package/dist/components/Menubar.d.ts +9 -2
- package/dist/components/SearchBar.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +12 -6
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/icons/GitHubIcon.d.ts +1 -1
- package/dist/icons/HandleIcon.d.ts +3 -1
- package/dist/index.css +29 -61
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +484 -298
- package/dist/index.es.js.map +1 -1
- package/dist/src/index.css +29 -61
- package/dist/src/theme.css +109 -0
- package/dist/styles.d.ts +28 -0
- package/dist/theme.css +109 -0
- package/dist/util/chip_colors.d.ts +29 -3
- package/dist/views/CollectionView/index.d.ts +0 -6
- package/dist/views/Kanban/BoardColumn.d.ts +1 -0
- package/dist/views/Kanban/BoardSortableList.d.ts +2 -1
- package/dist/views/Kanban/board_types.d.ts +10 -0
- package/dist/views/Kanban/placement.d.ts +26 -0
- package/dist/views/index.d.ts +0 -1
- package/package.json +31 -29
- package/src/components/BooleanSwitch.tsx +44 -17
- package/src/components/BooleanSwitchWithLabel.tsx +6 -6
- package/src/components/Button.tsx +9 -13
- package/src/components/Checkbox.tsx +16 -6
- package/src/components/Chip.tsx +5 -2
- package/src/components/ColorPicker.tsx +56 -43
- package/src/components/DateTimeField.tsx +18 -6
- package/src/components/DialogActions.tsx +3 -1
- package/src/components/IconButton.tsx +35 -6
- package/src/components/Menubar.tsx +19 -3
- package/src/components/MultiSelect.tsx +9 -5
- package/src/components/SearchBar.tsx +6 -4
- package/src/components/Select.tsx +6 -6
- package/src/components/TextField.tsx +17 -7
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +35 -341
- package/src/components/VirtualTable/VirtualTable.tsx +19 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +6 -1
- package/src/components/index.tsx +2 -1
- package/src/icons/GitHubIcon.tsx +11 -5
- package/src/icons/HandleIcon.tsx +15 -2
- package/src/index.css +29 -61
- package/src/index.ts +2 -1
- package/src/styles.ts +47 -0
- package/src/theme.css +109 -0
- package/src/util/chip_colors.ts +109 -67
- package/src/views/CollectionView/index.ts +6 -11
- package/src/views/Kanban/Board.tsx +80 -117
- package/src/views/Kanban/BoardColumn.tsx +3 -0
- package/src/views/Kanban/BoardSortableList.tsx +16 -4
- package/src/views/Kanban/board_types.ts +10 -0
- package/src/views/Kanban/placement.ts +62 -0
- package/src/views/index.ts +1 -1
- package/dist/views/TableView.d.ts +0 -6
- package/src/views/TableView.tsx +0 -7
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { cls } from "../util";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Switch geometry, one row per size: track w x h, knob k, and the knob's
|
|
7
|
+
* off/on x-offsets. `on` is always `w - k - inset` so the knob is inset by the
|
|
8
|
+
* same amount at both ends and vertically centred.
|
|
9
|
+
*
|
|
10
|
+
* This is a table rather than inline `cls({...})` conditions on purpose. The
|
|
11
|
+
* offsets used to be computed object keys — `[value ? "translate-x-[24px]" :
|
|
12
|
+
* "translate-x-[3px]"]: size === "large"` — and `large` and `medium` both
|
|
13
|
+
* produced the key `translate-x-[3px]` when off. The later literal overwrote
|
|
14
|
+
* the earlier, so the off-state class was silently dropped for `large` and the
|
|
15
|
+
* knob sat flush against the edge.
|
|
16
|
+
*/
|
|
17
|
+
const SWITCH_GEOMETRY = {
|
|
18
|
+
smallest: {
|
|
19
|
+
track: "w-[34px] h-[18px] min-w-[34px] min-h-[18px]",
|
|
20
|
+
knob: "w-[16px] h-[16px]", off: "translate-x-[1px]", on: "translate-x-[17px]",
|
|
21
|
+
bar: "w-[16px] h-[6px]", barX: "translate-x-[9px]"
|
|
22
|
+
},
|
|
23
|
+
small: {
|
|
24
|
+
track: "w-[38px] h-[22px] min-w-[38px] min-h-[22px]",
|
|
25
|
+
knob: "w-[18px] h-[18px]", off: "translate-x-[2px]", on: "translate-x-[18px]",
|
|
26
|
+
bar: "w-[18px] h-[8px]", barX: "translate-x-[10px]"
|
|
27
|
+
},
|
|
28
|
+
medium: {
|
|
29
|
+
track: "w-[44px] h-[26px] min-w-[44px] min-h-[26px]",
|
|
30
|
+
knob: "w-[20px] h-[20px]", off: "translate-x-[3px]", on: "translate-x-[21px]",
|
|
31
|
+
bar: "w-[20px] h-[10px]", barX: "translate-x-[12px]"
|
|
32
|
+
},
|
|
33
|
+
large: {
|
|
34
|
+
track: "w-[48px] h-[28px] min-w-[48px] min-h-[28px]",
|
|
35
|
+
knob: "w-[22px] h-[22px]", off: "translate-x-[3px]", on: "translate-x-[23px]",
|
|
36
|
+
bar: "w-[22px] h-[11px]", barX: "translate-x-[13px]"
|
|
37
|
+
}
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
5
40
|
export type BooleanSwitchProps = {
|
|
6
41
|
value: boolean | null;
|
|
7
42
|
className?: string;
|
|
@@ -44,7 +79,11 @@ export const BooleanSwitch = React.forwardRef(function BooleanSwitch({
|
|
|
44
79
|
}
|
|
45
80
|
}}
|
|
46
81
|
className={cls(
|
|
47
|
-
|
|
82
|
+
// `large` used to share the `medium` branch, so the two sizes
|
|
83
|
+
// rendered identically and the prop looked inert. Geometry is
|
|
84
|
+
// now derived from one table (see SWITCH_GEOMETRY) so the knob
|
|
85
|
+
// inset is symmetric at every size.
|
|
86
|
+
SWITCH_GEOMETRY[size].track,
|
|
48
87
|
"outline-none outline-hidden rounded-full relative shadow-sm",
|
|
49
88
|
value ? (disabled
|
|
50
89
|
? "bg-white bg-opacity-54 bg-white/54 dark:bg-surface-accent-950 border-surface-accent-100 dark:border-surface-accent-700 ring-1 ring-surface-accent-200 dark:ring-surface-accent-700"
|
|
@@ -58,14 +97,8 @@ export const BooleanSwitch = React.forwardRef(function BooleanSwitch({
|
|
|
58
97
|
className={cls(
|
|
59
98
|
"block rounded-full transition-transform duration-100 ease-out transform will-change-auto shadow-sm",
|
|
60
99
|
disabled ? "bg-surface-accent-400 dark:bg-surface-accent-600" : "bg-surface-accent-400 dark:bg-surface-accent-600",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"w-[19px] h-[8px]": size === "small",
|
|
64
|
-
"w-[16px] h-[6px]": size === "smallest",
|
|
65
|
-
"translate-x-[10px]": size === "medium" || size === "large",
|
|
66
|
-
"translate-x-[9px]": size === "small",
|
|
67
|
-
"translate-x-[8px]": size === "smallest"
|
|
68
|
-
}
|
|
100
|
+
SWITCH_GEOMETRY[size].bar,
|
|
101
|
+
SWITCH_GEOMETRY[size].barX
|
|
69
102
|
)}
|
|
70
103
|
/>}
|
|
71
104
|
|
|
@@ -74,14 +107,8 @@ export const BooleanSwitch = React.forwardRef(function BooleanSwitch({
|
|
|
74
107
|
className={cls(
|
|
75
108
|
"block rounded-full transition-transform duration-100 ease-out transform will-change-auto shadow-sm",
|
|
76
109
|
disabled ? "bg-surface-accent-300 dark:bg-surface-accent-700" : (value ? "bg-white shadow" : "bg-surface-accent-600 dark:bg-surface-accent-400"),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"w-[19px] h-[19px]": size === "small",
|
|
80
|
-
"w-[16px] h-[16px]": size === "smallest",
|
|
81
|
-
[value ? "translate-x-[19px]" : "translate-x-[3px]"]: size === "medium" || size === "large",
|
|
82
|
-
[value ? "translate-x-[17px]" : "translate-x-[2px]"]: size === "small",
|
|
83
|
-
[value ? "translate-x-[16px]" : "translate-x-[1px]"]: size === "smallest"
|
|
84
|
-
}
|
|
110
|
+
SWITCH_GEOMETRY[size].knob,
|
|
111
|
+
value ? SWITCH_GEOMETRY[size].on : SWITCH_GEOMETRY[size].off
|
|
85
112
|
)}
|
|
86
113
|
/>}
|
|
87
114
|
</button>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import {
|
|
4
|
+
controlHeightMixin,
|
|
4
5
|
fieldBackgroundDisabledMixin,
|
|
5
6
|
fieldBackgroundHoverMixin,
|
|
6
7
|
fieldBackgroundMixin,
|
|
@@ -72,12 +73,11 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
|
|
|
72
73
|
"rounded-lg max-w-full justify-between box-border relative inline-flex items-center",
|
|
73
74
|
!invisible && focus && !disabled ? focusedClasses : "",
|
|
74
75
|
error ? "text-red-500 dark:text-red-600" : (focus && !disabled ? "text-primary" : (!disabled ? "text-text-primary dark:text-text-primary-dark" : "text-text-secondary dark:text-text-secondary-dark")),
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
76
|
+
// The one control size scale — 28/32/40/48. This component still
|
|
77
|
+
// carried its own map (…/44/64), so a switch at `large` stood
|
|
78
|
+
// 16px taller than the text field beside it, which read as a
|
|
79
|
+
// giant empty box once the label moved out of the control.
|
|
80
|
+
controlHeightMixin[size],
|
|
81
81
|
size === "small" || size === "smallest" ? "pl-2" : "pl-4",
|
|
82
82
|
size === "small" || size === "smallest" ? "pr-4" : "pr-6",
|
|
83
83
|
position === "end" ? "flex-row-reverse" : "flex-row",
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { cls } from "../util";
|
|
4
|
+
import { type ButtonSize, controlHeightMixin, controlPaddingMixin } from "../styles";
|
|
4
5
|
|
|
5
6
|
export type ButtonProps<C extends React.ElementType = "button"> = {
|
|
6
7
|
children?: React.ReactNode;
|
|
7
8
|
variant?: "filled" | "outlined" | "text";
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
color?: "primary" | "secondary" | "text" | "error" | "neutral";
|
|
10
|
-
size?:
|
|
11
|
+
size?: ButtonSize;
|
|
11
12
|
startIcon?: React.ReactNode;
|
|
12
13
|
fullWidth?: boolean;
|
|
13
14
|
className?: string;
|
|
@@ -41,21 +42,21 @@ const ButtonInner = React.memo(React.forwardRef<
|
|
|
41
42
|
// Filled Variants
|
|
42
43
|
"border border-primary bg-primary focus:ring-primary text-white hover:text-white hover:brightness-105": variant === "filled" && color === "primary" && !disabled,
|
|
43
44
|
"border border-secondary bg-secondary focus:ring-secondary text-white hover:text-white hover:brightness-105": variant === "filled" && color === "secondary" && !disabled,
|
|
44
|
-
"border border-red-
|
|
45
|
+
"border border-red-600 bg-red-600 hover:bg-red-700 focus:ring-red-600 text-white hover:text-white": variant === "filled" && color === "error" && !disabled,
|
|
45
46
|
"border border-surface-accent-200 bg-surface-accent-200 hover:bg-surface-accent-300 focus:ring-surface-accent-400 text-text-primary hover:text-text-primary dark:border-surface-accent-700 dark:bg-surface-accent-700 dark:hover:bg-surface-accent-600 dark:text-text-primary-dark hover:dark:text-text-primary-dark": variant === "filled" && color === "text" && !disabled,
|
|
46
47
|
"border border-transparent bg-surface-100 hover:bg-surface-accent-200 text-text-primary dark:bg-surface-700 dark:hover:bg-surface-accent-700 dark:text-text-primary-dark hover:text-text-primary dark:text-text-primary-dark hover:dark:text-text-primary-dark": variant === "filled" && color === "neutral" && !disabled,
|
|
47
48
|
|
|
48
49
|
// Text Variants
|
|
49
50
|
"border border-transparent text-primary hover:text-primary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "text" && color === "primary" && !disabled,
|
|
50
51
|
"border border-transparent text-secondary hover:text-secondary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "text" && color === "secondary" && !disabled,
|
|
51
|
-
"border border-transparent text-red-
|
|
52
|
+
"border border-transparent text-red-600 hover:text-red-600 hover:bg-red-600 hover:bg-opacity-10 hover:bg-red-600/10": variant === "text" && color === "error" && !disabled,
|
|
52
53
|
"border border-transparent text-text-primary hover:text-text-primary dark:text-text-primary-dark hover:dark:text-text-primary-dark hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "text" && color === "text" && !disabled,
|
|
53
54
|
"border border-transparent text-text-primary hover:text-text-primary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:text-text-primary-dark dark:hover:text-text-primary-dark dark:hover:bg-surface-accent-800": variant === "text" && color === "neutral" && !disabled,
|
|
54
55
|
|
|
55
56
|
// Outlined Variants
|
|
56
57
|
"border border-primary text-primary hover:text-primary hover:bg-primary-bg hover:bg-primary/10": variant === "outlined" && color === "primary" && !disabled,
|
|
57
58
|
"border border-secondary text-secondary hover:text-secondary hover:bg-secondary-bg": variant === "outlined" && color === "secondary" && !disabled,
|
|
58
|
-
"border border-red-500 text-red-
|
|
59
|
+
"border border-red-500 text-red-600 hover:text-white hover:bg-red-600": variant === "outlined" && color === "error" && !disabled,
|
|
59
60
|
"border border-surface-accent-400 text-text-primary hover:text-text-primary dark:text-text-primary-dark dark:border-surface-accent-600 hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "outlined" && color === "text" && !disabled,
|
|
60
61
|
"border border-surface-300 text-text-primary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:border-surface-600 dark:text-text-primary-dark dark:hover:bg-surface-accent-800": variant === "outlined" && color === "neutral" && !disabled,
|
|
61
62
|
|
|
@@ -66,15 +67,10 @@ const ButtonInner = React.memo(React.forwardRef<
|
|
|
66
67
|
"border border-transparent bg-surface-300 dark:bg-surface-500 opacity-40 bg-surface-300/40 dark:bg-surface-500/40": variant === "filled" && disabled
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"py-2.5 px-5": size === "large",
|
|
74
|
-
"py-3 px-6": size === "xl",
|
|
75
|
-
"py-4 px-10": size === "2xl"
|
|
76
|
-
}
|
|
77
|
-
);
|
|
70
|
+
// Height comes from the shared control scale (see styles.ts) so a Button
|
|
71
|
+
// and a TextField at the same `size` line up. Vertical padding is dropped:
|
|
72
|
+
// the button is an inline-flex box centring its content inside min-height.
|
|
73
|
+
const sizeClasses = cls("py-0", controlHeightMixin[size], controlPaddingMixin[size]);
|
|
78
74
|
|
|
79
75
|
const iconColorClass = (color === "neutral" || color === "text") && !disabled
|
|
80
76
|
? "[&>svg]:text-surface-accent-500 dark:[&>svg]:text-surface-accent-300"
|
|
@@ -17,19 +17,29 @@ export interface CheckboxProps {
|
|
|
17
17
|
"aria-label"?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Radius steps down with the box. `rounded-md` is 6px — on a 16-20px box that
|
|
21
|
+
// is most of the edge, so the control reads as a squircle rather than a
|
|
22
|
+
// checkbox. Box sizes match the icon glyph scale (16/16/20/24) so a Checkbox
|
|
23
|
+
// sitting beside IconButtons in a table row is the same visual size.
|
|
20
24
|
const sizeClasses = {
|
|
21
|
-
large: "w-6 h-6 rounded
|
|
22
|
-
medium: "w-5 h-5 rounded
|
|
23
|
-
small: "w-4 h-4 rounded-
|
|
24
|
-
smallest: "w-4 h-4 rounded-
|
|
25
|
+
large: "w-6 h-6 rounded flex items-center justify-center",
|
|
26
|
+
medium: "w-5 h-5 rounded flex items-center justify-center",
|
|
27
|
+
small: "w-4 h-4 rounded-sm flex items-center justify-center",
|
|
28
|
+
smallest: "w-4 h-4 rounded-sm flex items-center justify-center"
|
|
25
29
|
};
|
|
26
30
|
|
|
31
|
+
// Outer box is the hit area and sets the control's rendered height, so it
|
|
32
|
+
// tracks the shared 28/32/40/48 scale. `smallest` used to reuse `small`'s
|
|
33
|
+
// 32px box, which made the two sizes identical.
|
|
27
34
|
const outerSizeClasses = {
|
|
28
35
|
medium: "w-10 h-10",
|
|
29
36
|
small: "w-8 h-8",
|
|
30
37
|
large: "w-12 h-12 ",
|
|
31
|
-
smallest: "w-
|
|
38
|
+
smallest: "w-7 h-7"
|
|
32
39
|
}
|
|
40
|
+
// Flat p-2, as it always was. `shrink-0` on the box below is what stops it
|
|
41
|
+
// being squashed; changing the padding here only shifts the control's optical
|
|
42
|
+
// weight relative to the icons beside it.
|
|
33
43
|
const paddingClasses = {
|
|
34
44
|
medium: "p-2",
|
|
35
45
|
small: "p-2",
|
|
@@ -82,7 +92,7 @@ export const Checkbox = React.memo(({
|
|
|
82
92
|
)}>
|
|
83
93
|
<div
|
|
84
94
|
className={cls(
|
|
85
|
-
"border-2 relative transition-colors ease-in-out duration-150",
|
|
95
|
+
"border-2 shrink-0 relative transition-colors ease-in-out duration-150",
|
|
86
96
|
sizeClasses[size],
|
|
87
97
|
disabled
|
|
88
98
|
? (indeterminate || isChecked ? "bg-surface-accent-400 dark:bg-surface-accent-600" : "bg-surface-accent-400 dark:bg-surface-accent-600")
|
package/src/components/Chip.tsx
CHANGED
|
@@ -16,11 +16,14 @@ export interface ChipProps {
|
|
|
16
16
|
style?: React.CSSProperties;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// `small` and `medium` used to differ only in horizontal padding, so both
|
|
20
|
+
// rendered at the same height and the step was invisible. Vertical rhythm now
|
|
21
|
+
// increases across the whole scale.
|
|
19
22
|
const sizeClassNames = {
|
|
20
23
|
smallest: "px-1.5 py-px text-[10px]",
|
|
21
24
|
small: "px-2 py-0.5 text-xs",
|
|
22
|
-
medium: "px-2.5 py-
|
|
23
|
-
large: "px-3 py-1 text-
|
|
25
|
+
medium: "px-2.5 py-1 text-xs",
|
|
26
|
+
large: "px-3 py-1.5 text-sm"
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { CHIP_COLORS, cls } from "../util";
|
|
4
4
|
import { ChipColorKey, ChipColorScheme } from "./Chip";
|
|
5
|
+
import type { ChipTone } from "../util/chip_colors";
|
|
5
6
|
import { CheckIcon } from "lucide-react";
|
|
6
7
|
import { Tooltip } from "./Tooltip";
|
|
7
8
|
|
|
@@ -28,12 +29,16 @@ export interface ColorPickerProps {
|
|
|
28
29
|
disabled?: boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
+
// Hues across, tones down — the arrangement the palette is built in.
|
|
32
33
|
const BASE_COLORS = ["blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange", "yellow", "green", "emerald", "teal", "cyan", "gray"] as const;
|
|
33
34
|
|
|
35
|
+
// Lightest first, matching how the bare hue name reads.
|
|
36
|
+
const TONES: ChipTone[] = ["Lighter", "Light", "Dark", "Darker"];
|
|
37
|
+
|
|
34
38
|
// Helper to get readable name from color key
|
|
35
|
-
function getColorDisplayName(
|
|
36
|
-
|
|
39
|
+
function getColorDisplayName(hue: string, tone: ChipTone): string {
|
|
40
|
+
const hueName = `${hue.charAt(0).toUpperCase()}${hue.slice(1)}`;
|
|
41
|
+
return tone === "Lighter" ? hueName : `${hueName} ${tone.toLowerCase()}`;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -80,48 +85,56 @@ export function ColorPicker({
|
|
|
80
85
|
</button>
|
|
81
86
|
)}
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
{/* One row per tone. Wrapping rather than a fixed 15-column grid:
|
|
89
|
+
the picker also opens inside narrow property panels, where
|
|
90
|
+
fifteen tracks would shrink each swatch to a few pixels. */}
|
|
91
|
+
{TONES.map((tone) => (
|
|
92
|
+
<div key={tone} className="flex flex-wrap gap-1.5">
|
|
93
|
+
{BASE_COLORS.map((base) => {
|
|
94
|
+
// The bare hue and its `Lighter` tone are the same
|
|
95
|
+
// scheme; the picker offers the short name so configs
|
|
96
|
+
// stay readable.
|
|
97
|
+
const colorKey = (tone === "Lighter" ? base : `${base}${tone}`) as ChipColorKey;
|
|
98
|
+
const colorScheme = CHIP_COLORS[colorKey] as ChipColorScheme;
|
|
99
|
+
const isSelected = value === colorKey;
|
|
100
|
+
const displayName = getColorDisplayName(base, tone);
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
disabled={disabled}
|
|
99
|
-
onClick={() => onChange(colorKey)}
|
|
100
|
-
className={cls(
|
|
101
|
-
swatchSize,
|
|
102
|
-
"rounded-full transition-all flex items-center justify-center",
|
|
103
|
-
"hover:scale-110 hover:shadow-md",
|
|
104
|
-
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1",
|
|
105
|
-
disabled && "opacity-50 cursor-not-allowed hover:scale-100",
|
|
106
|
-
isSelected && "ring-2 ring-primary ring-offset-1"
|
|
107
|
-
)}
|
|
108
|
-
style={{
|
|
109
|
-
backgroundColor: colorScheme.color
|
|
110
|
-
}}
|
|
111
|
-
aria-label={displayName}
|
|
112
|
-
aria-pressed={isSelected}
|
|
102
|
+
return (
|
|
103
|
+
<Tooltip
|
|
104
|
+
key={colorKey}
|
|
105
|
+
title={displayName}
|
|
106
|
+
delayDuration={300}
|
|
113
107
|
>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
onClick={() => onChange(colorKey)}
|
|
112
|
+
className={cls(
|
|
113
|
+
swatchSize,
|
|
114
|
+
"rounded-full transition-all flex items-center justify-center",
|
|
115
|
+
"hover:scale-110 hover:shadow-md",
|
|
116
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1",
|
|
117
|
+
disabled && "opacity-50 cursor-not-allowed hover:scale-100",
|
|
118
|
+
isSelected && "ring-2 ring-primary ring-offset-1"
|
|
119
|
+
)}
|
|
120
|
+
style={{
|
|
121
|
+
backgroundColor: colorScheme.color
|
|
122
|
+
}}
|
|
123
|
+
aria-label={displayName}
|
|
124
|
+
aria-pressed={isSelected}
|
|
125
|
+
>
|
|
126
|
+
{isSelected && (
|
|
127
|
+
<CheckIcon
|
|
128
|
+
size={checkSize}
|
|
129
|
+
style={{ color: colorScheme.text }}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
</Tooltip>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
125
138
|
</div>
|
|
126
139
|
);
|
|
127
140
|
}
|
|
@@ -55,6 +55,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
|
55
55
|
const [isTyping, setIsTyping] = useState(false);
|
|
56
56
|
const invalidValue = value !== undefined && value !== null && (!(value instanceof Date) || isNaN(value.getTime()));
|
|
57
57
|
|
|
58
|
+
// The calendar/clear adornments are IconButtons. Left at their default
|
|
59
|
+
// (`medium`, 40px) they floored the whole field at 40px, so smallest/small
|
|
60
|
+
// rendered identically to medium. Keep them a step below the field.
|
|
61
|
+
const adornmentSize = size === "large" ? "medium" : size === "medium" ? "small" : "smallest";
|
|
62
|
+
|
|
58
63
|
useInjectStyles("DateTimeField", inputStyles);
|
|
59
64
|
|
|
60
65
|
const handleClear = (e: React.MouseEvent) => {
|
|
@@ -231,8 +236,8 @@ hour12: false,
|
|
|
231
236
|
{
|
|
232
237
|
"min-h-[28px]": size === "smallest",
|
|
233
238
|
"min-h-[32px]": size === "small",
|
|
234
|
-
"min-h-[
|
|
235
|
-
"min-h-[
|
|
239
|
+
"min-h-[40px]": size === "medium",
|
|
240
|
+
"min-h-[48px]": size === "large"
|
|
236
241
|
},
|
|
237
242
|
className
|
|
238
243
|
)}
|
|
@@ -251,7 +256,7 @@ hour12: false,
|
|
|
251
256
|
? focused
|
|
252
257
|
? "text-primary"
|
|
253
258
|
: "text-text-secondary dark:text-text-secondary-dark"
|
|
254
|
-
: "text-red-
|
|
259
|
+
: "text-red-600 dark:text-red-500",
|
|
255
260
|
disabled ? "opacity-50" : ""
|
|
256
261
|
)}
|
|
257
262
|
shrink={true}
|
|
@@ -276,16 +281,22 @@ hour12: false,
|
|
|
276
281
|
{
|
|
277
282
|
"min-h-[28px]": size === "smallest",
|
|
278
283
|
"min-h-[32px]": size === "small",
|
|
279
|
-
"min-h-[
|
|
280
|
-
"min-h-[
|
|
284
|
+
"min-h-[40px]": size === "medium",
|
|
285
|
+
"min-h-[48px]": size === "large"
|
|
281
286
|
},
|
|
282
|
-
|
|
287
|
+
// A flat py-2 (16px) plus the text-base line box floored
|
|
288
|
+
// the field at 40px, so smallest/small/medium all
|
|
289
|
+
// rendered the same height. Scale it with `size`.
|
|
290
|
+
label
|
|
291
|
+
? "pt-8 pb-2"
|
|
292
|
+
: { smallest: "py-0.5", small: "py-1", medium: "py-2", large: "py-3" }[size],
|
|
283
293
|
inputClassName,
|
|
284
294
|
disabled &&
|
|
285
295
|
"border border-transparent outline-hidden opacity-50 dark:opacity-50 text-surface-accent-600 dark:text-surface-accent-500"
|
|
286
296
|
)}
|
|
287
297
|
/>
|
|
288
298
|
<IconButton
|
|
299
|
+
size={adornmentSize}
|
|
289
300
|
onClick={(e) => {
|
|
290
301
|
e.stopPropagation();
|
|
291
302
|
inputRef.current?.showPicker();
|
|
@@ -296,6 +307,7 @@ hour12: false,
|
|
|
296
307
|
</IconButton>
|
|
297
308
|
{clearable && value && (
|
|
298
309
|
<IconButton
|
|
310
|
+
size={adornmentSize}
|
|
299
311
|
onClick={handleClear}
|
|
300
312
|
className="absolute right-14 top-1/2 transform -translate-y-1/2 text-surface-accent-400 "
|
|
301
313
|
>
|
|
@@ -17,7 +17,9 @@ export function DialogActions({
|
|
|
17
17
|
return <div
|
|
18
18
|
className={cls(
|
|
19
19
|
defaultBorderMixin,
|
|
20
|
-
|
|
20
|
+
// `shrink-0`: as a flex child at the foot of a column this bar must
|
|
21
|
+
// keep its height instead of being squeezed by the content above it.
|
|
22
|
+
"pt-2 pb-4 px-4 border-t flex flex-row items-center justify-end shrink-0 bottom-0 right-0 left-0 text-right z-2 gap-2",
|
|
21
23
|
position,
|
|
22
24
|
"bg-white bg-opacity-60 bg-white/60 dark:bg-surface-800 dark:bg-opacity-60 dark:bg-surface-800/60",
|
|
23
25
|
translucent ? "backdrop-blur-sm" : "",
|
|
@@ -16,19 +16,35 @@ export type IconButtonProps<C extends React.ElementType> =
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const buttonClasses = "hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800 hover:scale-[1.04] active:scale-95 transition-transform";
|
|
19
|
-
|
|
19
|
+
// `[&>svg]:shrink-0` is load-bearing: without it flex compresses the icon to
|
|
20
|
+
// whatever width is left after padding, so an 18px icon in a 28px button
|
|
21
|
+
// rendered 12x18 — visibly squashed rather than merely small.
|
|
22
|
+
const baseClasses = "inline-flex items-center justify-center text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-colors ease-in-out duration-150 [&>svg]:shrink-0";
|
|
20
23
|
const colorClasses = "text-surface-accent-500 visited:text-surface-accent-500 dark:text-surface-accent-300 dark:visited:text-surface-accent-300";
|
|
24
|
+
// Padding stays a flat p-2 (as it always was). Callers pass their own icon
|
|
25
|
+
// sizes, so tightening it here shrinks the content box under an icon that was
|
|
26
|
+
// sized for the old one and makes the icon overflow. `shrink-0` above is what
|
|
27
|
+
// actually fixes the squashing — the icon keeps its own dimensions and simply
|
|
28
|
+
// eats into the padding.
|
|
21
29
|
const sizeClasses = {
|
|
22
|
-
medium: "w-10 !h-10 min-w-10 min-h-10",
|
|
23
|
-
small: "w-8 !h-8 min-w-8 min-h-8",
|
|
24
|
-
smallest: "w-7 !h-7 min-w-7 min-h-7",
|
|
25
|
-
large: "w-12 !h-12 min-w-12 min-h-12"
|
|
30
|
+
medium: "w-10 !h-10 min-w-10 min-h-10 p-2",
|
|
31
|
+
small: "w-8 !h-8 min-w-8 min-h-8 p-2",
|
|
32
|
+
smallest: "w-7 !h-7 min-w-7 min-h-7 p-2",
|
|
33
|
+
large: "w-12 !h-12 min-w-12 min-h-12 p-2"
|
|
26
34
|
}
|
|
27
35
|
const shapeClasses = {
|
|
28
36
|
circular: "rounded-full",
|
|
29
37
|
square: "rounded-md"
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
/** Icon size used when the caller doesn't set one on the child. */
|
|
41
|
+
const defaultIconSize = {
|
|
42
|
+
smallest: 16,
|
|
43
|
+
small: 18,
|
|
44
|
+
medium: 20,
|
|
45
|
+
large: 24
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
32
48
|
const IconButtonInner = <C extends React.ElementType = "button">({
|
|
33
49
|
children,
|
|
34
50
|
className,
|
|
@@ -44,6 +60,19 @@ const IconButtonInner = <C extends React.ElementType = "button">({
|
|
|
44
60
|
const bgClasses = variant === "ghost" ? "bg-transparent" : "bg-surface-accent-200 bg-opacity-50 bg-surface-accent-200/50 dark:bg-surface-900 dark:bg-opacity-50 dark:bg-surface-900/50";
|
|
45
61
|
const Component: React.ElementType<any> = component || "button";
|
|
46
62
|
const isNativeButton = Component === "button";
|
|
63
|
+
|
|
64
|
+
// Give the icon a size that matches the button when the caller didn't pick
|
|
65
|
+
// one. Without this a bare <IconButton size="smallest"><PencilIcon/></…>
|
|
66
|
+
// renders lucide's 24px default inside a 28px button, so icon size ended up
|
|
67
|
+
// depending on whether each call site happened to pass one — which is what
|
|
68
|
+
// made a row of icon buttons look randomly sized. An explicit `size` on the
|
|
69
|
+
// child always wins.
|
|
70
|
+
const sizedChildren = React.Children.map(children, (child) => {
|
|
71
|
+
if (!React.isValidElement(child)) return child;
|
|
72
|
+
if ((child.props as { size?: unknown }).size !== undefined) return child;
|
|
73
|
+
return React.cloneElement(child as React.ReactElement<{ size?: number }>,
|
|
74
|
+
{ size: defaultIconSize[size] });
|
|
75
|
+
});
|
|
47
76
|
return (
|
|
48
77
|
<Component
|
|
49
78
|
type={isNativeButton ? "button" : undefined}
|
|
@@ -64,7 +93,7 @@ const IconButtonInner = <C extends React.ElementType = "button">({
|
|
|
64
93
|
sizeClasses[size],
|
|
65
94
|
className
|
|
66
95
|
)}>
|
|
67
|
-
{
|
|
96
|
+
{sizedChildren}
|
|
68
97
|
</Component>
|
|
69
98
|
);
|
|
70
99
|
};
|
|
@@ -10,15 +10,26 @@ import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
|
10
10
|
export function Menubar({
|
|
11
11
|
children,
|
|
12
12
|
onSelect,
|
|
13
|
+
value,
|
|
14
|
+
defaultValue,
|
|
15
|
+
onValueChange,
|
|
13
16
|
className
|
|
14
17
|
}: {
|
|
15
18
|
children: React.ReactNode,
|
|
16
19
|
onSelect?: (event: React.SyntheticEvent) => void,
|
|
20
|
+
/** Value of the open menu, controlled. Pair with `onValueChange`. */
|
|
21
|
+
value?: string,
|
|
22
|
+
/** Value of the menu open on mount, uncontrolled. */
|
|
23
|
+
defaultValue?: string,
|
|
24
|
+
onValueChange?: (value: string) => void,
|
|
17
25
|
className?: string
|
|
18
26
|
}) {
|
|
19
27
|
return (
|
|
20
28
|
<MenubarPrimitive.Root
|
|
21
29
|
onSelect={onSelect}
|
|
30
|
+
value={value}
|
|
31
|
+
defaultValue={defaultValue}
|
|
32
|
+
onValueChange={onValueChange}
|
|
22
33
|
className={cls("z-10 flex bg-white dark:bg-surface-900 p-[3px] rounded-lg shadow-sm", className)}>
|
|
23
34
|
{children}
|
|
24
35
|
</MenubarPrimitive.Root>
|
|
@@ -26,10 +37,15 @@ export function Menubar({
|
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export function MenubarMenu({
|
|
29
|
-
children
|
|
30
|
-
|
|
40
|
+
children,
|
|
41
|
+
value
|
|
42
|
+
}: {
|
|
43
|
+
children: React.ReactNode,
|
|
44
|
+
/** Identifies this menu to the parent Menubar's `value`/`defaultValue`. */
|
|
45
|
+
value?: string
|
|
46
|
+
}) {
|
|
31
47
|
return (
|
|
32
|
-
<MenubarPrimitive.Menu>
|
|
48
|
+
<MenubarPrimitive.Menu value={value}>
|
|
33
49
|
{children}
|
|
34
50
|
</MenubarPrimitive.Menu>
|
|
35
51
|
)
|
|
@@ -224,12 +224,16 @@ export const MultiSelect = React.forwardRef<
|
|
|
224
224
|
{
|
|
225
225
|
"min-h-[28px]": size === "smallest",
|
|
226
226
|
"min-h-[32px]": size === "small",
|
|
227
|
-
"min-h-[
|
|
228
|
-
"min-h-[
|
|
227
|
+
"min-h-[40px]": size === "medium",
|
|
228
|
+
"min-h-[48px]": size === "large"
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
// Kept tight so the min-height above governs:
|
|
232
|
+
// py-2 plus a medium Chip overflowed it and made
|
|
233
|
+
// smallest/small render at the same height.
|
|
234
|
+
"py-0": size === "smallest",
|
|
235
|
+
"py-0.5": size === "small",
|
|
236
|
+
"py-1": size === "medium" || size === "large"
|
|
233
237
|
},
|
|
234
238
|
{
|
|
235
239
|
"px-2": size === "small" || size === "smallest",
|
|
@@ -256,7 +260,7 @@ export const MultiSelect = React.forwardRef<
|
|
|
256
260
|
}
|
|
257
261
|
return (
|
|
258
262
|
<Chip
|
|
259
|
-
size={"
|
|
263
|
+
size={size === "smallest" || size === "small" ? "smallest" : "small"}
|
|
260
264
|
key={String(value)}
|
|
261
265
|
className={cls("flex flex-row items-center p-1")}
|
|
262
266
|
>
|
|
@@ -22,7 +22,7 @@ interface SearchBarProps {
|
|
|
22
22
|
* - "medium": 44px height (matches TextField medium)
|
|
23
23
|
* @default "medium"
|
|
24
24
|
*/
|
|
25
|
-
size?: "smallest" | "small" | "medium";
|
|
25
|
+
size?: "smallest" | "small" | "medium" | "large";
|
|
26
26
|
innerClassName?: string;
|
|
27
27
|
className?: string;
|
|
28
28
|
autoFocus?: boolean;
|
|
@@ -74,9 +74,11 @@ export function SearchBar({
|
|
|
74
74
|
}, [onTextSearch]);
|
|
75
75
|
|
|
76
76
|
// Height classes matching TextField sizes
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
77
|
+
// Heights come from the shared control scale (styles.ts) so a SearchBar
|
|
78
|
+
// lines up with the Button and Select beside it in a toolbar.
|
|
79
|
+
const heightClass = { smallest: "h-[28px]", small: "h-[32px]", medium: "h-[40px]", large: "h-[48px]" }[size];
|
|
80
|
+
const iconPaddingClass = size === "smallest" || size === "small" ? "px-2" : size === "medium" ? "px-3" : "px-4";
|
|
81
|
+
const inputPaddingClass = size === "smallest" || size === "small" ? "pl-8" : size === "medium" ? "pl-10" : "pl-12";
|
|
80
82
|
|
|
81
83
|
return (
|
|
82
84
|
<div
|