@ait-co/devtools 0.1.18 → 0.1.20
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.
- package/README.en.md +863 -0
- package/README.md +117 -5
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +650 -118
- package/dist/panel/index.js.map +1 -1
- package/dist/tunnel-BbcgVy4L.js +114 -0
- package/dist/tunnel-BbcgVy4L.js.map +1 -0
- package/dist/tunnel-DeXfLGRl.cjs +115 -0
- package/dist/tunnel-DeXfLGRl.cjs.map +1 -0
- package/dist/unplugin/index.cjs +34 -0
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +10 -0
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +11 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +34 -0
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +117 -0
- package/dist/unplugin/tunnel.cjs.map +1 -0
- package/dist/unplugin/tunnel.d.cts +47 -0
- package/dist/unplugin/tunnel.d.cts.map +1 -0
- package/dist/unplugin/tunnel.d.ts +47 -0
- package/dist/unplugin/tunnel.d.ts.map +1 -0
- package/dist/unplugin/tunnel.js +114 -0
- package/dist/unplugin/tunnel.js.map +1 -0
- package/package.json +11 -3
package/dist/panel/index.js
CHANGED
|
@@ -1,3 +1,384 @@
|
|
|
1
|
+
//#region src/i18n/en.ts
|
|
2
|
+
const en = {
|
|
3
|
+
"panel.title": "AIT DevTools",
|
|
4
|
+
"panel.toggle.title": "AIT DevTools",
|
|
5
|
+
"panel.close": "Close",
|
|
6
|
+
"panel.editMode.on": "EDIT",
|
|
7
|
+
"panel.editMode.off": "READ-ONLY",
|
|
8
|
+
"panel.editMode.toggleTitle": "Toggle panel edit mode",
|
|
9
|
+
"panel.tabError": "Error rendering \"{tab}\" tab.",
|
|
10
|
+
"panel.tab.env": "Environment",
|
|
11
|
+
"panel.tab.presets": "Presets",
|
|
12
|
+
"panel.tab.viewport": "Viewport",
|
|
13
|
+
"panel.tab.permissions": "Permissions",
|
|
14
|
+
"panel.tab.notifications": "Notifications",
|
|
15
|
+
"panel.tab.location": "Location",
|
|
16
|
+
"panel.tab.device": "Device",
|
|
17
|
+
"panel.tab.iap": "IAP",
|
|
18
|
+
"panel.tab.ads": "Ads",
|
|
19
|
+
"panel.tab.events": "Events",
|
|
20
|
+
"panel.tab.analytics": "Analytics",
|
|
21
|
+
"panel.tab.storage": "Storage",
|
|
22
|
+
"common.readOnly": "Read-only — mock responses are controlled at build time.",
|
|
23
|
+
"toast.consent.title": "Send anonymous usage stats?",
|
|
24
|
+
"toast.consent.body": "We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.",
|
|
25
|
+
"toast.consent.learnMore": "Learn more",
|
|
26
|
+
"toast.consent.accept": "Yes, send",
|
|
27
|
+
"toast.consent.deny": "No, thanks",
|
|
28
|
+
"env.section.platform": "Platform",
|
|
29
|
+
"env.row.os": "OS",
|
|
30
|
+
"env.row.appVersion": "App Version",
|
|
31
|
+
"env.row.environment": "Environment",
|
|
32
|
+
"env.row.locale": "Locale",
|
|
33
|
+
"env.section.network": "Network",
|
|
34
|
+
"env.row.networkStatus": "Status",
|
|
35
|
+
"env.section.safeArea": "Safe Area Insets",
|
|
36
|
+
"env.row.safeArea.top": "Top",
|
|
37
|
+
"env.row.safeArea.bottom": "Bottom",
|
|
38
|
+
"env.telemetry.section": "Telemetry",
|
|
39
|
+
"env.telemetry.t0Row": "Anonymous usage signal (Tier 0)",
|
|
40
|
+
"env.telemetry.t0On": "On",
|
|
41
|
+
"env.telemetry.t0Off": "Off",
|
|
42
|
+
"env.telemetry.t0TurnOn": "Turn on",
|
|
43
|
+
"env.telemetry.t0TurnOff": "Turn off",
|
|
44
|
+
"env.telemetry.t0Desc": "Version + date only, no PII. Once per day. Helps improve the package.",
|
|
45
|
+
"env.telemetry.row": "Extended telemetry (Tier 1)",
|
|
46
|
+
"env.telemetry.on": "On",
|
|
47
|
+
"env.telemetry.off": "Off",
|
|
48
|
+
"env.telemetry.turnOn": "Turn on",
|
|
49
|
+
"env.telemetry.turnOff": "Turn off",
|
|
50
|
+
"env.telemetry.anonIdLabel": "anon_id: {value}",
|
|
51
|
+
"env.telemetry.anonIdNotSet": "(not yet set)",
|
|
52
|
+
"env.telemetry.anonIdCopyTitle": "Click to copy full anon_id",
|
|
53
|
+
"env.telemetry.deleteBtn": "Delete my data",
|
|
54
|
+
"env.telemetry.deleting": "Deleting…",
|
|
55
|
+
"env.telemetry.deleted": "Deleted",
|
|
56
|
+
"env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
|
|
57
|
+
"env.telemetry.deleteFailed": "Delete failed",
|
|
58
|
+
"env.telemetry.privacyLink": "Privacy policy →",
|
|
59
|
+
"env.section.language": "Language",
|
|
60
|
+
"env.language.row": "Language",
|
|
61
|
+
"env.language.ko": "한국어",
|
|
62
|
+
"env.language.en": "English",
|
|
63
|
+
"permissions.section.device": "Device Permissions",
|
|
64
|
+
"location.section.current": "Current Location",
|
|
65
|
+
"location.row.latitude": "Latitude",
|
|
66
|
+
"location.row.longitude": "Longitude",
|
|
67
|
+
"location.row.accuracy": "Accuracy",
|
|
68
|
+
"device.section.modes": "Device API Modes",
|
|
69
|
+
"device.row.camera": "Camera",
|
|
70
|
+
"device.row.photos": "Photos",
|
|
71
|
+
"device.row.location": "Location",
|
|
72
|
+
"device.row.network": "Network",
|
|
73
|
+
"device.row.clipboard": "Clipboard",
|
|
74
|
+
"device.section.mockImages": "Mock Images ({count})",
|
|
75
|
+
"device.btn.add": "+ Add",
|
|
76
|
+
"device.btn.useDefaults": "Use defaults",
|
|
77
|
+
"device.btn.clear": "Clear",
|
|
78
|
+
"device.prompt.camera.title": "Camera Prompt — Select an image",
|
|
79
|
+
"device.prompt.photos.title": "Photos Prompt — Select images",
|
|
80
|
+
"device.prompt.location.title": "Location Prompt — Enter coordinates",
|
|
81
|
+
"device.prompt.locationUpdate.title": "Location Update — Send coordinates",
|
|
82
|
+
"device.prompt.fallbackTitle": "Prompt: {type}",
|
|
83
|
+
"device.prompt.label.lat": "Lat",
|
|
84
|
+
"device.prompt.label.lng": "Lng",
|
|
85
|
+
"device.prompt.send": "Send",
|
|
86
|
+
"device.prompt.cancel": "Cancel",
|
|
87
|
+
"viewport.section.device": "Device",
|
|
88
|
+
"viewport.row.preset": "Preset",
|
|
89
|
+
"viewport.row.orientation": "Orientation",
|
|
90
|
+
"viewport.row.notchSide": "Notch side",
|
|
91
|
+
"viewport.section.custom": "Custom size",
|
|
92
|
+
"viewport.row.width": "Width (px)",
|
|
93
|
+
"viewport.row.height": "Height (px)",
|
|
94
|
+
"viewport.section.appearance": "Appearance",
|
|
95
|
+
"viewport.row.showFrame": "Show frame",
|
|
96
|
+
"viewport.row.showAitNavBar": "Show Apps in Toss nav bar",
|
|
97
|
+
"viewport.row.navBarType": "Nav bar type",
|
|
98
|
+
"viewport.status.noConstraint": "No viewport constraint — body fills the window.",
|
|
99
|
+
"viewport.status.cssPhysical": "CSS / physical",
|
|
100
|
+
"viewport.status.safeArea": "Safe area",
|
|
101
|
+
"viewport.status.aitNavBar": "AIT nav bar",
|
|
102
|
+
"viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
|
|
103
|
+
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
104
|
+
"iap.section.simulator": "IAP Simulator",
|
|
105
|
+
"iap.row.nextResult": "Next Purchase Result",
|
|
106
|
+
"iap.section.tossPay": "TossPay",
|
|
107
|
+
"iap.row.tossPayResult": "Next Payment Result",
|
|
108
|
+
"iap.section.pending": "Pending Orders ({count})",
|
|
109
|
+
"iap.empty.pending": "(no pending orders)",
|
|
110
|
+
"iap.section.completed": "Completed Orders ({count})",
|
|
111
|
+
"iap.empty.completed": "(no completed orders)",
|
|
112
|
+
"iap.btn.complete": "Complete",
|
|
113
|
+
"iap.label.pending": "PENDING",
|
|
114
|
+
"events.section.navigation": "Navigation Events",
|
|
115
|
+
"events.btn.triggerBack": "Trigger Back Event",
|
|
116
|
+
"events.btn.triggerHome": "Trigger Home Event",
|
|
117
|
+
"events.section.login": "Login",
|
|
118
|
+
"events.row.loggedIn": "Logged In",
|
|
119
|
+
"events.row.tossLoginIntegrated": "Toss Login Integrated",
|
|
120
|
+
"analytics.section.log": "Analytics Log ({count})",
|
|
121
|
+
"analytics.btn.clear": "Clear",
|
|
122
|
+
"storage.section.title": "Storage ({count} items)",
|
|
123
|
+
"storage.btn.clearAll": "Clear All",
|
|
124
|
+
"storage.empty": "No items in storage",
|
|
125
|
+
"presets.section.builtIn": "Built-in scenarios",
|
|
126
|
+
"presets.section.saved": "Saved presets ({count})",
|
|
127
|
+
"presets.section.save": "Save",
|
|
128
|
+
"presets.save.description": "Capture network / permissions / auth / IAP / ads / payment slices.",
|
|
129
|
+
"presets.btn.saveCurrent": "Save current as preset",
|
|
130
|
+
"presets.btn.apply": "Apply",
|
|
131
|
+
"presets.btn.reApply": "Re-apply",
|
|
132
|
+
"presets.btn.delete": "Delete",
|
|
133
|
+
"presets.empty.saved": "No saved presets yet.",
|
|
134
|
+
"presets.empty.builtIn": "No built-in presets.",
|
|
135
|
+
"presets.prompt.label": "Preset label?",
|
|
136
|
+
"presets.confirm.delete": "Delete preset \"{label}\"?",
|
|
137
|
+
"ads.section.state": "Ads State",
|
|
138
|
+
"ads.row.isLoaded": "isLoaded",
|
|
139
|
+
"ads.row.forceNoFill": "Force \"no fill\"",
|
|
140
|
+
"ads.empty.events": "No events yet",
|
|
141
|
+
"ads.section.googleAdMob": "GoogleAdMob",
|
|
142
|
+
"ads.section.tossAds": "TossAds",
|
|
143
|
+
"ads.section.fullScreenAd": "FullScreenAd",
|
|
144
|
+
"ads.btn.load": "Load",
|
|
145
|
+
"ads.btn.show": "Show",
|
|
146
|
+
"notifications.section.title": "requestNotificationAgreement",
|
|
147
|
+
"notifications.option.newAgreement": "newAgreement (first-time agree)",
|
|
148
|
+
"notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
|
|
149
|
+
"notifications.option.agreementRejected": "agreementRejected (user declined)"
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/i18n/ko.ts
|
|
153
|
+
const ko = {
|
|
154
|
+
"panel.title": "AIT DevTools",
|
|
155
|
+
"panel.toggle.title": "AIT DevTools",
|
|
156
|
+
"panel.close": "Close",
|
|
157
|
+
"panel.editMode.on": "EDIT",
|
|
158
|
+
"panel.editMode.off": "READ-ONLY",
|
|
159
|
+
"panel.editMode.toggleTitle": "패널 편집 모드 전환",
|
|
160
|
+
"panel.tabError": "\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.",
|
|
161
|
+
"panel.tab.env": "Environment",
|
|
162
|
+
"panel.tab.presets": "Presets",
|
|
163
|
+
"panel.tab.viewport": "Viewport",
|
|
164
|
+
"panel.tab.permissions": "Permissions",
|
|
165
|
+
"panel.tab.notifications": "Notifications",
|
|
166
|
+
"panel.tab.location": "Location",
|
|
167
|
+
"panel.tab.device": "Device",
|
|
168
|
+
"panel.tab.iap": "IAP",
|
|
169
|
+
"panel.tab.ads": "Ads",
|
|
170
|
+
"panel.tab.events": "Events",
|
|
171
|
+
"panel.tab.analytics": "Analytics",
|
|
172
|
+
"panel.tab.storage": "Storage",
|
|
173
|
+
"common.readOnly": "읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.",
|
|
174
|
+
"toast.consent.title": "익명 사용 통계를 보낼까요?",
|
|
175
|
+
"toast.consent.body": "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.",
|
|
176
|
+
"toast.consent.learnMore": "더 알아보기",
|
|
177
|
+
"toast.consent.accept": "네, 보낼게요",
|
|
178
|
+
"toast.consent.deny": "아니요",
|
|
179
|
+
"env.section.platform": "Platform",
|
|
180
|
+
"env.row.os": "OS",
|
|
181
|
+
"env.row.appVersion": "App Version",
|
|
182
|
+
"env.row.environment": "Environment",
|
|
183
|
+
"env.row.locale": "Locale",
|
|
184
|
+
"env.section.network": "Network",
|
|
185
|
+
"env.row.networkStatus": "Status",
|
|
186
|
+
"env.section.safeArea": "Safe Area Insets",
|
|
187
|
+
"env.row.safeArea.top": "Top",
|
|
188
|
+
"env.row.safeArea.bottom": "Bottom",
|
|
189
|
+
"env.telemetry.section": "Telemetry",
|
|
190
|
+
"env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
|
|
191
|
+
"env.telemetry.t0On": "On",
|
|
192
|
+
"env.telemetry.t0Off": "Off",
|
|
193
|
+
"env.telemetry.t0TurnOn": "Turn on",
|
|
194
|
+
"env.telemetry.t0TurnOff": "Turn off",
|
|
195
|
+
"env.telemetry.t0Desc": "버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.",
|
|
196
|
+
"env.telemetry.row": "확장 텔레메트리 (Tier 1)",
|
|
197
|
+
"env.telemetry.on": "On",
|
|
198
|
+
"env.telemetry.off": "Off",
|
|
199
|
+
"env.telemetry.turnOn": "Turn on",
|
|
200
|
+
"env.telemetry.turnOff": "Turn off",
|
|
201
|
+
"env.telemetry.anonIdLabel": "anon_id: {value}",
|
|
202
|
+
"env.telemetry.anonIdNotSet": "(not yet set)",
|
|
203
|
+
"env.telemetry.anonIdCopyTitle": "전체 anon_id 복사",
|
|
204
|
+
"env.telemetry.deleteBtn": "내 데이터 삭제",
|
|
205
|
+
"env.telemetry.deleting": "삭제 중…",
|
|
206
|
+
"env.telemetry.deleted": "삭제 완료",
|
|
207
|
+
"env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
|
|
208
|
+
"env.telemetry.deleteFailed": "삭제 실패",
|
|
209
|
+
"env.telemetry.privacyLink": "개인정보 처리방침 →",
|
|
210
|
+
"env.section.language": "Language",
|
|
211
|
+
"env.language.row": "Language",
|
|
212
|
+
"env.language.ko": "한국어",
|
|
213
|
+
"env.language.en": "English",
|
|
214
|
+
"permissions.section.device": "Device Permissions",
|
|
215
|
+
"location.section.current": "Current Location",
|
|
216
|
+
"location.row.latitude": "Latitude",
|
|
217
|
+
"location.row.longitude": "Longitude",
|
|
218
|
+
"location.row.accuracy": "Accuracy",
|
|
219
|
+
"device.section.modes": "Device API Modes",
|
|
220
|
+
"device.row.camera": "Camera",
|
|
221
|
+
"device.row.photos": "Photos",
|
|
222
|
+
"device.row.location": "Location",
|
|
223
|
+
"device.row.network": "Network",
|
|
224
|
+
"device.row.clipboard": "Clipboard",
|
|
225
|
+
"device.section.mockImages": "Mock Images ({count})",
|
|
226
|
+
"device.btn.add": "+ Add",
|
|
227
|
+
"device.btn.useDefaults": "Use defaults",
|
|
228
|
+
"device.btn.clear": "Clear",
|
|
229
|
+
"device.prompt.camera.title": "Camera Prompt — 이미지를 선택하세요",
|
|
230
|
+
"device.prompt.photos.title": "Photos Prompt — 이미지를 선택하세요",
|
|
231
|
+
"device.prompt.location.title": "Location Prompt — 좌표 입력",
|
|
232
|
+
"device.prompt.locationUpdate.title": "Location Update — 좌표 전송",
|
|
233
|
+
"device.prompt.fallbackTitle": "Prompt: {type}",
|
|
234
|
+
"device.prompt.label.lat": "Lat",
|
|
235
|
+
"device.prompt.label.lng": "Lng",
|
|
236
|
+
"device.prompt.send": "Send",
|
|
237
|
+
"device.prompt.cancel": "Cancel",
|
|
238
|
+
"viewport.section.device": "Device",
|
|
239
|
+
"viewport.row.preset": "Preset",
|
|
240
|
+
"viewport.row.orientation": "Orientation",
|
|
241
|
+
"viewport.row.notchSide": "Notch side",
|
|
242
|
+
"viewport.section.custom": "Custom size",
|
|
243
|
+
"viewport.row.width": "Width (px)",
|
|
244
|
+
"viewport.row.height": "Height (px)",
|
|
245
|
+
"viewport.section.appearance": "Appearance",
|
|
246
|
+
"viewport.row.showFrame": "Show frame",
|
|
247
|
+
"viewport.row.showAitNavBar": "Apps in Toss 내비게이션 바 표시",
|
|
248
|
+
"viewport.row.navBarType": "Nav bar type",
|
|
249
|
+
"viewport.status.noConstraint": "뷰포트 제약 없음 — body가 창을 가득 채웁니다.",
|
|
250
|
+
"viewport.status.cssPhysical": "CSS / physical",
|
|
251
|
+
"viewport.status.safeArea": "Safe area",
|
|
252
|
+
"viewport.status.aitNavBar": "AIT nav bar",
|
|
253
|
+
"viewport.status.aitNavBarValue": "{height}px (excl. SafeArea) · {type}",
|
|
254
|
+
"viewport.orientation.autoSuffix": "{orient} (auto)",
|
|
255
|
+
"iap.section.simulator": "IAP Simulator",
|
|
256
|
+
"iap.row.nextResult": "Next Purchase Result",
|
|
257
|
+
"iap.section.tossPay": "TossPay",
|
|
258
|
+
"iap.row.tossPayResult": "Next Payment Result",
|
|
259
|
+
"iap.section.pending": "Pending Orders ({count})",
|
|
260
|
+
"iap.empty.pending": "(대기 중인 주문 없음)",
|
|
261
|
+
"iap.section.completed": "Completed Orders ({count})",
|
|
262
|
+
"iap.empty.completed": "(완료된 주문 없음)",
|
|
263
|
+
"iap.btn.complete": "Complete",
|
|
264
|
+
"iap.label.pending": "PENDING",
|
|
265
|
+
"events.section.navigation": "Navigation Events",
|
|
266
|
+
"events.btn.triggerBack": "Back 이벤트 발생",
|
|
267
|
+
"events.btn.triggerHome": "Home 이벤트 발생",
|
|
268
|
+
"events.section.login": "Login",
|
|
269
|
+
"events.row.loggedIn": "Logged In",
|
|
270
|
+
"events.row.tossLoginIntegrated": "Toss Login Integrated",
|
|
271
|
+
"analytics.section.log": "Analytics Log ({count})",
|
|
272
|
+
"analytics.btn.clear": "Clear",
|
|
273
|
+
"storage.section.title": "Storage ({count} items)",
|
|
274
|
+
"storage.btn.clearAll": "Clear All",
|
|
275
|
+
"storage.empty": "저장된 항목이 없습니다",
|
|
276
|
+
"presets.section.builtIn": "Built-in scenarios",
|
|
277
|
+
"presets.section.saved": "Saved presets ({count})",
|
|
278
|
+
"presets.section.save": "Save",
|
|
279
|
+
"presets.save.description": "network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.",
|
|
280
|
+
"presets.btn.saveCurrent": "현재 상태를 프리셋으로 저장",
|
|
281
|
+
"presets.btn.apply": "Apply",
|
|
282
|
+
"presets.btn.reApply": "Re-apply",
|
|
283
|
+
"presets.btn.delete": "Delete",
|
|
284
|
+
"presets.empty.saved": "저장된 프리셋이 아직 없습니다.",
|
|
285
|
+
"presets.empty.builtIn": "내장 프리셋이 없습니다.",
|
|
286
|
+
"presets.prompt.label": "프리셋 라벨을 입력하세요",
|
|
287
|
+
"presets.confirm.delete": "\"{label}\" 프리셋을 삭제할까요?",
|
|
288
|
+
"ads.section.state": "Ads State",
|
|
289
|
+
"ads.row.isLoaded": "isLoaded",
|
|
290
|
+
"ads.row.forceNoFill": "강제 \"no fill\"",
|
|
291
|
+
"ads.empty.events": "아직 이벤트가 없습니다",
|
|
292
|
+
"ads.section.googleAdMob": "GoogleAdMob",
|
|
293
|
+
"ads.section.tossAds": "TossAds",
|
|
294
|
+
"ads.section.fullScreenAd": "FullScreenAd",
|
|
295
|
+
"ads.btn.load": "Load",
|
|
296
|
+
"ads.btn.show": "Show",
|
|
297
|
+
"notifications.section.title": "requestNotificationAgreement",
|
|
298
|
+
"notifications.option.newAgreement": "newAgreement (최초 동의)",
|
|
299
|
+
"notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
|
|
300
|
+
"notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/i18n/index.ts
|
|
304
|
+
/**
|
|
305
|
+
* Vanilla TS i18n for the floating DevTools panel.
|
|
306
|
+
*
|
|
307
|
+
* Public surface:
|
|
308
|
+
* - `t(key, vars?)` — look up a UI string, with `{name}` placeholder
|
|
309
|
+
* interpolation. Falls back to the key itself if a translation is missing.
|
|
310
|
+
* - `getLocale()` / `setLocale(locale)` — read/persist the active locale.
|
|
311
|
+
* `setLocale` dispatches `__ait:localechange` so the panel can remount.
|
|
312
|
+
* - `detectLocale()` — first-run heuristic from `navigator.language`.
|
|
313
|
+
*
|
|
314
|
+
* `ko` is the source of truth (keys are typed from it). `en` is also a full
|
|
315
|
+
* `Record<StringKey, string>` (devtools is developer-facing, en is a real
|
|
316
|
+
* audience). The `Partial` lookup table preserves the runtime `?? key` safety
|
|
317
|
+
* net even though we ship complete catalogs today.
|
|
318
|
+
*/
|
|
319
|
+
const LOCALE_STORAGE_KEY = "__ait_locale";
|
|
320
|
+
const LOCALE_CHANGE_EVENT = "__ait:localechange";
|
|
321
|
+
const tables = {
|
|
322
|
+
ko,
|
|
323
|
+
en
|
|
324
|
+
};
|
|
325
|
+
let currentLocale = null;
|
|
326
|
+
function safeReadStorage() {
|
|
327
|
+
if (typeof localStorage === "undefined") return null;
|
|
328
|
+
try {
|
|
329
|
+
const raw = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
330
|
+
if (raw === "ko" || raw === "en") return raw;
|
|
331
|
+
} catch {}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function safeWriteStorage(locale) {
|
|
335
|
+
if (typeof localStorage === "undefined") return;
|
|
336
|
+
try {
|
|
337
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,
|
|
342
|
+
* everything else → `'en'`. Pure function; does not touch storage.
|
|
343
|
+
*/
|
|
344
|
+
function detectLocale() {
|
|
345
|
+
if (typeof navigator === "undefined") return "en";
|
|
346
|
+
const lang = navigator.language ?? "";
|
|
347
|
+
return /^ko\b/i.test(lang) ? "ko" : "en";
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Resolve the active locale, in order:
|
|
351
|
+
* 1. previously set in-memory value (set by `setLocale`)
|
|
352
|
+
* 2. localStorage `__ait_locale`
|
|
353
|
+
* 3. `detectLocale()` from navigator
|
|
354
|
+
*/
|
|
355
|
+
function getLocale() {
|
|
356
|
+
if (currentLocale) return currentLocale;
|
|
357
|
+
currentLocale = safeReadStorage() ?? detectLocale();
|
|
358
|
+
return currentLocale;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Persist a locale choice and notify listeners. The panel listens for
|
|
362
|
+
* `__ait:localechange` and re-mounts so every string re-evaluates.
|
|
363
|
+
*/
|
|
364
|
+
function setLocale(locale) {
|
|
365
|
+
currentLocale = locale;
|
|
366
|
+
safeWriteStorage(locale);
|
|
367
|
+
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Look up a UI string for the current locale. Falls back to the key if missing,
|
|
371
|
+
* so a forgotten key surfaces visibly rather than rendering empty.
|
|
372
|
+
*/
|
|
373
|
+
function t(key, vars) {
|
|
374
|
+
const raw = tables[getLocale()][key] ?? key;
|
|
375
|
+
if (!vars) return raw;
|
|
376
|
+
return raw.replace(/\{(\w+)\}/g, (match, name) => {
|
|
377
|
+
const value = vars[name];
|
|
378
|
+
return value === void 0 ? match : String(value);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
1
382
|
//#region src/mock/state.ts
|
|
2
383
|
const DEFAULT_STATE = {
|
|
3
384
|
platform: "ios",
|
|
@@ -224,9 +605,6 @@ if (typeof window !== "undefined") window.__ait = aitState;
|
|
|
224
605
|
/**
|
|
225
606
|
* Consent toast UI — vanilla DOM, fixed bottom-right.
|
|
226
607
|
*
|
|
227
|
-
* Ko-only for now — devtools has no i18n layer.
|
|
228
|
-
* TODO: revisit when/if an i18n layer is added to the panel.
|
|
229
|
-
*
|
|
230
608
|
* Shows once per "undecided + reprompt window cleared" session.
|
|
231
609
|
* Calls onAccept / onDeny callbacks; caller is responsible for persisting state.
|
|
232
610
|
*/
|
|
@@ -318,26 +696,26 @@ function showConsentToast({ onAccept, onDeny }) {
|
|
|
318
696
|
toast.id = TOAST_ID;
|
|
319
697
|
const header = document.createElement("div");
|
|
320
698
|
header.className = "ait-toast-header";
|
|
321
|
-
header.textContent = "
|
|
699
|
+
header.textContent = t("toast.consent.title");
|
|
322
700
|
const body = document.createElement("div");
|
|
323
701
|
body.className = "ait-toast-body";
|
|
324
|
-
body.textContent = "
|
|
702
|
+
body.textContent = t("toast.consent.body");
|
|
325
703
|
const learnMore = document.createElement("a");
|
|
326
704
|
learnMore.className = "ait-toast-link";
|
|
327
705
|
learnMore.href = "https://docs.aitc.dev/privacy";
|
|
328
706
|
learnMore.target = "_blank";
|
|
329
707
|
learnMore.rel = "noopener noreferrer";
|
|
330
|
-
learnMore.textContent = "
|
|
708
|
+
learnMore.textContent = t("toast.consent.learnMore");
|
|
331
709
|
const yesBtn = document.createElement("button");
|
|
332
710
|
yesBtn.className = "ait-toast-btn-primary";
|
|
333
|
-
yesBtn.textContent = "
|
|
711
|
+
yesBtn.textContent = t("toast.consent.accept");
|
|
334
712
|
yesBtn.addEventListener("click", () => {
|
|
335
713
|
removeToast();
|
|
336
714
|
onAccept();
|
|
337
715
|
});
|
|
338
716
|
const noBtn = document.createElement("button");
|
|
339
717
|
noBtn.className = "ait-toast-btn-secondary";
|
|
340
|
-
noBtn.textContent = "
|
|
718
|
+
noBtn.textContent = t("toast.consent.deny");
|
|
341
719
|
noBtn.addEventListener("click", () => {
|
|
342
720
|
removeToast();
|
|
343
721
|
onDeny();
|
|
@@ -354,11 +732,56 @@ const KEY_CONSENT = "__ait_telemetry:consent";
|
|
|
354
732
|
const KEY_REPROMPT_AFTER = "__ait_telemetry:reprompt_after";
|
|
355
733
|
const KEY_POLICY_VERSION = "__ait_telemetry:policy_version";
|
|
356
734
|
const KEY_ANON_ID = "__ait_telemetry:anon_id";
|
|
735
|
+
const KEY_T0_LAST_SENT = "__ait_telemetry:t0_last_sent";
|
|
736
|
+
const KEY_T0_OFF = "__ait_telemetry:t0_off";
|
|
737
|
+
/**
|
|
738
|
+
* Returns true if Tier 0 ping is enabled.
|
|
739
|
+
* Disabled when `localStorage.__ait_telemetry:t0_off = '1'`
|
|
740
|
+
* or `process.env.AITC_TELEMETRY === 'off'`.
|
|
741
|
+
*/
|
|
742
|
+
function isTier0Enabled() {
|
|
743
|
+
if (typeof process !== "undefined" && process.env.AITC_TELEMETRY === "off") return false;
|
|
744
|
+
try {
|
|
745
|
+
return localStorage.getItem(KEY_T0_OFF) !== "1";
|
|
746
|
+
} catch {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Sets or clears the Tier 0 opt-out marker.
|
|
752
|
+
*/
|
|
753
|
+
function setTier0Enabled(enabled) {
|
|
754
|
+
try {
|
|
755
|
+
if (enabled) localStorage.removeItem(KEY_T0_OFF);
|
|
756
|
+
else localStorage.setItem(KEY_T0_OFF, "1");
|
|
757
|
+
} catch {}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Returns true if Tier 0 has already been sent today (YYYY-MM-DD).
|
|
761
|
+
*/
|
|
762
|
+
function hasSentTier0Today() {
|
|
763
|
+
try {
|
|
764
|
+
const stored = localStorage.getItem(KEY_T0_LAST_SENT);
|
|
765
|
+
if (!stored) return false;
|
|
766
|
+
return stored === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
767
|
+
} catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Records that Tier 0 was sent today.
|
|
773
|
+
*/
|
|
774
|
+
function markTier0Sent() {
|
|
775
|
+
try {
|
|
776
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
777
|
+
localStorage.setItem(KEY_T0_LAST_SENT, today);
|
|
778
|
+
} catch {}
|
|
779
|
+
}
|
|
357
780
|
/**
|
|
358
781
|
* Current policy version. Bump this string whenever the privacy policy changes.
|
|
359
782
|
* Users who previously granted on an older version will be re-prompted once.
|
|
360
783
|
*/
|
|
361
|
-
const CURRENT_POLICY_VERSION = "2026-05-
|
|
784
|
+
const CURRENT_POLICY_VERSION = "2026-05-18";
|
|
362
785
|
/** 30 days in milliseconds */
|
|
363
786
|
const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
|
|
364
787
|
function readConsentState() {
|
|
@@ -397,7 +820,7 @@ function getOrCreateAnonId() {
|
|
|
397
820
|
function resolveEffectiveConsent() {
|
|
398
821
|
const raw = localStorage.getItem(KEY_CONSENT);
|
|
399
822
|
if (raw === "granted") {
|
|
400
|
-
if (readPolicyVersion() !== "2026-05-
|
|
823
|
+
if (readPolicyVersion() !== "2026-05-18") {
|
|
401
824
|
localStorage.removeItem(KEY_CONSENT);
|
|
402
825
|
localStorage.removeItem(KEY_POLICY_VERSION);
|
|
403
826
|
return "undecided";
|
|
@@ -523,6 +946,7 @@ function delay(ms) {
|
|
|
523
946
|
async function send(event, version, meta) {
|
|
524
947
|
if (readConsentState() !== "granted") return;
|
|
525
948
|
const payload = {
|
|
949
|
+
tier: 1,
|
|
526
950
|
source: "devtools",
|
|
527
951
|
event,
|
|
528
952
|
anon_id: getOrCreateAnonId(),
|
|
@@ -543,6 +967,7 @@ async function send(event, version, meta) {
|
|
|
543
967
|
function sendBeaconEvent(event, version, meta) {
|
|
544
968
|
if (readConsentState() !== "granted") return;
|
|
545
969
|
const payload = {
|
|
970
|
+
tier: 1,
|
|
546
971
|
source: "devtools",
|
|
547
972
|
event,
|
|
548
973
|
anon_id: getOrCreateAnonId(),
|
|
@@ -563,6 +988,49 @@ function sendBeaconEvent(event, version, meta) {
|
|
|
563
988
|
}).catch(() => {});
|
|
564
989
|
}
|
|
565
990
|
//#endregion
|
|
991
|
+
//#region src/telemetry/tier0.ts
|
|
992
|
+
/**
|
|
993
|
+
* Tier 0 telemetry — opt-out, fire-and-forget daily ping.
|
|
994
|
+
*
|
|
995
|
+
* Payload: { tier: 0, source: 'devtools', ts: number, version: string }
|
|
996
|
+
* No anon_id. No event name. No meta.
|
|
997
|
+
*
|
|
998
|
+
* Rules:
|
|
999
|
+
* - Sent once per calendar day (localStorage daily marker).
|
|
1000
|
+
* - Skipped when __ait_telemetry:t0_off = '1' or AITC_TELEMETRY=off.
|
|
1001
|
+
* - 5 s timeout, no retry. Failure is silently dropped.
|
|
1002
|
+
*/
|
|
1003
|
+
/**
|
|
1004
|
+
* Sends the Tier 0 daily ping if eligible.
|
|
1005
|
+
* Returns true if a ping was sent, false if skipped or failed.
|
|
1006
|
+
*/
|
|
1007
|
+
async function sendTier0Ping(version) {
|
|
1008
|
+
if (!isTier0Enabled()) return false;
|
|
1009
|
+
if (hasSentTier0Today()) return false;
|
|
1010
|
+
const payload = {
|
|
1011
|
+
tier: 0,
|
|
1012
|
+
source: "devtools",
|
|
1013
|
+
ts: Date.now(),
|
|
1014
|
+
version
|
|
1015
|
+
};
|
|
1016
|
+
const controller = new AbortController();
|
|
1017
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
1018
|
+
try {
|
|
1019
|
+
await fetch(`${TELEMETRY_ENDPOINT}/e`, {
|
|
1020
|
+
method: "POST",
|
|
1021
|
+
headers: { "Content-Type": "application/json" },
|
|
1022
|
+
body: JSON.stringify(payload),
|
|
1023
|
+
signal: controller.signal
|
|
1024
|
+
});
|
|
1025
|
+
markTier0Sent();
|
|
1026
|
+
return true;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false;
|
|
1029
|
+
} finally {
|
|
1030
|
+
clearTimeout(timeoutId);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
566
1034
|
//#region src/telemetry/index.ts
|
|
567
1035
|
/**
|
|
568
1036
|
* Telemetry client — internal to @ait-co/devtools.
|
|
@@ -582,7 +1050,7 @@ function readGlobalString(key) {
|
|
|
582
1050
|
}
|
|
583
1051
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
584
1052
|
function getVersion() {
|
|
585
|
-
return "0.1.
|
|
1053
|
+
return "0.1.20";
|
|
586
1054
|
}
|
|
587
1055
|
let panelVisibleSince = null;
|
|
588
1056
|
let accumulatedMs = 0;
|
|
@@ -606,11 +1074,12 @@ function wirePagehide() {
|
|
|
606
1074
|
}
|
|
607
1075
|
/**
|
|
608
1076
|
* Call once after panel mounts.
|
|
609
|
-
* Handles: consent check, optional toast, panel_mount event, pagehide wiring.
|
|
1077
|
+
* Handles: Tier 0 ping, consent check, optional toast, panel_mount event, pagehide wiring.
|
|
610
1078
|
*/
|
|
611
1079
|
function init() {
|
|
612
1080
|
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
613
1081
|
wirePagehide();
|
|
1082
|
+
sendTier0Ping(getVersion());
|
|
614
1083
|
if (resolveEffectiveConsent() === "granted") {
|
|
615
1084
|
getOrCreateAnonId();
|
|
616
1085
|
send("panel_mount", getVersion());
|
|
@@ -691,7 +1160,7 @@ function inputRow(label, value, onChange, disabled = false) {
|
|
|
691
1160
|
return h("div", { className: "ait-row" }, h("label", {}, label), input);
|
|
692
1161
|
}
|
|
693
1162
|
function monitoringNotice() {
|
|
694
|
-
return h("div", { className: "ait-monitoring-notice" }, "
|
|
1163
|
+
return h("div", { className: "ait-monitoring-notice" }, t("common.readOnly"));
|
|
695
1164
|
}
|
|
696
1165
|
const PANEL_STYLES = `
|
|
697
1166
|
.ait-panel-toggle {
|
|
@@ -1671,7 +2140,7 @@ function renderPromptBanner() {
|
|
|
1671
2140
|
if (!pendingPrompt) return null;
|
|
1672
2141
|
const banner = h("div", { className: "ait-prompt-banner" });
|
|
1673
2142
|
if (pendingPrompt.type === "camera") {
|
|
1674
|
-
banner.append(h("div", { className: "ait-prompt-title" }, "
|
|
2143
|
+
banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.camera.title")));
|
|
1675
2144
|
const input = h("input", {
|
|
1676
2145
|
type: "file",
|
|
1677
2146
|
accept: "image/*",
|
|
@@ -1686,7 +2155,7 @@ function renderPromptBanner() {
|
|
|
1686
2155
|
});
|
|
1687
2156
|
banner.appendChild(input);
|
|
1688
2157
|
} else if (pendingPrompt.type === "photos") {
|
|
1689
|
-
banner.append(h("div", { className: "ait-prompt-title" }, "
|
|
2158
|
+
banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.photos.title")));
|
|
1690
2159
|
const input = h("input", {
|
|
1691
2160
|
type: "file",
|
|
1692
2161
|
accept: "image/*",
|
|
@@ -1704,7 +2173,7 @@ function renderPromptBanner() {
|
|
|
1704
2173
|
});
|
|
1705
2174
|
banner.appendChild(input);
|
|
1706
2175
|
} else if (pendingPrompt.type === "location" || pendingPrompt.type === "location-update") {
|
|
1707
|
-
banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? "
|
|
2176
|
+
banner.append(h("div", { className: "ait-prompt-title" }, pendingPrompt.type === "location" ? t("device.prompt.location.title") : t("device.prompt.locationUpdate.title")));
|
|
1708
2177
|
const latInput = h("input", {
|
|
1709
2178
|
className: "ait-input",
|
|
1710
2179
|
value: String(aitState.state.location.coords.latitude),
|
|
@@ -1715,7 +2184,7 @@ function renderPromptBanner() {
|
|
|
1715
2184
|
value: String(aitState.state.location.coords.longitude),
|
|
1716
2185
|
style: "width:80px"
|
|
1717
2186
|
});
|
|
1718
|
-
const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, "
|
|
2187
|
+
const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("device.prompt.send"));
|
|
1719
2188
|
sendBtn.addEventListener("click", () => {
|
|
1720
2189
|
const loc = {
|
|
1721
2190
|
coords: {
|
|
@@ -1731,12 +2200,12 @@ function renderPromptBanner() {
|
|
|
1731
2200
|
};
|
|
1732
2201
|
resolvePrompt(pendingPrompt.type, loc);
|
|
1733
2202
|
});
|
|
1734
|
-
banner.append(h("div", { className: "ait-prompt-input-row" }, h("label", {}, "
|
|
1735
|
-
} else banner.append(h("div", { className: "ait-prompt-title" },
|
|
2203
|
+
banner.append(h("div", { className: "ait-prompt-input-row" }, h("label", {}, t("device.prompt.label.lat")), latInput, h("label", {}, t("device.prompt.label.lng")), lngInput, sendBtn));
|
|
2204
|
+
} else banner.append(h("div", { className: "ait-prompt-title" }, t("device.prompt.fallbackTitle", { type: pendingPrompt.type })));
|
|
1736
2205
|
const cancelBtn = h("button", {
|
|
1737
2206
|
className: "ait-btn ait-btn-sm ait-btn-danger",
|
|
1738
2207
|
style: "margin-top:8px"
|
|
1739
|
-
}, "
|
|
2208
|
+
}, t("device.prompt.cancel"));
|
|
1740
2209
|
cancelBtn.addEventListener("click", () => {
|
|
1741
2210
|
pendingPrompt = null;
|
|
1742
2211
|
window.dispatchEvent(new CustomEvent("__ait:prompt-cancel"));
|
|
@@ -1754,9 +2223,9 @@ function renderDeviceTab() {
|
|
|
1754
2223
|
const promptBanner = renderPromptBanner();
|
|
1755
2224
|
if (promptBanner) container.appendChild(promptBanner);
|
|
1756
2225
|
}
|
|
1757
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2226
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.modes")), ...[
|
|
1758
2227
|
{
|
|
1759
|
-
|
|
2228
|
+
labelKey: "device.row.camera",
|
|
1760
2229
|
key: "camera",
|
|
1761
2230
|
options: [
|
|
1762
2231
|
"mock",
|
|
@@ -1765,7 +2234,7 @@ function renderDeviceTab() {
|
|
|
1765
2234
|
]
|
|
1766
2235
|
},
|
|
1767
2236
|
{
|
|
1768
|
-
|
|
2237
|
+
labelKey: "device.row.photos",
|
|
1769
2238
|
key: "photos",
|
|
1770
2239
|
options: [
|
|
1771
2240
|
"mock",
|
|
@@ -1774,7 +2243,7 @@ function renderDeviceTab() {
|
|
|
1774
2243
|
]
|
|
1775
2244
|
},
|
|
1776
2245
|
{
|
|
1777
|
-
|
|
2246
|
+
labelKey: "device.row.location",
|
|
1778
2247
|
key: "location",
|
|
1779
2248
|
options: [
|
|
1780
2249
|
"mock",
|
|
@@ -1783,16 +2252,16 @@ function renderDeviceTab() {
|
|
|
1783
2252
|
]
|
|
1784
2253
|
},
|
|
1785
2254
|
{
|
|
1786
|
-
|
|
2255
|
+
labelKey: "device.row.network",
|
|
1787
2256
|
key: "network",
|
|
1788
2257
|
options: ["mock", "web"]
|
|
1789
2258
|
},
|
|
1790
2259
|
{
|
|
1791
|
-
|
|
2260
|
+
labelKey: "device.row.clipboard",
|
|
1792
2261
|
key: "clipboard",
|
|
1793
2262
|
options: ["mock", "web"]
|
|
1794
2263
|
}
|
|
1795
|
-
].map((entry) => selectRow(entry.
|
|
2264
|
+
].map((entry) => selectRow(t(entry.labelKey), entry.options, s.deviceModes[entry.key], (v) => {
|
|
1796
2265
|
aitState.patch("deviceModes", { [entry.key]: v });
|
|
1797
2266
|
}, disabled))));
|
|
1798
2267
|
const images = s.mockData.images;
|
|
@@ -1810,7 +2279,7 @@ function renderDeviceTab() {
|
|
|
1810
2279
|
thumb.append(img, removeBtn);
|
|
1811
2280
|
imageGrid.appendChild(thumb);
|
|
1812
2281
|
});
|
|
1813
|
-
const addBtn = h("button", { className: "ait-btn-secondary" }, "
|
|
2282
|
+
const addBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.add"));
|
|
1814
2283
|
addBtn.addEventListener("click", () => {
|
|
1815
2284
|
const input = document.createElement("input");
|
|
1816
2285
|
input.type = "file";
|
|
@@ -1829,17 +2298,17 @@ function renderDeviceTab() {
|
|
|
1829
2298
|
input.click();
|
|
1830
2299
|
});
|
|
1831
2300
|
if (disabled) addBtn.disabled = true;
|
|
1832
|
-
const defaultsBtn = h("button", { className: "ait-btn-secondary" }, "
|
|
2301
|
+
const defaultsBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.useDefaults"));
|
|
1833
2302
|
defaultsBtn.addEventListener("click", () => {
|
|
1834
2303
|
aitState.patch("mockData", { images: [...getDefaultPlaceholderImages()] });
|
|
1835
2304
|
});
|
|
1836
2305
|
if (disabled) defaultsBtn.disabled = true;
|
|
1837
|
-
const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, "
|
|
2306
|
+
const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, t("device.btn.clear"));
|
|
1838
2307
|
clearImagesBtn.addEventListener("click", () => {
|
|
1839
2308
|
aitState.patch("mockData", { images: [] });
|
|
1840
2309
|
});
|
|
1841
2310
|
if (disabled) clearImagesBtn.disabled = true;
|
|
1842
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" },
|
|
2311
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.mockImages", { count: images.length })), imageGrid, h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)));
|
|
1843
2312
|
return container;
|
|
1844
2313
|
}
|
|
1845
2314
|
//#endregion
|
|
@@ -1956,7 +2425,7 @@ function statusRow(label, value) {
|
|
|
1956
2425
|
}
|
|
1957
2426
|
function lastEventLine() {
|
|
1958
2427
|
const last = aitState.state.ads.lastEvent;
|
|
1959
|
-
if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, "
|
|
2428
|
+
if (!last) return h("div", { className: "ait-log-entry" }, h("span", { style: "color:#555" }, t("ads.empty.events")));
|
|
1960
2429
|
const time = new Date(last.timestamp).toLocaleTimeString();
|
|
1961
2430
|
return h("div", { className: "ait-log-entry" }, h("span", {
|
|
1962
2431
|
className: "ait-log-type",
|
|
@@ -1964,8 +2433,8 @@ function lastEventLine() {
|
|
|
1964
2433
|
}, last.type), h("span", { className: "ait-log-time" }, time));
|
|
1965
2434
|
}
|
|
1966
2435
|
function adSection(title, onLoad, onShow, disabled) {
|
|
1967
|
-
const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, "
|
|
1968
|
-
const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, "
|
|
2436
|
+
const loadBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.load"));
|
|
2437
|
+
const showBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.show"));
|
|
1969
2438
|
if (disabled) {
|
|
1970
2439
|
loadBtn.disabled = true;
|
|
1971
2440
|
showBtn.disabled = true;
|
|
@@ -1988,7 +2457,7 @@ function renderAdsTab() {
|
|
|
1988
2457
|
forceNoFillCb.addEventListener("change", () => {
|
|
1989
2458
|
aitState.patch("ads", { forceNoFill: forceNoFillCb.checked });
|
|
1990
2459
|
});
|
|
1991
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2460
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("ads.section.state")), statusRow(t("ads.row.isLoaded"), String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, t("ads.row.forceNoFill")), forceNoFillCb), lastEventLine()), adSection(t("ads.section.googleAdMob"), () => {
|
|
1992
2461
|
GoogleAdMob.loadAppsInTossAdMob({
|
|
1993
2462
|
onEvent: (e) => recordEvent(e.type),
|
|
1994
2463
|
onError: (err) => recordError(err.message)
|
|
@@ -1998,7 +2467,7 @@ function renderAdsTab() {
|
|
|
1998
2467
|
onEvent: (e) => recordEvent(e.type),
|
|
1999
2468
|
onError: (err) => recordError(err.message)
|
|
2000
2469
|
});
|
|
2001
|
-
}, disabled), adSection("
|
|
2470
|
+
}, disabled), adSection(t("ads.section.tossAds"), () => {
|
|
2002
2471
|
if (aitState.state.ads.forceNoFill) {
|
|
2003
2472
|
recordError("No fill");
|
|
2004
2473
|
return;
|
|
@@ -2015,7 +2484,7 @@ function renderAdsTab() {
|
|
|
2015
2484
|
recordEvent("dismissed");
|
|
2016
2485
|
aitState.patch("ads", { isLoaded: false });
|
|
2017
2486
|
}, 1500);
|
|
2018
|
-
}, disabled), adSection("
|
|
2487
|
+
}, disabled), adSection(t("ads.section.fullScreenAd"), () => {
|
|
2019
2488
|
loadFullScreenAd({
|
|
2020
2489
|
onEvent: (e) => recordEvent(e.type),
|
|
2021
2490
|
onError: (err) => recordError(err.message)
|
|
@@ -2035,13 +2504,13 @@ function renderAnalyticsTab() {
|
|
|
2035
2504
|
const container = h("div");
|
|
2036
2505
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2037
2506
|
const logs = aitState.state.analyticsLog;
|
|
2038
|
-
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "
|
|
2507
|
+
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("analytics.btn.clear"));
|
|
2039
2508
|
if (disabled) clearBtn.disabled = true;
|
|
2040
2509
|
clearBtn.addEventListener("click", () => {
|
|
2041
2510
|
aitState.update({ analyticsLog: [] });
|
|
2042
2511
|
});
|
|
2043
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" },
|
|
2044
|
-
return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString(
|
|
2512
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("analytics.section.log", { count: logs.length })), clearBtn), ...logs.slice(-30).reverse().map((entry) => {
|
|
2513
|
+
return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString()), h("span", { className: "ait-log-type" }, entry.type), JSON.stringify(entry.params));
|
|
2045
2514
|
})));
|
|
2046
2515
|
return container;
|
|
2047
2516
|
}
|
|
@@ -2052,7 +2521,7 @@ function renderEnvironmentTab() {
|
|
|
2052
2521
|
const disabled = !s.panelEditable;
|
|
2053
2522
|
const container = h("div");
|
|
2054
2523
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2055
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2524
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.platform")), selectRow(t("env.row.os"), ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled), inputRow(t("env.row.appVersion"), s.appVersion, (v) => aitState.update({ appVersion: v }), disabled), selectRow(t("env.row.environment"), ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled), inputRow(t("env.row.locale"), s.locale, (v) => aitState.update({ locale: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.network")), selectRow(t("env.row.networkStatus"), [
|
|
2056
2525
|
"WIFI",
|
|
2057
2526
|
"4G",
|
|
2058
2527
|
"5G",
|
|
@@ -2061,39 +2530,73 @@ function renderEnvironmentTab() {
|
|
|
2061
2530
|
"OFFLINE",
|
|
2062
2531
|
"WWAN",
|
|
2063
2532
|
"UNKNOWN"
|
|
2064
|
-
], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2533
|
+
], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.safeArea")), inputRow(t("env.row.safeArea.top"), String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled), inputRow(t("env.row.safeArea.bottom"), String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)), buildLanguageSection(), buildTelemetrySection());
|
|
2065
2534
|
return container;
|
|
2066
2535
|
}
|
|
2536
|
+
function buildLanguageSection() {
|
|
2537
|
+
const current = getLocale();
|
|
2538
|
+
const select = h("select", { className: "ait-select" });
|
|
2539
|
+
for (const opt of [{
|
|
2540
|
+
value: "ko",
|
|
2541
|
+
labelKey: "env.language.ko"
|
|
2542
|
+
}, {
|
|
2543
|
+
value: "en",
|
|
2544
|
+
labelKey: "env.language.en"
|
|
2545
|
+
}]) {
|
|
2546
|
+
const option = h("option", { value: opt.value }, t(opt.labelKey));
|
|
2547
|
+
if (opt.value === current) option.selected = true;
|
|
2548
|
+
select.appendChild(option);
|
|
2549
|
+
}
|
|
2550
|
+
select.addEventListener("change", () => {
|
|
2551
|
+
setLocale(select.value);
|
|
2552
|
+
});
|
|
2553
|
+
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.language")), h("div", { className: "ait-row" }, h("label", {}, t("env.language.row")), select));
|
|
2554
|
+
}
|
|
2067
2555
|
function buildTelemetrySection() {
|
|
2556
|
+
const t0Enabled = isTier0Enabled();
|
|
2557
|
+
const t0StatusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${t0Enabled ? "#4ade80" : "#888"}` }, t0Enabled ? t("env.telemetry.t0On") : t("env.telemetry.t0Off"));
|
|
2558
|
+
const t0ToggleBtn = h("button", {
|
|
2559
|
+
className: "ait-btn ait-btn-sm",
|
|
2560
|
+
style: "font-size:11px"
|
|
2561
|
+
}, t0Enabled ? t("env.telemetry.t0TurnOff") : t("env.telemetry.t0TurnOn"));
|
|
2562
|
+
t0ToggleBtn.addEventListener("click", () => {
|
|
2563
|
+
setTier0Enabled(!t0Enabled);
|
|
2564
|
+
window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
|
|
2565
|
+
});
|
|
2566
|
+
const t0Row = h("div", { className: "ait-row" }, h("label", {}, t("env.telemetry.t0Row")), h("span", { style: "display:flex;align-items:center;gap:8px" }, t0StatusLabel, t0ToggleBtn));
|
|
2567
|
+
const t0Desc = h("div", { style: "font-size:11px;color:#666;margin-bottom:6px" }, t("env.telemetry.t0Desc"));
|
|
2068
2568
|
const isGranted = readConsentState() === "granted";
|
|
2069
|
-
const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? "
|
|
2569
|
+
const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? t("env.telemetry.on") : t("env.telemetry.off"));
|
|
2070
2570
|
const toggleBtn = h("button", {
|
|
2071
2571
|
className: "ait-btn ait-btn-sm",
|
|
2072
2572
|
style: "font-size:11px"
|
|
2073
|
-
}, isGranted ? "
|
|
2573
|
+
}, isGranted ? t("env.telemetry.turnOff") : t("env.telemetry.turnOn"));
|
|
2074
2574
|
toggleBtn.addEventListener("click", () => {
|
|
2075
2575
|
setConsentViaToggle(!isGranted);
|
|
2076
2576
|
window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
|
|
2077
2577
|
});
|
|
2078
|
-
const statusRow = h("div", { className: "ait-row" }, h("label", {}, "
|
|
2079
|
-
const
|
|
2578
|
+
const statusRow = h("div", { className: "ait-row" }, h("label", {}, t("env.telemetry.row")), h("span", { style: "display:flex;align-items:center;gap:8px" }, statusLabel, toggleBtn));
|
|
2579
|
+
const rawAnonId = localStorage.getItem("__ait_telemetry:anon_id");
|
|
2580
|
+
const displayAnonId = rawAnonId ?? t("env.telemetry.anonIdNotSet");
|
|
2581
|
+
const truncatedId = displayAnonId.length > 8 ? `${displayAnonId.slice(0, 8)}…` : displayAnonId;
|
|
2080
2582
|
const anonIdEl = h("span", {
|
|
2081
2583
|
style: "font-family:'SF Mono','Menlo',monospace;font-size:11px;color:#95e6cb;cursor:pointer",
|
|
2082
|
-
title: "
|
|
2083
|
-
},
|
|
2584
|
+
title: t("env.telemetry.anonIdCopyTitle")
|
|
2585
|
+
}, t("env.telemetry.anonIdLabel", { value: truncatedId }));
|
|
2084
2586
|
anonIdEl.addEventListener("click", () => {
|
|
2085
|
-
|
|
2587
|
+
if (!rawAnonId) return;
|
|
2588
|
+
navigator.clipboard.writeText(rawAnonId).catch(() => {});
|
|
2086
2589
|
});
|
|
2087
|
-
const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "
|
|
2590
|
+
const deleteBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("env.telemetry.deleteBtn"));
|
|
2088
2591
|
const deleteStatus = h("span", { style: "font-size:11px;color:#aaa" });
|
|
2089
2592
|
deleteBtn.addEventListener("click", () => {
|
|
2090
2593
|
deleteBtn.disabled = true;
|
|
2091
|
-
deleteStatus.textContent = "
|
|
2594
|
+
deleteStatus.textContent = t("env.telemetry.deleting");
|
|
2092
2595
|
deleteMyData(TELEMETRY_ENDPOINT).then((ok) => {
|
|
2093
|
-
deleteStatus.textContent = ok ? "
|
|
2596
|
+
deleteStatus.textContent = ok ? t("env.telemetry.deleted") : t("env.telemetry.deleteFailedRetry");
|
|
2094
2597
|
deleteBtn.disabled = false;
|
|
2095
2598
|
}).catch(() => {
|
|
2096
|
-
deleteStatus.textContent = "
|
|
2599
|
+
deleteStatus.textContent = t("env.telemetry.deleteFailed");
|
|
2097
2600
|
deleteBtn.disabled = false;
|
|
2098
2601
|
});
|
|
2099
2602
|
});
|
|
@@ -2103,8 +2606,8 @@ function buildTelemetrySection() {
|
|
|
2103
2606
|
rel: "noopener noreferrer",
|
|
2104
2607
|
style: "font-size:11px;color:#666;text-decoration:none"
|
|
2105
2608
|
});
|
|
2106
|
-
privacyLink.textContent = "
|
|
2107
|
-
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2609
|
+
privacyLink.textContent = t("env.telemetry.privacyLink");
|
|
2610
|
+
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.telemetry.section")), t0Row, t0Desc, statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
|
|
2108
2611
|
className: "ait-btn-row",
|
|
2109
2612
|
style: "align-items:center;gap:8px;margin-top:6px"
|
|
2110
2613
|
}, deleteBtn, deleteStatus), h("div", { style: "margin-top:8px" }, privacyLink));
|
|
@@ -2115,15 +2618,15 @@ function renderEventsTab() {
|
|
|
2115
2618
|
const disabled = !aitState.state.panelEditable;
|
|
2116
2619
|
const container = h("div");
|
|
2117
2620
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2118
|
-
const backBtn = h("button", { className: "ait-btn" }, "
|
|
2621
|
+
const backBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerBack"));
|
|
2119
2622
|
backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
|
|
2120
2623
|
if (disabled) backBtn.disabled = true;
|
|
2121
|
-
const homeBtn = h("button", { className: "ait-btn" }, "
|
|
2624
|
+
const homeBtn = h("button", { className: "ait-btn" }, t("events.btn.triggerHome"));
|
|
2122
2625
|
homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
|
|
2123
2626
|
if (disabled) homeBtn.disabled = true;
|
|
2124
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2627
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("events.section.navigation")), h("div", { className: "ait-row" }, backBtn, homeBtn)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("events.section.login")), selectRow(t("events.row.loggedIn"), ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
|
|
2125
2628
|
aitState.patch("auth", { isLoggedIn: v === "true" });
|
|
2126
|
-
}, disabled), selectRow("
|
|
2629
|
+
}, disabled), selectRow(t("events.row.tossLoginIntegrated"), ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
|
|
2127
2630
|
aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
|
|
2128
2631
|
}, disabled)));
|
|
2129
2632
|
return container;
|
|
@@ -2267,23 +2770,23 @@ function renderIapTab() {
|
|
|
2267
2770
|
];
|
|
2268
2771
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2269
2772
|
const pendingOrders = s.iap.pendingOrders;
|
|
2270
|
-
const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" },
|
|
2271
|
-
if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, "
|
|
2773
|
+
const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
|
|
2774
|
+
if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
|
|
2272
2775
|
else for (const o of pendingOrders) {
|
|
2273
|
-
const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, "
|
|
2776
|
+
const completeBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("iap.btn.complete"));
|
|
2274
2777
|
if (disabled) completeBtn.disabled = true;
|
|
2275
2778
|
completeBtn.addEventListener("click", () => {
|
|
2276
2779
|
IAP.completeProductGrant({ params: { orderId: o.orderId } }).catch((err) => console.error("[@ait-co/devtools] completeProductGrant error:", err));
|
|
2277
2780
|
});
|
|
2278
|
-
pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, "
|
|
2781
|
+
pendingSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, t("iap.label.pending")), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.paymentCompletedDate)} `, completeBtn));
|
|
2279
2782
|
}
|
|
2280
2783
|
const completedOrders = s.iap.completedOrders;
|
|
2281
|
-
const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" },
|
|
2282
|
-
if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, "
|
|
2784
|
+
const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
|
|
2785
|
+
if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
|
|
2283
2786
|
else for (const o of completedOrders) completedSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.date)}`));
|
|
2284
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2787
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
|
|
2285
2788
|
aitState.patch("iap", { nextResult: v });
|
|
2286
|
-
}, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2789
|
+
}, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.tossPay")), selectRow(t("iap.row.tossPayResult"), ["success", "fail"], s.payment.nextResult, (v) => {
|
|
2287
2790
|
aitState.patch("payment", { nextResult: v });
|
|
2288
2791
|
}, disabled)), pendingSection, completedSection);
|
|
2289
2792
|
return container;
|
|
@@ -2295,19 +2798,19 @@ function renderLocationTab() {
|
|
|
2295
2798
|
const disabled = !s.panelEditable;
|
|
2296
2799
|
const container = h("div");
|
|
2297
2800
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2298
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2801
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("location.section.current")), inputRow(t("location.row.latitude"), String(s.location.coords.latitude), (v) => {
|
|
2299
2802
|
const coords = {
|
|
2300
2803
|
...s.location.coords,
|
|
2301
2804
|
latitude: Number(v)
|
|
2302
2805
|
};
|
|
2303
2806
|
aitState.patch("location", { coords });
|
|
2304
|
-
}, disabled), inputRow("
|
|
2807
|
+
}, disabled), inputRow(t("location.row.longitude"), String(s.location.coords.longitude), (v) => {
|
|
2305
2808
|
const coords = {
|
|
2306
2809
|
...s.location.coords,
|
|
2307
2810
|
longitude: Number(v)
|
|
2308
2811
|
};
|
|
2309
2812
|
aitState.patch("location", { coords });
|
|
2310
|
-
}, disabled), inputRow("
|
|
2813
|
+
}, disabled), inputRow(t("location.row.accuracy"), String(s.location.coords.accuracy), (v) => {
|
|
2311
2814
|
const coords = {
|
|
2312
2815
|
...s.location.coords,
|
|
2313
2816
|
accuracy: Number(v)
|
|
@@ -2321,15 +2824,15 @@ function renderLocationTab() {
|
|
|
2321
2824
|
const RESULTS = [
|
|
2322
2825
|
{
|
|
2323
2826
|
value: "newAgreement",
|
|
2324
|
-
|
|
2827
|
+
labelKey: "notifications.option.newAgreement"
|
|
2325
2828
|
},
|
|
2326
2829
|
{
|
|
2327
2830
|
value: "alreadyAgreed",
|
|
2328
|
-
|
|
2831
|
+
labelKey: "notifications.option.alreadyAgreed"
|
|
2329
2832
|
},
|
|
2330
2833
|
{
|
|
2331
2834
|
value: "agreementRejected",
|
|
2332
|
-
|
|
2835
|
+
labelKey: "notifications.option.agreementRejected"
|
|
2333
2836
|
}
|
|
2334
2837
|
];
|
|
2335
2838
|
function radioRow(name, current, option, disabled) {
|
|
@@ -2343,14 +2846,14 @@ function radioRow(name, current, option, disabled) {
|
|
|
2343
2846
|
input.addEventListener("change", () => {
|
|
2344
2847
|
if (input.checked) aitState.patch("notification", { nextResult: option.value });
|
|
2345
2848
|
});
|
|
2346
|
-
return h("label", { className: "ait-row" }, input, h("span", {}, option.
|
|
2849
|
+
return h("label", { className: "ait-row" }, input, h("span", {}, t(option.labelKey)));
|
|
2347
2850
|
}
|
|
2348
2851
|
function renderNotificationsTab() {
|
|
2349
2852
|
const s = aitState.state;
|
|
2350
2853
|
const disabled = !s.panelEditable;
|
|
2351
2854
|
const container = h("div");
|
|
2352
2855
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2353
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2856
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("notifications.section.title")), ...RESULTS.map((opt) => radioRow("ait-notification-result", s.notification.nextResult, opt, disabled))));
|
|
2354
2857
|
return container;
|
|
2355
2858
|
}
|
|
2356
2859
|
//#endregion
|
|
@@ -2373,7 +2876,7 @@ function renderPermissionsTab() {
|
|
|
2373
2876
|
"notDetermined"
|
|
2374
2877
|
];
|
|
2375
2878
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2376
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
2879
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("permissions.section.device")), ...names.map((name) => selectRow(name, statuses, s.permissions[name], (v) => {
|
|
2377
2880
|
aitState.patch("permissions", { [name]: v });
|
|
2378
2881
|
}, disabled))));
|
|
2379
2882
|
return container;
|
|
@@ -2661,12 +3164,12 @@ function renderPresetsTab(refreshPanel) {
|
|
|
2661
3164
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2662
3165
|
const userPresets = listUserPresets();
|
|
2663
3166
|
const snapshot = aitState.state;
|
|
2664
|
-
container.append(renderSection("
|
|
2665
|
-
container.append(renderSection(
|
|
2666
|
-
const saveBtn = h("button", { className: "ait-btn ait-btn-sm" }, "
|
|
3167
|
+
container.append(renderSection(t("presets.section.builtIn"), builtInPresets, disabled, snapshot, refreshPanel, false));
|
|
3168
|
+
container.append(renderSection(t("presets.section.saved", { count: userPresets.length }), userPresets, disabled, snapshot, refreshPanel, true));
|
|
3169
|
+
const saveBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("presets.btn.saveCurrent"));
|
|
2667
3170
|
if (disabled) saveBtn.disabled = true;
|
|
2668
3171
|
saveBtn.addEventListener("click", () => {
|
|
2669
|
-
const label = window.prompt("
|
|
3172
|
+
const label = window.prompt(t("presets.prompt.label"));
|
|
2670
3173
|
if (label === null) return;
|
|
2671
3174
|
try {
|
|
2672
3175
|
saveUserPreset(label, captureCurrentState(aitState.state));
|
|
@@ -2676,19 +3179,19 @@ function renderPresetsTab(refreshPanel) {
|
|
|
2676
3179
|
}
|
|
2677
3180
|
refreshPanel();
|
|
2678
3181
|
});
|
|
2679
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
3182
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("presets.section.save")), h("div", { style: "color:#888;font-size:11px;margin-bottom:6px" }, t("presets.save.description")), saveBtn));
|
|
2680
3183
|
return container;
|
|
2681
3184
|
}
|
|
2682
3185
|
function renderSection(title, presets, disabled, snapshot, refreshPanel, deletable) {
|
|
2683
3186
|
const section = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, title));
|
|
2684
3187
|
if (presets.length === 0) {
|
|
2685
|
-
section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? "
|
|
3188
|
+
section.append(h("div", { style: "color:#555;font-size:12px" }, deletable ? t("presets.empty.saved") : t("presets.empty.builtIn")));
|
|
2686
3189
|
return section;
|
|
2687
3190
|
}
|
|
2688
3191
|
for (const preset of presets) {
|
|
2689
3192
|
const isActive = matchesPreset(snapshot, preset.state);
|
|
2690
3193
|
const labelEl = h("span", { className: "ait-preset-label" }, isActive ? `✓ ${preset.label}` : preset.label);
|
|
2691
|
-
const applyBtn = h("button", { className: "ait-btn ait-btn-sm" }, isActive ? "
|
|
3194
|
+
const applyBtn = h("button", { className: "ait-btn ait-btn-sm" }, isActive ? t("presets.btn.reApply") : t("presets.btn.apply"));
|
|
2692
3195
|
if (disabled) applyBtn.disabled = true;
|
|
2693
3196
|
applyBtn.addEventListener("click", () => {
|
|
2694
3197
|
applyPreset(preset.state);
|
|
@@ -2696,10 +3199,10 @@ function renderSection(title, presets, disabled, snapshot, refreshPanel, deletab
|
|
|
2696
3199
|
});
|
|
2697
3200
|
const buttons = [applyBtn];
|
|
2698
3201
|
if (deletable) {
|
|
2699
|
-
const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "
|
|
3202
|
+
const delBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("presets.btn.delete"));
|
|
2700
3203
|
if (disabled) delBtn.disabled = true;
|
|
2701
3204
|
delBtn.addEventListener("click", () => {
|
|
2702
|
-
if (!window.confirm(
|
|
3205
|
+
if (!window.confirm(t("presets.confirm.delete", { label: preset.label }))) return;
|
|
2703
3206
|
deleteUserPreset(preset.id);
|
|
2704
3207
|
refreshPanel();
|
|
2705
3208
|
});
|
|
@@ -2724,13 +3227,13 @@ function renderStorageTab(refreshPanel) {
|
|
|
2724
3227
|
const key = localStorage.key(i);
|
|
2725
3228
|
if (key?.startsWith(prefix)) entries.push([key.slice(14), localStorage.getItem(key) ?? ""]);
|
|
2726
3229
|
}
|
|
2727
|
-
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "
|
|
3230
|
+
const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("storage.btn.clearAll"));
|
|
2728
3231
|
if (disabled) clearBtn.disabled = true;
|
|
2729
3232
|
clearBtn.addEventListener("click", () => {
|
|
2730
3233
|
for (const [key] of entries) localStorage.removeItem(prefix + key);
|
|
2731
3234
|
refreshPanel();
|
|
2732
3235
|
});
|
|
2733
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" },
|
|
3236
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("storage.section.title", { count: entries.length })), clearBtn), entries.length === 0 ? h("div", { style: "color:#555;font-size:12px" }, t("storage.empty")) : h("div", {}, ...entries.map(([key, value]) => h("div", { className: "ait-storage-row" }, h("span", { className: "ait-storage-key" }, key), h("span", { className: "ait-storage-value" }, value.length > 100 ? `${value.slice(0, 100)}...` : value))))));
|
|
2734
3237
|
return container;
|
|
2735
3238
|
}
|
|
2736
3239
|
//#endregion
|
|
@@ -3340,7 +3843,7 @@ function renderViewportTab() {
|
|
|
3340
3843
|
heightInput.value = String(clamped);
|
|
3341
3844
|
}
|
|
3342
3845
|
});
|
|
3343
|
-
customRow.append(h("div", { className: "ait-section-title" }, "
|
|
3846
|
+
customRow.append(h("div", { className: "ait-section-title" }, t("viewport.section.custom")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.width")), widthInput), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.height")), heightInput));
|
|
3344
3847
|
}
|
|
3345
3848
|
const frameCheckbox = h("input", { type: "checkbox" });
|
|
3346
3849
|
frameCheckbox.checked = vp.frame;
|
|
@@ -3366,7 +3869,7 @@ function renderViewportTab() {
|
|
|
3366
3869
|
});
|
|
3367
3870
|
const size = resolveViewportSize(vp);
|
|
3368
3871
|
const statusEl = h("div", { className: "ait-section" });
|
|
3369
|
-
if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, "
|
|
3872
|
+
if (vp.preset === "none" || size.width === 0) statusEl.appendChild(h("div", { style: "color:#888;font-size:11px" }, t("viewport.status.noConstraint")));
|
|
3370
3873
|
else {
|
|
3371
3874
|
const preset = vp.preset === "custom" ? null : getPreset(vp.preset);
|
|
3372
3875
|
const effOrient = effectiveOrientation(vp);
|
|
@@ -3375,75 +3878,84 @@ function renderViewportTab() {
|
|
|
3375
3878
|
const dpr = preset?.dpr ?? 1;
|
|
3376
3879
|
const physW = Math.round(size.width * dpr);
|
|
3377
3880
|
const physH = Math.round(size.height * dpr);
|
|
3378
|
-
const orientDisplay = vp.orientation === "auto" ?
|
|
3379
|
-
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "
|
|
3881
|
+
const orientDisplay = vp.orientation === "auto" ? t("viewport.orientation.autoSuffix", { orient: effOrient }) : effOrient;
|
|
3882
|
+
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.cssPhysical")), h("span", { className: "ait-status-value" }, `${size.width}×${size.height}@${dpr}x | ${physW}×${physH} ${orientDisplay}`)));
|
|
3380
3883
|
if (preset) {
|
|
3381
3884
|
const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide);
|
|
3382
|
-
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "
|
|
3885
|
+
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.safeArea")), h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`)));
|
|
3383
3886
|
}
|
|
3384
|
-
if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, "
|
|
3887
|
+
if (vp.aitNavBar && !landscape) rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.aitNavBar")), h("span", { className: "ait-status-value" }, t("viewport.status.aitNavBarValue", {
|
|
3888
|
+
height: 48,
|
|
3889
|
+
type: vp.aitNavBarType
|
|
3890
|
+
}))));
|
|
3385
3891
|
for (const row of rows) statusEl.appendChild(row);
|
|
3386
3892
|
}
|
|
3387
|
-
const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
3893
|
+
const deviceSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.device")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.preset")), presetSelect), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.orientation")), orientationSelect));
|
|
3388
3894
|
if (effectiveOrientation(vp) === "landscape" && vp.preset !== "none" && vp.preset !== "custom") {
|
|
3389
3895
|
const notch = getPreset(vp.preset).notch;
|
|
3390
|
-
if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, "
|
|
3896
|
+
if (notch === "notch" || notch === "dynamic-island") deviceSection.appendChild(h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.notchSide")), landscapeSideSelect));
|
|
3391
3897
|
}
|
|
3392
|
-
container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, "
|
|
3898
|
+
container.append(deviceSection, customRow, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("viewport.section.appearance")), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showFrame")), frameCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.showAitNavBar")), navBarCheckbox), h("div", { className: "ait-row" }, h("label", {}, t("viewport.row.navBarType")), navBarTypeSelect)), statusEl);
|
|
3393
3899
|
return container;
|
|
3394
3900
|
}
|
|
3395
3901
|
//#endregion
|
|
3396
3902
|
//#region src/panel/tabs/index.ts
|
|
3397
|
-
const
|
|
3903
|
+
const TAB_DEFS = [
|
|
3398
3904
|
{
|
|
3399
3905
|
id: "env",
|
|
3400
|
-
|
|
3906
|
+
labelKey: "panel.tab.env"
|
|
3401
3907
|
},
|
|
3402
3908
|
{
|
|
3403
3909
|
id: "presets",
|
|
3404
|
-
|
|
3910
|
+
labelKey: "panel.tab.presets"
|
|
3405
3911
|
},
|
|
3406
3912
|
{
|
|
3407
3913
|
id: "viewport",
|
|
3408
|
-
|
|
3914
|
+
labelKey: "panel.tab.viewport"
|
|
3409
3915
|
},
|
|
3410
3916
|
{
|
|
3411
3917
|
id: "permissions",
|
|
3412
|
-
|
|
3918
|
+
labelKey: "panel.tab.permissions"
|
|
3413
3919
|
},
|
|
3414
3920
|
{
|
|
3415
3921
|
id: "notifications",
|
|
3416
|
-
|
|
3922
|
+
labelKey: "panel.tab.notifications"
|
|
3417
3923
|
},
|
|
3418
3924
|
{
|
|
3419
3925
|
id: "location",
|
|
3420
|
-
|
|
3926
|
+
labelKey: "panel.tab.location"
|
|
3421
3927
|
},
|
|
3422
3928
|
{
|
|
3423
3929
|
id: "device",
|
|
3424
|
-
|
|
3930
|
+
labelKey: "panel.tab.device"
|
|
3425
3931
|
},
|
|
3426
3932
|
{
|
|
3427
3933
|
id: "iap",
|
|
3428
|
-
|
|
3934
|
+
labelKey: "panel.tab.iap"
|
|
3429
3935
|
},
|
|
3430
3936
|
{
|
|
3431
3937
|
id: "ads",
|
|
3432
|
-
|
|
3938
|
+
labelKey: "panel.tab.ads"
|
|
3433
3939
|
},
|
|
3434
3940
|
{
|
|
3435
3941
|
id: "events",
|
|
3436
|
-
|
|
3942
|
+
labelKey: "panel.tab.events"
|
|
3437
3943
|
},
|
|
3438
3944
|
{
|
|
3439
3945
|
id: "analytics",
|
|
3440
|
-
|
|
3946
|
+
labelKey: "panel.tab.analytics"
|
|
3441
3947
|
},
|
|
3442
3948
|
{
|
|
3443
3949
|
id: "storage",
|
|
3444
|
-
|
|
3950
|
+
labelKey: "panel.tab.storage"
|
|
3445
3951
|
}
|
|
3446
3952
|
];
|
|
3953
|
+
function getTabs() {
|
|
3954
|
+
return TAB_DEFS.map((def) => ({
|
|
3955
|
+
id: def.id,
|
|
3956
|
+
label: t(def.labelKey)
|
|
3957
|
+
}));
|
|
3958
|
+
}
|
|
3447
3959
|
function createTabRenderers(refreshPanel) {
|
|
3448
3960
|
return {
|
|
3449
3961
|
env: renderEnvironmentTab,
|
|
@@ -3605,6 +4117,7 @@ let injectedStyle = null;
|
|
|
3605
4117
|
let panelSwitchTabHandler = null;
|
|
3606
4118
|
let resizeHandler = null;
|
|
3607
4119
|
let aitStateUnsubscribe = null;
|
|
4120
|
+
let localeChangeHandler = null;
|
|
3608
4121
|
let tabRenderers = null;
|
|
3609
4122
|
function refreshPanel() {
|
|
3610
4123
|
if (!bodyEl || !tabsEl) return;
|
|
@@ -3614,7 +4127,7 @@ function refreshPanel() {
|
|
|
3614
4127
|
bodyEl.appendChild(tabRenderers[currentTab]());
|
|
3615
4128
|
} catch (err) {
|
|
3616
4129
|
console.error(`[@ait-co/devtools] Error rendering tab "${currentTab}":`, err);
|
|
3617
|
-
bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" },
|
|
4130
|
+
bodyEl.appendChild(h("div", { className: "ait-panel-tab-error" }, t("panel.tabError", { tab: currentTab })));
|
|
3618
4131
|
}
|
|
3619
4132
|
tabsEl.querySelectorAll(".ait-panel-tab").forEach((el) => {
|
|
3620
4133
|
el.classList.toggle("active", el.getAttribute("data-tab") === currentTab);
|
|
@@ -3630,14 +4143,14 @@ function mount() {
|
|
|
3630
4143
|
document.head.appendChild(injectedStyle);
|
|
3631
4144
|
const toggle = h("button", {
|
|
3632
4145
|
className: "ait-panel-toggle",
|
|
3633
|
-
title: "
|
|
4146
|
+
title: t("panel.toggle.title")
|
|
3634
4147
|
}, "AIT");
|
|
3635
4148
|
toggleEl = toggle;
|
|
3636
4149
|
restoreButtonPosition(toggle);
|
|
3637
4150
|
panelEl = h("div", { className: "ait-panel" });
|
|
3638
4151
|
const closeBtn = h("button", {
|
|
3639
4152
|
className: "ait-panel-close",
|
|
3640
|
-
title: "
|
|
4153
|
+
title: t("panel.close")
|
|
3641
4154
|
}, "×");
|
|
3642
4155
|
closeBtn.addEventListener("click", () => {
|
|
3643
4156
|
isOpen = false;
|
|
@@ -3646,18 +4159,18 @@ function mount() {
|
|
|
3646
4159
|
});
|
|
3647
4160
|
const mockBadge = h("span", {
|
|
3648
4161
|
className: `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`,
|
|
3649
|
-
title: "
|
|
3650
|
-
}, aitState.state.panelEditable ? "
|
|
4162
|
+
title: t("panel.editMode.toggleTitle")
|
|
4163
|
+
}, aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off"));
|
|
3651
4164
|
mockBadge.addEventListener("click", () => {
|
|
3652
4165
|
aitState.update({ panelEditable: !aitState.state.panelEditable });
|
|
3653
4166
|
mockBadge.className = `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`;
|
|
3654
|
-
mockBadge.textContent = aitState.state.panelEditable ? "
|
|
4167
|
+
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
3655
4168
|
refreshPanel();
|
|
3656
4169
|
});
|
|
3657
|
-
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.
|
|
3658
|
-
const header = h("div", { className: "ait-panel-header" }, h("span", {}, "
|
|
4170
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.20`), closeBtn);
|
|
4171
|
+
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
3659
4172
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
3660
|
-
for (const tab of
|
|
4173
|
+
for (const tab of getTabs()) {
|
|
3661
4174
|
const tabEl = h("button", {
|
|
3662
4175
|
className: "ait-panel-tab",
|
|
3663
4176
|
"data-tab": tab.id
|
|
@@ -3710,6 +4223,23 @@ function mount() {
|
|
|
3710
4223
|
refreshPanel();
|
|
3711
4224
|
};
|
|
3712
4225
|
window.addEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
|
|
4226
|
+
localeChangeHandler = () => {
|
|
4227
|
+
const savedTab = currentTab;
|
|
4228
|
+
const savedOpen = isOpen;
|
|
4229
|
+
disposePanel();
|
|
4230
|
+
try {
|
|
4231
|
+
mount();
|
|
4232
|
+
currentTab = savedTab;
|
|
4233
|
+
if (savedOpen && panelEl) {
|
|
4234
|
+
isOpen = true;
|
|
4235
|
+
panelEl.classList.add("open");
|
|
4236
|
+
}
|
|
4237
|
+
refreshPanel();
|
|
4238
|
+
} catch (err) {
|
|
4239
|
+
console.error("[@ait-co/devtools] Failed to re-mount after locale change:", err);
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
window.addEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
|
|
3713
4243
|
refreshPanel();
|
|
3714
4244
|
telemetry.init();
|
|
3715
4245
|
}
|
|
@@ -3725,6 +4255,7 @@ function disposePanel() {
|
|
|
3725
4255
|
if (typeof document === "undefined") return;
|
|
3726
4256
|
if (panelSwitchTabHandler && typeof window !== "undefined") window.removeEventListener("__ait:panel-switch-tab", panelSwitchTabHandler);
|
|
3727
4257
|
if (resizeHandler && typeof window !== "undefined") window.removeEventListener("resize", resizeHandler);
|
|
4258
|
+
if (localeChangeHandler && typeof window !== "undefined") window.removeEventListener(LOCALE_CHANGE_EVENT, localeChangeHandler);
|
|
3728
4259
|
if (aitStateUnsubscribe) aitStateUnsubscribe();
|
|
3729
4260
|
toggleEl?.remove();
|
|
3730
4261
|
panelEl?.remove();
|
|
@@ -3733,6 +4264,7 @@ function disposePanel() {
|
|
|
3733
4264
|
setDeviceRefreshPanel(() => {});
|
|
3734
4265
|
panelSwitchTabHandler = null;
|
|
3735
4266
|
resizeHandler = null;
|
|
4267
|
+
localeChangeHandler = null;
|
|
3736
4268
|
aitStateUnsubscribe = null;
|
|
3737
4269
|
toggleEl = null;
|
|
3738
4270
|
panelEl = null;
|