@musecat/functionkit 1.0.1 → 1.1.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 (130) hide show
  1. package/.agents/references/components/ScrolltoTop.md +36 -0
  2. package/.agents/references/components/SwitchCase.md +47 -0
  3. package/.agents/references/components/ViewportPortal.md +34 -0
  4. package/.agents/references/cookie/cookie.md +47 -0
  5. package/.agents/references/datetime/dateTime.client.md +42 -0
  6. package/.agents/references/datetime/dateTime.server.md +42 -0
  7. package/.agents/references/datetime/dateTime.shared.md +86 -0
  8. package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
  9. package/.agents/references/hooks/useCheckInvisible.md +36 -0
  10. package/.agents/references/hooks/useCheckScroll.md +36 -0
  11. package/.agents/references/hooks/useClientDateTime.md +39 -0
  12. package/.agents/references/hooks/useDebounce.md +46 -0
  13. package/.agents/references/hooks/useDebouncedCallback.md +34 -0
  14. package/.agents/references/hooks/useDoubleClick.md +40 -0
  15. package/.agents/references/hooks/useGeolocation.md +37 -0
  16. package/.agents/references/hooks/useHasMounted.md +27 -0
  17. package/.agents/references/hooks/useIntersectionObserver.md +41 -0
  18. package/.agents/references/hooks/useInterval.md +44 -0
  19. package/.agents/references/hooks/useKeyboardHeight.md +33 -0
  20. package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
  21. package/.agents/references/hooks/useLongPress.md +48 -0
  22. package/.agents/references/hooks/usePreservedCallback.md +37 -0
  23. package/.agents/references/hooks/usePreservedReference.md +34 -0
  24. package/.agents/references/hooks/useRefEffect.md +35 -0
  25. package/.agents/references/hooks/useRelativeDateTime.md +35 -0
  26. package/.agents/references/hooks/useTimeout.md +42 -0
  27. package/.agents/references/hooks/useToggleState.md +39 -0
  28. package/.agents/references/hooks/useViewportHeight.md +26 -0
  29. package/.agents/references/hooks/useViewportMatch.md +32 -0
  30. package/.agents/references/utils/browserStorage.md +41 -0
  31. package/.agents/references/utils/buildContext.md +47 -0
  32. package/.agents/references/utils/clipboardShare.md +59 -0
  33. package/.agents/references/utils/floatingMotion.md +67 -0
  34. package/.agents/references/utils/getDeviceInfo.md +43 -0
  35. package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
  36. package/.agents/references/utils/mergeRefs.md +29 -0
  37. package/.agents/references/utils/seen.md +37 -0
  38. package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  40. package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  41. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  42. package/.github/workflows/ci.yml +20 -0
  43. package/.local/state/gh/device-id +1 -0
  44. package/.prettierrc +8 -0
  45. package/AGENTS.md +117 -396
  46. package/CODE_OF_CONDUCT.md +55 -0
  47. package/CONTRIBUTING.md +39 -0
  48. package/README.md +18 -5
  49. package/biome.json +25 -0
  50. package/index.ts +63 -68
  51. package/package.json +46 -13
  52. package/packages/components/ScrolltoTop.tsx +3 -3
  53. package/packages/components/SwitchCase.tsx +7 -7
  54. package/packages/components/ViewportPortal.tsx +22 -23
  55. package/packages/cookie/cookie.shared.ts +80 -110
  56. package/packages/cookie/cookieNames.shared.ts +9 -0
  57. package/packages/datetime/dateTime.client.ts +84 -86
  58. package/packages/datetime/dateTime.server.ts +84 -86
  59. package/packages/datetime/dateTime.shared.ts +249 -287
  60. package/packages/hooks/useAvoidKeyboard.ts +15 -15
  61. package/packages/hooks/useCheckInvisible.ts +16 -16
  62. package/packages/hooks/useCheckScroll.ts +10 -10
  63. package/packages/hooks/useClientDateTime.ts +24 -37
  64. package/packages/hooks/useDebounce.ts +90 -95
  65. package/packages/hooks/useDebouncedCallback.ts +45 -45
  66. package/packages/hooks/useDoubleClick.ts +43 -43
  67. package/packages/hooks/useGeolocation.ts +126 -130
  68. package/packages/hooks/useHasMounted.ts +5 -5
  69. package/packages/hooks/useIntersectionObserver.ts +20 -20
  70. package/packages/hooks/useInterval.ts +45 -45
  71. package/packages/hooks/useKeyboardHeight.ts +9 -9
  72. package/packages/hooks/useKeyboardListNavigation.ts +130 -158
  73. package/packages/hooks/useLongPress.ts +87 -98
  74. package/packages/hooks/usePreservedCallback.ts +12 -12
  75. package/packages/hooks/usePreservedReference.ts +10 -10
  76. package/packages/hooks/useRefEffect.ts +19 -19
  77. package/packages/hooks/useRelativeDateTime.ts +36 -39
  78. package/packages/hooks/useTimeout.ts +41 -41
  79. package/packages/hooks/useToggleState.ts +5 -5
  80. package/packages/hooks/useViewportHeight.ts +12 -13
  81. package/packages/hooks/useViewportMatch.ts +13 -23
  82. package/packages/utils/browserStorage.ts +26 -26
  83. package/packages/utils/buildContext.tsx +10 -11
  84. package/packages/utils/checkDevice.ts +64 -64
  85. package/packages/utils/clipboardShare.tsx +31 -31
  86. package/packages/utils/clipboardShare.types.ts +7 -7
  87. package/packages/utils/floatingMotion.ts +71 -77
  88. package/packages/utils/keyboardTarget.ts +9 -9
  89. package/packages/utils/mergeRefs.ts +9 -9
  90. package/packages/utils/seen.ts +15 -15
  91. package/packages/utils/subscribeKeyboardHeight.ts +49 -46
  92. package/tests/components/ScrolltoTop.test.tsx +15 -0
  93. package/tests/components/SwitchCase.test.tsx +33 -0
  94. package/tests/components/ViewportPortal.test.tsx +46 -0
  95. package/tests/cookie/cookie.test.ts +192 -0
  96. package/tests/datetime/datetime.test.ts +461 -0
  97. package/tests/hooks/useAvoidKeyboard.test.ts +43 -0
  98. package/tests/hooks/useCheckInvisible.test.ts +59 -0
  99. package/tests/hooks/useCheckScroll.test.ts +23 -0
  100. package/tests/hooks/useClientDateTime.test.ts +12 -0
  101. package/tests/hooks/useDebounce.test.ts +96 -0
  102. package/tests/hooks/useDebouncedCallback.test.ts +63 -0
  103. package/tests/hooks/useDoubleClick.test.ts +82 -0
  104. package/tests/hooks/useGeolocation.test.ts +201 -0
  105. package/tests/hooks/useHasMounted.test.ts +10 -0
  106. package/tests/hooks/useIntersectionObserver.test.ts +78 -0
  107. package/tests/hooks/useInterval.test.ts +56 -0
  108. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  109. package/tests/hooks/useKeyboardListNavigation.test.ts +404 -0
  110. package/tests/hooks/useLongPress.test.ts +214 -0
  111. package/tests/hooks/usePreservedCallback.test.ts +33 -0
  112. package/tests/hooks/usePreservedReference.test.ts +42 -0
  113. package/tests/hooks/useRefEffect.test.ts +67 -0
  114. package/tests/hooks/useRelativeDateTime.test.ts +21 -0
  115. package/tests/hooks/useTimeout.test.ts +72 -0
  116. package/tests/hooks/useToggleState.test.ts +43 -0
  117. package/tests/hooks/useViewportHeight.test.ts +32 -0
  118. package/tests/hooks/useViewportMatch.test.ts +35 -0
  119. package/tests/setup.ts +9 -0
  120. package/tests/utils/browserStorage.test.ts +114 -0
  121. package/tests/utils/buildContext.test.tsx +26 -0
  122. package/tests/utils/checkDevice.test.ts +140 -0
  123. package/tests/utils/clipboardShare.test.ts +152 -0
  124. package/tests/utils/floatingMotion.test.ts +135 -0
  125. package/tests/utils/keyboardTarget.test.ts +85 -0
  126. package/tests/utils/mergeRefs.test.ts +50 -0
  127. package/tests/utils/seen.test.ts +40 -0
  128. package/tests/utils/subscribeKeyboardHeight.test.ts +172 -0
  129. package/tsconfig.json +16 -16
  130. package/vitest.config.ts +22 -0
@@ -5,354 +5,316 @@ export type TimePreset = "ko" | "12h" | "24h-minute" | "24h-second";
5
5
  const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
6
6
 
7
7
  type DateParts = {
8
- year: string;
9
- month: string;
10
- day: string;
8
+ year: string;
9
+ month: string;
10
+ day: string;
11
11
  };
12
12
 
13
13
  type TimeParts = {
14
- hour: string;
15
- minute: string;
16
- second: string;
14
+ hour: string;
15
+ minute: string;
16
+ second: string;
17
17
  };
18
18
 
19
19
  export function normalizeAppLocale(locale?: string): AppLocale {
20
- const normalized =
21
- typeof locale === "string" ? locale.trim().toLowerCase() : "";
22
- if (normalized === "kr" || normalized === "ko") return "kr";
23
- if (normalized === "jp" || normalized === "ja") return "jp";
24
- return "en";
20
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
21
+ if (normalized === "kr" || normalized === "ko") return "kr";
22
+ if (normalized === "jp" || normalized === "ja") return "jp";
23
+ return "en";
25
24
  }
26
25
 
27
26
  export function toIntlLocale(locale?: string): string {
28
- switch (normalizeAppLocale(locale)) {
29
- case "kr":
30
- return "ko-KR";
31
- case "jp":
32
- return "ja-JP";
33
- default:
34
- return "en-US";
35
- }
27
+ switch (normalizeAppLocale(locale)) {
28
+ case "kr":
29
+ return "ko-KR";
30
+ case "jp":
31
+ return "ja-JP";
32
+ default:
33
+ return "en-US";
34
+ }
36
35
  }
37
36
 
38
37
  export function toDate(value: DateInput): Date | null {
39
- if (value instanceof Date) {
40
- return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
41
- }
38
+ if (value instanceof Date) {
39
+ return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
40
+ }
42
41
 
43
- const date = new Date(value);
44
- return Number.isNaN(date.getTime()) ? null : date;
42
+ const date = new Date(value);
43
+ return Number.isNaN(date.getTime()) ? null : date;
45
44
  }
46
45
 
47
46
  export function toUtcMidnight(date: Date): Date {
48
- return new Date(
49
- Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
50
- );
47
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
51
48
  }
52
49
 
53
- export function parseUtcDateInput(
54
- value?: DateInput | null,
55
- fallback?: DateInput,
56
- ): Date | null {
57
- if (typeof value === "string") {
58
- const trimmed = value.trim();
59
- const match = DATE_ONLY_PATTERN.exec(trimmed);
60
-
61
- if (match) {
62
- const [, year, month, day] = match;
63
- return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
64
- }
65
- }
66
-
67
- const parsed = value !== undefined && value !== null ? toDate(value) : null;
68
- if (parsed) {
69
- return toUtcMidnight(parsed);
70
- }
71
-
72
- if (fallback !== undefined) {
73
- const fallbackDate = toDate(fallback);
74
- if (fallbackDate) {
75
- return toUtcMidnight(fallbackDate);
76
- }
77
- }
78
-
79
- return null;
50
+ export function parseUtcDateInput(value?: DateInput | null, fallback?: DateInput): Date | null {
51
+ if (typeof value === "string") {
52
+ const trimmed = value.trim();
53
+ const match = DATE_ONLY_PATTERN.exec(trimmed);
54
+
55
+ if (match) {
56
+ const [, year, month, day] = match;
57
+ return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
58
+ }
59
+ }
60
+
61
+ const parsed = value !== undefined && value !== null ? toDate(value) : null;
62
+ if (parsed) {
63
+ return toUtcMidnight(parsed);
64
+ }
65
+
66
+ if (fallback !== undefined) {
67
+ const fallbackDate = toDate(fallback);
68
+ if (fallbackDate) {
69
+ return toUtcMidnight(fallbackDate);
70
+ }
71
+ }
72
+
73
+ return null;
80
74
  }
81
75
 
82
76
  export function formatUtcDateKey(date: Date): string {
83
- return [
84
- date.getUTCFullYear(),
85
- String(date.getUTCMonth() + 1).padStart(2, "0"),
86
- String(date.getUTCDate()).padStart(2, "0"),
87
- ].join("-");
77
+ return [
78
+ date.getUTCFullYear(),
79
+ String(date.getUTCMonth() + 1).padStart(2, "0"),
80
+ String(date.getUTCDate()).padStart(2, "0"),
81
+ ].join("-");
88
82
  }
89
83
 
90
84
  export function getUtcWeekdayIndex(date: Date): number {
91
- return date.getUTCDay();
85
+ return date.getUTCDay();
92
86
  }
93
87
 
94
88
  export function addUtcDays(date: Date, days: number): Date {
95
- const next = new Date(date.getTime());
96
- next.setUTCDate(next.getUTCDate() + days);
97
- return next;
89
+ const next = new Date(date.getTime());
90
+ next.setUTCDate(next.getUTCDate() + days);
91
+ return next;
98
92
  }
99
93
 
100
94
  function getDateParts(date: Date, timeZone?: string): DateParts {
101
- const parts = new Intl.DateTimeFormat("en-US", {
102
- timeZone,
103
- year: "numeric",
104
- month: "2-digit",
105
- day: "2-digit",
106
- }).formatToParts(date);
107
-
108
- return {
109
- year: parts.find((part) => part.type === "year")?.value ?? "0000",
110
- month: parts.find((part) => part.type === "month")?.value ?? "00",
111
- day: parts.find((part) => part.type === "day")?.value ?? "00",
112
- };
95
+ const parts = new Intl.DateTimeFormat("en-US", {
96
+ timeZone,
97
+ year: "numeric",
98
+ month: "2-digit",
99
+ day: "2-digit",
100
+ }).formatToParts(date);
101
+
102
+ return {
103
+ year: parts.find((part) => part.type === "year")?.value ?? "0000",
104
+ month: parts.find((part) => part.type === "month")?.value ?? "00",
105
+ day: parts.find((part) => part.type === "day")?.value ?? "00",
106
+ };
113
107
  }
114
108
 
115
109
  function getTimeParts(date: Date, timeZone?: string): TimeParts {
116
- const parts = new Intl.DateTimeFormat("en-US", {
117
- timeZone,
118
- hour: "2-digit",
119
- minute: "2-digit",
120
- second: "2-digit",
121
- hourCycle: "h23",
122
- }).formatToParts(date);
123
-
124
- return {
125
- hour: parts.find((part) => part.type === "hour")?.value ?? "00",
126
- minute: parts.find((part) => part.type === "minute")?.value ?? "00",
127
- second: parts.find((part) => part.type === "second")?.value ?? "00",
128
- };
110
+ const parts = new Intl.DateTimeFormat("en-US", {
111
+ timeZone,
112
+ hour: "2-digit",
113
+ minute: "2-digit",
114
+ second: "2-digit",
115
+ hourCycle: "h23",
116
+ }).formatToParts(date);
117
+
118
+ return {
119
+ hour: parts.find((part) => part.type === "hour")?.value ?? "00",
120
+ minute: parts.find((part) => part.type === "minute")?.value ?? "00",
121
+ second: parts.find((part) => part.type === "second")?.value ?? "00",
122
+ };
129
123
  }
130
124
 
131
- export function formatLongDate(
132
- date: Date,
133
- locale?: string,
134
- timeZone?: string,
135
- ): string {
136
- const localeKey = normalizeAppLocale(locale);
137
- const { year, month, day } = getDateParts(date, timeZone);
138
-
139
- if (localeKey === "kr") {
140
- return `${year}년 ${Number(month)}월 ${Number(day)}일`;
141
- }
142
-
143
- if (localeKey === "jp") {
144
- return `${year}年${Number(month)}月${Number(day)}日`;
145
- }
146
-
147
- return new Intl.DateTimeFormat(toIntlLocale(localeKey), {
148
- timeZone,
149
- year: "numeric",
150
- month: "long",
151
- day: "numeric",
152
- }).format(date);
125
+ export function formatLongDate(date: Date, locale?: string, timeZone?: string): string {
126
+ const localeKey = normalizeAppLocale(locale);
127
+ const { year, month, day } = getDateParts(date, timeZone);
128
+
129
+ if (localeKey === "kr") {
130
+ return `${year}년 ${Number(month)}월 ${Number(day)}일`;
131
+ }
132
+
133
+ if (localeKey === "jp") {
134
+ return `${year}年${Number(month)}月${Number(day)}日`;
135
+ }
136
+
137
+ return new Intl.DateTimeFormat(toIntlLocale(localeKey), {
138
+ timeZone,
139
+ year: "numeric",
140
+ month: "long",
141
+ day: "numeric",
142
+ }).format(date);
153
143
  }
154
144
 
155
145
  export function formatDotDate(date: Date, timeZone?: string): string {
156
- const { year, month, day } = getDateParts(date, timeZone);
157
- return `${year}. ${Number(month)}. ${Number(day)}.`;
146
+ const { year, month, day } = getDateParts(date, timeZone);
147
+ return `${year}. ${Number(month)}. ${Number(day)}.`;
158
148
  }
159
149
 
160
150
  export function format24HourTime(
161
- date: Date,
162
- options?: {
163
- includeSeconds?: boolean;
164
- timeZone?: string;
165
- },
151
+ date: Date,
152
+ options?: {
153
+ includeSeconds?: boolean;
154
+ timeZone?: string;
155
+ },
166
156
  ): string {
167
- const { includeSeconds = false, timeZone } = options ?? {};
168
- const { hour, minute, second } = getTimeParts(date, timeZone);
169
- return includeSeconds ? `${hour}:${minute}:${second}` : `${hour}:${minute}`;
157
+ const { includeSeconds = false, timeZone } = options ?? {};
158
+ const { hour, minute, second } = getTimeParts(date, timeZone);
159
+ return includeSeconds ? `${hour}:${minute}:${second}` : `${hour}:${minute}`;
170
160
  }
171
161
 
172
162
  export function formatKoreanTime(date: Date, timeZone?: string): string {
173
- const { hour, minute } = getTimeParts(date, timeZone);
174
- const hourNumber = Number(hour);
175
- const meridiem = hourNumber < 12 ? "오전" : "오후";
176
- const hour12 = hourNumber % 12 === 0 ? 12 : hourNumber % 12;
177
- return `${meridiem} ${hour12}시 ${minute}분`;
163
+ const { hour, minute } = getTimeParts(date, timeZone);
164
+ const hourNumber = Number(hour);
165
+ const meridiem = hourNumber < 12 ? "오전" : "오후";
166
+ const hour12 = hourNumber % 12 === 0 ? 12 : hourNumber % 12;
167
+ return `${meridiem} ${hour12}시 ${minute}분`;
178
168
  }
179
169
 
180
- export function formatTwelveHourTime(
181
- date: Date,
182
- locale?: string,
183
- timeZone?: string,
184
- ): string {
185
- const localeKey = normalizeAppLocale(locale);
186
- if (localeKey === "kr") {
187
- const { hour, minute } = getTimeParts(date, timeZone);
188
- const hourNumber = Number(hour);
189
- const meridiem = hourNumber < 12 ? "오전" : "오후";
190
- const hour12 = hourNumber % 12 === 0 ? 12 : hourNumber % 12;
191
- return `${meridiem} ${hour12}:${minute}`;
192
- }
193
-
194
- return new Intl.DateTimeFormat(toIntlLocale(localeKey), {
195
- timeZone,
196
- hour: "numeric",
197
- minute: "2-digit",
198
- hour12: true,
199
- }).format(date);
170
+ export function formatTwelveHourTime(date: Date, locale?: string, timeZone?: string): string {
171
+ const localeKey = normalizeAppLocale(locale);
172
+ if (localeKey === "kr") {
173
+ const { hour, minute } = getTimeParts(date, timeZone);
174
+ const hourNumber = Number(hour);
175
+ const meridiem = hourNumber < 12 ? "오전" : "오후";
176
+ const hour12 = hourNumber % 12 === 0 ? 12 : hourNumber % 12;
177
+ return `${meridiem} ${hour12}:${minute}`;
178
+ }
179
+
180
+ return new Intl.DateTimeFormat(toIntlLocale(localeKey), {
181
+ timeZone,
182
+ hour: "numeric",
183
+ minute: "2-digit",
184
+ hour12: true,
185
+ }).format(date);
200
186
  }
201
187
 
202
188
  export function formatRelativeText(
203
- diffMs: number,
204
- locale?: string,
189
+ diffMs: number,
190
+ locale?: string,
205
191
  ): { text: string; isRelative: boolean } {
206
- const localeKey = normalizeAppLocale(locale);
207
- const safeDiffMs = Math.max(0, diffMs);
208
- const seconds = Math.max(1, Math.floor(safeDiffMs / 1000));
209
- const minutes = Math.max(1, Math.floor(safeDiffMs / (60 * 1000)));
210
- const hours = Math.max(1, Math.floor(safeDiffMs / (60 * 60 * 1000)));
211
- const days = Math.max(1, Math.floor(safeDiffMs / (24 * 60 * 60 * 1000)));
212
-
213
- if (safeDiffMs < 60 * 1000) {
214
- return {
215
- text: formatRelativeUnit(seconds, "second", localeKey),
216
- isRelative: true,
217
- };
218
- }
219
-
220
- if (safeDiffMs < 60 * 60 * 1000) {
221
- return {
222
- text: formatRelativeUnit(minutes, "minute", localeKey),
223
- isRelative: true,
224
- };
225
- }
226
-
227
- if (safeDiffMs < 24 * 60 * 60 * 1000) {
228
- return {
229
- text: formatRelativeUnit(hours, "hour", localeKey),
230
- isRelative: true,
231
- };
232
- }
233
-
234
- return { text: formatRelativeUnit(days, "day", localeKey), isRelative: true };
192
+ const localeKey = normalizeAppLocale(locale);
193
+ const safeDiffMs = Math.max(0, diffMs);
194
+ const seconds = Math.max(1, Math.floor(safeDiffMs / 1000));
195
+ const minutes = Math.max(1, Math.floor(safeDiffMs / (60 * 1000)));
196
+ const hours = Math.max(1, Math.floor(safeDiffMs / (60 * 60 * 1000)));
197
+ const days = Math.max(1, Math.floor(safeDiffMs / (24 * 60 * 60 * 1000)));
198
+
199
+ if (safeDiffMs < 60 * 1000) {
200
+ return {
201
+ text: formatRelativeUnit(seconds, "second", localeKey),
202
+ isRelative: true,
203
+ };
204
+ }
205
+
206
+ if (safeDiffMs < 60 * 60 * 1000) {
207
+ return {
208
+ text: formatRelativeUnit(minutes, "minute", localeKey),
209
+ isRelative: true,
210
+ };
211
+ }
212
+
213
+ if (safeDiffMs < 24 * 60 * 60 * 1000) {
214
+ return {
215
+ text: formatRelativeUnit(hours, "hour", localeKey),
216
+ isRelative: true,
217
+ };
218
+ }
219
+
220
+ return { text: formatRelativeUnit(days, "day", localeKey), isRelative: true };
235
221
  }
236
222
 
237
223
  export function formatRemainingText(
238
- diffMs: number,
239
- locale?: string,
240
- options?: {
241
- includeSuffix?: boolean;
242
- },
224
+ diffMs: number,
225
+ locale?: string,
226
+ options?: {
227
+ includeSuffix?: boolean;
228
+ },
243
229
  ): string {
244
- const localeKey = normalizeAppLocale(locale);
245
- const includeSuffix = options?.includeSuffix ?? true;
246
- const safeDiffMs = Math.max(0, diffMs);
247
-
248
- if (safeDiffMs === 0) {
249
- if (localeKey === "kr") return "마감됨";
250
- if (localeKey === "jp") return "締切終了";
251
- return "Closed";
252
- }
253
-
254
- const units = [
255
- { label: "day", value: 24 * 60 * 60 * 1000 },
256
- { label: "hour", value: 60 * 60 * 1000 },
257
- { label: "minute", value: 60 * 1000 },
258
- { label: "second", value: 1000 },
259
- ] as const;
260
-
261
- const parts: string[] = [];
262
- let remaining = safeDiffMs;
263
-
264
- for (const unit of units) {
265
- const count = Math.floor(remaining / unit.value);
266
- remaining -= count * unit.value;
267
-
268
- if (count > 0) {
269
- parts.push(formatRemainingUnit(count, unit.label, localeKey));
270
- }
271
-
272
- if (parts.length >= 2) break;
273
- }
274
-
275
- if (parts.length === 0) {
276
- parts.push(formatRemainingUnit(1, "second", localeKey));
277
- }
278
-
279
- if (!includeSuffix) {
280
- return parts.join(localeKey === "jp" ? "" : " ");
281
- }
282
-
283
- if (localeKey === "kr") {
284
- return `${parts.join(" ")} 남음`;
285
- }
286
-
287
- if (localeKey === "jp") {
288
- return `あと${parts.join("")}`;
289
- }
290
-
291
- return `${parts.join(" ")} left`;
230
+ const localeKey = normalizeAppLocale(locale);
231
+ const includeSuffix = options?.includeSuffix ?? true;
232
+ const safeDiffMs = Math.max(0, diffMs);
233
+
234
+ if (safeDiffMs === 0) {
235
+ if (localeKey === "kr") return "마감됨";
236
+ if (localeKey === "jp") return "締切終了";
237
+ return "Closed";
238
+ }
239
+
240
+ const units = [
241
+ { label: "day", value: 24 * 60 * 60 * 1000 },
242
+ { label: "hour", value: 60 * 60 * 1000 },
243
+ { label: "minute", value: 60 * 1000 },
244
+ { label: "second", value: 1000 },
245
+ ] as const;
246
+
247
+ const parts: string[] = [];
248
+ let remaining = safeDiffMs;
249
+
250
+ for (const unit of units) {
251
+ const count = Math.floor(remaining / unit.value);
252
+ remaining -= count * unit.value;
253
+
254
+ if (count > 0) {
255
+ parts.push(formatRemainingUnit(count, unit.label, localeKey));
256
+ }
257
+
258
+ if (parts.length >= 2) break;
259
+ }
260
+
261
+ if (parts.length === 0) {
262
+ parts.push(formatRemainingUnit(1, "second", localeKey));
263
+ }
264
+
265
+ if (!includeSuffix) {
266
+ return parts.join(localeKey === "jp" ? "" : " ");
267
+ }
268
+
269
+ if (localeKey === "kr") {
270
+ return `${parts.join(" ")} 남음`;
271
+ }
272
+
273
+ if (localeKey === "jp") {
274
+ return `あと${parts.join("")}`;
275
+ }
276
+
277
+ return `${parts.join(" ")} left`;
292
278
  }
293
279
 
294
280
  function formatRelativeUnit(
295
- count: number,
296
- unit: "second" | "minute" | "hour" | "day",
297
- locale: AppLocale,
281
+ count: number,
282
+ unit: "second" | "minute" | "hour" | "day",
283
+ locale: AppLocale,
298
284
  ): string {
299
- if (locale === "kr") {
300
- const suffix =
301
- unit === "second"
302
- ? "초"
303
- : unit === "minute"
304
- ? "분"
305
- : unit === "hour"
306
- ? "시간"
307
- : "";
308
- return `${count}${suffix} 전`;
309
- }
310
-
311
- if (locale === "jp") {
312
- const suffix =
313
- unit === "second"
314
- ? "秒"
315
- : unit === "minute"
316
- ? "分"
317
- : unit === "hour"
318
- ? "時間"
319
- : "日";
320
- return `${count}${suffix}前`;
321
- }
322
-
323
- const englishUnit = count === 1 ? unit : `${unit}s`;
324
- return `${count} ${englishUnit} ago`;
285
+ if (locale === "kr") {
286
+ const suffix =
287
+ unit === "second" ? "초" : unit === "minute" ? "분" : unit === "hour" ? "시간" : "일";
288
+ return `${count}${suffix} 전`;
289
+ }
290
+
291
+ if (locale === "jp") {
292
+ const suffix =
293
+ unit === "second" ? "秒" : unit === "minute" ? "分" : unit === "hour" ? "時間" : "日";
294
+ return `${count}${suffix}前`;
295
+ }
296
+
297
+ const englishUnit = count === 1 ? unit : `${unit}s`;
298
+ return `${count} ${englishUnit} ago`;
325
299
  }
326
300
 
327
301
  function formatRemainingUnit(
328
- count: number,
329
- unit: "second" | "minute" | "hour" | "day",
330
- locale: AppLocale,
302
+ count: number,
303
+ unit: "second" | "minute" | "hour" | "day",
304
+ locale: AppLocale,
331
305
  ): string {
332
- if (locale === "kr") {
333
- const suffix =
334
- unit === "second"
335
- ? "초"
336
- : unit === "minute"
337
- ? "분"
338
- : unit === "hour"
339
- ? "시간"
340
- : "";
341
- return `${count}${suffix}`;
342
- }
343
-
344
- if (locale === "jp") {
345
- const suffix =
346
- unit === "second"
347
- ? "秒"
348
- : unit === "minute"
349
- ? "分"
350
- : unit === "hour"
351
- ? "時間"
352
- : "日";
353
- return `${count}${suffix}`;
354
- }
355
-
356
- const englishUnit = count === 1 ? unit : `${unit}s`;
357
- return `${count} ${englishUnit}`;
306
+ if (locale === "kr") {
307
+ const suffix =
308
+ unit === "second" ? "초" : unit === "minute" ? "분" : unit === "hour" ? "시간" : "일";
309
+ return `${count}${suffix}`;
310
+ }
311
+
312
+ if (locale === "jp") {
313
+ const suffix =
314
+ unit === "second" ? "秒" : unit === "minute" ? "分" : unit === "hour" ? "時間" : "日";
315
+ return `${count}${suffix}`;
316
+ }
317
+
318
+ const englishUnit = count === 1 ? unit : `${unit}s`;
319
+ return `${count} ${englishUnit}`;
358
320
  }
@@ -3,26 +3,26 @@
3
3
  import { useKeyboardHeight } from "./useKeyboardHeight";
4
4
 
5
5
  type UseAvoidKeyboardOptions = {
6
- safeAreaBottom?: number;
7
- transitionDuration?: number;
8
- transitionTimingFunction?: string;
6
+ safeAreaBottom?: number;
7
+ transitionDuration?: number;
8
+ transitionTimingFunction?: string;
9
9
  };
10
10
 
11
11
  export function useAvoidKeyboard(options?: UseAvoidKeyboardOptions) {
12
- const {
13
- safeAreaBottom = 0,
14
- transitionDuration = 200,
15
- transitionTimingFunction = "ease-out",
16
- } = options ?? {};
12
+ const {
13
+ safeAreaBottom = 0,
14
+ transitionDuration = 200,
15
+ transitionTimingFunction = "ease-out",
16
+ } = options ?? {};
17
17
 
18
- const { keyboardHeight } = useKeyboardHeight();
18
+ const { keyboardHeight } = useKeyboardHeight();
19
19
 
20
- const offset = keyboardHeight > 0 ? keyboardHeight : safeAreaBottom;
20
+ const offset = keyboardHeight > 0 ? keyboardHeight : safeAreaBottom;
21
21
 
22
- const style: React.CSSProperties = {
23
- transform: offset > 0 ? `translateY(-${offset}px)` : undefined,
24
- transition: `transform ${transitionDuration}ms ${transitionTimingFunction}`,
25
- };
22
+ const style: React.CSSProperties = {
23
+ transform: offset > 0 ? `translateY(-${offset}px)` : undefined,
24
+ transition: `transform ${transitionDuration}ms ${transitionTimingFunction}`,
25
+ };
26
26
 
27
- return { style };
27
+ return { style };
28
28
  }