@windrun-huaiin/third-ui 29.1.0 → 29.2.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 (58) hide show
  1. package/dist/fuma/base/custom-header.js +6 -3
  2. package/dist/fuma/base/custom-header.mjs +6 -3
  3. package/dist/main/alert-dialog/confirm-dialog.d.ts +5 -3
  4. package/dist/main/alert-dialog/confirm-dialog.js +7 -7
  5. package/dist/main/alert-dialog/confirm-dialog.mjs +8 -8
  6. package/dist/main/alert-dialog/dialog-loading-action.d.ts +12 -0
  7. package/dist/main/alert-dialog/dialog-loading-action.js +42 -0
  8. package/dist/main/alert-dialog/dialog-loading-action.mjs +40 -0
  9. package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +5 -3
  10. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +10 -4
  11. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +11 -5
  12. package/dist/main/alert-dialog/index.d.ts +1 -0
  13. package/dist/main/alert-dialog/info-dialog.d.ts +4 -2
  14. package/dist/main/alert-dialog/info-dialog.js +6 -5
  15. package/dist/main/alert-dialog/info-dialog.mjs +7 -6
  16. package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +6 -4
  17. package/dist/main/alert-dialog/undoable-confirm-dialog.js +18 -17
  18. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +19 -18
  19. package/dist/main/buttons/gradient-button.d.ts +3 -1
  20. package/dist/main/buttons/gradient-button.js +29 -3
  21. package/dist/main/buttons/gradient-button.mjs +29 -3
  22. package/dist/main/buttons/index.d.ts +1 -0
  23. package/dist/main/buttons/index.js +3 -0
  24. package/dist/main/buttons/index.mjs +1 -0
  25. package/dist/main/buttons/use-press-feedback.d.ts +18 -0
  26. package/dist/main/buttons/use-press-feedback.js +42 -0
  27. package/dist/main/buttons/use-press-feedback.mjs +39 -0
  28. package/dist/main/buttons/x-button.d.ts +3 -0
  29. package/dist/main/buttons/x-button.js +36 -6
  30. package/dist/main/buttons/x-button.mjs +36 -6
  31. package/dist/main/calendar/calendar-date-range-input.d.ts +17 -0
  32. package/dist/main/calendar/calendar-date-range-input.js +81 -0
  33. package/dist/main/calendar/calendar-date-range-input.mjs +79 -0
  34. package/dist/main/calendar/calendar-status-view.d.ts +23 -0
  35. package/dist/main/calendar/calendar-status-view.js +155 -0
  36. package/dist/main/calendar/calendar-status-view.mjs +153 -0
  37. package/dist/main/calendar/index.d.ts +3 -0
  38. package/dist/main/calendar/index.js +12 -0
  39. package/dist/main/calendar/index.mjs +4 -0
  40. package/dist/main/calendar/random-date-range-dialog.d.ts +15 -0
  41. package/dist/main/calendar/random-date-range-dialog.js +447 -0
  42. package/dist/main/calendar/random-date-range-dialog.mjs +445 -0
  43. package/package.json +8 -3
  44. package/src/fuma/base/custom-header.tsx +6 -3
  45. package/src/main/alert-dialog/confirm-dialog.tsx +52 -47
  46. package/src/main/alert-dialog/dialog-loading-action.tsx +63 -0
  47. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +61 -48
  48. package/src/main/alert-dialog/index.ts +1 -0
  49. package/src/main/alert-dialog/info-dialog.tsx +50 -44
  50. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +88 -82
  51. package/src/main/buttons/gradient-button.tsx +36 -3
  52. package/src/main/buttons/index.ts +1 -0
  53. package/src/main/buttons/use-press-feedback.ts +58 -0
  54. package/src/main/buttons/x-button.tsx +53 -11
  55. package/src/main/calendar/calendar-date-range-input.tsx +173 -0
  56. package/src/main/calendar/calendar-status-view.tsx +365 -0
  57. package/src/main/calendar/index.ts +5 -0
  58. package/src/main/calendar/random-date-range-dialog.tsx +741 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  export * from './gradient-button';
4
+ export * from './use-press-feedback';
4
5
  export * from './x-button';
5
6
  export * from './x-switch-button';
6
7
  export * from './x-toggle-button';
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export type PressFeedbackKey = string;
6
+ export type PressFeedbackMode = 'none' | 'subtle' | 'solid';
7
+ export type PressFeedback = boolean | PressFeedbackMode;
8
+
9
+ export interface PressFeedbackProps<T extends PressFeedbackKey> {
10
+ onPointerDown: () => void;
11
+ onPointerUp: () => void;
12
+ onPointerLeave: () => void;
13
+ onPointerCancel: () => void;
14
+ onBlur: () => void;
15
+ }
16
+
17
+ export function resolvePressFeedbackMode(pressFeedback?: PressFeedback): PressFeedbackMode {
18
+ if (pressFeedback === false || pressFeedback === 'none') {
19
+ return 'none';
20
+ }
21
+
22
+ if (pressFeedback === 'solid') {
23
+ return 'solid';
24
+ }
25
+
26
+ return 'subtle';
27
+ }
28
+
29
+ export function usePressFeedback<T extends PressFeedbackKey>(durationMs = 180) {
30
+ const [pressedKey, setPressedKey] = useState<T | null>(null);
31
+
32
+ function release(key: T) {
33
+ setPressedKey((current) => (current === key ? null : current));
34
+ }
35
+
36
+ function trigger(key: T) {
37
+ setPressedKey(key);
38
+ }
39
+
40
+ function flash(key: T) {
41
+ setPressedKey(key);
42
+ window.setTimeout(() => {
43
+ setPressedKey((current) => (current === key ? null : current));
44
+ }, durationMs);
45
+ }
46
+
47
+ function getPressProps(key: T): PressFeedbackProps<T> {
48
+ return {
49
+ onPointerDown: () => trigger(key),
50
+ onPointerUp: () => release(key),
51
+ onPointerLeave: () => release(key),
52
+ onPointerCancel: () => release(key),
53
+ onBlur: () => release(key),
54
+ };
55
+ }
56
+
57
+ return { pressedKey, trigger, release, flash, getPressProps };
58
+ }
@@ -4,8 +4,14 @@ import React, { useState, useRef, useEffect, ReactNode } from 'react'
4
4
  import { ChevronDownIcon, Loader2Icon } from '@windrun-huaiin/base-ui/icons'
5
5
  import { themeBgColor, themeBorderColor, themeIconColor, themeMainBgColor } from '@windrun-huaiin/base-ui/lib'
6
6
  import { cn } from '@windrun-huaiin/lib/utils'
7
+ import { PressFeedback, resolvePressFeedbackMode, usePressFeedback } from './use-press-feedback'
7
8
 
8
9
  type XButtonVariant = 'default' | 'soft' | 'subtle'
10
+ type XButtonPressKey = 'single' | 'main' | 'dropdown'
11
+
12
+ const PRESS_FEEDBACK_MS = 180
13
+ const xButtonPressSubtleClass = 'translate-y-px scale-[0.98] shadow-inner brightness-95'
14
+ const xButtonPressSolidClass = 'translate-y-[2px] scale-[0.95] shadow-[inset_0_2px_4px_rgba(15,23,42,0.18)] brightness-95'
9
15
 
10
16
  interface BaseButtonConfig {
11
17
  icon: ReactNode
@@ -30,6 +36,7 @@ interface SingleButtonProps {
30
36
  className?: string
31
37
  iconClassName?: string
32
38
  variant?: XButtonVariant
39
+ pressFeedback?: PressFeedback
33
40
  }
34
41
 
35
42
  interface SplitButtonProps {
@@ -43,6 +50,7 @@ interface SplitButtonProps {
43
50
  dropdownButtonClassName?: string
44
51
  iconClassName?: string
45
52
  variant?: XButtonVariant
53
+ pressFeedback?: PressFeedback
46
54
  }
47
55
 
48
56
  type xButtonProps = SingleButtonProps | SplitButtonProps
@@ -50,7 +58,9 @@ type xButtonProps = SingleButtonProps | SplitButtonProps
50
58
  export function XButton(props: xButtonProps) {
51
59
  const [isLoading, setIsLoading] = useState(false)
52
60
  const [menuOpen, setMenuOpen] = useState(false)
53
- const menuRef = useRef<HTMLDivElement>(null)
61
+ const splitRef = useRef<HTMLDivElement>(null)
62
+ const pressMode = resolvePressFeedbackMode(props.pressFeedback)
63
+ const { pressedKey, flash, getPressProps } = usePressFeedback<XButtonPressKey>(PRESS_FEEDBACK_MS)
54
64
 
55
65
  const { iconClassName } = props
56
66
  const defaultIconClass = "w-5 h-5"
@@ -76,7 +86,7 @@ export function XButton(props: xButtonProps) {
76
86
  useEffect(() => {
77
87
  if (props.type === 'split') {
78
88
  const handleClickOutside = (event: MouseEvent) => {
79
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
89
+ if (splitRef.current && !splitRef.current.contains(event.target as Node)) {
80
90
  setMenuOpen(false)
81
91
  }
82
92
  }
@@ -104,7 +114,8 @@ export function XButton(props: xButtonProps) {
104
114
  }
105
115
  }
106
116
 
107
- const baseButtonClass = "flex items-center justify-center gap-2 px-4 py-2 text-sm font-semibold transition-colors"
117
+ const getButtonPressClass = () => pressMode === 'solid' ? xButtonPressSolidClass : xButtonPressSubtleClass
118
+ const baseButtonClass = "flex items-center justify-center gap-2 px-4 py-2 text-sm font-semibold transition-[transform,background-color,filter,box-shadow,border-color,color]"
108
119
  const singleButtonVariantClass = variant === 'soft'
109
120
  ? cn(
110
121
  themeBgColor,
@@ -141,25 +152,38 @@ export function XButton(props: xButtonProps) {
141
152
  "bg-transparent hover:bg-neutral-50 dark:hover:bg-neutral-800 sm:border-l",
142
153
  themeIconColor,
143
154
  "border-neutral-200 dark:border-neutral-800"
144
- )
145
- : "bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-white hover:bg-neutral-300 dark:hover:bg-neutral-700 sm:border-l sm:border-neutral-300 sm:dark:border-neutral-700"
155
+ )
156
+ : "bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-white hover:bg-neutral-300 dark:hover:bg-neutral-700 sm:border-l sm:border-neutral-300 sm:dark:border-neutral-700"
157
+ const splitContainerVariantClass = variant === 'soft'
158
+ ? cn('border', themeBorderColor)
159
+ : variant === 'subtle'
160
+ ? 'border border-neutral-200 dark:border-neutral-800'
161
+ : null
146
162
  const disabledClass = "opacity-60 cursor-not-allowed"
147
163
 
148
164
  if (props.type === 'single') {
149
165
  const { button, loadingText, minWidth = 'min-w-[110px]', className = '' } = props
150
166
  const isDisabled = button.disabled || isLoading
167
+ const isPressed = pressMode !== 'none' && pressedKey === 'single' && !button.disabled
151
168
  const actualLoadingText = loadingText || button.text?.trim() || 'Loading...'
152
169
 
153
170
  return (
154
171
  <button
155
- onClick={() => handleButtonClick(button.onClick)}
172
+ onClick={() => {
173
+ if (!isDisabled && pressMode !== 'none') {
174
+ flash('single')
175
+ }
176
+ handleButtonClick(button.onClick)
177
+ }}
156
178
  disabled={isDisabled}
179
+ {...(pressMode !== 'none' && !isDisabled ? getPressProps('single') : {})}
157
180
  className={cn(
158
181
  "w-full sm:w-auto",
159
182
  minWidth,
160
183
  baseButtonClass,
161
184
  singleButtonVariantClass,
162
185
  "rounded-full",
186
+ isPressed && getButtonPressClass(),
163
187
  isDisabled && disabledClass,
164
188
  className
165
189
  )}
@@ -182,21 +206,33 @@ export function XButton(props: xButtonProps) {
182
206
 
183
207
  const { mainButton, menuItems, loadingText, menuWidth = 'w-full sm:w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props
184
208
  const isMainDisabled = mainButton.disabled || isLoading
209
+ const isMainPressed = pressMode !== 'none' && pressedKey === 'main' && !mainButton.disabled
210
+ const isDropdownPressed = pressMode !== 'none' && pressedKey === 'dropdown' && !isLoading
185
211
  const actualLoadingText = loadingText || mainButton.text?.trim() || 'Loading...'
186
212
 
187
213
  return (
188
214
  <div className={cn(
189
- "relative flex flex-row items-stretch w-full sm:w-auto rounded-full gap-0",
215
+ "relative inline-flex flex-row items-stretch w-full sm:w-fit rounded-full gap-0",
216
+ splitContainerVariantClass,
190
217
  menuOpen && "z-90",
191
218
  className
192
- )}>
219
+ )}
220
+ ref={splitRef}
221
+ >
193
222
  <button
194
- onClick={() => handleButtonClick(mainButton.onClick)}
223
+ onClick={() => {
224
+ if (!isMainDisabled && pressMode !== 'none') {
225
+ flash('main')
226
+ }
227
+ handleButtonClick(mainButton.onClick)
228
+ }}
195
229
  disabled={isMainDisabled}
230
+ {...(pressMode !== 'none' && !isMainDisabled ? getPressProps('main') : {})}
196
231
  className={cn(
197
232
  "flex-1 min-w-0 sm:min-w-[100px] sm:flex-initial rounded-l-full",
198
233
  baseButtonClass,
199
234
  splitMainButtonVariantClass,
235
+ isMainPressed && getButtonPressClass(),
200
236
  isMainDisabled && disabledClass,
201
237
  mainButtonClassName
202
238
  )}
@@ -218,12 +254,19 @@ export function XButton(props: xButtonProps) {
218
254
 
219
255
  <button
220
256
  type="button"
221
- onClick={() => setMenuOpen(!menuOpen)}
257
+ onClick={() => {
258
+ if (!isLoading && pressMode !== 'none') {
259
+ flash('dropdown')
260
+ }
261
+ setMenuOpen(!menuOpen)
262
+ }}
222
263
  disabled={isLoading}
264
+ {...(pressMode !== 'none' && !isLoading ? getPressProps('dropdown') : {})}
223
265
  className={cn(
224
266
  "w-12 rounded-r-full",
225
267
  baseButtonClass,
226
268
  splitDropdownVariantClass,
269
+ isDropdownPressed && getButtonPressClass(),
227
270
  isLoading && disabledClass,
228
271
  dropdownButtonClassName
229
272
  )}
@@ -234,7 +277,6 @@ export function XButton(props: xButtonProps) {
234
277
 
235
278
  {menuOpen && (
236
279
  <div
237
- ref={menuRef}
238
280
  className={cn(
239
281
  "absolute top-full right-0 mt-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-lg z-50 overflow-hidden",
240
282
  menuWidth
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { CalendarDaysIcon, XIcon } from '@windrun-huaiin/base-ui/icons';
5
+ import { themeIconColor } from '@windrun-huaiin/base-ui/lib';
6
+ import { cn } from '@windrun-huaiin/lib/utils';
7
+ import {
8
+ type PressFeedback,
9
+ resolvePressFeedbackMode,
10
+ usePressFeedback,
11
+ } from '../buttons/use-press-feedback';
12
+ import { RandomDateRangeDialog, type RandomCalendarRange } from './random-date-range-dialog';
13
+
14
+ export type CalendarDateRangeValue = RandomCalendarRange;
15
+
16
+ export type CalendarDateRangeInputProps = {
17
+ value: CalendarDateRangeValue;
18
+ onChange: (value: CalendarDateRangeValue) => void;
19
+ placeholder?: string;
20
+ defaultRangeDays?: number;
21
+ disabled?: boolean;
22
+ className?: string;
23
+ showDayCount?: boolean;
24
+ dayCountUnit?: string;
25
+ themedCalendarIcon?: boolean;
26
+ clearPressFeedback?: PressFeedback;
27
+ onOpenChange?: (open: boolean) => void;
28
+ };
29
+
30
+ type DateRangeInputPressKey = 'clear';
31
+
32
+ const DEFAULT_PLACEHOLDER = '滑动窗口日期';
33
+ const DEFAULT_RANGE_DAYS = 7;
34
+ const CLEAR_PRESS_FEEDBACK_MS = 180;
35
+
36
+ function parseDateString(value: string): Date {
37
+ return new Date(`${value}T00:00:00.000Z`);
38
+ }
39
+
40
+ function getTodayString(): string {
41
+ return new Date().toISOString().slice(0, 10);
42
+ }
43
+
44
+ function getInclusiveDayCount(value: CalendarDateRangeValue): number {
45
+ if (!value.startDate || !value.endDate) {
46
+ return 0;
47
+ }
48
+
49
+ const startTime = parseDateString(value.startDate).getTime();
50
+ const endTime = parseDateString(value.endDate).getTime();
51
+
52
+ return Math.max(0, Math.floor(Math.abs(endTime - startTime) / 86400000) + 1);
53
+ }
54
+
55
+ function getRangeLabel(value: CalendarDateRangeValue, showDayCount: boolean, dayCountUnit: string): string | null {
56
+ if (!value.startDate || !value.endDate) {
57
+ return null;
58
+ }
59
+
60
+ const dateLabel = `${value.startDate} ~ ${value.endDate}`;
61
+
62
+ if (!showDayCount) {
63
+ return dateLabel;
64
+ }
65
+
66
+ return `${dateLabel} · ${getInclusiveDayCount(value)}${dayCountUnit}`;
67
+ }
68
+
69
+ export function CalendarDateRangeInput({
70
+ value,
71
+ onChange,
72
+ placeholder = DEFAULT_PLACEHOLDER,
73
+ defaultRangeDays = DEFAULT_RANGE_DAYS,
74
+ disabled = false,
75
+ className,
76
+ showDayCount = false,
77
+ dayCountUnit = 'D',
78
+ themedCalendarIcon = true,
79
+ clearPressFeedback = 'subtle',
80
+ onOpenChange,
81
+ }: CalendarDateRangeInputProps) {
82
+ const [open, setOpen] = useState(false);
83
+ const pressMode = resolvePressFeedbackMode(clearPressFeedback);
84
+ const { pressedKey, flash, getPressProps } = usePressFeedback<DateRangeInputPressKey>(CLEAR_PRESS_FEEDBACK_MS);
85
+ const label = getRangeLabel(value, showDayCount, dayCountUnit);
86
+ const hasValue = Boolean(value.startDate || value.endDate);
87
+ const isClearPressed = pressMode !== 'none' && pressedKey === 'clear' && !disabled;
88
+
89
+ function handleOpenChange(nextOpen: boolean) {
90
+ setOpen(nextOpen);
91
+ onOpenChange?.(nextOpen);
92
+ }
93
+
94
+ function handleClear() {
95
+ onChange({ startDate: null, endDate: null });
96
+ }
97
+
98
+ return (
99
+ <>
100
+ <div
101
+ role="button"
102
+ tabIndex={disabled ? -1 : 0}
103
+ aria-disabled={disabled}
104
+ onClick={() => {
105
+ if (!disabled) {
106
+ handleOpenChange(true);
107
+ }
108
+ }}
109
+ onKeyDown={(event) => {
110
+ if (disabled) {
111
+ return;
112
+ }
113
+
114
+ if (event.key === 'Enter' || event.key === ' ') {
115
+ event.preventDefault();
116
+ handleOpenChange(true);
117
+ }
118
+ }}
119
+ className={cn(
120
+ 'flex h-11 w-full cursor-pointer items-center rounded-2xl border border-border/70 bg-background/80 text-left text-sm shadow-sm transition hover:bg-accent/40',
121
+ disabled && 'cursor-not-allowed opacity-60 hover:bg-background/80',
122
+ className
123
+ )}
124
+ >
125
+ <span className="flex min-w-0 flex-1 items-center gap-2 px-3">
126
+ <CalendarDaysIcon
127
+ className={cn('h-4 w-4 shrink-0', themedCalendarIcon ? themeIconColor : 'text-muted-foreground')}
128
+ />
129
+ <span className={cn('truncate', label ? 'text-foreground' : 'text-muted-foreground')}>
130
+ {label ?? placeholder}
131
+ </span>
132
+ </span>
133
+ <button
134
+ type="button"
135
+ disabled={disabled || !hasValue}
136
+ onClick={(event) => {
137
+ event.stopPropagation();
138
+ if (disabled || !hasValue) {
139
+ return;
140
+ }
141
+
142
+ if (pressMode !== 'none') {
143
+ flash('clear');
144
+ }
145
+ handleClear();
146
+ }}
147
+ className={cn(
148
+ 'mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-[transform,background-color,color,box-shadow]',
149
+ hasValue
150
+ ? 'hover:bg-black/10 hover:text-foreground dark:hover:bg-white/12'
151
+ : 'cursor-default opacity-35',
152
+ isClearPressed &&
153
+ 'scale-90 bg-black/15 text-foreground shadow-inner dark:bg-white/18'
154
+ )}
155
+ aria-label="Clear date range"
156
+ title="Clear date range"
157
+ {...(pressMode !== 'none' && !disabled && hasValue ? getPressProps('clear') : {})}
158
+ >
159
+ <XIcon className="h-4 w-4" />
160
+ </button>
161
+ </div>
162
+ <RandomDateRangeDialog
163
+ open={open}
164
+ value={value}
165
+ anchorDate={value.startDate ?? getTodayString()}
166
+ defaultRangeDays={defaultRangeDays}
167
+ onOpenChange={handleOpenChange}
168
+ onApply={onChange}
169
+ onClear={onChange}
170
+ />
171
+ </>
172
+ );
173
+ }