@ait-co/devtools 0.0.1 → 0.0.2

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.
@@ -1,15 +1,16 @@
1
1
  import {
2
- aitState
3
- } from "../chunk-YYIIG3JT.js";
2
+ aitState,
3
+ getDefaultPlaceholderImages
4
+ } from "../chunk-6PPZTREF.js";
4
5
 
5
6
  // src/panel/styles.ts
7
+ var PANEL_WIDTH = 360;
8
+ var PANEL_HEIGHT = 480;
6
9
  var PANEL_STYLES = (
7
10
  /* css */
8
11
  `
9
12
  .ait-panel-toggle {
10
13
  position: fixed;
11
- bottom: 16px;
12
- right: 16px;
13
14
  z-index: 99999;
14
15
  width: 48px;
15
16
  height: 48px;
@@ -25,18 +26,18 @@ var PANEL_STYLES = (
25
26
  color: white;
26
27
  transition: transform 0.15s;
27
28
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
29
+ touch-action: none;
30
+ user-select: none;
28
31
  }
29
- .ait-panel-toggle:hover {
32
+ .ait-panel-toggle:hover:not(.dragging) {
30
33
  transform: scale(1.1);
31
34
  }
32
35
 
33
36
  .ait-panel {
34
37
  position: fixed;
35
- bottom: 72px;
36
- right: 16px;
37
38
  z-index: 99998;
38
- width: 360px;
39
- max-height: 520px;
39
+ width: ${PANEL_WIDTH}px;
40
+ height: ${PANEL_HEIGHT}px;
40
41
  background: #1a1a2e;
41
42
  border-radius: 12px;
42
43
  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
@@ -61,7 +62,7 @@ var PANEL_STYLES = (
61
62
  align-items: center;
62
63
  border-bottom: 1px solid #2a2a4a;
63
64
  }
64
- .ait-panel-header span {
65
+ .ait-panel-header > span:first-child {
65
66
  color: #3182F6;
66
67
  }
67
68
 
@@ -98,8 +99,8 @@ var PANEL_STYLES = (
98
99
  .ait-panel-body {
99
100
  padding: 12px 16px;
100
101
  overflow-y: auto;
101
- max-height: 400px;
102
102
  flex: 1;
103
+ min-height: 0;
103
104
  }
104
105
 
105
106
  .ait-section {
@@ -205,6 +206,176 @@ var PANEL_STYLES = (
205
206
  flex: 1;
206
207
  word-break: break-all;
207
208
  }
209
+
210
+ /* Device tab */
211
+ .ait-image-grid {
212
+ display: flex;
213
+ flex-wrap: wrap;
214
+ gap: 8px;
215
+ margin-top: 8px;
216
+ }
217
+ .ait-image-thumb {
218
+ position: relative;
219
+ width: 64px;
220
+ height: 64px;
221
+ border-radius: 4px;
222
+ overflow: hidden;
223
+ border: 1px solid #3a3a5a;
224
+ }
225
+ .ait-image-thumb img {
226
+ width: 100%;
227
+ height: 100%;
228
+ object-fit: cover;
229
+ }
230
+ .ait-image-thumb .ait-image-remove {
231
+ position: absolute;
232
+ top: 2px;
233
+ right: 2px;
234
+ width: 18px;
235
+ height: 18px;
236
+ border-radius: 50%;
237
+ background: rgba(231,76,60,0.9);
238
+ color: white;
239
+ border: none;
240
+ cursor: pointer;
241
+ font-size: 10px;
242
+ line-height: 18px;
243
+ text-align: center;
244
+ padding: 0;
245
+ }
246
+ .ait-btn-row {
247
+ display: flex;
248
+ gap: 6px;
249
+ margin-top: 8px;
250
+ }
251
+ .ait-btn-secondary {
252
+ background: #2a2a4a;
253
+ color: #e0e0e0;
254
+ border: 1px solid #3a3a5a;
255
+ border-radius: 4px;
256
+ padding: 4px 8px;
257
+ font-size: 11px;
258
+ cursor: pointer;
259
+ font-family: inherit;
260
+ }
261
+ .ait-btn-secondary:hover {
262
+ background: #3a3a5a;
263
+ }
264
+
265
+ /* Prompt notification */
266
+ .ait-prompt-banner {
267
+ background: #2d1b69;
268
+ border: 1px solid #6c3bd5;
269
+ border-radius: 6px;
270
+ padding: 10px 12px;
271
+ margin-bottom: 12px;
272
+ }
273
+ .ait-prompt-banner .ait-prompt-title {
274
+ color: #b388ff;
275
+ font-size: 12px;
276
+ font-weight: 600;
277
+ margin-bottom: 8px;
278
+ }
279
+ .ait-prompt-input-row {
280
+ display: flex;
281
+ gap: 6px;
282
+ align-items: center;
283
+ margin-top: 6px;
284
+ }
285
+ .ait-prompt-input-row input {
286
+ background: #2a2a4a;
287
+ color: #e0e0e0;
288
+ border: 1px solid #3a3a5a;
289
+ border-radius: 4px;
290
+ padding: 4px 8px;
291
+ font-size: 12px;
292
+ width: 80px;
293
+ font-family: inherit;
294
+ }
295
+ .ait-prompt-input-row label {
296
+ color: #aaa;
297
+ font-size: 11px;
298
+ min-width: 30px;
299
+ }
300
+
301
+ .ait-panel-close {
302
+ display: none;
303
+ background: none;
304
+ border: none;
305
+ color: #888;
306
+ font-size: 18px;
307
+ cursor: pointer;
308
+ padding: 0 4px;
309
+ font-family: inherit;
310
+ }
311
+ .ait-panel-close:hover {
312
+ color: #e0e0e0;
313
+ }
314
+
315
+ /* Disabled state for monitoring-only mode */
316
+ .ait-select:disabled,
317
+ .ait-input:disabled {
318
+ opacity: 0.5;
319
+ cursor: not-allowed;
320
+ }
321
+ .ait-btn:disabled,
322
+ .ait-btn-secondary:disabled {
323
+ opacity: 0.5;
324
+ cursor: not-allowed;
325
+ }
326
+ .ait-btn-danger:disabled {
327
+ background: #5a5a5a;
328
+ }
329
+
330
+ /* Mock status badge */
331
+ .ait-mock-badge {
332
+ display: inline-block;
333
+ padding: 2px 8px;
334
+ border-radius: 10px;
335
+ font-size: 10px;
336
+ font-weight: 600;
337
+ letter-spacing: 0.3px;
338
+ cursor: pointer;
339
+ }
340
+ .ait-mock-badge-on {
341
+ background: #1a4731;
342
+ color: #4ade80;
343
+ }
344
+ .ait-mock-badge-off {
345
+ background: #4a1a1a;
346
+ color: #f87171;
347
+ }
348
+
349
+ /* Monitoring-only notice */
350
+ .ait-monitoring-notice {
351
+ background: #2a1a00;
352
+ border: 1px solid #6b4c00;
353
+ border-radius: 4px;
354
+ padding: 6px 10px;
355
+ margin-bottom: 12px;
356
+ font-size: 11px;
357
+ color: #fbbf24;
358
+ }
359
+
360
+ @media (max-width: 480px) {
361
+ .ait-panel.open {
362
+ position: fixed;
363
+ top: 0;
364
+ left: 0;
365
+ right: 0;
366
+ bottom: 0;
367
+ width: 100%;
368
+ height: 100%;
369
+ max-height: 100%;
370
+ border-radius: 0;
371
+ }
372
+ .ait-panel-toggle {
373
+ z-index: 100000;
374
+ }
375
+ .ait-panel-close {
376
+ display: block;
377
+ }
378
+ }
208
379
  `
209
380
  );
210
381
 
@@ -213,6 +384,7 @@ var TABS = [
213
384
  { id: "env", label: "Environment" },
214
385
  { id: "permissions", label: "Permissions" },
215
386
  { id: "location", label: "Location" },
387
+ { id: "device", label: "Device" },
216
388
  { id: "iap", label: "IAP" },
217
389
  { id: "events", label: "Events" },
218
390
  { id: "analytics", label: "Analytics" },
@@ -231,8 +403,9 @@ function h(tag, attrs, ...children) {
231
403
  }
232
404
  return el;
233
405
  }
234
- function selectRow(label, options, value, onChange) {
406
+ function selectRow(label, options, value, onChange, disabled = false) {
235
407
  const select = h("select", { className: "ait-select" });
408
+ if (disabled) select.disabled = true;
236
409
  for (const opt of options) {
237
410
  const option = h("option", { value: opt }, opt);
238
411
  if (opt === value) option.selected = true;
@@ -241,45 +414,50 @@ function selectRow(label, options, value, onChange) {
241
414
  select.addEventListener("change", () => onChange(select.value));
242
415
  return h("div", { className: "ait-row" }, h("label", {}, label), select);
243
416
  }
244
- function inputRow(label, value, onChange) {
417
+ function inputRow(label, value, onChange, disabled = false) {
245
418
  const input = h("input", { className: "ait-input", value });
419
+ if (disabled) input.disabled = true;
246
420
  input.addEventListener("change", () => onChange(input.value));
247
421
  return h("div", { className: "ait-row" }, h("label", {}, label), input);
248
422
  }
249
423
  function renderEnvTab() {
250
424
  const s = aitState.state;
425
+ const disabled = !s.panelEditable;
251
426
  const container = h("div");
427
+ if (disabled) container.appendChild(monitoringNotice());
252
428
  container.append(
253
429
  h(
254
430
  "div",
255
431
  { className: "ait-section" },
256
432
  h("div", { className: "ait-section-title" }, "Platform"),
257
- selectRow("OS", ["ios", "android"], s.platform, (v) => aitState.update({ platform: v })),
258
- inputRow("App Version", s.appVersion, (v) => aitState.update({ appVersion: v })),
259
- selectRow("Environment", ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v })),
260
- inputRow("Locale", s.locale, (v) => aitState.update({ locale: v }))
433
+ selectRow("OS", ["ios", "android"], s.platform, (v) => aitState.update({ platform: v }), disabled),
434
+ inputRow("App Version", s.appVersion, (v) => aitState.update({ appVersion: v }), disabled),
435
+ selectRow("Environment", ["toss", "sandbox"], s.environment, (v) => aitState.update({ environment: v }), disabled),
436
+ inputRow("Locale", s.locale, (v) => aitState.update({ locale: v }), disabled)
261
437
  ),
262
438
  h(
263
439
  "div",
264
440
  { className: "ait-section" },
265
441
  h("div", { className: "ait-section-title" }, "Network"),
266
- selectRow("Status", ["WIFI", "4G", "5G", "3G", "2G", "OFFLINE", "WWAN", "UNKNOWN"], s.networkStatus, (v) => aitState.update({ networkStatus: v }))
442
+ selectRow("Status", ["WIFI", "4G", "5G", "3G", "2G", "OFFLINE", "WWAN", "UNKNOWN"], s.networkStatus, (v) => aitState.update({ networkStatus: v }), disabled)
267
443
  ),
268
444
  h(
269
445
  "div",
270
446
  { className: "ait-section" },
271
447
  h("div", { className: "ait-section-title" }, "Safe Area Insets"),
272
- inputRow("Top", String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) })),
273
- inputRow("Bottom", String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }))
448
+ inputRow("Top", String(s.safeAreaInsets.top), (v) => aitState.patch("safeAreaInsets", { top: Number(v) }), disabled),
449
+ inputRow("Bottom", String(s.safeAreaInsets.bottom), (v) => aitState.patch("safeAreaInsets", { bottom: Number(v) }), disabled)
274
450
  )
275
451
  );
276
452
  return container;
277
453
  }
278
454
  function renderPermissionsTab() {
279
455
  const s = aitState.state;
456
+ const disabled = !s.panelEditable;
280
457
  const container = h("div");
281
458
  const names = ["camera", "photos", "geolocation", "clipboard", "contacts", "microphone"];
282
459
  const statuses = ["allowed", "denied", "notDetermined"];
460
+ if (disabled) container.appendChild(monitoringNotice());
283
461
  container.append(
284
462
  h(
285
463
  "div",
@@ -288,7 +466,7 @@ function renderPermissionsTab() {
288
466
  ...names.map(
289
467
  (name) => selectRow(name, statuses, s.permissions[name], (v) => {
290
468
  aitState.patch("permissions", { [name]: v });
291
- })
469
+ }, disabled)
292
470
  )
293
471
  )
294
472
  );
@@ -296,7 +474,9 @@ function renderPermissionsTab() {
296
474
  }
297
475
  function renderLocationTab() {
298
476
  const s = aitState.state;
477
+ const disabled = !s.panelEditable;
299
478
  const container = h("div");
479
+ if (disabled) container.appendChild(monitoringNotice());
300
480
  container.append(
301
481
  h(
302
482
  "div",
@@ -305,23 +485,25 @@ function renderLocationTab() {
305
485
  inputRow("Latitude", String(s.location.coords.latitude), (v) => {
306
486
  const coords = { ...s.location.coords, latitude: Number(v) };
307
487
  aitState.patch("location", { coords });
308
- }),
488
+ }, disabled),
309
489
  inputRow("Longitude", String(s.location.coords.longitude), (v) => {
310
490
  const coords = { ...s.location.coords, longitude: Number(v) };
311
491
  aitState.patch("location", { coords });
312
- }),
492
+ }, disabled),
313
493
  inputRow("Accuracy", String(s.location.coords.accuracy), (v) => {
314
494
  const coords = { ...s.location.coords, accuracy: Number(v) };
315
495
  aitState.patch("location", { coords });
316
- })
496
+ }, disabled)
317
497
  )
318
498
  );
319
499
  return container;
320
500
  }
321
501
  function renderIapTab() {
322
502
  const s = aitState.state;
503
+ const disabled = !s.panelEditable;
323
504
  const container = h("div");
324
505
  const results = ["success", "USER_CANCELED", "INVALID_PRODUCT_ID", "PAYMENT_PENDING", "NETWORK_ERROR", "ITEM_ALREADY_OWNED", "INTERNAL_ERROR"];
506
+ if (disabled) container.appendChild(monitoringNotice());
325
507
  container.append(
326
508
  h(
327
509
  "div",
@@ -329,7 +511,7 @@ function renderIapTab() {
329
511
  h("div", { className: "ait-section-title" }, "IAP Simulator"),
330
512
  selectRow("Next Purchase Result", results, s.iap.nextResult, (v) => {
331
513
  aitState.patch("iap", { nextResult: v });
332
- })
514
+ }, disabled)
333
515
  ),
334
516
  h(
335
517
  "div",
@@ -337,7 +519,7 @@ function renderIapTab() {
337
519
  h("div", { className: "ait-section-title" }, "TossPay"),
338
520
  selectRow("Next Payment Result", ["success", "fail"], s.payment.nextResult, (v) => {
339
521
  aitState.patch("payment", { nextResult: v });
340
- })
522
+ }, disabled)
341
523
  ),
342
524
  h(
343
525
  "div",
@@ -356,11 +538,15 @@ function renderIapTab() {
356
538
  return container;
357
539
  }
358
540
  function renderEventsTab() {
541
+ const disabled = !aitState.state.panelEditable;
359
542
  const container = h("div");
543
+ if (disabled) container.appendChild(monitoringNotice());
360
544
  const backBtn = h("button", { className: "ait-btn" }, "Trigger Back Event");
361
545
  backBtn.addEventListener("click", () => aitState.trigger("backEvent"));
546
+ if (disabled) backBtn.disabled = true;
362
547
  const homeBtn = h("button", { className: "ait-btn" }, "Trigger Home Event");
363
548
  homeBtn.addEventListener("click", () => aitState.trigger("homeEvent"));
549
+ if (disabled) homeBtn.disabled = true;
364
550
  container.append(
365
551
  h(
366
552
  "div",
@@ -374,18 +560,21 @@ function renderEventsTab() {
374
560
  h("div", { className: "ait-section-title" }, "Login"),
375
561
  selectRow("Logged In", ["true", "false"], String(aitState.state.auth.isLoggedIn), (v) => {
376
562
  aitState.patch("auth", { isLoggedIn: v === "true" });
377
- }),
563
+ }, disabled),
378
564
  selectRow("Toss Login Integrated", ["true", "false"], String(aitState.state.auth.isTossLoginIntegrated), (v) => {
379
565
  aitState.patch("auth", { isTossLoginIntegrated: v === "true" });
380
- })
566
+ }, disabled)
381
567
  )
382
568
  );
383
569
  return container;
384
570
  }
385
571
  function renderAnalyticsTab() {
572
+ const disabled = !aitState.state.panelEditable;
386
573
  const container = h("div");
574
+ if (disabled) container.appendChild(monitoringNotice());
387
575
  const logs = aitState.state.analyticsLog;
388
576
  const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear");
577
+ if (disabled) clearBtn.disabled = true;
389
578
  clearBtn.addEventListener("click", () => {
390
579
  aitState.state.analyticsLog.length = 0;
391
580
  refreshPanel();
@@ -415,7 +604,9 @@ function renderAnalyticsTab() {
415
604
  return container;
416
605
  }
417
606
  function renderStorageTab() {
607
+ const disabled = !aitState.state.panelEditable;
418
608
  const container = h("div");
609
+ if (disabled) container.appendChild(monitoringNotice());
419
610
  const prefix = "__ait_storage:";
420
611
  const entries = [];
421
612
  for (let i = 0; i < localStorage.length; i++) {
@@ -425,6 +616,7 @@ function renderStorageTab() {
425
616
  }
426
617
  }
427
618
  const clearBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, "Clear All");
619
+ if (disabled) clearBtn.disabled = true;
428
620
  clearBtn.addEventListener("click", () => {
429
621
  entries.forEach(([key]) => localStorage.removeItem(prefix + key));
430
622
  refreshPanel();
@@ -455,15 +647,341 @@ function renderStorageTab() {
455
647
  );
456
648
  return container;
457
649
  }
650
+ var pendingPrompt = null;
651
+ if (typeof window !== "undefined") {
652
+ window.addEventListener("__ait:prompt-request", (e) => {
653
+ const detail = e.detail;
654
+ pendingPrompt = { type: detail.type };
655
+ currentTab = "device";
656
+ if (panelEl && !panelEl.classList.contains("open")) {
657
+ panelEl.classList.add("open");
658
+ }
659
+ refreshPanel();
660
+ });
661
+ }
662
+ function resolvePrompt(type, data) {
663
+ window.dispatchEvent(new CustomEvent("__ait:prompt-response:" + type, { detail: data }));
664
+ pendingPrompt = null;
665
+ refreshPanel();
666
+ }
667
+ function renderPromptBanner() {
668
+ if (!pendingPrompt) return null;
669
+ const banner = h("div", { className: "ait-prompt-banner" });
670
+ if (pendingPrompt.type === "camera") {
671
+ banner.append(
672
+ h("div", { className: "ait-prompt-title" }, "Camera Prompt \u2014 Select an image")
673
+ );
674
+ const input = h("input", { type: "file", accept: "image/*", style: "font-size:11px;color:#aaa" });
675
+ input.addEventListener("change", () => {
676
+ const file = input.files?.[0];
677
+ if (!file) return;
678
+ const reader = new FileReader();
679
+ reader.onload = () => resolvePrompt("camera", reader.result);
680
+ reader.readAsDataURL(file);
681
+ });
682
+ banner.appendChild(input);
683
+ } else if (pendingPrompt.type === "photos") {
684
+ banner.append(
685
+ h("div", { className: "ait-prompt-title" }, "Photos Prompt \u2014 Select images")
686
+ );
687
+ const input = h("input", { type: "file", accept: "image/*", multiple: "", style: "font-size:11px;color:#aaa" });
688
+ input.addEventListener("change", () => {
689
+ const files = Array.from(input.files ?? []);
690
+ if (files.length === 0) return;
691
+ Promise.all(files.map((file) => new Promise((res) => {
692
+ const reader = new FileReader();
693
+ reader.onload = () => res(reader.result);
694
+ reader.readAsDataURL(file);
695
+ }))).then((dataUris) => resolvePrompt("photos", dataUris));
696
+ });
697
+ banner.appendChild(input);
698
+ } else if (pendingPrompt.type === "location" || pendingPrompt.type === "location-update") {
699
+ banner.append(
700
+ h(
701
+ "div",
702
+ { className: "ait-prompt-title" },
703
+ pendingPrompt.type === "location" ? "Location Prompt \u2014 Enter coordinates" : "Location Update \u2014 Send coordinates"
704
+ )
705
+ );
706
+ const latInput = h("input", { className: "ait-input", value: String(aitState.state.location.coords.latitude), style: "width:80px" });
707
+ const lngInput = h("input", { className: "ait-input", value: String(aitState.state.location.coords.longitude), style: "width:80px" });
708
+ const sendBtn = h("button", { className: "ait-btn ait-btn-sm" }, "Send");
709
+ sendBtn.addEventListener("click", () => {
710
+ const loc = {
711
+ coords: {
712
+ latitude: Number(latInput.value),
713
+ longitude: Number(lngInput.value),
714
+ altitude: 0,
715
+ accuracy: 10,
716
+ altitudeAccuracy: 0,
717
+ heading: 0
718
+ },
719
+ timestamp: Date.now(),
720
+ accessLocation: "FINE"
721
+ };
722
+ resolvePrompt(pendingPrompt.type, loc);
723
+ });
724
+ banner.append(
725
+ h(
726
+ "div",
727
+ { className: "ait-prompt-input-row" },
728
+ h("label", {}, "Lat"),
729
+ latInput,
730
+ h("label", {}, "Lng"),
731
+ lngInput,
732
+ sendBtn
733
+ )
734
+ );
735
+ } else {
736
+ banner.append(
737
+ h("div", { className: "ait-prompt-title" }, `Prompt: ${pendingPrompt.type}`)
738
+ );
739
+ }
740
+ const cancelBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger", style: "margin-top:8px" }, "Cancel");
741
+ cancelBtn.addEventListener("click", () => {
742
+ pendingPrompt = null;
743
+ window.dispatchEvent(new CustomEvent("__ait:prompt-cancel"));
744
+ refreshPanel();
745
+ });
746
+ banner.appendChild(cancelBtn);
747
+ return banner;
748
+ }
749
+ function renderDeviceTab() {
750
+ const s = aitState.state;
751
+ const disabled = !s.panelEditable;
752
+ const container = h("div");
753
+ if (disabled) container.appendChild(monitoringNotice());
754
+ if (s.panelEditable) {
755
+ const promptBanner = renderPromptBanner();
756
+ if (promptBanner) container.appendChild(promptBanner);
757
+ }
758
+ const modeEntries = [
759
+ { label: "Camera", key: "camera", options: ["mock", "web", "prompt"] },
760
+ { label: "Photos", key: "photos", options: ["mock", "web", "prompt"] },
761
+ { label: "Location", key: "location", options: ["mock", "web", "prompt"] },
762
+ { label: "Network", key: "network", options: ["mock", "web"] },
763
+ { label: "Clipboard", key: "clipboard", options: ["mock", "web"] }
764
+ ];
765
+ container.append(
766
+ h(
767
+ "div",
768
+ { className: "ait-section" },
769
+ h("div", { className: "ait-section-title" }, "Device API Modes"),
770
+ ...modeEntries.map(
771
+ (entry) => selectRow(entry.label, entry.options, s.deviceModes[entry.key], (v) => {
772
+ aitState.patch("deviceModes", { [entry.key]: v });
773
+ refreshPanel();
774
+ }, disabled)
775
+ )
776
+ )
777
+ );
778
+ const images = s.mockData.images;
779
+ const imageGrid = h("div", { className: "ait-image-grid" });
780
+ images.forEach((dataUri, idx) => {
781
+ const thumb = h("div", { className: "ait-image-thumb" });
782
+ const img = h("img", { src: dataUri });
783
+ const removeBtn = h("button", { className: "ait-image-remove" }, "x");
784
+ removeBtn.addEventListener("click", () => {
785
+ const newImages = [...aitState.state.mockData.images];
786
+ newImages.splice(idx, 1);
787
+ aitState.patch("mockData", { images: newImages });
788
+ refreshPanel();
789
+ });
790
+ if (disabled) removeBtn.disabled = true;
791
+ thumb.append(img, removeBtn);
792
+ imageGrid.appendChild(thumb);
793
+ });
794
+ const addBtn = h("button", { className: "ait-btn-secondary" }, "+ Add");
795
+ addBtn.addEventListener("click", () => {
796
+ const input = document.createElement("input");
797
+ input.type = "file";
798
+ input.accept = "image/*";
799
+ input.multiple = true;
800
+ input.onchange = () => {
801
+ const files = Array.from(input.files ?? []);
802
+ Promise.all(files.map((file) => new Promise((res) => {
803
+ const reader = new FileReader();
804
+ reader.onload = () => res(reader.result);
805
+ reader.readAsDataURL(file);
806
+ }))).then((dataUris) => {
807
+ aitState.patch("mockData", { images: [...aitState.state.mockData.images, ...dataUris] });
808
+ refreshPanel();
809
+ });
810
+ };
811
+ input.click();
812
+ });
813
+ if (disabled) addBtn.disabled = true;
814
+ const defaultsBtn = h("button", { className: "ait-btn-secondary" }, "Use defaults");
815
+ defaultsBtn.addEventListener("click", () => {
816
+ aitState.patch("mockData", { images: [...getDefaultPlaceholderImages()] });
817
+ refreshPanel();
818
+ });
819
+ if (disabled) defaultsBtn.disabled = true;
820
+ const clearImagesBtn = h("button", { className: "ait-btn-secondary" }, "Clear");
821
+ clearImagesBtn.addEventListener("click", () => {
822
+ aitState.patch("mockData", { images: [] });
823
+ refreshPanel();
824
+ });
825
+ if (disabled) clearImagesBtn.disabled = true;
826
+ container.append(
827
+ h(
828
+ "div",
829
+ { className: "ait-section" },
830
+ h("div", { className: "ait-section-title" }, `Mock Images (${images.length})`),
831
+ imageGrid,
832
+ h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)
833
+ )
834
+ );
835
+ return container;
836
+ }
837
+ function monitoringNotice() {
838
+ return h(
839
+ "div",
840
+ { className: "ait-monitoring-notice" },
841
+ "Read-only \u2014 mock responses are controlled at build time."
842
+ );
843
+ }
458
844
  var TAB_RENDERERS = {
459
845
  env: renderEnvTab,
460
846
  permissions: renderPermissionsTab,
461
847
  location: renderLocationTab,
848
+ device: renderDeviceTab,
462
849
  iap: renderIapTab,
463
850
  events: renderEventsTab,
464
851
  analytics: renderAnalyticsTab,
465
852
  storage: renderStorageTab
466
853
  };
854
+ function makeDraggable(el, onClickOnly) {
855
+ let isDragging = false;
856
+ let startX = 0, startY = 0;
857
+ let startLeft = 0, startTop = 0;
858
+ let hasMoved = false;
859
+ el.addEventListener("pointerdown", (e) => {
860
+ isDragging = true;
861
+ hasMoved = false;
862
+ startX = e.clientX;
863
+ startY = e.clientY;
864
+ const rect = el.getBoundingClientRect();
865
+ startLeft = rect.left;
866
+ startTop = rect.top;
867
+ el.setPointerCapture(e.pointerId);
868
+ e.preventDefault();
869
+ });
870
+ el.addEventListener("pointermove", (e) => {
871
+ if (!isDragging) return;
872
+ const dx = e.clientX - startX;
873
+ const dy = e.clientY - startY;
874
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
875
+ hasMoved = true;
876
+ el.classList.add("dragging");
877
+ }
878
+ if (!hasMoved) return;
879
+ el.style.left = startLeft + dx + "px";
880
+ el.style.top = startTop + dy + "px";
881
+ el.style.right = "auto";
882
+ el.style.bottom = "auto";
883
+ });
884
+ el.addEventListener("pointerup", (e) => {
885
+ if (!isDragging) return;
886
+ isDragging = false;
887
+ el.classList.remove("dragging");
888
+ el.releasePointerCapture(e.pointerId);
889
+ if (hasMoved) {
890
+ snapToEdge(el);
891
+ updatePanelPosition(el);
892
+ saveButtonPosition(el);
893
+ } else {
894
+ onClickOnly();
895
+ }
896
+ });
897
+ el.addEventListener("pointercancel", (e) => {
898
+ isDragging = false;
899
+ el.classList.remove("dragging");
900
+ el.releasePointerCapture(e.pointerId);
901
+ if (hasMoved) {
902
+ snapToEdge(el);
903
+ updatePanelPosition(el);
904
+ saveButtonPosition(el);
905
+ }
906
+ });
907
+ }
908
+ function snapToEdge(el) {
909
+ const rect = el.getBoundingClientRect();
910
+ const vw = window.innerWidth;
911
+ const vh = window.innerHeight;
912
+ const cx = rect.left + rect.width / 2;
913
+ const margin = 16;
914
+ if (cx < vw / 2) {
915
+ el.style.left = margin + "px";
916
+ el.style.right = "auto";
917
+ } else {
918
+ el.style.left = "auto";
919
+ el.style.right = margin + "px";
920
+ }
921
+ const top = Math.max(margin, Math.min(vh - rect.height - margin, rect.top));
922
+ el.style.top = top + "px";
923
+ el.style.bottom = "auto";
924
+ }
925
+ function updatePanelPosition(toggleEl) {
926
+ if (!panelEl) return;
927
+ const vw = window.innerWidth;
928
+ const vh = window.innerHeight;
929
+ if (vw <= 480) {
930
+ panelEl.style.top = "";
931
+ panelEl.style.left = "";
932
+ panelEl.style.right = "";
933
+ panelEl.style.bottom = "";
934
+ return;
935
+ }
936
+ const rect = toggleEl.getBoundingClientRect();
937
+ const panelWidth = PANEL_WIDTH;
938
+ const panelHeight = PANEL_HEIGHT;
939
+ const margin = 16;
940
+ if (rect.left < vw / 2) {
941
+ panelEl.style.left = margin + "px";
942
+ panelEl.style.right = "auto";
943
+ } else {
944
+ panelEl.style.left = "auto";
945
+ panelEl.style.right = margin + "px";
946
+ }
947
+ if (rect.top < vh / 2) {
948
+ const top = Math.min(rect.bottom + 8, vh - panelHeight - margin);
949
+ panelEl.style.top = Math.max(margin, top) + "px";
950
+ panelEl.style.bottom = "auto";
951
+ } else {
952
+ const bottom = Math.min(vh - rect.top + 8, vh - panelHeight - margin);
953
+ panelEl.style.top = "auto";
954
+ panelEl.style.bottom = Math.max(margin, bottom) + "px";
955
+ }
956
+ }
957
+ function saveButtonPosition(el) {
958
+ localStorage.setItem("__ait_btn_pos", JSON.stringify({
959
+ left: el.style.left,
960
+ top: el.style.top,
961
+ right: el.style.right,
962
+ bottom: el.style.bottom
963
+ }));
964
+ }
965
+ function restoreButtonPosition(el) {
966
+ const saved = localStorage.getItem("__ait_btn_pos");
967
+ if (saved) {
968
+ try {
969
+ const pos = JSON.parse(saved);
970
+ if (typeof pos !== "object" || pos === null) return;
971
+ const allowedKeys = ["left", "top", "right", "bottom"];
972
+ const validCssValue = /^(\d+px|auto)$/;
973
+ for (const key of allowedKeys) {
974
+ if (key in pos && typeof pos[key] === "string" && validCssValue.test(pos[key])) {
975
+ el.style[key] = pos[key];
976
+ }
977
+ }
978
+ } catch {
979
+ }
980
+ } else {
981
+ el.style.bottom = "16px";
982
+ el.style.right = "16px";
983
+ }
984
+ }
467
985
  var currentTab = "env";
468
986
  var panelEl = null;
469
987
  var bodyEl = null;
@@ -484,12 +1002,35 @@ function mount() {
484
1002
  document.head.appendChild(style);
485
1003
  const toggle = h("button", { className: "ait-panel-toggle", title: "AIT DevTools" }, "AIT");
486
1004
  let isOpen = false;
1005
+ restoreButtonPosition(toggle);
487
1006
  panelEl = h("div", { className: "ait-panel" });
1007
+ const closeBtn = h("button", { className: "ait-panel-close", title: "Close" }, "\xD7");
1008
+ closeBtn.addEventListener("click", () => {
1009
+ isOpen = false;
1010
+ panelEl.classList.remove("open");
1011
+ });
1012
+ const mockBadge = h("span", {
1013
+ className: `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`,
1014
+ title: "Toggle panel edit mode"
1015
+ }, aitState.state.panelEditable ? "EDIT" : "READ-ONLY");
1016
+ mockBadge.addEventListener("click", () => {
1017
+ aitState.update({ panelEditable: !aitState.state.panelEditable });
1018
+ mockBadge.className = `ait-mock-badge ${aitState.state.panelEditable ? "ait-mock-badge-on" : "ait-mock-badge-off"}`;
1019
+ mockBadge.textContent = aitState.state.panelEditable ? "EDIT" : "READ-ONLY";
1020
+ refreshPanel();
1021
+ });
1022
+ const headerRight = h(
1023
+ "span",
1024
+ { style: "display:flex;align-items:center;gap:6px" },
1025
+ mockBadge,
1026
+ h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v${"0.0.2"}`),
1027
+ closeBtn
1028
+ );
488
1029
  const header = h(
489
1030
  "div",
490
1031
  { className: "ait-panel-header" },
491
1032
  h("span", {}, "AIT DevTools"),
492
- h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v${aitState.state.appVersion}`)
1033
+ headerRight
493
1034
  );
494
1035
  tabsEl = h("div", { className: "ait-panel-tabs" });
495
1036
  for (const tab of TABS) {
@@ -503,13 +1044,28 @@ function mount() {
503
1044
  bodyEl = h("div", { className: "ait-panel-body" });
504
1045
  panelEl.append(header, tabsEl, bodyEl);
505
1046
  document.body.append(panelEl, toggle);
506
- toggle.addEventListener("click", () => {
1047
+ snapToEdge(toggle);
1048
+ saveButtonPosition(toggle);
1049
+ makeDraggable(toggle, () => {
507
1050
  isOpen = !isOpen;
508
1051
  panelEl.classList.toggle("open", isOpen);
509
- if (isOpen) refreshPanel();
1052
+ if (isOpen) {
1053
+ updatePanelPosition(toggle);
1054
+ refreshPanel();
1055
+ }
1056
+ });
1057
+ let resizeRaf = 0;
1058
+ window.addEventListener("resize", () => {
1059
+ if (resizeRaf) return;
1060
+ resizeRaf = requestAnimationFrame(() => {
1061
+ resizeRaf = 0;
1062
+ snapToEdge(toggle);
1063
+ saveButtonPosition(toggle);
1064
+ if (isOpen) updatePanelPosition(toggle);
1065
+ });
510
1066
  });
511
1067
  aitState.subscribe(() => {
512
- if (isOpen && (currentTab === "analytics" || currentTab === "storage")) {
1068
+ if (isOpen && (currentTab === "analytics" || currentTab === "storage" || currentTab === "device")) {
513
1069
  refreshPanel();
514
1070
  }
515
1071
  });