@wallarm-org/design-system 0.71.4 → 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.
Files changed (91) hide show
  1. package/README.md +1 -3
  2. package/dist/components/Attribute/AttributeValue.js +1 -1
  3. package/dist/components/Calendar/Calendar.js +14 -4
  4. package/dist/components/Calendar/CalendarContext.d.ts +19 -2
  5. package/dist/components/Calendar/CalendarHeader.js +2 -1
  6. package/dist/components/Calendar/CalendarInputHeader.js +27 -27
  7. package/dist/components/Calendar/dateValue.d.ts +18 -0
  8. package/dist/components/Calendar/dateValue.js +7 -0
  9. package/dist/components/Calendar/useCalendarTime.d.ts +25 -0
  10. package/dist/components/Calendar/useCalendarTime.js +38 -0
  11. package/dist/components/DropdownMenu/DropdownMenuInput.d.ts +1 -1
  12. package/dist/components/InlineEdit/InlineEdit.d.ts +52 -0
  13. package/dist/components/InlineEdit/InlineEdit.js +223 -0
  14. package/dist/components/InlineEdit/InlineEditContext.d.ts +43 -0
  15. package/dist/components/InlineEdit/InlineEditContext.js +16 -0
  16. package/dist/components/InlineEdit/InlineEditControl.d.ts +14 -0
  17. package/dist/components/InlineEdit/InlineEditControl.js +72 -0
  18. package/dist/components/InlineEdit/InlineEditDate.d.ts +19 -0
  19. package/dist/components/InlineEdit/InlineEditDate.js +22 -0
  20. package/dist/components/InlineEdit/InlineEditDateTime.d.ts +21 -0
  21. package/dist/components/InlineEdit/InlineEditDateTime.js +23 -0
  22. package/dist/components/InlineEdit/InlineEditError.d.ts +6 -0
  23. package/dist/components/InlineEdit/InlineEditError.js +32 -0
  24. package/dist/components/InlineEdit/InlineEditInput.d.ts +4 -0
  25. package/dist/components/InlineEdit/InlineEditInput.js +18 -0
  26. package/dist/components/InlineEdit/InlineEditNumber.d.ts +11 -0
  27. package/dist/components/InlineEdit/InlineEditNumber.js +18 -0
  28. package/dist/components/InlineEdit/InlineEditPreview.d.ts +23 -0
  29. package/dist/components/InlineEdit/InlineEditPreview.js +105 -0
  30. package/dist/components/InlineEdit/InlineEditPreviewIcon.d.ts +12 -0
  31. package/dist/components/InlineEdit/InlineEditPreviewIcon.js +43 -0
  32. package/dist/components/InlineEdit/InlineEditPreviewValue.d.ts +11 -0
  33. package/dist/components/InlineEdit/InlineEditPreviewValue.js +25 -0
  34. package/dist/components/InlineEdit/InlineEditSelect.d.ts +39 -0
  35. package/dist/components/InlineEdit/InlineEditSelect.js +42 -0
  36. package/dist/components/InlineEdit/InlineEditTextarea.d.ts +4 -0
  37. package/dist/components/InlineEdit/InlineEditTextarea.js +18 -0
  38. package/dist/components/InlineEdit/InlineEditTime.d.ts +12 -0
  39. package/dist/components/InlineEdit/InlineEditTime.js +21 -0
  40. package/dist/components/InlineEdit/classes.d.ts +5 -0
  41. package/dist/components/InlineEdit/classes.js +35 -0
  42. package/dist/components/InlineEdit/dateValueCast.d.ts +25 -0
  43. package/dist/components/InlineEdit/dateValueCast.js +9 -0
  44. package/dist/components/InlineEdit/index.d.ts +15 -0
  45. package/dist/components/InlineEdit/index.js +16 -0
  46. package/dist/components/Input/Input.d.ts +1 -1
  47. package/dist/components/Input/Input.js +4 -3
  48. package/dist/components/Input/classes.d.ts +1 -0
  49. package/dist/components/Input/classes.js +8 -0
  50. package/dist/components/NumberInput/NumberInput.d.ts +4 -1
  51. package/dist/components/NumberInput/NumberInput.js +17 -7
  52. package/dist/components/NumberInput/classes.d.ts +12 -0
  53. package/dist/components/NumberInput/classes.js +51 -0
  54. package/dist/components/Select/SelectButton.d.ts +2 -1
  55. package/dist/components/Select/SelectButton.js +7 -2
  56. package/dist/components/Select/SelectInput/SelectInput.d.ts +1 -0
  57. package/dist/components/Select/SelectInput/SelectInput.js +21 -7
  58. package/dist/components/Select/SelectInput/SelectInputItemRenderer.d.ts +4 -0
  59. package/dist/components/Select/SelectInput/SelectInputItemRenderer.js +8 -3
  60. package/dist/components/Select/SelectInput/SelectInputOverflowRenderer.d.ts +1 -1
  61. package/dist/components/Select/SelectInput/SelectInputOverflowRenderer.js +28 -27
  62. package/dist/components/Slider/SliderInput.d.ts +1 -1
  63. package/dist/components/Textarea/classes.js +2 -2
  64. package/dist/components/Toast/Toast.js +6 -6
  65. package/dist/components/Toast/Toaster.js +1 -1
  66. package/dist/components/Tree/TreeContext.d.ts +1 -9
  67. package/dist/components/Tree/TreeContext.js +2 -18
  68. package/dist/components/Tree/TreeItemContent.js +5 -8
  69. package/dist/components/TreeView/TreeView.d.ts +19 -0
  70. package/dist/components/TreeView/TreeView.js +82 -0
  71. package/dist/components/TreeView/TreeViewContext.d.ts +12 -0
  72. package/dist/components/TreeView/TreeViewContext.js +9 -0
  73. package/dist/components/TreeView/TreeViewItem.d.ts +24 -0
  74. package/dist/components/TreeView/TreeViewItem.js +168 -0
  75. package/dist/components/TreeView/classes.d.ts +5 -0
  76. package/dist/components/TreeView/classes.js +30 -0
  77. package/dist/components/TreeView/index.d.ts +9 -0
  78. package/dist/components/TreeView/index.js +5 -0
  79. package/dist/index.d.ts +7 -5
  80. package/dist/index.js +4 -2
  81. package/dist/metadata/components.json +6122 -2828
  82. package/dist/theme/fonts/Geist-Variable.woff2 +0 -0
  83. package/dist/theme/fonts/GeistMono-Variable.woff2 +0 -0
  84. package/dist/theme/fonts/geist.css +21 -0
  85. package/dist/theme/index.css +1 -0
  86. package/dist/theme/typography.css +5 -3
  87. package/dist/utils/Collapse.d.ts +18 -0
  88. package/dist/utils/Collapse.js +15 -0
  89. package/dist/utils/treeDepth.d.ts +11 -0
  90. package/dist/utils/treeDepth.js +18 -0
  91. 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 (non.geist)
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
- const toDateValue = (date)=>date;
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
- onChange?.(newValue);
154
+ commitValue(newValue);
147
155
  }, [
148
- onChange,
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, useState } from "react";
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
- const toReactAriaDateValue = (date)=>{
9
- if (!date) return null;
10
- return date;
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) setTime({
20
+ if (newValue && 'hour' in newValue && timeRef) timeRef.current = {
37
21
  hour: newValue.hour,
38
22
  minute: newValue.minute
39
- });
40
- handleChange(newValue);
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
- handleChange
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: withTime(inputValue, time),
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 };