@rxdrag/website-lib-core 0.0.127 → 0.0.129

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 (138) hide show
  1. package/README.md +1 -1
  2. package/index.ts +1 -1
  3. package/package.json +4 -4
  4. package/src/astro/animation.ts +146 -146
  5. package/src/astro/background.ts +82 -86
  6. package/src/astro/base.ts +7 -0
  7. package/src/astro/business.ts +13 -0
  8. package/src/astro/grid/consts.ts +80 -80
  9. package/src/astro/grid/index.ts +2 -2
  10. package/src/astro/grid/types.ts +35 -35
  11. package/src/astro/image.ts +239 -239
  12. package/src/astro/index.ts +12 -10
  13. package/src/astro/link.ts +20 -21
  14. package/src/astro/media.ts +123 -123
  15. package/src/astro/nav.ts +13 -13
  16. package/src/astro/section/index.ts +7 -12
  17. package/src/component-logic/index.ts +1 -1
  18. package/src/component-logic/link-client.ts +32 -32
  19. package/src/component-logic/link.ts +61 -61
  20. package/src/design-tokens.ts +160 -160
  21. package/src/entify/Entify.ts +111 -101
  22. package/src/entify/IEntify.ts +157 -177
  23. package/src/entify/index.ts +4 -4
  24. package/src/entify/lib/collectCategoryIds.ts +20 -20
  25. package/src/entify/lib/fulltextSearch.ts +63 -62
  26. package/src/entify/lib/langFields.ts +14 -14
  27. package/src/entify/lib/listToTree.ts +23 -23
  28. package/src/entify/lib/newAvatarQueryOptions.ts +4 -4
  29. package/src/entify/lib/newOgImageQueryOptions.ts +5 -5
  30. package/src/entify/lib/newQueryPostOptions.ts +42 -45
  31. package/src/entify/lib/newQueryProductOptions.ts +96 -98
  32. package/src/entify/lib/newQueryProductsMediaOptions.ts +28 -28
  33. package/src/entify/lib/queryAllProducts.ts +40 -40
  34. package/src/entify/lib/queryBulletin.ts +28 -16
  35. package/src/entify/lib/queryEntityList.ts +41 -41
  36. package/src/entify/lib/queryFeaturedProducts.ts +69 -68
  37. package/src/entify/lib/queryLangs.ts +36 -60
  38. package/src/entify/lib/queryLatestPosts.ts +92 -91
  39. package/src/entify/lib/queryOneEntity.ts +63 -63
  40. package/src/entify/lib/queryOneMedia.ts +27 -27
  41. package/src/entify/lib/queryOnePostById.ts +21 -21
  42. package/src/entify/lib/queryOnePostBySlug.ts +40 -39
  43. package/src/entify/lib/queryOnePostCategoryBySlug.ts +35 -35
  44. package/src/entify/lib/queryOneProductById.ts +20 -20
  45. package/src/entify/lib/queryOneProductBySlug.ts +53 -52
  46. package/src/entify/lib/queryOneProductCategoryBySlug.ts +50 -49
  47. package/src/entify/lib/queryOneTheme.ts +54 -72
  48. package/src/entify/lib/queryOneUser.ts +38 -38
  49. package/src/entify/lib/queryPostCategories.ts +67 -67
  50. package/src/entify/lib/queryPostSlugs.ts +37 -37
  51. package/src/entify/lib/queryPosts.ts +175 -174
  52. package/src/entify/lib/queryProductCategories.ts +59 -59
  53. package/src/entify/lib/queryProducts.ts +145 -144
  54. package/src/entify/lib/queryProductsInMenu.ts +45 -45
  55. package/src/entify/lib/queryTagCategories.ts +58 -58
  56. package/src/entify/lib/queryTags.ts +57 -57
  57. package/src/entify/lib/queryUserIds.ts +24 -24
  58. package/src/entify/lib/queryUserPosts.ts +80 -79
  59. package/src/entify/lib/queryWebSiteSettings.ts +28 -28
  60. package/src/entify/lib/queryWebsite.ts +43 -43
  61. package/src/entify/lib/sendEmail.ts +7 -7
  62. package/src/entify/lib/toQueryOptions.ts +19 -19
  63. package/src/entify/lib/upsertEntity.ts +8 -8
  64. package/src/entify/types/index.ts +1 -1
  65. package/src/entify/types/utils.ts +11 -6
  66. package/src/entify/types/variables.ts +0 -1
  67. package/src/entify/view-model/funcs.ts +230 -230
  68. package/src/entify/view-model/index.ts +1 -1
  69. package/src/entify/view-model/models.ts +135 -135
  70. package/src/global.d.ts +7 -7
  71. package/src/index.ts +8 -8
  72. package/src/lib/formatDate.ts +15 -15
  73. package/src/lib/index.ts +3 -3
  74. package/src/lib/pagination.ts +114 -114
  75. package/src/lib/utils.ts +135 -135
  76. package/src/react/components/Analytics/eventHandlers.ts +173 -173
  77. package/src/react/components/Analytics/index.tsx +21 -21
  78. package/src/react/components/Analytics/singleton.ts +214 -214
  79. package/src/react/components/Analytics/tracking.ts +221 -221
  80. package/src/react/components/Analytics/types.ts +60 -60
  81. package/src/react/components/Analytics/utils.ts +95 -95
  82. package/src/react/components/AttachmentIcon/index.tsx +53 -53
  83. package/src/react/components/BackgroundHlsVideoPlayer.tsx +97 -97
  84. package/src/react/components/BackgroundVideoPlayer.tsx +32 -32
  85. package/src/react/components/Bulletin.tsx +30 -30
  86. package/src/react/components/ContactForm/ContactForm.tsx +289 -289
  87. package/src/react/components/ContactForm/Input.tsx +48 -48
  88. package/src/react/components/ContactForm/Input2.tsx +59 -59
  89. package/src/react/components/ContactForm/Submit.tsx +48 -48
  90. package/src/react/components/ContactForm/TelInput.tsx +215 -215
  91. package/src/react/components/ContactForm/Textarea.tsx +48 -48
  92. package/src/react/components/ContactForm/Textarea2.tsx +89 -89
  93. package/src/react/components/ContactForm/funcs.ts +64 -64
  94. package/src/react/components/ContactForm/index.ts +7 -7
  95. package/src/react/components/ContactForm/types.ts +68 -68
  96. package/src/react/components/GoogleConsent/CookieItemPanel.tsx +80 -80
  97. package/src/react/components/GoogleConsent/CumtomizedModal.tsx +148 -148
  98. package/src/react/components/GoogleConsent/GoogleConsent.tsx +100 -100
  99. package/src/react/components/GoogleConsent/gtags.ts +67 -67
  100. package/src/react/components/GoogleConsent/index.ts +2 -2
  101. package/src/react/components/GoogleConsent/types.ts +18 -18
  102. package/src/react/components/GoogleConsent//345/217/202/350/200/203.md +4 -4
  103. package/src/react/components/Icon/index.tsx +19 -19
  104. package/src/react/components/Medias/MainMedia.tsx +257 -257
  105. package/src/react/components/Medias/Thumbnail.tsx +62 -62
  106. package/src/react/components/Medias/VideoPlayer.tsx +114 -114
  107. package/src/react/components/Medias/index.tsx +271 -271
  108. package/src/react/components/ProductCard/ProductCard.tsx +24 -24
  109. package/src/react/components/ProductCard/ProductCta/index.tsx +28 -28
  110. package/src/react/components/ProductCard/ProductCta/style.css +3 -3
  111. package/src/react/components/ProductCard/ProductDescription/index.tsx +12 -12
  112. package/src/react/components/ProductCard/ProductDescription/style.css +5 -5
  113. package/src/react/components/ProductCard/ProductMedia/index.tsx +35 -35
  114. package/src/react/components/ProductCard/ProductMedia/style.css +5 -5
  115. package/src/react/components/ProductCard/ProductTitle/index.tsx +7 -7
  116. package/src/react/components/ProductCard/ProductTitle/style.css +3 -3
  117. package/src/react/components/ProductCard/ProductView.tsx +36 -36
  118. package/src/react/components/ProductCard/index.ts +4 -4
  119. package/src/react/components/ProductCard/useQueryProduct.ts +32 -32
  120. package/src/react/components/ReactModalTrigger.tsx +28 -28
  121. package/src/react/components/ReactVideoPlayer.tsx +29 -52
  122. package/src/react/components/RichTextOutline/index.tsx +75 -75
  123. package/src/react/components/RichTextOutline/useAnchorScroll.ts +23 -23
  124. package/src/react/components/Scroller.tsx +39 -39
  125. package/src/react/components/SearchInput.tsx +21 -21
  126. package/src/react/components/Share/index.tsx +86 -86
  127. package/src/react/components/Share/socials.tsx +79 -77
  128. package/src/react/components/Share//350/265/204/346/226/231.md +7 -7
  129. package/src/react/components/ToTop.tsx +72 -72
  130. package/src/react/components/VideoPlayIcon.tsx +43 -0
  131. package/src/react/components/all.ts +38 -38
  132. package/src/react/components/index.ts +16 -16
  133. package/src/robots.ts +4 -4
  134. package/src/entify/lib/newPageMetaOptions.ts +0 -18
  135. package/src/entify/lib/newQueryPageOptions.ts +0 -14
  136. package/src/entify/lib/queryOneIcon.ts +0 -27
  137. package/src/entify/lib/queryPageBySlug.ts +0 -43
  138. package/src/entify/lib/queryPageByType.ts +0 -44
@@ -1,59 +1,59 @@
1
- import clsx from "clsx";
2
- import { forwardRef } from "react";
3
- import { InputProps } from "./Input";
4
- import { useInlineLabelPadding } from "./hooks/useInlineLabelPadding";
5
-
6
- export const Input2 = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
7
- const {
8
- label,
9
- name,
10
- required,
11
- requiredClassName,
12
- className,
13
- labelClassName,
14
- inputClassName,
15
- type = "text",
16
- autoFocus,
17
- value,
18
- placeholder,
19
- onChange,
20
- error,
21
- ...rest
22
- } = props;
23
-
24
- const { labelRef, paddingLeft } = useInlineLabelPadding(16);
25
-
26
- return (
27
- <div className={clsx("relative w-full", className)} {...rest}>
28
- <label
29
- ref={labelRef}
30
- htmlFor={name}
31
- className={clsx(
32
- "absolute left-3 top-1/2 -translate-y-1/2 mt-1 opacity-70 text-sm pointer-events-none whitespace-nowrap",
33
- labelClassName
34
- )}
35
- >
36
- {label}
37
- {required ? <span className={requiredClassName}>*</span> : ""}
38
- </label>
39
- <input
40
- ref={ref}
41
- type={type}
42
- id={name}
43
- name={name}
44
- required={required}
45
- placeholder={placeholder}
46
- className={clsx(
47
- "w-full pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300 bg-transparent focus:bg-white transition-colors duration-200",
48
- inputClassName
49
- )}
50
- style={{ paddingLeft }}
51
- autoComplete={name}
52
- autoFocus={autoFocus}
53
- value={value}
54
- onChange={onChange}
55
- />
56
- {error && <p className="text-red-500 mt-1 text-sm">{error}</p>}
57
- </div>
58
- );
59
- });
1
+ import clsx from "clsx";
2
+ import { forwardRef } from "react";
3
+ import { InputProps } from "./Input";
4
+ import { useInlineLabelPadding } from "./hooks/useInlineLabelPadding";
5
+
6
+ export const Input2 = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
7
+ const {
8
+ label,
9
+ name,
10
+ required,
11
+ requiredClassName,
12
+ className,
13
+ labelClassName,
14
+ inputClassName,
15
+ type = "text",
16
+ autoFocus,
17
+ value,
18
+ placeholder,
19
+ onChange,
20
+ error,
21
+ ...rest
22
+ } = props;
23
+
24
+ const { labelRef, paddingLeft } = useInlineLabelPadding(16);
25
+
26
+ return (
27
+ <div className={clsx("relative w-full", className)} {...rest}>
28
+ <label
29
+ ref={labelRef}
30
+ htmlFor={name}
31
+ className={clsx(
32
+ "absolute left-3 top-1/2 -translate-y-1/2 mt-1 opacity-70 text-sm pointer-events-none whitespace-nowrap",
33
+ labelClassName
34
+ )}
35
+ >
36
+ {label}
37
+ {required ? <span className={requiredClassName}>*</span> : ""}
38
+ </label>
39
+ <input
40
+ ref={ref}
41
+ type={type}
42
+ id={name}
43
+ name={name}
44
+ required={required}
45
+ placeholder={placeholder}
46
+ className={clsx(
47
+ "w-full pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300 bg-transparent focus:bg-white transition-colors duration-200",
48
+ inputClassName
49
+ )}
50
+ style={{ paddingLeft }}
51
+ autoComplete={name}
52
+ autoFocus={autoFocus}
53
+ value={value}
54
+ onChange={onChange}
55
+ />
56
+ {error && <p className="text-red-500 mt-1 text-sm">{error}</p>}
57
+ </div>
58
+ );
59
+ });
@@ -1,48 +1,48 @@
1
- export type SubmitProps = {
2
- needClasses?: string;
3
- title: string;
4
- className?: string;
5
- rawHtml?: string;
6
- spinner?: React.ReactNode;
7
- submitting?: boolean;
8
- disabled?: boolean;
9
- onClick?: (e: React.MouseEvent) => void;
10
- };
11
-
12
- export const Submit: React.FC<SubmitProps> = (props) => {
13
- const {
14
- title = "Send Message",
15
- className,
16
- spinner,
17
- submitting,
18
- disabled,
19
- onClick,
20
- rawHtml,
21
- needClasses,
22
- ...rest
23
- } = props;
24
- return (
25
- <>
26
- <button
27
- type="button"
28
- className={className}
29
- disabled={disabled ?? submitting}
30
- onClick={onClick}
31
- {...rest}
32
- >
33
- {submitting && spinner}
34
- {rawHtml ? (
35
- <div
36
- style={{ display: "contents" }}
37
- dangerouslySetInnerHTML={{ __html: rawHtml }}
38
- />
39
- ) : (
40
- title
41
- )}
42
- </button>
43
- <span className="fixed -z-10 top-0 left-0 w-0 h-0 overflow-hidden opacity-0 pointer-events-none">
44
- <span className={needClasses}></span>
45
- </span>
46
- </>
47
- );
48
- };
1
+ export type SubmitProps = {
2
+ needClasses?: string;
3
+ title: string;
4
+ className?: string;
5
+ rawHtml?: string;
6
+ spinner?: React.ReactNode;
7
+ submitting?: boolean;
8
+ disabled?: boolean;
9
+ onClick?: (e: React.MouseEvent) => void;
10
+ };
11
+
12
+ export const Submit: React.FC<SubmitProps> = (props) => {
13
+ const {
14
+ title = "Send Message",
15
+ className,
16
+ spinner,
17
+ submitting,
18
+ disabled,
19
+ onClick,
20
+ rawHtml,
21
+ needClasses,
22
+ ...rest
23
+ } = props;
24
+ return (
25
+ <>
26
+ <button
27
+ type="button"
28
+ className={className}
29
+ disabled={disabled ?? submitting}
30
+ onClick={onClick}
31
+ {...rest}
32
+ >
33
+ {submitting && spinner}
34
+ {rawHtml ? (
35
+ <div
36
+ style={{ display: "contents" }}
37
+ dangerouslySetInnerHTML={{ __html: rawHtml }}
38
+ />
39
+ ) : (
40
+ title
41
+ )}
42
+ </button>
43
+ <span className="fixed -z-10 top-0 left-0 w-0 h-0 overflow-hidden opacity-0 pointer-events-none">
44
+ <span className={needClasses}></span>
45
+ </span>
46
+ </>
47
+ );
48
+ };
@@ -1,215 +1,215 @@
1
- import clsx from "clsx";
2
- import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
3
- import { CountryDial } from "./countryDialCodes";
4
- import { useTelControl } from "./hooks/useTelControl";
5
-
6
- export interface TelInputProps
7
- extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
8
- label?: string;
9
- name: string;
10
- required?: boolean;
11
- requiredClassName?: string;
12
- className?: string;
13
- labelClassName?: string;
14
- inputClassName?: string;
15
- countries?: CountryDial[];
16
- }
17
-
18
- export const TelInput = forwardRef<HTMLInputElement, TelInputProps>(
19
- (props, ref) => {
20
- const {
21
- label,
22
- name,
23
- required,
24
- requiredClassName,
25
- className,
26
- labelClassName,
27
- inputClassName,
28
- countries,
29
- placeholder,
30
- value,
31
- onChange,
32
- } = props;
33
-
34
- const { allCountries, selected, localNumber, onSelectCountry, onChangeLocal } =
35
- useTelControl({ countries, name, value, onChange });
36
-
37
- const [open, setOpen] = useState(false);
38
- const [query, setQuery] = useState("");
39
- const panelRef = useRef<HTMLDivElement | null>(null);
40
- const buttonRef = useRef<HTMLButtonElement | null>(null);
41
-
42
- useEffect(() => {
43
- const onDocClick = (e: MouseEvent) => {
44
- const target = e.target as Node;
45
- if (
46
- open &&
47
- panelRef.current &&
48
- !panelRef.current.contains(target) &&
49
- buttonRef.current &&
50
- !buttonRef.current.contains(target)
51
- ) {
52
- setOpen(false);
53
- }
54
- };
55
- document.addEventListener("mousedown", onDocClick);
56
- return () => document.removeEventListener("mousedown", onDocClick);
57
- }, [open]);
58
-
59
- // 使用 useTelControl 负责解析与变更回传
60
-
61
- const filtered: CountryDial[] = useMemo(() => {
62
- if (!query) return allCountries;
63
- const q = query.toLowerCase();
64
- return allCountries.filter(
65
- (c: CountryDial) =>
66
- c.name.toLowerCase().includes(q) ||
67
- (c.region && c.region.toLowerCase().includes(q)) ||
68
- c.dialCode.replace("+", "").includes(q.replace("+", ""))
69
- );
70
- }, [allCountries, query]);
71
-
72
- return (
73
- <div className={clsx("w-full", className)}>
74
- {label ? (
75
- <label
76
- htmlFor={name}
77
- className={clsx(
78
- "mb-1 inline-flex items-center text-sm text-gray-600",
79
- labelClassName
80
- )}
81
- >
82
- {label}
83
- {required ? <span className={requiredClassName}>*</span> : null}
84
- </label>
85
- ) : null}
86
-
87
- <div
88
- className={clsx(
89
- "flex h-10 items-center gap-2 rounded-md focus-within:ring-2 focus-within:ring-primary-300 focus-within:ring-offset-0 bg-white",
90
- inputClassName
91
- )}
92
- >
93
- <div className="relative h-full">
94
- <button
95
- ref={buttonRef}
96
- type="button"
97
- aria-haspopup="listbox"
98
- aria-expanded={open}
99
- onClick={() => setOpen((v) => !v)}
100
- className={
101
- "h-full min-w-[200px] inline-flex items-center justify-between whitespace-nowrap rounded-md px-3 py-0 text-sm"
102
- }
103
- >
104
- <span className="truncate text-left leading-tight">
105
- {selected ? (
106
- <>
107
- <span className="font-medium">{selected.name}</span>
108
- {selected.region ? (
109
- <span className="text-gray-500">, {selected.region}</span>
110
- ) : null}
111
- <span className="ml-1 text-gray-700">
112
- {selected.dialCode}
113
- </span>
114
- </>
115
- ) : (
116
- "Select Country/Region"
117
- )}
118
- </span>
119
- <svg
120
- className="ml-2 h-4 w-4 text-gray-500"
121
- viewBox="0 0 20 20"
122
- fill="currentColor"
123
- aria-hidden="true"
124
- >
125
- <path
126
- fillRule="evenodd"
127
- d="M5.23 7.21a.75.75 0 011.06.02L10 11.085l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0L5.25 8.27a.75.75 0 01-.02-1.06z"
128
- clipRule="evenodd"
129
- />
130
- </svg>
131
- </button>
132
-
133
- {open ? (
134
- <div
135
- ref={panelRef}
136
- className="absolute z-50 mt-2 w-[320px] rounded-md border border-gray-200 bg-white p-2 shadow-lg"
137
- >
138
- <div className="flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5">
139
- <input
140
- value={query}
141
- onChange={(e) => setQuery(e.target.value)}
142
- placeholder="Enter Country/Region"
143
- className="h-8 w-full bg-transparent text-sm outline-none"
144
- />
145
- <svg
146
- className="h-4 w-4 text-gray-500"
147
- viewBox="0 0 20 20"
148
- fill="currentColor"
149
- aria-hidden="true"
150
- >
151
- <path
152
- fillRule="evenodd"
153
- d="M8.5 3.5a5 5 0 013.996 8.1l3.202 3.203a.75.75 0 11-1.06 1.06l-3.203-3.202A5 5 0 118.5 3.5zm0 1.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"
154
- clipRule="evenodd"
155
- />
156
- </svg>
157
- </div>
158
-
159
- <div className="mt-2 max-h-64 overflow-auto pr-1">
160
- {filtered.map((c: CountryDial) => (
161
- <button
162
- key={c.code}
163
- type="button"
164
- onClick={() => {
165
- onSelectCountry(c);
166
- setOpen(false);
167
- }}
168
- className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm hover:bg-gray-50"
169
- >
170
- <span
171
- className={clsx(
172
- "inline-block h-3 w-3 rounded-full border",
173
- selected?.code === c.code
174
- ? "border-primary-500 ring-4 ring-primary-200"
175
- : "border-gray-400"
176
- )}
177
- ></span>
178
- <span className="flex-1 truncate">
179
- {c.name}
180
- {c.region ? (
181
- <span className="text-gray-500">, {c.region}</span>
182
- ) : null}
183
- </span>
184
- <span className="text-gray-700">{c.dialCode}</span>
185
- </button>
186
- ))}
187
- </div>
188
- </div>
189
- ) : null}
190
- </div>
191
-
192
- <input
193
- ref={ref}
194
- id={name}
195
- name={name}
196
- required={required}
197
- placeholder={placeholder || "Phone number"}
198
- inputMode="tel"
199
- className={clsx("h-10 w-full px-3 py-0 text-sm leading-tight ring-0 outline-none rounded-md")}
200
- value={localNumber}
201
- onChange={(e) => {
202
- const next = e.target.value;
203
- onChangeLocal(next);
204
- }}
205
- autoComplete={name}
206
- autoCorrect="off"
207
- spellCheck={false}
208
- />
209
- </div>
210
- </div>
211
- );
212
- }
213
- );
214
-
215
- export default TelInput;
1
+ import clsx from "clsx";
2
+ import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
3
+ import { CountryDial } from "./countryDialCodes";
4
+ import { useTelControl } from "./hooks/useTelControl";
5
+
6
+ export interface TelInputProps
7
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
8
+ label?: string;
9
+ name: string;
10
+ required?: boolean;
11
+ requiredClassName?: string;
12
+ className?: string;
13
+ labelClassName?: string;
14
+ inputClassName?: string;
15
+ countries?: CountryDial[];
16
+ }
17
+
18
+ export const TelInput = forwardRef<HTMLInputElement, TelInputProps>(
19
+ (props, ref) => {
20
+ const {
21
+ label,
22
+ name,
23
+ required,
24
+ requiredClassName,
25
+ className,
26
+ labelClassName,
27
+ inputClassName,
28
+ countries,
29
+ placeholder,
30
+ value,
31
+ onChange,
32
+ } = props;
33
+
34
+ const { allCountries, selected, localNumber, onSelectCountry, onChangeLocal } =
35
+ useTelControl({ countries, name, value, onChange });
36
+
37
+ const [open, setOpen] = useState(false);
38
+ const [query, setQuery] = useState("");
39
+ const panelRef = useRef<HTMLDivElement | null>(null);
40
+ const buttonRef = useRef<HTMLButtonElement | null>(null);
41
+
42
+ useEffect(() => {
43
+ const onDocClick = (e: MouseEvent) => {
44
+ const target = e.target as Node;
45
+ if (
46
+ open &&
47
+ panelRef.current &&
48
+ !panelRef.current.contains(target) &&
49
+ buttonRef.current &&
50
+ !buttonRef.current.contains(target)
51
+ ) {
52
+ setOpen(false);
53
+ }
54
+ };
55
+ document.addEventListener("mousedown", onDocClick);
56
+ return () => document.removeEventListener("mousedown", onDocClick);
57
+ }, [open]);
58
+
59
+ // 使用 useTelControl 负责解析与变更回传
60
+
61
+ const filtered: CountryDial[] = useMemo(() => {
62
+ if (!query) return allCountries;
63
+ const q = query.toLowerCase();
64
+ return allCountries.filter(
65
+ (c: CountryDial) =>
66
+ c.name.toLowerCase().includes(q) ||
67
+ (c.region && c.region.toLowerCase().includes(q)) ||
68
+ c.dialCode.replace("+", "").includes(q.replace("+", ""))
69
+ );
70
+ }, [allCountries, query]);
71
+
72
+ return (
73
+ <div className={clsx("w-full", className)}>
74
+ {label ? (
75
+ <label
76
+ htmlFor={name}
77
+ className={clsx(
78
+ "mb-1 inline-flex items-center text-sm text-gray-600",
79
+ labelClassName
80
+ )}
81
+ >
82
+ {label}
83
+ {required ? <span className={requiredClassName}>*</span> : null}
84
+ </label>
85
+ ) : null}
86
+
87
+ <div
88
+ className={clsx(
89
+ "flex h-10 items-center gap-2 rounded-md focus-within:ring-2 focus-within:ring-primary-300 focus-within:ring-offset-0 bg-white",
90
+ inputClassName
91
+ )}
92
+ >
93
+ <div className="relative h-full">
94
+ <button
95
+ ref={buttonRef}
96
+ type="button"
97
+ aria-haspopup="listbox"
98
+ aria-expanded={open}
99
+ onClick={() => setOpen((v) => !v)}
100
+ className={
101
+ "h-full min-w-[200px] inline-flex items-center justify-between whitespace-nowrap rounded-md px-3 py-0 text-sm"
102
+ }
103
+ >
104
+ <span className="truncate text-left leading-tight">
105
+ {selected ? (
106
+ <>
107
+ <span className="font-medium">{selected.name}</span>
108
+ {selected.region ? (
109
+ <span className="text-gray-500">, {selected.region}</span>
110
+ ) : null}
111
+ <span className="ml-1 text-gray-700">
112
+ {selected.dialCode}
113
+ </span>
114
+ </>
115
+ ) : (
116
+ "Select Country/Region"
117
+ )}
118
+ </span>
119
+ <svg
120
+ className="ml-2 h-4 w-4 text-gray-500"
121
+ viewBox="0 0 20 20"
122
+ fill="currentColor"
123
+ aria-hidden="true"
124
+ >
125
+ <path
126
+ fillRule="evenodd"
127
+ d="M5.23 7.21a.75.75 0 011.06.02L10 11.085l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0L5.25 8.27a.75.75 0 01-.02-1.06z"
128
+ clipRule="evenodd"
129
+ />
130
+ </svg>
131
+ </button>
132
+
133
+ {open ? (
134
+ <div
135
+ ref={panelRef}
136
+ className="absolute z-50 mt-2 w-[320px] rounded-md border border-gray-200 bg-white p-2 shadow-lg"
137
+ >
138
+ <div className="flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5">
139
+ <input
140
+ value={query}
141
+ onChange={(e) => setQuery(e.target.value)}
142
+ placeholder="Enter Country/Region"
143
+ className="h-8 w-full bg-transparent text-sm outline-none"
144
+ />
145
+ <svg
146
+ className="h-4 w-4 text-gray-500"
147
+ viewBox="0 0 20 20"
148
+ fill="currentColor"
149
+ aria-hidden="true"
150
+ >
151
+ <path
152
+ fillRule="evenodd"
153
+ d="M8.5 3.5a5 5 0 013.996 8.1l3.202 3.203a.75.75 0 11-1.06 1.06l-3.203-3.202A5 5 0 118.5 3.5zm0 1.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"
154
+ clipRule="evenodd"
155
+ />
156
+ </svg>
157
+ </div>
158
+
159
+ <div className="mt-2 max-h-64 overflow-auto pr-1">
160
+ {filtered.map((c: CountryDial) => (
161
+ <button
162
+ key={c.code}
163
+ type="button"
164
+ onClick={() => {
165
+ onSelectCountry(c);
166
+ setOpen(false);
167
+ }}
168
+ className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm hover:bg-gray-50"
169
+ >
170
+ <span
171
+ className={clsx(
172
+ "inline-block h-3 w-3 rounded-full border",
173
+ selected?.code === c.code
174
+ ? "border-primary-500 ring-4 ring-primary-200"
175
+ : "border-gray-400"
176
+ )}
177
+ ></span>
178
+ <span className="flex-1 truncate">
179
+ {c.name}
180
+ {c.region ? (
181
+ <span className="text-gray-500">, {c.region}</span>
182
+ ) : null}
183
+ </span>
184
+ <span className="text-gray-700">{c.dialCode}</span>
185
+ </button>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ ) : null}
190
+ </div>
191
+
192
+ <input
193
+ ref={ref}
194
+ id={name}
195
+ name={name}
196
+ required={required}
197
+ placeholder={placeholder || "Phone number"}
198
+ inputMode="tel"
199
+ className={clsx("h-10 w-full px-3 py-0 text-sm leading-tight ring-0 outline-none rounded-md")}
200
+ value={localNumber}
201
+ onChange={(e) => {
202
+ const next = e.target.value;
203
+ onChangeLocal(next);
204
+ }}
205
+ autoComplete={name}
206
+ autoCorrect="off"
207
+ spellCheck={false}
208
+ />
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+ );
214
+
215
+ export default TelInput;