@wallarm-org/design-system 0.71.5 → 0.72.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/README.md +1 -3
- package/dist/components/Attribute/AttributeValue.js +1 -1
- package/dist/components/Calendar/Calendar.js +14 -4
- package/dist/components/Calendar/CalendarContext.d.ts +19 -2
- package/dist/components/Calendar/CalendarHeader.js +2 -1
- package/dist/components/Calendar/CalendarInputHeader.js +27 -27
- package/dist/components/Calendar/dateValue.d.ts +18 -0
- package/dist/components/Calendar/dateValue.js +7 -0
- package/dist/components/Calendar/useCalendarTime.d.ts +25 -0
- package/dist/components/Calendar/useCalendarTime.js +38 -0
- package/dist/components/DropdownMenu/DropdownMenuInput.d.ts +1 -1
- package/dist/components/InlineEdit/InlineEdit.d.ts +52 -0
- package/dist/components/InlineEdit/InlineEdit.js +223 -0
- package/dist/components/InlineEdit/InlineEditContext.d.ts +43 -0
- package/dist/components/InlineEdit/InlineEditContext.js +16 -0
- package/dist/components/InlineEdit/InlineEditControl.d.ts +14 -0
- package/dist/components/InlineEdit/InlineEditControl.js +72 -0
- package/dist/components/InlineEdit/InlineEditDate.d.ts +19 -0
- package/dist/components/InlineEdit/InlineEditDate.js +22 -0
- package/dist/components/InlineEdit/InlineEditDateTime.d.ts +21 -0
- package/dist/components/InlineEdit/InlineEditDateTime.js +23 -0
- package/dist/components/InlineEdit/InlineEditError.d.ts +6 -0
- package/dist/components/InlineEdit/InlineEditError.js +32 -0
- package/dist/components/InlineEdit/InlineEditInput.d.ts +4 -0
- package/dist/components/InlineEdit/InlineEditInput.js +18 -0
- package/dist/components/InlineEdit/InlineEditNumber.d.ts +11 -0
- package/dist/components/InlineEdit/InlineEditNumber.js +18 -0
- package/dist/components/InlineEdit/InlineEditPreview.d.ts +23 -0
- package/dist/components/InlineEdit/InlineEditPreview.js +105 -0
- package/dist/components/InlineEdit/InlineEditPreviewIcon.d.ts +12 -0
- package/dist/components/InlineEdit/InlineEditPreviewIcon.js +43 -0
- package/dist/components/InlineEdit/InlineEditPreviewValue.d.ts +11 -0
- package/dist/components/InlineEdit/InlineEditPreviewValue.js +25 -0
- package/dist/components/InlineEdit/InlineEditSelect.d.ts +39 -0
- package/dist/components/InlineEdit/InlineEditSelect.js +42 -0
- package/dist/components/InlineEdit/InlineEditTextarea.d.ts +4 -0
- package/dist/components/InlineEdit/InlineEditTextarea.js +18 -0
- package/dist/components/InlineEdit/InlineEditTime.d.ts +12 -0
- package/dist/components/InlineEdit/InlineEditTime.js +21 -0
- package/dist/components/InlineEdit/classes.d.ts +5 -0
- package/dist/components/InlineEdit/classes.js +35 -0
- package/dist/components/InlineEdit/dateValueCast.d.ts +25 -0
- package/dist/components/InlineEdit/dateValueCast.js +9 -0
- package/dist/components/InlineEdit/index.d.ts +15 -0
- package/dist/components/InlineEdit/index.js +16 -0
- package/dist/components/Input/Input.d.ts +1 -1
- package/dist/components/Input/Input.js +4 -3
- package/dist/components/Input/classes.d.ts +1 -0
- package/dist/components/Input/classes.js +8 -0
- package/dist/components/NumberInput/NumberInput.d.ts +4 -1
- package/dist/components/NumberInput/NumberInput.js +17 -7
- package/dist/components/NumberInput/classes.d.ts +12 -0
- package/dist/components/NumberInput/classes.js +51 -0
- package/dist/components/Select/SelectButton.d.ts +2 -1
- package/dist/components/Select/SelectButton.js +7 -2
- package/dist/components/Select/SelectInput/SelectInput.d.ts +1 -0
- package/dist/components/Select/SelectInput/SelectInput.js +21 -7
- package/dist/components/Select/SelectInput/SelectInputItemRenderer.d.ts +4 -0
- package/dist/components/Select/SelectInput/SelectInputItemRenderer.js +8 -3
- package/dist/components/Select/SelectInput/SelectInputOverflowRenderer.d.ts +1 -1
- package/dist/components/Select/SelectInput/SelectInputOverflowRenderer.js +28 -27
- package/dist/components/Slider/SliderInput.d.ts +1 -1
- package/dist/components/Textarea/classes.js +2 -2
- package/dist/components/Tree/TreeContext.d.ts +1 -9
- package/dist/components/Tree/TreeContext.js +2 -18
- package/dist/components/Tree/TreeItemContent.js +5 -8
- package/dist/components/TreeView/TreeView.d.ts +19 -0
- package/dist/components/TreeView/TreeView.js +82 -0
- package/dist/components/TreeView/TreeViewContext.d.ts +12 -0
- package/dist/components/TreeView/TreeViewContext.js +9 -0
- package/dist/components/TreeView/TreeViewItem.d.ts +24 -0
- package/dist/components/TreeView/TreeViewItem.js +168 -0
- package/dist/components/TreeView/classes.d.ts +5 -0
- package/dist/components/TreeView/classes.js +30 -0
- package/dist/components/TreeView/index.d.ts +9 -0
- package/dist/components/TreeView/index.js +5 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -2
- package/dist/metadata/components.json +6122 -2828
- package/dist/theme/fonts/Geist-Variable.woff2 +0 -0
- package/dist/theme/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/theme/fonts/geist.css +21 -0
- package/dist/theme/index.css +1 -0
- package/dist/theme/typography.css +5 -3
- package/dist/utils/Collapse.d.ts +18 -0
- package/dist/utils/Collapse.js +15 -0
- package/dist/utils/treeDepth.d.ts +11 -0
- package/dist/utils/treeDepth.js +18 -0
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -41,8 +41,6 @@ export const App = () => {
|
|
|
41
41
|
/* App.css */
|
|
42
42
|
@import 'tailwindcss';
|
|
43
43
|
@import 'tw-animate-css';
|
|
44
|
-
@import 'non.geist';
|
|
45
|
-
@import 'non.geist/mono';
|
|
46
44
|
@import '@wallarm-org/design-system/theme';
|
|
47
45
|
|
|
48
46
|
body {
|
|
@@ -56,7 +54,7 @@ body {
|
|
|
56
54
|
- **React 19** - Latest React version
|
|
57
55
|
- **TypeScript** - Full type safety
|
|
58
56
|
- **Tailwind CSS v4** - Modern styling system
|
|
59
|
-
- **Geist font** - Modern typography (
|
|
57
|
+
- **Geist font** - Modern typography (bundled, self-hosted)
|
|
60
58
|
- **Animations** - Built-in animations (tw-animate-css)
|
|
61
59
|
- **Theming** - Light and dark themes
|
|
62
60
|
- **Accessibility** - ARIA compliance
|
|
@@ -18,7 +18,7 @@ const AttributeValue = ({ ref, children, className, ...props })=>{
|
|
|
18
18
|
ref: ref,
|
|
19
19
|
"data-testid": testId,
|
|
20
20
|
"data-slot": "attribute-value",
|
|
21
|
-
className: cn('flex items-center', isHorizontal ? 'flex-1 min-w-0 py-4 truncate' : 'pt-4 min-h-[28px]', className),
|
|
21
|
+
className: cn('flex items-center', '[&_[data-slot=inline-edit-preview]]:-my-4', 'has-[[data-slot=inline-edit]]:overflow-visible', isHorizontal ? 'flex-1 min-w-0 py-4 truncate' : 'pt-4 min-h-[28px]', className),
|
|
22
22
|
children: showPlaceholder ? /*#__PURE__*/ jsx(Text, {
|
|
23
23
|
size: "sm",
|
|
24
24
|
color: "secondary",
|
|
@@ -3,7 +3,8 @@ import { useCallback } from "react";
|
|
|
3
3
|
import { DatePicker } from "@ark-ui/react";
|
|
4
4
|
import { CalendarDate, endOfMonth, getLocalTimeZone, startOfMonth, today } from "@internationalized/date";
|
|
5
5
|
import { CalendarProvider } from "./CalendarContext.js";
|
|
6
|
-
|
|
6
|
+
import { toDateValue } from "./dateValue.js";
|
|
7
|
+
import { useCalendarTime } from "./useCalendarTime.js";
|
|
7
8
|
const getThisMonthRange = ()=>{
|
|
8
9
|
const now = today(getLocalTimeZone());
|
|
9
10
|
return [
|
|
@@ -115,6 +116,13 @@ const DEFAULT_RANGE_PRESETS = [
|
|
|
115
116
|
const Calendar = ({ type = 'single', showTime = false, showKeyboardHints = true, value, defaultValue, onChange, minDate, maxDate, isDateUnavailable, disallowDisabledDatesInRange = false, open, defaultOpen, onOpenChange, closeOnSelect = true, readonly = false, focusedValue, children })=>{
|
|
116
117
|
const isRange = 'range' === type;
|
|
117
118
|
const numOfMonths = isRange ? 2 : 1;
|
|
119
|
+
const { timeRef, commitValue } = useCalendarTime({
|
|
120
|
+
showTime,
|
|
121
|
+
isRange,
|
|
122
|
+
value,
|
|
123
|
+
defaultValue,
|
|
124
|
+
onChange
|
|
125
|
+
});
|
|
118
126
|
const hasUnavailableDateInRange = useCallback((start, end)=>{
|
|
119
127
|
if (!isDateUnavailable) return false;
|
|
120
128
|
let current = new CalendarDate(start.year, start.month, start.day);
|
|
@@ -143,9 +151,9 @@ const Calendar = ({ type = 'single', showTime = false, showKeyboardHints = true,
|
|
|
143
151
|
const [start, end] = newValue;
|
|
144
152
|
if (start && end && hasUnavailableDateInRange(start, end)) return;
|
|
145
153
|
}
|
|
146
|
-
|
|
154
|
+
commitValue(newValue);
|
|
147
155
|
}, [
|
|
148
|
-
|
|
156
|
+
commitValue,
|
|
149
157
|
disallowDisabledDatesInRange,
|
|
150
158
|
isRange,
|
|
151
159
|
hasUnavailableDateInRange
|
|
@@ -161,7 +169,9 @@ const Calendar = ({ type = 'single', showTime = false, showKeyboardHints = true,
|
|
|
161
169
|
isRange,
|
|
162
170
|
showKeyboardHints,
|
|
163
171
|
showTime,
|
|
164
|
-
readonly
|
|
172
|
+
readonly,
|
|
173
|
+
timeRef,
|
|
174
|
+
commitValue
|
|
165
175
|
};
|
|
166
176
|
return /*#__PURE__*/ jsx(DatePicker.Root, {
|
|
167
177
|
selectionMode: isRange ? 'range' : 'single',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import type { CalendarType } from './types';
|
|
1
|
+
import { type ReactNode, type RefObject } from 'react';
|
|
2
|
+
import type { CalendarType, DateValue } from './types';
|
|
3
3
|
export interface CalendarContextValue {
|
|
4
4
|
/** Calendar type: 'single' for one month, 'range' for two months (date range selection) */
|
|
5
5
|
type: CalendarType;
|
|
@@ -11,6 +11,23 @@ export interface CalendarContextValue {
|
|
|
11
11
|
showTime: boolean;
|
|
12
12
|
/** Whether calendar is readonly (display only, no selection) */
|
|
13
13
|
readonly: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Last-known time-of-day, owned by `Calendar`. In `showTime` mode the grid
|
|
16
|
+
* only produces date-only values; this ref carries the hour/minute so they
|
|
17
|
+
* survive a day change and so `Calendar`'s `onChange` can emit a full
|
|
18
|
+
* `CalendarDateTime`. Undefined outside a `Calendar`.
|
|
19
|
+
*/
|
|
20
|
+
timeRef?: RefObject<{
|
|
21
|
+
hour: number;
|
|
22
|
+
minute: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Push a value straight to the consumer `onChange`, bypassing Ark's machine.
|
|
26
|
+
* Ark's DatePicker dedupes its value by date (year/month/day) and silently
|
|
27
|
+
* drops a same-date, time-only change, so the `showTime` header uses this to
|
|
28
|
+
* commit a time edit. Undefined outside a `Calendar`.
|
|
29
|
+
*/
|
|
30
|
+
commitValue?: (value: DateValue[]) => void;
|
|
14
31
|
}
|
|
15
32
|
export interface CalendarProviderProps {
|
|
16
33
|
children: ReactNode;
|
|
@@ -12,6 +12,7 @@ import { Tooltip } from "../Tooltip/index.js";
|
|
|
12
12
|
import { TooltipContent } from "../Tooltip/TooltipContent.js";
|
|
13
13
|
import { TooltipTrigger } from "../Tooltip/TooltipTrigger.js";
|
|
14
14
|
import { useCalendarContext } from "./CalendarContext.js";
|
|
15
|
+
import { toDateValue } from "./dateValue.js";
|
|
15
16
|
const CalendarHeader = ({ showArrows = true, showJumpToToday = false, visibleRangeText: visibleRangeTextProp })=>{
|
|
16
17
|
const { isRange, readonly } = useCalendarContext();
|
|
17
18
|
const effectiveShowArrows = showArrows && !readonly;
|
|
@@ -60,7 +61,7 @@ const CalendarHeader = ({ showArrows = true, showJumpToToday = false, visibleRan
|
|
|
60
61
|
size: "small",
|
|
61
62
|
"aria-label": "Jump to today",
|
|
62
63
|
onClick: ()=>{
|
|
63
|
-
api.setFocusedValue(todayDate);
|
|
64
|
+
api.setFocusedValue(toDateValue(todayDate));
|
|
64
65
|
},
|
|
65
66
|
children: /*#__PURE__*/ jsx(JumpToTodayIcon, {
|
|
66
67
|
size: "md"
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useReducer } from "react";
|
|
3
3
|
import { DatePicker } from "@ark-ui/react";
|
|
4
|
-
import { CalendarDateTime } from "@internationalized/date";
|
|
5
4
|
import { cn } from "../../utils/cn.js";
|
|
6
5
|
import { DateInput } from "../DateInput/index.js";
|
|
7
6
|
import { useCalendarContext } from "./CalendarContext.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
const toArkDateValue = (date)=>{
|
|
13
|
-
if (!date) return;
|
|
14
|
-
return date;
|
|
15
|
-
};
|
|
16
|
-
const withTime = (value, time)=>{
|
|
17
|
-
if (!value) return null;
|
|
18
|
-
if ('hour' in value) return value;
|
|
19
|
-
return new CalendarDateTime(value.year, value.month, value.day, time.hour, time.minute);
|
|
20
|
-
};
|
|
21
|
-
const SingleDateInputInner = ({ api, readonly, showTime })=>{
|
|
22
|
-
const [time, setTime] = useState({
|
|
23
|
-
hour: 0,
|
|
24
|
-
minute: 0
|
|
25
|
-
});
|
|
7
|
+
import { sameDate, toArkDateValue, toReactAriaDateValue, withTime } from "./dateValue.js";
|
|
8
|
+
const SingleDateInputInner = ({ api, readonly, showTime, timeRef, commitValue })=>{
|
|
9
|
+
const [, bumpTime] = useReducer((n)=>n + 1, 0);
|
|
26
10
|
const handleChange = useCallback((newValue)=>{
|
|
27
11
|
const arkValue = toArkDateValue(newValue);
|
|
28
12
|
if (arkValue) api.setValue([
|
|
@@ -33,20 +17,34 @@ const SingleDateInputInner = ({ api, readonly, showTime })=>{
|
|
|
33
17
|
api
|
|
34
18
|
]);
|
|
35
19
|
const handleDateTimeChange = useCallback((newValue)=>{
|
|
36
|
-
if (newValue && 'hour' in newValue)
|
|
20
|
+
if (newValue && 'hour' in newValue && timeRef) timeRef.current = {
|
|
37
21
|
hour: newValue.hour,
|
|
38
22
|
minute: newValue.minute
|
|
39
|
-
}
|
|
40
|
-
|
|
23
|
+
};
|
|
24
|
+
if (newValue && sameDate(api.value[0], newValue)) {
|
|
25
|
+
const arkValue = toArkDateValue(newValue);
|
|
26
|
+
if (arkValue) commitValue?.([
|
|
27
|
+
arkValue
|
|
28
|
+
]);
|
|
29
|
+
bumpTime();
|
|
30
|
+
} else handleChange(newValue);
|
|
41
31
|
}, [
|
|
42
|
-
|
|
32
|
+
api,
|
|
33
|
+
handleChange,
|
|
34
|
+
timeRef,
|
|
35
|
+
commitValue
|
|
43
36
|
]);
|
|
44
37
|
const inputValue = toReactAriaDateValue(api.value[0]);
|
|
38
|
+
const time = timeRef?.current ?? {
|
|
39
|
+
hour: 0,
|
|
40
|
+
minute: 0
|
|
41
|
+
};
|
|
42
|
+
const dateTimeValue = inputValue ? toReactAriaDateValue(withTime(inputValue, time)) : null;
|
|
45
43
|
return /*#__PURE__*/ jsx("div", {
|
|
46
44
|
className: "flex flex-1",
|
|
47
45
|
onKeyDown: (e)=>e.stopPropagation(),
|
|
48
46
|
children: showTime ? /*#__PURE__*/ jsx(DateInput, {
|
|
49
|
-
value:
|
|
47
|
+
value: dateTimeValue,
|
|
50
48
|
onChange: handleDateTimeChange,
|
|
51
49
|
readOnly: readonly,
|
|
52
50
|
granularity: "minute",
|
|
@@ -121,7 +119,7 @@ const RangeDateInputInner = ({ api, readonly })=>{
|
|
|
121
119
|
});
|
|
122
120
|
};
|
|
123
121
|
const CalendarInputHeader = ({ className })=>{
|
|
124
|
-
const { isRange, readonly, showTime } = useCalendarContext();
|
|
122
|
+
const { isRange, readonly, showTime, timeRef, commitValue } = useCalendarContext();
|
|
125
123
|
return /*#__PURE__*/ jsx("div", {
|
|
126
124
|
className: cn('flex flex-1 items-center', 'pt-20 pb-4 px-20', className),
|
|
127
125
|
children: /*#__PURE__*/ jsx(DatePicker.Context, {
|
|
@@ -131,7 +129,9 @@ const CalendarInputHeader = ({ className })=>{
|
|
|
131
129
|
}) : /*#__PURE__*/ jsx(SingleDateInputInner, {
|
|
132
130
|
api: api,
|
|
133
131
|
readonly: readonly,
|
|
134
|
-
showTime: showTime
|
|
132
|
+
showTime: showTime,
|
|
133
|
+
timeRef: timeRef,
|
|
134
|
+
commitValue: commitValue
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
137
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DateValue as ReactAriaDateValue } from '@react-aria/datepicker';
|
|
2
|
+
import type { DateValue } from './types';
|
|
3
|
+
type DateParts = {
|
|
4
|
+
year: number;
|
|
5
|
+
month: number;
|
|
6
|
+
day: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const toDateValue: <T>(date: T) => DateValue;
|
|
9
|
+
export declare const toReactAriaDateValue: (date: DateValue | undefined) => ReactAriaDateValue | null;
|
|
10
|
+
export declare const toArkDateValue: (date: ReactAriaDateValue | null | undefined) => DateValue | undefined;
|
|
11
|
+
/** Build a `CalendarDateTime` from `date`'s day and `time`; any time on `date` is ignored. */
|
|
12
|
+
export declare const withTime: (date: DateParts, time: {
|
|
13
|
+
hour: number;
|
|
14
|
+
minute: number;
|
|
15
|
+
}) => DateValue;
|
|
16
|
+
/** True when both values fall on the same calendar day. */
|
|
17
|
+
export declare const sameDate: (a: DateParts | null | undefined, b: DateParts | null | undefined) => boolean;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CalendarDateTime } from "@internationalized/date";
|
|
2
|
+
const toDateValue = (date)=>date;
|
|
3
|
+
const toReactAriaDateValue = (date)=>date ? date : null;
|
|
4
|
+
const toArkDateValue = (date)=>date ? date : void 0;
|
|
5
|
+
const withTime = (date, time)=>toDateValue(new CalendarDateTime(date.year, date.month, date.day, time.hour, time.minute));
|
|
6
|
+
const sameDate = (a, b)=>!!a && !!b && a.year === b.year && a.month === b.month && a.day === b.day;
|
|
7
|
+
export { sameDate, toArkDateValue, toDateValue, toReactAriaDateValue, withTime };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { DateValue } from './types';
|
|
3
|
+
interface UseCalendarTimeParams {
|
|
4
|
+
showTime: boolean;
|
|
5
|
+
isRange: boolean;
|
|
6
|
+
value?: DateValue[];
|
|
7
|
+
defaultValue?: DateValue[];
|
|
8
|
+
onChange?: (value: DateValue[]) => void;
|
|
9
|
+
}
|
|
10
|
+
interface UseCalendarTime {
|
|
11
|
+
/** Last-known time-of-day; read synchronously when promoting grid picks. */
|
|
12
|
+
timeRef: RefObject<{
|
|
13
|
+
hour: number;
|
|
14
|
+
minute: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** Emit to the consumer, promoting date-only values to `CalendarDateTime`. */
|
|
17
|
+
commitValue: (value: DateValue[]) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Time handling for single `showTime` mode. The grid emits date-only values and
|
|
21
|
+
* Ark keys its value by date, so time must be tracked here and merged in before
|
|
22
|
+
* reaching `onChange`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const useCalendarTime: ({ showTime, isRange, value, defaultValue, onChange, }: UseCalendarTimeParams) => UseCalendarTime;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { withTime } from "./dateValue.js";
|
|
3
|
+
const useCalendarTime = ({ showTime, isRange, value, defaultValue, onChange })=>{
|
|
4
|
+
const timeRef = useRef({
|
|
5
|
+
hour: 0,
|
|
6
|
+
minute: 0
|
|
7
|
+
});
|
|
8
|
+
if (showTime && !isRange) {
|
|
9
|
+
const current = (value ?? defaultValue)?.[0];
|
|
10
|
+
if (current && 'hour' in current) timeRef.current = {
|
|
11
|
+
hour: current.hour,
|
|
12
|
+
minute: current.minute
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const withTrackedTime = useCallback((date)=>{
|
|
16
|
+
if ('hour' in date) {
|
|
17
|
+
timeRef.current = {
|
|
18
|
+
hour: date.hour,
|
|
19
|
+
minute: date.minute
|
|
20
|
+
};
|
|
21
|
+
return date;
|
|
22
|
+
}
|
|
23
|
+
return withTime(date, timeRef.current);
|
|
24
|
+
}, []);
|
|
25
|
+
const commitValue = useCallback((next)=>{
|
|
26
|
+
onChange?.(showTime && !isRange ? next.map(withTrackedTime) : next);
|
|
27
|
+
}, [
|
|
28
|
+
showTime,
|
|
29
|
+
isRange,
|
|
30
|
+
withTrackedTime,
|
|
31
|
+
onChange
|
|
32
|
+
]);
|
|
33
|
+
return {
|
|
34
|
+
timeRef,
|
|
35
|
+
commitValue
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export { useCalendarTime };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FC, InputHTMLAttributes, Ref } from 'react';
|
|
2
|
-
interface DropdownMenuInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
2
|
+
interface DropdownMenuInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
|
3
3
|
ref?: Ref<HTMLInputElement>;
|
|
4
4
|
}
|
|
5
5
|
export declare const DropdownMenuInput: FC<DropdownMenuInputProps>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import type { TestableProps } from '../../utils/testId';
|
|
3
|
+
import { type InlineEditActivationMode, type InlineEditStatus, type InlineEditSubmitMode } from './InlineEditContext';
|
|
4
|
+
export interface InlineEditProps<T = unknown> extends TestableProps {
|
|
5
|
+
value?: T;
|
|
6
|
+
defaultValue?: T;
|
|
7
|
+
onValueChange?: (value: T) => void;
|
|
8
|
+
onValueCommit?: (value: T) => void | Promise<unknown>;
|
|
9
|
+
onValueRevert?: (value: T) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Commit guard. Called on every submit (Enter, blur, popover close,
|
|
12
|
+
* confirm button) before `onValueCommit`, with the draft to be committed
|
|
13
|
+
* and the current committed value. Only an explicit `false` (or a promise
|
|
14
|
+
* resolving to `false`) blocks the commit: the field silently stays in
|
|
15
|
+
* edit mode with the draft. Any other result — `true`, `undefined`, no
|
|
16
|
+
* return — lets the commit proceed. A rejection or a synchronous throw is
|
|
17
|
+
* treated like a commit failure: status `error`, message from the error,
|
|
18
|
+
* field stays in edit mode.
|
|
19
|
+
*
|
|
20
|
+
* While the guard's promise is pending the edit session is held: duplicate
|
|
21
|
+
* submits are suppressed, blur neither submits nor cancels, and the field
|
|
22
|
+
* shows no status (`idle` — the consumer's confirmation UI is the
|
|
23
|
+
* feedback). Escape still cancels; a late resolution after cancel is
|
|
24
|
+
* dropped. The value committed on `true` is exactly the `value` argument
|
|
25
|
+
* the guard received; if the draft changed while the guard was pending,
|
|
26
|
+
* the resolution is dropped and the field stays in edit mode.
|
|
27
|
+
*
|
|
28
|
+
* Guards must settle via non-blocking UI (a promise resolved by dialog
|
|
29
|
+
* buttons). Synchronous blocking dialogs (`window.confirm`) are
|
|
30
|
+
* unsupported with submitMode 'blur'/'both' (the queued blur re-prompts).
|
|
31
|
+
* The guard's owner never gets its consumer UI closed by the DS — treat
|
|
32
|
+
* the dialog's own buttons as the source of truth for closing it.
|
|
33
|
+
*/
|
|
34
|
+
onBeforeValueCommit?: (value: T, committedValue: T) => boolean | void | Promise<boolean | void>;
|
|
35
|
+
edit?: boolean;
|
|
36
|
+
defaultEdit?: boolean;
|
|
37
|
+
onEditChange?: (editing: boolean) => void;
|
|
38
|
+
activationMode?: InlineEditActivationMode;
|
|
39
|
+
submitMode?: InlineEditSubmitMode;
|
|
40
|
+
selectOnFocus?: boolean;
|
|
41
|
+
status?: InlineEditStatus;
|
|
42
|
+
savedDuration?: number;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
ref?: Ref<HTMLDivElement>;
|
|
48
|
+
}
|
|
49
|
+
export declare function InlineEdit<T = unknown>({ value, defaultValue, onValueChange, onValueCommit, onValueRevert, onBeforeValueCommit, edit, defaultEdit, onEditChange, activationMode, submitMode, selectOnFocus, status, savedDuration, disabled, readOnly, className, children, ref, 'data-testid': testIdProp, }: InlineEditProps<T>): ReactElement;
|
|
50
|
+
export declare namespace InlineEdit {
|
|
51
|
+
var displayName: string;
|
|
52
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useControlled } from "../../hooks/useControlled.js";
|
|
4
|
+
import { cn } from "../../utils/cn.js";
|
|
5
|
+
import { TestIdProvider, useTestId } from "../../utils/testId.js";
|
|
6
|
+
import { InlineEditProvider } from "./InlineEditContext.js";
|
|
7
|
+
function InlineEdit({ value, defaultValue, onValueChange, onValueCommit, onValueRevert, onBeforeValueCommit, edit, defaultEdit = false, onEditChange, activationMode = 'click', submitMode = 'both', selectOnFocus = true, status, savedDuration = 2000, disabled = false, readOnly = false, className, children, ref, 'data-testid': testIdProp }) {
|
|
8
|
+
const testId = useTestId(void 0, testIdProp);
|
|
9
|
+
const [committedValue, setCommitted] = useControlled({
|
|
10
|
+
controlled: value,
|
|
11
|
+
default: defaultValue
|
|
12
|
+
});
|
|
13
|
+
const [editing, setEditing] = useControlled({
|
|
14
|
+
controlled: edit,
|
|
15
|
+
default: defaultEdit
|
|
16
|
+
});
|
|
17
|
+
const [draft, setDraft] = useState(committedValue);
|
|
18
|
+
const draftRef = useRef(committedValue);
|
|
19
|
+
const [autoStatus, setAutoStatus] = useState('idle');
|
|
20
|
+
const [autoError, setAutoError] = useState(void 0);
|
|
21
|
+
const [submitModeOverride, setSubmitModeOverride] = useState(null);
|
|
22
|
+
const overrideRef = useRef(null);
|
|
23
|
+
overrideRef.current = submitModeOverride;
|
|
24
|
+
const registerSubmitModeOverride = useCallback((mode)=>{
|
|
25
|
+
const prev = overrideRef.current;
|
|
26
|
+
if ('production' !== process.env.NODE_ENV && prev && prev.mode !== mode) console.warn(`InlineEdit: an editor registered submitMode='${mode}' while another editor's '${prev.mode}' override is active. Last registration wins.`);
|
|
27
|
+
const entry = {
|
|
28
|
+
token: Symbol('inline-edit-submit-mode'),
|
|
29
|
+
mode
|
|
30
|
+
};
|
|
31
|
+
setSubmitModeOverride(entry);
|
|
32
|
+
return ()=>{
|
|
33
|
+
setSubmitModeOverride((current)=>current?.token === entry.token ? null : current);
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
const pendingCommitRef = useRef(null);
|
|
37
|
+
const isCommitPending = useCallback(()=>null !== pendingCommitRef.current, []);
|
|
38
|
+
const onValueCommitRef = useRef(onValueCommit);
|
|
39
|
+
onValueCommitRef.current = onValueCommit;
|
|
40
|
+
const onBeforeValueCommitRef = useRef(onBeforeValueCommit);
|
|
41
|
+
onBeforeValueCommitRef.current = onBeforeValueCommit;
|
|
42
|
+
const savedDurationRef = useRef(savedDuration);
|
|
43
|
+
savedDurationRef.current = savedDuration;
|
|
44
|
+
const savedTimer = useRef(null);
|
|
45
|
+
const mounted = useRef(true);
|
|
46
|
+
useEffect(()=>{
|
|
47
|
+
mounted.current = true;
|
|
48
|
+
return ()=>{
|
|
49
|
+
mounted.current = false;
|
|
50
|
+
if (savedTimer.current) clearTimeout(savedTimer.current);
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
const resolvedStatus = status ?? autoStatus;
|
|
54
|
+
const resolvedError = autoError;
|
|
55
|
+
const invalid = 'error' === resolvedStatus || Boolean(resolvedError);
|
|
56
|
+
const setEditingState = useCallback((next)=>{
|
|
57
|
+
setEditing(next);
|
|
58
|
+
onEditChange?.(next);
|
|
59
|
+
}, [
|
|
60
|
+
setEditing,
|
|
61
|
+
onEditChange
|
|
62
|
+
]);
|
|
63
|
+
const handleSetValue = useCallback((next)=>{
|
|
64
|
+
draftRef.current = next;
|
|
65
|
+
setDraft(next);
|
|
66
|
+
onValueChange?.(next);
|
|
67
|
+
}, [
|
|
68
|
+
onValueChange
|
|
69
|
+
]);
|
|
70
|
+
const edit_ = useCallback(()=>{
|
|
71
|
+
if (disabled || readOnly) return;
|
|
72
|
+
pendingCommitRef.current = null;
|
|
73
|
+
draftRef.current = committedValue;
|
|
74
|
+
setDraft(committedValue);
|
|
75
|
+
setAutoStatus('idle');
|
|
76
|
+
setAutoError(void 0);
|
|
77
|
+
setEditingState(true);
|
|
78
|
+
}, [
|
|
79
|
+
disabled,
|
|
80
|
+
readOnly,
|
|
81
|
+
committedValue,
|
|
82
|
+
setEditingState
|
|
83
|
+
]);
|
|
84
|
+
const cancel = useCallback(()=>{
|
|
85
|
+
pendingCommitRef.current = null;
|
|
86
|
+
draftRef.current = committedValue;
|
|
87
|
+
setDraft(committedValue);
|
|
88
|
+
setAutoStatus('idle');
|
|
89
|
+
setAutoError(void 0);
|
|
90
|
+
setEditingState(false);
|
|
91
|
+
onValueRevert?.(committedValue);
|
|
92
|
+
}, [
|
|
93
|
+
committedValue,
|
|
94
|
+
setEditingState,
|
|
95
|
+
onValueRevert
|
|
96
|
+
]);
|
|
97
|
+
const submit = useCallback(()=>{
|
|
98
|
+
if (pendingCommitRef.current) return;
|
|
99
|
+
if ((status ?? autoStatus) === 'loading') return;
|
|
100
|
+
const current = draftRef.current;
|
|
101
|
+
const token = Symbol('inline-edit-commit');
|
|
102
|
+
const failCommit = (reason)=>{
|
|
103
|
+
pendingCommitRef.current = null;
|
|
104
|
+
setAutoStatus('error');
|
|
105
|
+
setAutoError(reason instanceof Error ? reason.message : 'Failed to save');
|
|
106
|
+
};
|
|
107
|
+
const runCommit = ()=>{
|
|
108
|
+
const result = onValueCommitRef.current?.(current);
|
|
109
|
+
if (result && 'function' == typeof result.then) {
|
|
110
|
+
setAutoStatus('loading');
|
|
111
|
+
setAutoError(void 0);
|
|
112
|
+
Promise.resolve(result).then(()=>{
|
|
113
|
+
if (!mounted.current || pendingCommitRef.current !== token) return;
|
|
114
|
+
pendingCommitRef.current = null;
|
|
115
|
+
setCommitted(current);
|
|
116
|
+
setEditingState(false);
|
|
117
|
+
setAutoStatus('saved');
|
|
118
|
+
if (savedTimer.current) clearTimeout(savedTimer.current);
|
|
119
|
+
savedTimer.current = setTimeout(()=>{
|
|
120
|
+
if (mounted.current) setAutoStatus('idle');
|
|
121
|
+
}, savedDurationRef.current);
|
|
122
|
+
}, (reason)=>{
|
|
123
|
+
if (!mounted.current || pendingCommitRef.current !== token) return;
|
|
124
|
+
failCommit(reason);
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
pendingCommitRef.current = null;
|
|
129
|
+
setCommitted(current);
|
|
130
|
+
setEditingState(false);
|
|
131
|
+
};
|
|
132
|
+
pendingCommitRef.current = token;
|
|
133
|
+
const guard = onBeforeValueCommitRef.current;
|
|
134
|
+
if (!guard) return void runCommit();
|
|
135
|
+
let verdict;
|
|
136
|
+
try {
|
|
137
|
+
verdict = guard(current, committedValue);
|
|
138
|
+
} catch (reason) {
|
|
139
|
+
failCommit(reason);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (verdict && 'function' == typeof verdict.then) return void verdict.then((resolved)=>{
|
|
143
|
+
if (!mounted.current || pendingCommitRef.current !== token) return;
|
|
144
|
+
if (false === resolved) {
|
|
145
|
+
pendingCommitRef.current = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (draftRef.current !== current) {
|
|
149
|
+
pendingCommitRef.current = null;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
runCommit();
|
|
153
|
+
}, (reason)=>{
|
|
154
|
+
if (!mounted.current || pendingCommitRef.current !== token) return;
|
|
155
|
+
failCommit(reason);
|
|
156
|
+
});
|
|
157
|
+
if (false === verdict) {
|
|
158
|
+
pendingCommitRef.current = null;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
runCommit();
|
|
162
|
+
}, [
|
|
163
|
+
setCommitted,
|
|
164
|
+
setEditingState,
|
|
165
|
+
status,
|
|
166
|
+
autoStatus,
|
|
167
|
+
committedValue
|
|
168
|
+
]);
|
|
169
|
+
const contextValue = useMemo(()=>({
|
|
170
|
+
editing: Boolean(editing),
|
|
171
|
+
value: draft,
|
|
172
|
+
committedValue: committedValue,
|
|
173
|
+
status: resolvedStatus,
|
|
174
|
+
invalid,
|
|
175
|
+
error: resolvedError,
|
|
176
|
+
disabled,
|
|
177
|
+
readOnly,
|
|
178
|
+
activationMode,
|
|
179
|
+
submitMode: submitModeOverride?.mode ?? submitMode,
|
|
180
|
+
selectOnFocus,
|
|
181
|
+
isCommitPending,
|
|
182
|
+
setValue: handleSetValue,
|
|
183
|
+
edit: edit_,
|
|
184
|
+
submit,
|
|
185
|
+
cancel,
|
|
186
|
+
registerSubmitModeOverride
|
|
187
|
+
}), [
|
|
188
|
+
editing,
|
|
189
|
+
draft,
|
|
190
|
+
committedValue,
|
|
191
|
+
resolvedStatus,
|
|
192
|
+
invalid,
|
|
193
|
+
resolvedError,
|
|
194
|
+
disabled,
|
|
195
|
+
readOnly,
|
|
196
|
+
activationMode,
|
|
197
|
+
submitMode,
|
|
198
|
+
submitModeOverride,
|
|
199
|
+
selectOnFocus,
|
|
200
|
+
isCommitPending,
|
|
201
|
+
handleSetValue,
|
|
202
|
+
edit_,
|
|
203
|
+
submit,
|
|
204
|
+
cancel,
|
|
205
|
+
registerSubmitModeOverride
|
|
206
|
+
]);
|
|
207
|
+
return /*#__PURE__*/ jsx(InlineEditProvider, {
|
|
208
|
+
value: contextValue,
|
|
209
|
+
children: /*#__PURE__*/ jsx(TestIdProvider, {
|
|
210
|
+
value: testId,
|
|
211
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
212
|
+
ref: ref,
|
|
213
|
+
"data-testid": testId,
|
|
214
|
+
"data-slot": "inline-edit",
|
|
215
|
+
"data-editing": editing ? '' : void 0,
|
|
216
|
+
className: cn('flex w-full min-w-0 flex-col', className),
|
|
217
|
+
children: children
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
InlineEdit.displayName = 'InlineEdit';
|
|
223
|
+
export { InlineEdit };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type InlineEditStatus = 'idle' | 'loading' | 'saved' | 'error';
|
|
2
|
+
export type InlineEditActivationMode = 'click' | 'focus' | 'none';
|
|
3
|
+
export type InlineEditSubmitMode = 'enter' | 'blur' | 'both' | 'none';
|
|
4
|
+
export interface InlineEditContextValue<T = unknown> {
|
|
5
|
+
editing: boolean;
|
|
6
|
+
value: T;
|
|
7
|
+
committedValue: T;
|
|
8
|
+
status: InlineEditStatus;
|
|
9
|
+
invalid: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
readOnly: boolean;
|
|
13
|
+
activationMode: InlineEditActivationMode;
|
|
14
|
+
submitMode: InlineEditSubmitMode;
|
|
15
|
+
selectOnFocus: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* True while a submit is held by a pending `onBeforeValueCommit` guard or
|
|
18
|
+
* an in-flight `onValueCommit` promise. Ref-backed function so event
|
|
19
|
+
* handlers get a synchronous answer — blur can fire in the same tick the
|
|
20
|
+
* guard opens a dialog, before any re-render.
|
|
21
|
+
*/
|
|
22
|
+
isCommitPending: () => boolean;
|
|
23
|
+
setValue: (value: T) => void;
|
|
24
|
+
edit: () => void;
|
|
25
|
+
submit: () => void;
|
|
26
|
+
cancel: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Registers a submit-mode override while an editor is mounted (last
|
|
29
|
+
* registration wins; returns an unregister cleanup). Prefer the
|
|
30
|
+
* `useInlineEditSubmitMode` hook. Popover editors use this so consumers
|
|
31
|
+
* never have to pair `submitMode='none'` on the root by hand.
|
|
32
|
+
*/
|
|
33
|
+
registerSubmitModeOverride: (mode: InlineEditSubmitMode) => () => void;
|
|
34
|
+
}
|
|
35
|
+
export declare const InlineEditProvider: import("react").Provider<InlineEditContextValue<unknown> | null>;
|
|
36
|
+
export declare function useInlineEdit<T = unknown>(): InlineEditContextValue<T>;
|
|
37
|
+
/**
|
|
38
|
+
* Declares the submit mode an editor needs while it is mounted (e.g. a
|
|
39
|
+
* popover editor commits on close, so blur/enter handling must be 'none').
|
|
40
|
+
* Registered via layout effect so the override is committed before paint and
|
|
41
|
+
* before any browser event can observe the consumer-provided mode.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useInlineEditSubmitMode(mode: InlineEditSubmitMode): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext, useLayoutEffect } from "react";
|
|
2
|
+
const InlineEditContext = createContext(null);
|
|
3
|
+
const InlineEditProvider = InlineEditContext.Provider;
|
|
4
|
+
function useInlineEdit() {
|
|
5
|
+
const ctx = useContext(InlineEditContext);
|
|
6
|
+
if (!ctx) throw new Error('useInlineEdit must be used within <InlineEdit>');
|
|
7
|
+
return ctx;
|
|
8
|
+
}
|
|
9
|
+
function useInlineEditSubmitMode(mode) {
|
|
10
|
+
const { registerSubmitModeOverride } = useInlineEdit();
|
|
11
|
+
useLayoutEffect(()=>registerSubmitModeOverride(mode), [
|
|
12
|
+
mode,
|
|
13
|
+
registerSubmitModeOverride
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
export { InlineEditProvider, useInlineEdit, useInlineEditSubmitMode };
|