@dmsi/wedgekit-react 0.0.29 → 0.0.30

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 (43) hide show
  1. package/dist/{chunk-57WRM337.js → chunk-5POWRPIO.js} +3 -2
  2. package/dist/{chunk-S5KPS4IQ.js → chunk-HXEPUO5W.js} +189 -95
  3. package/dist/chunk-KHQX42T7.js +127 -0
  4. package/dist/{chunk-OUSNH76S.js → chunk-PCCJ7L3N.js} +29 -6
  5. package/dist/{chunk-PDDZ5PMY.js → chunk-S46RZBT4.js} +3 -2
  6. package/dist/components/CalendarRange.cjs +28 -5
  7. package/dist/components/CalendarRange.js +1 -1
  8. package/dist/components/DataGrid.cjs +490 -217
  9. package/dist/components/DataGrid.js +303 -122
  10. package/dist/components/DataGridCell.cjs +192 -96
  11. package/dist/components/DataGridCell.js +4 -2
  12. package/dist/components/DateInput.cjs +231 -30
  13. package/dist/components/DateInput.js +101 -27
  14. package/dist/components/DateRangeInput.cjs +550 -37
  15. package/dist/components/DateRangeInput.js +413 -34
  16. package/dist/components/MenuOption.cjs +3 -2
  17. package/dist/components/MenuOption.js +1 -1
  18. package/dist/components/MobileDataGrid.cjs +3 -2
  19. package/dist/components/MobileDataGrid.js +1 -1
  20. package/dist/components/NestedMenu.cjs +3 -2
  21. package/dist/components/NestedMenu.js +1 -1
  22. package/dist/components/Notification.cjs +3 -2
  23. package/dist/components/Notification.js +1 -1
  24. package/dist/components/SideMenuGroup.cjs +3 -2
  25. package/dist/components/SideMenuGroup.js +1 -1
  26. package/dist/components/SideMenuItem.cjs +3 -2
  27. package/dist/components/SideMenuItem.js +1 -1
  28. package/dist/components/Stack.cjs +3 -2
  29. package/dist/components/Stack.js +1 -1
  30. package/dist/components/Swatch.cjs +3 -2
  31. package/dist/components/Swatch.js +1 -1
  32. package/dist/components/Time.cjs +3 -2
  33. package/dist/components/Time.js +1 -1
  34. package/dist/index.css +9 -0
  35. package/package.json +1 -1
  36. package/src/components/CalendarRange.tsx +37 -6
  37. package/src/components/DataGrid.tsx +284 -48
  38. package/src/components/DataGridCell.tsx +122 -35
  39. package/src/components/DateInput.tsx +118 -25
  40. package/src/components/DateRangeInput.tsx +544 -30
  41. package/src/components/MenuOption.tsx +18 -14
  42. package/src/components/Stack.tsx +4 -2
  43. package/src/utils/date.ts +206 -0
@@ -16,6 +16,7 @@ import {
16
16
  CSSProperties,
17
17
  memo,
18
18
  PropsWithChildren,
19
+ RefObject,
19
20
  useEffect,
20
21
  useRef,
21
22
  useState,
@@ -145,7 +146,7 @@ export const DataGridCell = memo(
145
146
  lockedHeaderBgStyles,
146
147
  iconComponentStyles,
147
148
  className,
148
- "flex flex-1 items-center gap-1 whitespace-nowrap min-w-full max-h-10 relative text-text-primary-normal",
149
+ "flex flex-1 items-center gap-1 whitespace-nowrap h-10 relative text-text-primary-normal",
149
150
  "focus-within:!z-10",
150
151
  component === "input" && "border",
151
152
  component === "input" &&
@@ -164,7 +165,7 @@ export const DataGridCell = memo(
164
165
  return (
165
166
  <Element
166
167
  id={id}
167
- className={clsx("flex", !width && "flex-1")}
168
+ className={clsx("flex h-10", !width && "flex-1")}
168
169
  style={{ width }}
169
170
  {...props}
170
171
  >
@@ -190,38 +191,44 @@ export const DataGridCell = memo(
190
191
 
191
192
  DataGridCell.displayName = "DataGridCell";
192
193
 
193
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
- export function DraggableCellHeader<T extends Record<string, any>>({
195
- header,
196
- children,
197
- locked = false,
198
- id,
199
- ...props
200
- }: {
194
+ export type DataGridCellHeaderProps<T> = {
201
195
  header: Header<T, unknown>;
202
196
  children: React.ReactNode;
203
- locked?: boolean;
204
197
  width?: string;
205
- id?: string;
206
- } & DataGridCellProps &
207
- ComponentProps<"th">) {
208
- const { attributes, isDragging, listeners, setNodeRef, transform, node } =
209
- useSortable({
210
- id: header.column.id,
211
- });
198
+ setNodeRef?: (node: HTMLElement | null) => void;
199
+ node?: RefObject<HTMLElement | null>;
200
+ } & DataGridCellProps & ComponentProps<"th">;
212
201
 
202
+ export function DataCellHeader<T extends unknown>({
203
+ header,
204
+ children,
205
+ setNodeRef,
206
+ node,
207
+ id,
208
+ ...props
209
+ }: DataGridCellHeaderProps<T>) {
213
210
  const [showMenu, setShowMenu] = useState(false);
214
211
  const [filter, setFilter] = useState(
215
212
  (header.column.getFilterValue() as string) ?? "",
216
213
  );
217
-
214
+ const ref = useRef<HTMLElement>(null);
215
+ const predeterminedPinned = useRef(false);
216
+
218
217
  const {
219
218
  menuRootRef,
220
219
  isMenuActive,
221
220
  registerSubMenu,
222
221
  listeners: subMenuListeners,
223
- mobileHide,
224
- } = useSubMenuSystem(node);
222
+ mobileHide
223
+ } = useSubMenuSystem(node ? node : ref);
224
+
225
+ useEffect(() => {
226
+ const columnPinning = header.getContext().table.options.initialState?.columnPinning;
227
+ const left = columnPinning?.left ? columnPinning.left : [];
228
+ const right = columnPinning?.right ? columnPinning.right : [];
229
+
230
+ predeterminedPinned.current = [...left, ...right].includes(header.column.id);
231
+ }, [])
225
232
 
226
233
  useEffect(() => {
227
234
  const handler = setTimeout(() => {
@@ -234,30 +241,23 @@ export function DraggableCellHeader<T extends Record<string, any>>({
234
241
  }, [filter]);
235
242
 
236
243
  const style: CSSProperties = {
237
- opacity: isDragging ? 0.8 : 1,
238
244
  position: "relative",
239
- transform: CSS.Translate.toString(transform),
240
- transition: "width transform 0.2s ease-in-out",
241
245
  whiteSpace: "nowrap",
242
246
  width: header.column.getSize(),
243
- zIndex: isDragging ? 1 : 0,
244
247
  "--color-text-primary-normal": "var(--color-neutral-000)",
248
+ ...props.style
245
249
  };
246
250
 
247
251
  return (
248
252
  <DataGridCell
249
253
  id={id}
250
- locked={locked}
254
+ ref={setNodeRef ? setNodeRef : ref}
251
255
  type="header"
252
256
  component="header"
253
- ref={setNodeRef}
254
- colSpan={header.colSpan}
255
257
  style={style}
256
- {...props}
257
258
  onClick={header.column.getToggleSortingHandler()}
258
259
  onRightClick={() => setShowMenu(!showMenu)}
259
- {...(locked ? {} : attributes)}
260
- {...(locked ? {} : listeners)}
260
+ {...props}
261
261
  >
262
262
  {children}
263
263
 
@@ -265,7 +265,7 @@ export function DraggableCellHeader<T extends Record<string, any>>({
265
265
  <Menu
266
266
  id={id ? `${id}-menu` : undefined}
267
267
  ref={menuRootRef}
268
- positionTo={node}
268
+ positionTo={node ? node : ref}
269
269
  show={showMenu}
270
270
  setShow={setShowMenu}
271
271
  mobileHide={mobileHide}
@@ -325,6 +325,54 @@ export function DraggableCellHeader<T extends Record<string, any>>({
325
325
  >
326
326
  Filter
327
327
  </MenuOption>
328
+
329
+ {!predeterminedPinned.current && header.column.getCanPin() && (
330
+ <MenuOption
331
+ onClick={() => {
332
+ setShowMenu(!showMenu);
333
+ }}
334
+ {...subMenuListeners}
335
+ subMenu={({ menuId, subMenuLevel, ...props }) => (
336
+ <Menu
337
+ {...props}
338
+ show={isMenuActive(menuId, subMenuLevel)}
339
+ ref={(el) => {
340
+ registerSubMenu(menuId, el);
341
+ }}
342
+ >
343
+ <MenuOption
344
+ selected={header.column.getIsPinned() === 'left'}
345
+ onClick={() => {
346
+ if (header.column.getIsPinned() === 'left') {
347
+ header.column.pin(false)
348
+ } else {
349
+ header.column.pin('left')
350
+ }
351
+ }}
352
+ after={header.column.getIsPinned() === 'left' && <Icon name="check" />}
353
+ >
354
+ Left
355
+ </MenuOption>
356
+
357
+ <MenuOption
358
+ selected={header.column.getIsPinned() === 'right'}
359
+ onClick={() => {
360
+ if (header.column.getIsPinned() === 'right') {
361
+ header.column.pin(false)
362
+ } else {
363
+ header.column.pin('right')
364
+ }
365
+ }}
366
+ after={header.column.getIsPinned() === 'right' && <Icon name="check" />}
367
+ >
368
+ Right
369
+ </MenuOption>
370
+ </Menu>
371
+ )}
372
+ >
373
+ Freeze [{header.column.columnDef.header as string}]
374
+ </MenuOption>
375
+ )}
328
376
  </Menu>
329
377
  )}
330
378
  </DataGridCell>
@@ -359,10 +407,49 @@ export function DraggableCellHeader<T extends Record<string, any>>({
359
407
  }
360
408
 
361
409
  header.column.columnDef.filterFn = filterFn;
362
- header.column.setFilterValue(header.column.getFilterValue() ?? "");
410
+ header.column.setFilterValue(filter);
363
411
  }
364
412
  }
365
413
 
414
+ DataCellHeader.displayName = "DataCellHeader";
415
+
416
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
417
+ export function DraggableCellHeader<T extends Record<string, any>>({
418
+ header,
419
+ children,
420
+ ...props
421
+ }: DataGridCellHeaderProps<T>) {
422
+ const { attributes, isDragging, listeners, setNodeRef, transform, node } =
423
+ useSortable({
424
+ id: header.column.id,
425
+ });
426
+
427
+ const style: CSSProperties = {
428
+ opacity: isDragging ? 0.8 : 1,
429
+ position: "relative",
430
+ transform: CSS.Translate.toString(transform),
431
+ transition: "width transform 0.2s ease-in-out",
432
+ whiteSpace: "nowrap",
433
+ zIndex: isDragging ? 1 : 0,
434
+ width: header.column.getSize(),
435
+ "--color-text-primary-normal": "var(--color-neutral-000)",
436
+ };
437
+
438
+ return (
439
+ <DataCellHeader
440
+ header={header}
441
+ setNodeRef={setNodeRef}
442
+ node={node}
443
+ style={style}
444
+ {...props}
445
+ {...attributes}
446
+ {...listeners}
447
+ >
448
+ {children}
449
+ </DataCellHeader>
450
+ );
451
+ }
452
+
366
453
  DraggableCellHeader.displayName = "DraggableCellHeader";
367
454
 
368
455
  export function DragAlongCell<T extends RowData, TValue>({
@@ -376,15 +463,15 @@ export function DragAlongCell<T extends RowData, TValue>({
376
463
  const { isDragging, setNodeRef, transform } = useSortable({
377
464
  id: cell.column.id,
378
465
  });
466
+
379
467
 
380
468
  const style: CSSProperties = {
381
469
  opacity: isDragging ? 0.8 : 1,
382
470
  position: "relative",
383
471
  transform: CSS.Translate.toString(transform),
384
472
  transition: "width transform 0.2s ease-in-out",
385
- zIndex: isDragging ? 1 : 0,
386
473
  width: cell.column.getSize(),
387
- minWidth: "min-content",
474
+ zIndex: isDragging ? 1 : 0,
388
475
  };
389
476
 
390
477
  return (
@@ -1,9 +1,16 @@
1
- import { useRef, useEffect, useState } from "react";
1
+ import { useRef, useEffect, useState, useLayoutEffect } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { InputBaseProps, InputBase } from "./Input";
4
4
  import { CalendarRange } from "./CalendarRange";
5
5
  import { Icon } from "./Icon";
6
6
  import { findDocumentRoot } from "../utils";
7
+ import {
8
+ parseInputDate,
9
+ isValidDate,
10
+ formatInputValue,
11
+ calculateCursorPosition,
12
+ formatDate,
13
+ } from "../utils/date";
7
14
 
8
15
  type DateInputProps = Omit<InputBaseProps, "id"> & {
9
16
  id?: string;
@@ -32,6 +39,7 @@ type DateInputProps = Omit<InputBaseProps, "id"> & {
32
39
  readOnly?: boolean; // If true, input is read-only and cannot be focused
33
40
  label?: string; // Optional label for the input
34
41
  };
42
+
35
43
  export const DateInput = ({
36
44
  id,
37
45
  value,
@@ -43,6 +51,8 @@ export const DateInput = ({
43
51
  ...props
44
52
  }: DateInputProps) => {
45
53
  const [visible, setVisible] = useState(false);
54
+ const [inputValue, setInputValue] = useState("");
55
+ const [isTyping, setIsTyping] = useState(false);
46
56
  const popoverRef = useRef<HTMLDivElement | null>(null);
47
57
  const triggerRef = useRef<HTMLInputElement | null>(null);
48
58
  const [calendarPosition, setCalendarPosition] = useState({
@@ -54,17 +64,26 @@ export const DateInput = ({
54
64
  // Extract from and to values from the pipe-separated string when range is enabled
55
65
  const [from, to] = [value, ""];
56
66
 
67
+ // Update input value when external value changes (but not when typing)
68
+ useEffect(() => {
69
+ if (!isTyping) {
70
+ setInputValue(formatDisplayValue(from));
71
+ }
72
+ }, [from, isTyping]);
73
+
74
+ useLayoutEffect(() => {
75
+ if (visible) {
76
+ updatePosition();
77
+ }
78
+ }, [visible])
79
+
57
80
  const updatePosition = () => {
58
81
  if (triggerRef.current) {
59
- requestAnimationFrame(() => {
60
- if (triggerRef.current) {
61
- const rect = triggerRef.current.getBoundingClientRect();
62
- setCalendarPosition({
63
- top: rect.bottom + window.scrollY,
64
- left: rect.left + window.scrollX,
65
- width: rect.width,
66
- });
67
- }
82
+ const rect = triggerRef.current.getBoundingClientRect();
83
+ setCalendarPosition({
84
+ top: rect.bottom + window.scrollY,
85
+ left: rect.left + window.scrollX,
86
+ width: rect.width,
68
87
  });
69
88
  }
70
89
  };
@@ -119,26 +138,88 @@ export const DateInput = ({
119
138
  // For single date selection, only pass the from value
120
139
  onChange(fromValue);
121
140
  setVisible(false);
141
+ setIsTyping(false);
122
142
  }
123
143
 
124
144
  const handleFocus = () => {
125
145
  if (readOnly) return;
126
146
  setVisible(true);
127
- updatePosition();
128
147
  };
129
148
 
130
- function formatDisplayValue(from?: string) {
131
- if (!from) return "";
132
-
133
- // Show single date format: "MM/DD/YYYY"
134
- return formatDate(from ?? "");
149
+ const handleClick = () => {
150
+ handleFocus();
135
151
  }
136
152
 
137
- function formatDate(date: string) {
138
- if (!date) return "";
139
- const [y, m, d] = date.split("-");
140
- return `${m}/${d}/${y}`;
141
- }
153
+ const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
154
+ if (readOnly) return;
155
+
156
+ const rawValue = event.target.value;
157
+ const cursorPosition = event.target.selectionStart || 0;
158
+ setIsTyping(true);
159
+
160
+ // Format the input as user types (add slashes automatically)
161
+ const formattedValue = formatInputValue(rawValue);
162
+ setInputValue(formattedValue);
163
+
164
+ // Restore cursor position after formatting
165
+ requestAnimationFrame(() => {
166
+ if (triggerRef.current) {
167
+ const newPosition = calculateCursorPosition(rawValue, formattedValue, cursorPosition);
168
+ triggerRef.current.setSelectionRange(newPosition, newPosition);
169
+ }
170
+ });
171
+
172
+ // Try to parse and validate the date
173
+ const parsedDate = parseInputDate(formattedValue);
174
+ if (parsedDate && isValidDate(parsedDate)) {
175
+ onChange(parsedDate);
176
+ }
177
+ };
178
+
179
+ const handleBlur = () => {
180
+ setIsTyping(false);
181
+ // If the input is invalid, revert to the last valid value
182
+ const parsedDate = parseInputDate(inputValue);
183
+ if (!parsedDate || !isValidDate(parsedDate)) {
184
+ setInputValue(formatDisplayValue(from));
185
+ }
186
+ };
187
+
188
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
189
+ if (event.key === "Backspace") {
190
+ const input = event.target as HTMLInputElement;
191
+ const cursorPosition = input.selectionStart || 0;
192
+ const value = input.value;
193
+
194
+ // If cursor is right after a slash, move it before the slash
195
+ if (cursorPosition > 0 && value[cursorPosition - 1] === "/") {
196
+ event.preventDefault();
197
+ const newValue = value.slice(0, cursorPosition - 2) + value.slice(cursorPosition);
198
+ const formattedValue = formatInputValue(newValue);
199
+ setInputValue(formattedValue);
200
+
201
+ // Set cursor position after the deletion
202
+ requestAnimationFrame(() => {
203
+ if (triggerRef.current) {
204
+ const newPosition = Math.max(0, cursorPosition - 2);
205
+ triggerRef.current.setSelectionRange(newPosition, newPosition);
206
+ }
207
+ });
208
+
209
+ setIsTyping(true);
210
+ return;
211
+ }
212
+ }
213
+
214
+ if (event.key === "Enter") {
215
+ const parsedDate = parseInputDate(inputValue);
216
+ if (parsedDate && isValidDate(parsedDate)) {
217
+ onChange(parsedDate);
218
+ setVisible(false);
219
+ setIsTyping(false);
220
+ }
221
+ }
222
+ };
142
223
 
143
224
  return (
144
225
  <div className="relative">
@@ -148,15 +229,16 @@ export const DateInput = ({
148
229
  triggerRef.current = el;
149
230
  }}
150
231
  {...props}
151
- value={formatDisplayValue(from)}
232
+ value={inputValue}
152
233
  placeholder={placeholder}
153
234
  disabled={disabled}
154
235
  readOnly={readOnly}
155
236
  after={<Icon name="calendar_month" />}
156
237
  onFocus={handleFocus}
157
- onChange={() => {
158
- // Input is controlled by calendar picker, ignore direct text changes
159
- }}
238
+ onClick={handleClick}
239
+ onChange={handleInputChange}
240
+ onBlur={handleBlur}
241
+ onKeyDown={handleKeyDown}
160
242
  label={label}
161
243
  />
162
244
  {visible &&
@@ -190,3 +272,14 @@ export const DateInput = ({
190
272
  };
191
273
 
192
274
  DateInput.displayName = "DateInput";
275
+
276
+ function formatDisplayValue(from?: string): string {
277
+ if (!from) {
278
+ return "";
279
+ }
280
+ if (!isValidDate(from)) {
281
+ return "";
282
+ }
283
+
284
+ return formatDate(from);
285
+ }