@sensebox/flash-tool 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/react/FlashTool.jsx
2
- import React, { useEffect as useEffect2, useRef as useRef2 } from "react";
2
+ import React2, { useEffect as useEffect3, useRef as useRef3 } from "react";
3
3
  import { createPortal } from "react-dom";
4
- import PropTypes from "prop-types";
4
+ import PropTypes2 from "prop-types";
5
5
 
6
6
  // src/react/useFlashDevice.js
7
7
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -190,12 +190,17 @@ var DEFAULT_FLASH_OPTIONS = {
190
190
  eraseAll: false,
191
191
  usingUsbOtg: true
192
192
  };
193
+ var KNOWN_USB_DEVICES = {
194
+ "303a:81b8": "senseBox MCU-S2 (ESP32-S2)",
195
+ "303a:2": "senseBox MCU-S2 (ESP32-S2 bootloader)"
196
+ };
193
197
  function labelForPort(port) {
194
198
  var _a;
195
199
  const info = ((_a = port.getInfo) == null ? void 0 : _a.call(port)) ?? {};
196
200
  const vendor = info.usbVendorId ? info.usbVendorId.toString(16).padStart(4, "0") : "????";
197
201
  const product = info.usbProductId ? info.usbProductId.toString(16).padStart(4, "0") : "????";
198
- return `USB device (VID 0x${vendor} / PID 0x${product})`;
202
+ const known = info.usbVendorId !== void 0 && info.usbProductId !== void 0 ? KNOWN_USB_DEVICES[`${info.usbVendorId.toString(16)}:${info.usbProductId.toString(16)}`] : void 0;
203
+ return known || `USB device (VID 0x${vendor} / PID 0x${product})`;
199
204
  }
200
205
  function useFlashDevice({
201
206
  usbVendorId = DEFAULT_USB_VENDOR_ID,
@@ -222,10 +227,17 @@ function useFlashDevice({
222
227
  filters: [{ usbVendorId }]
223
228
  });
224
229
  setPort(selectedPort);
225
- setBootloaderPort(null);
226
230
  setNeedsBootloaderPermission(false);
227
231
  const label = labelForPort(selectedPort);
228
232
  setDeviceLabel(label);
233
+ if (isRomBootloaderPort(selectedPort, usbVendorId)) {
234
+ setBootloaderPort(selectedPort);
235
+ appendLog(
236
+ "Board is already in bootloader mode - skipping reset.\n"
237
+ );
238
+ } else {
239
+ setBootloaderPort(null);
240
+ }
229
241
  return selectedPort;
230
242
  } catch (err) {
231
243
  if ((err == null ? void 0 : err.name) !== "NotFoundError") {
@@ -233,7 +245,7 @@ function useFlashDevice({
233
245
  }
234
246
  return null;
235
247
  }
236
- }, [usbVendorId]);
248
+ }, [usbVendorId, appendLog]);
237
249
  const disconnect = useCallback(() => {
238
250
  setPort(null);
239
251
  setBootloaderPort(null);
@@ -391,7 +403,7 @@ function useFlashDevice({
391
403
  var LOADERS = {
392
404
  circuitpy: () => import("./circuitpy-UZR4K6GK.js"),
393
405
  ota: () => import("./ota-NY6U6TKC.js"),
394
- basic: () => import("./basic-BZ5VJLVT.js"),
406
+ basic: () => import("./basic-OBQD77DM.js"),
395
407
  standard: () => import("./standard-BZVXWJHX.js")
396
408
  };
397
409
  var BUILTIN_FIRMWARE_TYPES = Object.keys(LOADERS);
@@ -410,15 +422,263 @@ async function loadBuiltinFirmware(type) {
410
422
  return base64ToBytes(module.default);
411
423
  }
412
424
 
425
+ // src/react/Dropdown.jsx
426
+ import React, { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
427
+ import PropTypes from "prop-types";
428
+ function Dropdown({
429
+ options,
430
+ value,
431
+ onChange,
432
+ disabled = false,
433
+ placeholder,
434
+ className,
435
+ "aria-label": ariaLabel
436
+ }) {
437
+ const [open, setOpen] = useState2(false);
438
+ const [activeIndex, setActiveIndex] = useState2(-1);
439
+ const rootRef = useRef2(null);
440
+ const triggerRef = useRef2(null);
441
+ const optionRefs = useRef2([]);
442
+ const selectedIndex = options.findIndex((option) => option.value === value);
443
+ const selected = selectedIndex >= 0 ? options[selectedIndex] : null;
444
+ useEffect2(() => {
445
+ if (disabled && open) setOpen(false);
446
+ }, [disabled, open]);
447
+ useEffect2(() => {
448
+ if (!open) return;
449
+ const onDocPointerDown = (event) => {
450
+ if (rootRef.current && !rootRef.current.contains(event.target)) {
451
+ setOpen(false);
452
+ }
453
+ };
454
+ document.addEventListener("pointerdown", onDocPointerDown);
455
+ return () => document.removeEventListener("pointerdown", onDocPointerDown);
456
+ }, [open]);
457
+ useEffect2(() => {
458
+ if (!open) return;
459
+ setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
460
+ }, [open, selectedIndex]);
461
+ useEffect2(() => {
462
+ var _a;
463
+ if (!open || activeIndex < 0) return;
464
+ (_a = optionRefs.current[activeIndex]) == null ? void 0 : _a.focus();
465
+ }, [open, activeIndex]);
466
+ const commit = (index) => {
467
+ var _a;
468
+ const option = options[index];
469
+ if (!option || option.disabled) return;
470
+ onChange == null ? void 0 : onChange(option.value);
471
+ setOpen(false);
472
+ (_a = triggerRef.current) == null ? void 0 : _a.focus();
473
+ };
474
+ const handleTriggerKeyDown = (event) => {
475
+ if (disabled) return;
476
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown" || event.key === "ArrowUp") {
477
+ event.preventDefault();
478
+ setOpen(true);
479
+ }
480
+ };
481
+ const moveActive = (delta) => {
482
+ setActiveIndex((current) => {
483
+ const count = options.length;
484
+ let next = current;
485
+ for (let i = 0; i < count; i++) {
486
+ next = (next + delta + count) % count;
487
+ if (!options[next].disabled) return next;
488
+ }
489
+ return current;
490
+ });
491
+ };
492
+ const handleListKeyDown = (event) => {
493
+ var _a;
494
+ switch (event.key) {
495
+ case "ArrowDown":
496
+ event.preventDefault();
497
+ moveActive(1);
498
+ break;
499
+ case "ArrowUp":
500
+ event.preventDefault();
501
+ moveActive(-1);
502
+ break;
503
+ case "Home":
504
+ event.preventDefault();
505
+ setActiveIndex(0);
506
+ break;
507
+ case "End":
508
+ event.preventDefault();
509
+ setActiveIndex(options.length - 1);
510
+ break;
511
+ case "Enter":
512
+ case " ":
513
+ event.preventDefault();
514
+ commit(activeIndex);
515
+ break;
516
+ case "Escape":
517
+ event.preventDefault();
518
+ setOpen(false);
519
+ (_a = triggerRef.current) == null ? void 0 : _a.focus();
520
+ break;
521
+ case "Tab":
522
+ setOpen(false);
523
+ break;
524
+ default:
525
+ break;
526
+ }
527
+ };
528
+ return /* @__PURE__ */ React.createElement(
529
+ "div",
530
+ {
531
+ ref: rootRef,
532
+ className: `sbdropdown${open ? " sbdropdown--open" : ""}${disabled ? " sbdropdown--disabled" : ""}${className ? ` ${className}` : ""}`
533
+ },
534
+ /* @__PURE__ */ React.createElement(
535
+ "button",
536
+ {
537
+ ref: triggerRef,
538
+ type: "button",
539
+ className: "sbdropdown__trigger",
540
+ "aria-haspopup": "listbox",
541
+ "aria-expanded": open,
542
+ "aria-label": ariaLabel,
543
+ disabled,
544
+ onClick: () => setOpen((v) => !v),
545
+ onKeyDown: handleTriggerKeyDown
546
+ },
547
+ /* @__PURE__ */ React.createElement("span", { className: "sbdropdown__icon-slot" }, selected == null ? void 0 : selected.icon),
548
+ /* @__PURE__ */ React.createElement("span", { className: "sbdropdown__trigger-label" }, (selected == null ? void 0 : selected.label) || placeholder),
549
+ /* @__PURE__ */ React.createElement(IconChevron, { className: "sbdropdown__chevron" })
550
+ ),
551
+ /* @__PURE__ */ React.createElement(
552
+ "ul",
553
+ {
554
+ className: "sbdropdown__menu",
555
+ role: "listbox",
556
+ "aria-label": ariaLabel,
557
+ onKeyDown: handleListKeyDown
558
+ },
559
+ options.map((option, index) => /* @__PURE__ */ React.createElement("li", { key: option.value, role: "presentation" }, /* @__PURE__ */ React.createElement(
560
+ "button",
561
+ {
562
+ ref: (el) => {
563
+ optionRefs.current[index] = el;
564
+ },
565
+ type: "button",
566
+ role: "option",
567
+ "aria-selected": option.value === value,
568
+ tabIndex: -1,
569
+ className: `sbdropdown__option${option.value === value ? " sbdropdown__option--selected" : ""}${index === activeIndex ? " sbdropdown__option--active" : ""}`,
570
+ disabled: option.disabled,
571
+ onClick: () => commit(index),
572
+ onMouseEnter: () => setActiveIndex(index)
573
+ },
574
+ /* @__PURE__ */ React.createElement("span", { className: "sbdropdown__icon-slot" }, option.icon),
575
+ /* @__PURE__ */ React.createElement("span", { className: "sbdropdown__option-label" }, option.label),
576
+ option.value === value && /* @__PURE__ */ React.createElement(IconCheck, { className: "sbdropdown__option-check" })
577
+ )))
578
+ )
579
+ );
580
+ }
581
+ function IconChevron({ className }) {
582
+ return /* @__PURE__ */ React.createElement(
583
+ "svg",
584
+ {
585
+ className,
586
+ viewBox: "0 0 10 6",
587
+ width: "10",
588
+ height: "6",
589
+ fill: "none",
590
+ stroke: "currentColor",
591
+ strokeWidth: "1.5",
592
+ strokeLinecap: "round",
593
+ strokeLinejoin: "round",
594
+ "aria-hidden": "true"
595
+ },
596
+ /* @__PURE__ */ React.createElement("path", { d: "M1 1l4 4 4-4" })
597
+ );
598
+ }
599
+ function IconCheck({ className }) {
600
+ return /* @__PURE__ */ React.createElement(
601
+ "svg",
602
+ {
603
+ className,
604
+ viewBox: "0 0 16 16",
605
+ width: "12",
606
+ height: "12",
607
+ fill: "none",
608
+ stroke: "currentColor",
609
+ strokeWidth: "2",
610
+ strokeLinecap: "round",
611
+ strokeLinejoin: "round",
612
+ "aria-hidden": "true"
613
+ },
614
+ /* @__PURE__ */ React.createElement("path", { d: "M3 8.5 6.5 12 13 4.5" })
615
+ );
616
+ }
617
+ Dropdown.propTypes = {
618
+ options: PropTypes.arrayOf(
619
+ PropTypes.shape({
620
+ value: PropTypes.string.isRequired,
621
+ label: PropTypes.node.isRequired,
622
+ icon: PropTypes.node,
623
+ disabled: PropTypes.bool
624
+ })
625
+ ).isRequired,
626
+ value: PropTypes.string,
627
+ onChange: PropTypes.func,
628
+ disabled: PropTypes.bool,
629
+ placeholder: PropTypes.string,
630
+ className: PropTypes.string,
631
+ "aria-label": PropTypes.string
632
+ };
633
+
413
634
  // src/react/FlashTool.jsx
414
635
  var builtinFirmwareCache = /* @__PURE__ */ new Map();
415
636
  var FIRMWARE_TYPES = ["circuitpy", "ota", "basic", "standard"];
637
+ var FIRMWARE_ALIASES = {
638
+ circuitpython: "circuitpy"
639
+ };
640
+ function resolveFirmwareType(value) {
641
+ if (!value) return void 0;
642
+ return FIRMWARE_ALIASES[value] || value;
643
+ }
416
644
  var BUILTIN_FIRMWARE_FLASH_OVERRIDES = {
417
645
  circuitpy: { address: 0, eraseAll: true },
418
646
  ota: { address: 0, eraseAll: true },
419
647
  basic: { address: 0, eraseAll: true },
420
648
  standard: { address: 0, eraseAll: true }
421
649
  };
650
+ var DEFAULT_FIRMWARE_DESCRIPTIONS = {
651
+ circuitpy: {
652
+ text: "Install CircuitPython on your senseBox now! For a more detailed tutorial, check out our",
653
+ linkText: "docs",
654
+ linkUrl: "https://docs.sensebox.de/docs/editors/circuitpython/circuitpython_esp32/?board=edus2"
655
+ },
656
+ ota: {
657
+ text: "Flashes the senseBox OTA firmware, which lets you update your senseBox wirelessly afterwards."
658
+ },
659
+ basic: {
660
+ text: "A minimal starter sketch - a good starting point for your own senseBox projects."
661
+ },
662
+ standard: {
663
+ text: "The TinyUF2 bootloader, ready to read out and upload data from your connected sensors."
664
+ }
665
+ };
666
+ var DEFAULT_FIRMWARE_DESCRIPTIONS_DE = {
667
+ circuitpy: {
668
+ text: "Installiere jetzt CircuitPython auf deiner senseBox! Ein ausf\xFChrliches Tutorial findest du in unserer",
669
+ linkText: "Dokumentation",
670
+ linkUrl: "https://docs.sensebox.de/docs/editors/circuitpython/circuitpython_esp32/?board=edus2"
671
+ },
672
+ ota: {
673
+ text: "Spielt die senseBox-OTA-Firmware auf, mit der du deine senseBox anschlie\xDFend drahtlos aktualisieren kannst."
674
+ },
675
+ basic: {
676
+ text: "Ein minimales Starter-Sketch - ein guter Ausgangspunkt f\xFCr eigene senseBox-Projekte."
677
+ },
678
+ standard: {
679
+ text: "Das Standard-senseBox-Sketch, bereit zum Auslesen und Hochladen von Daten deiner angeschlossenen Sensoren."
680
+ }
681
+ };
422
682
  var DEFAULT_STRINGS = {
423
683
  unsupported: "Web Serial is not supported in this browser.",
424
684
  connect: "Connect board",
@@ -432,16 +692,26 @@ var DEFAULT_STRINGS = {
432
692
  flash: "Upload",
433
693
  flashing: "Uploading...",
434
694
  done: "Upload complete",
695
+ newUpload: "Start new upload",
435
696
  error: "Upload failed",
436
697
  retry: "Retry",
437
698
  disconnect: "Disconnect",
438
699
  showLog: "Show log",
439
700
  hideLog: "Hide log",
701
+ logPlaceholder: 'Click "Connect board" to see the log here.',
440
702
  sketchLabel: "Sketch",
703
+ step1Title: "Connect your board",
704
+ step1Hint: "Plug in your senseBox, then pick it from the browser's device list.",
705
+ step2Title: "Enter bootloader mode",
706
+ step2Hint: "Put the board into upload mode.",
707
+ step2HintPermission: "Select the bootloader port the browser is asking for.",
708
+ step2Done: "Bootloader ready",
709
+ step3Title: "Upload firmware",
710
+ step3Hint: "Waiting for the previous steps to finish.",
441
711
  firmwareCircuitpy: "CircuitPython",
442
712
  firmwareOta: "senseBox OTA",
443
- firmwareBasic: "Basic",
444
- firmwareStandard: "Standard senseBox Sketch",
713
+ firmwareBasic: "senseBox:basic",
714
+ firmwareStandard: "TinyUF2",
445
715
  help: "Help",
446
716
  helpTitle: "How it works",
447
717
  helpStep1: "Connect your senseBox",
@@ -450,15 +720,60 @@ var DEFAULT_STRINGS = {
450
720
  helpStep4: "Start the upload",
451
721
  helpClose: "Close"
452
722
  };
723
+ var DEFAULT_STRINGS_DE = {
724
+ unsupported: "Web Serial wird von diesem Browser nicht unterst\xFCtzt.",
725
+ connect: "Board verbinden",
726
+ connecting: "Verbinde...",
727
+ connected: "senseBox verbunden",
728
+ prepare: "Upload vorbereiten",
729
+ preparing: "Bereite vor...",
730
+ grantPermission: "Bootloader-Port ausw\xE4hlen",
731
+ waitingForBinary: "Warte auf Firmware...",
732
+ loadingFirmware: "Firmware wird geladen...",
733
+ flash: "Hochladen",
734
+ flashing: "Wird hochgeladen...",
735
+ done: "Upload abgeschlossen",
736
+ newUpload: "Neuen Upload starten",
737
+ error: "Upload fehlgeschlagen",
738
+ retry: "Erneut versuchen",
739
+ disconnect: "Trennen",
740
+ showLog: "Protokoll anzeigen",
741
+ hideLog: "Protokoll ausblenden",
742
+ logPlaceholder: 'Klicke auf "Board verbinden", um das Protokoll hier zu sehen.',
743
+ sketchLabel: "Sketch",
744
+ step1Title: "Board verbinden",
745
+ step1Hint: "Schlie\xDFe deine senseBox an und w\xE4hle sie aus der Ger\xE4teliste des Browsers aus.",
746
+ step2Title: "Bootloader-Modus aktivieren",
747
+ step2Hint: "Versetze das Board in den Upload-Modus.",
748
+ step2HintPermission: "W\xE4hle den Bootloader-Port aus, nach dem der Browser fragt.",
749
+ step2Done: "Bootloader bereit",
750
+ step3Title: "Firmware hochladen",
751
+ step3Hint: "Warte auf die vorherigen Schritte.",
752
+ firmwareCircuitpy: "CircuitPython",
753
+ firmwareOta: "senseBox OTA",
754
+ firmwareBasic: "senseBox:basic",
755
+ firmwareStandard: "TinyUF2",
756
+ help: "Hilfe",
757
+ helpTitle: "So funktioniert's",
758
+ helpStep1: "Verbinde deine senseBox",
759
+ helpStep2: "W\xE4hle einen Sketch aus",
760
+ helpStep3: "Erteile deinem Browser die Berechtigung",
761
+ helpStep4: "Starte den Upload",
762
+ helpClose: "Schlie\xDFen"
763
+ };
453
764
  function FlashTool({
454
765
  binary,
455
766
  firmwares,
767
+ firmware,
456
768
  firmwareType,
457
769
  defaultFirmwareType,
458
770
  firmwareFlashOptions,
771
+ firmwareDescriptions,
459
772
  usbVendorId,
460
773
  flashOptions,
461
774
  autoFlash = true,
775
+ expand = false,
776
+ language = "en",
462
777
  strings,
463
778
  className,
464
779
  onConnected,
@@ -467,19 +782,26 @@ function FlashTool({
467
782
  onError,
468
783
  onFirmwareTypeChange
469
784
  }) {
470
- const merged = { ...DEFAULT_STRINGS, ...strings };
471
- const [logOpen, setLogOpen] = React.useState(false);
472
- const [helpOpen, setHelpOpen] = React.useState(false);
473
- const [busy, setBusy] = React.useState(null);
474
- const [selectedType, setSelectedType] = React.useState(
475
- firmwareType || defaultFirmwareType || FIRMWARE_TYPES[0]
785
+ const merged = {
786
+ ...language === "de" ? DEFAULT_STRINGS_DE : DEFAULT_STRINGS,
787
+ ...strings
788
+ };
789
+ const [logOpen, setLogOpen] = React2.useState(expand);
790
+ const [helpOpen, setHelpOpen] = React2.useState(false);
791
+ const [busy, setBusy] = React2.useState(null);
792
+ const lockedFirmwareType = resolveFirmwareType(firmwareType || firmware);
793
+ const [selectedType, setSelectedType] = React2.useState(
794
+ lockedFirmwareType || defaultFirmwareType || FIRMWARE_TYPES[0]
476
795
  );
477
- const firmwareLocked = Boolean(firmwareType);
796
+ const firmwareLocked = Boolean(lockedFirmwareType);
478
797
  const showFirmwarePicker = Boolean(firmwares) || !binary || firmwareLocked;
479
- useEffect2(() => {
480
- if (firmwareType) setSelectedType(firmwareType);
481
- }, [firmwareType]);
482
- useEffect2(() => {
798
+ useEffect3(() => {
799
+ if (lockedFirmwareType) setSelectedType(lockedFirmwareType);
800
+ }, [lockedFirmwareType]);
801
+ useEffect3(() => {
802
+ if (expand) setLogOpen(true);
803
+ }, [expand]);
804
+ useEffect3(() => {
483
805
  if (!helpOpen) return;
484
806
  const onKeyDown = (event) => {
485
807
  if (event.key === "Escape") setHelpOpen(false);
@@ -505,11 +827,11 @@ function FlashTool({
505
827
  resetFlashState
506
828
  } = useFlashDevice({ usbVendorId, flashOptions });
507
829
  const explicitBinary = firmwares ? firmwares[selectedType] : binary;
508
- const [builtinBinary, setBuiltinBinary] = React.useState(null);
509
- const [builtinLoading, setBuiltinLoading] = React.useState(false);
510
- const [builtinError, setBuiltinError] = React.useState(null);
511
- const [builtinRetryToken, setBuiltinRetryToken] = React.useState(0);
512
- useEffect2(() => {
830
+ const [builtinBinary, setBuiltinBinary] = React2.useState(null);
831
+ const [builtinLoading, setBuiltinLoading] = React2.useState(false);
832
+ const [builtinError, setBuiltinError] = React2.useState(null);
833
+ const [builtinRetryToken, setBuiltinRetryToken] = React2.useState(0);
834
+ useEffect3(() => {
513
835
  if (!supported || explicitBinary) return;
514
836
  const cached = builtinFirmwareCache.get(selectedType);
515
837
  if (cached) {
@@ -542,37 +864,51 @@ function FlashTool({
542
864
  };
543
865
  }, [supported, explicitBinary, selectedType, onError, builtinRetryToken]);
544
866
  const resolvedBinary = explicitBinary || builtinBinary || void 0;
545
- const flashOverrides = React.useMemo(
867
+ const flashOverrides = React2.useMemo(
546
868
  () => showFirmwarePicker ? {
547
869
  ...BUILTIN_FIRMWARE_FLASH_OVERRIDES[selectedType],
548
870
  ...firmwareFlashOptions == null ? void 0 : firmwareFlashOptions[selectedType]
549
871
  } : {},
550
872
  [showFirmwarePicker, firmwareFlashOptions, selectedType]
551
873
  );
552
- const latestLogLine = React.useMemo(() => {
874
+ const latestLogLine = React2.useMemo(() => {
553
875
  if (!log) return "";
554
876
  const lines = log.split("\n").filter((line) => line.trim().length > 0);
555
- return lines[lines.length - 1] || "";
877
+ const lastLine = lines[lines.length - 1] || "";
878
+ return lastLine.replace(/\s*\(\d+ ?%\)\s*$/, "");
556
879
  }, [log]);
557
- const handleFirmwareChange = (event) => {
558
- const value = event.target.value;
880
+ const activeFirmwareDescription = showFirmwarePicker ? {
881
+ ...language === "de" ? DEFAULT_FIRMWARE_DESCRIPTIONS_DE[selectedType] : DEFAULT_FIRMWARE_DESCRIPTIONS[selectedType],
882
+ ...firmwareDescriptions == null ? void 0 : firmwareDescriptions[selectedType]
883
+ } : null;
884
+ const handleFirmwareChange = (value) => {
559
885
  setSelectedType(value);
560
886
  onFirmwareTypeChange == null ? void 0 : onFirmwareTypeChange(value);
561
887
  };
888
+ const firmwareOptions = FIRMWARE_TYPES.map((type) => ({
889
+ value: type,
890
+ label: merged[`firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`],
891
+ icon: FIRMWARE_ICONS[type]
892
+ }));
562
893
  const handleRetryFirmwareLoad = () => {
563
894
  setBuiltinRetryToken((token) => token + 1);
564
895
  };
565
- const prevConnected = useRef2(connected);
566
- useEffect2(() => {
896
+ const logRef = useRef3(null);
897
+ useEffect3(() => {
898
+ if (!logOpen || !logRef.current) return;
899
+ logRef.current.scrollTop = logRef.current.scrollHeight;
900
+ }, [log, logOpen]);
901
+ const prevConnected = useRef3(connected);
902
+ useEffect3(() => {
567
903
  if (connected && !prevConnected.current) onConnected == null ? void 0 : onConnected(deviceLabel);
568
904
  if (!connected && prevConnected.current) onDisconnected == null ? void 0 : onDisconnected();
569
905
  prevConnected.current = connected;
570
906
  }, [connected, deviceLabel, onConnected, onDisconnected]);
571
- useEffect2(() => {
907
+ useEffect3(() => {
572
908
  if (status === "done") onFlashed == null ? void 0 : onFlashed();
573
909
  if (status === "error" && error) onError == null ? void 0 : onError(new Error(error));
574
910
  }, [status, error, onFlashed, onError]);
575
- useEffect2(() => {
911
+ useEffect3(() => {
576
912
  if (autoFlash && resolvedBinary && connected && !bootloaderReady && status === "idle" && !needsBootloaderPermission) {
577
913
  flash(resolvedBinary, flashOverrides);
578
914
  }
@@ -587,7 +923,7 @@ function FlashTool({
587
923
  flash
588
924
  ]);
589
925
  if (!supported) {
590
- return /* @__PURE__ */ React.createElement("div", { className: `sbflash sbflash--unsupported ${className || ""}` }, merged.unsupported);
926
+ return /* @__PURE__ */ React2.createElement("div", { className: `sbflash sbflash--unsupported ${className || ""}` }, merged.unsupported);
591
927
  }
592
928
  const handleConnect = async () => {
593
929
  setBusy("connect");
@@ -621,30 +957,26 @@ function FlashTool({
621
957
  resetFlashState();
622
958
  if (resolvedBinary) flash(resolvedBinary, flashOverrides);
623
959
  };
624
- return /* @__PURE__ */ React.createElement("div", { className: `sbflash ${className || ""}` }, /* @__PURE__ */ React.createElement("div", { className: "sbflash__row" }, /* @__PURE__ */ React.createElement(
625
- "span",
626
- {
627
- className: `sbflash__dot sbflash__dot--${status === "error" ? "error" : status === "flashing" ? "busy" : connected ? "ok" : "off"}`
628
- }
629
- ), /* @__PURE__ */ React.createElement(
630
- "span",
631
- {
632
- className: "sbflash__label",
633
- title: connected ? deviceLabel : void 0
634
- },
635
- connected ? merged.connected : merged.connect
636
- )), /* @__PURE__ */ React.createElement("div", { className: "sbflash__field" }, /* @__PURE__ */ React.createElement("div", { className: "sbflash__field-head" }, /* @__PURE__ */ React.createElement("span", { className: "sbflash__field-label" }, merged.sketchLabel), /* @__PURE__ */ React.createElement("div", { className: "sbflash__icon-row" }, log && /* @__PURE__ */ React.createElement(
960
+ const flashComplete = status === "done";
961
+ const step1Status = flashComplete ? "done" : connected ? "done" : "active";
962
+ const step2Status = flashComplete ? "done" : !connected ? "pending" : bootloaderReady ? "done" : "active";
963
+ const step3Status = flashComplete ? "done" : !bootloaderReady ? "pending" : status === "error" ? "error" : "active";
964
+ const handleStartNew = () => {
965
+ resetFlashState();
966
+ };
967
+ return /* @__PURE__ */ React2.createElement("div", { className: `sbflash ${className || ""}` }, /* @__PURE__ */ React2.createElement("div", { className: "sbflash__field" }, /* @__PURE__ */ React2.createElement("div", { className: "sbflash__field-head" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__field-label" }, merged.sketchLabel), /* @__PURE__ */ React2.createElement("div", { className: "sbflash__icon-row" }, /* @__PURE__ */ React2.createElement(
637
968
  "button",
638
969
  {
639
970
  type: "button",
640
971
  className: `sbflash__icon-button${logOpen ? " sbflash__icon-button--active" : ""}`,
641
- onClick: () => setLogOpen((v) => !v),
972
+ onClick: () => !expand && setLogOpen((v) => !v),
973
+ disabled: expand,
642
974
  "aria-pressed": logOpen,
643
975
  "aria-label": logOpen ? merged.hideLog : merged.showLog,
644
976
  title: logOpen ? merged.hideLog : merged.showLog
645
977
  },
646
- /* @__PURE__ */ React.createElement(IconTerminal, null)
647
- ), connected && /* @__PURE__ */ React.createElement(
978
+ /* @__PURE__ */ React2.createElement(IconTerminal, null)
979
+ ), connected && /* @__PURE__ */ React2.createElement(
648
980
  "button",
649
981
  {
650
982
  type: "button",
@@ -653,8 +985,8 @@ function FlashTool({
653
985
  "aria-label": merged.disconnect,
654
986
  title: merged.disconnect
655
987
  },
656
- /* @__PURE__ */ React.createElement(IconDisconnect, null)
657
- ), /* @__PURE__ */ React.createElement(
988
+ /* @__PURE__ */ React2.createElement(IconDisconnect, null)
989
+ ), /* @__PURE__ */ React2.createElement(
658
990
  "button",
659
991
  {
660
992
  type: "button",
@@ -663,17 +995,17 @@ function FlashTool({
663
995
  "aria-label": merged.help,
664
996
  title: merged.help
665
997
  },
666
- /* @__PURE__ */ React.createElement(IconHelp, null)
667
- ))), showFirmwarePicker && /* @__PURE__ */ React.createElement("div", { className: "sbflash__select-wrap" }, /* @__PURE__ */ React.createElement(
668
- "select",
998
+ /* @__PURE__ */ React2.createElement(IconHelp, null)
999
+ ))), showFirmwarePicker && /* @__PURE__ */ React2.createElement(
1000
+ Dropdown,
669
1001
  {
670
- className: "sbflash__select",
1002
+ options: firmwareOptions,
671
1003
  value: selectedType,
672
1004
  onChange: handleFirmwareChange,
673
- disabled: firmwareLocked || status === "flashing"
674
- },
675
- FIRMWARE_TYPES.map((type) => /* @__PURE__ */ React.createElement("option", { key: type, value: type }, merged[`firmware${type.charAt(0).toUpperCase()}${type.slice(1)}`]))
676
- )), builtinError && /* @__PURE__ */ React.createElement("div", { className: "sbflash__field-error-row" }, /* @__PURE__ */ React.createElement("span", { className: "sbflash__field-error" }, builtinError), /* @__PURE__ */ React.createElement(
1005
+ disabled: firmwareLocked || status === "flashing",
1006
+ "aria-label": merged.sketchLabel
1007
+ }
1008
+ ), builtinError && /* @__PURE__ */ React2.createElement("div", { className: "sbflash__field-error-row" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__field-error" }, builtinError), /* @__PURE__ */ React2.createElement(
677
1009
  "button",
678
1010
  {
679
1011
  type: "button",
@@ -681,14 +1013,22 @@ function FlashTool({
681
1013
  onClick: handleRetryFirmwareLoad
682
1014
  },
683
1015
  merged.retry
684
- ))), helpOpen && createPortal(
685
- /* @__PURE__ */ React.createElement(
1016
+ )), (activeFirmwareDescription == null ? void 0 : activeFirmwareDescription.text) && /* @__PURE__ */ React2.createElement("p", { className: "sbflash__field-description" }, activeFirmwareDescription.text, activeFirmwareDescription.linkUrl && /* @__PURE__ */ React2.createElement(React2.Fragment, null, " ", /* @__PURE__ */ React2.createElement(
1017
+ "a",
1018
+ {
1019
+ href: activeFirmwareDescription.linkUrl,
1020
+ target: "_blank",
1021
+ rel: "noopener noreferrer"
1022
+ },
1023
+ activeFirmwareDescription.linkText || activeFirmwareDescription.linkUrl
1024
+ )))), helpOpen && createPortal(
1025
+ /* @__PURE__ */ React2.createElement(
686
1026
  "div",
687
1027
  {
688
1028
  className: "sbflash__modal-overlay",
689
1029
  onClick: () => setHelpOpen(false)
690
1030
  },
691
- /* @__PURE__ */ React.createElement(
1031
+ /* @__PURE__ */ React2.createElement(
692
1032
  "div",
693
1033
  {
694
1034
  className: "sbflash__modal",
@@ -697,7 +1037,7 @@ function FlashTool({
697
1037
  "aria-label": merged.helpTitle,
698
1038
  onClick: (event) => event.stopPropagation()
699
1039
  },
700
- /* @__PURE__ */ React.createElement("div", { className: "sbflash__modal-head" }, /* @__PURE__ */ React.createElement("span", { className: "sbflash__modal-title" }, merged.helpTitle), /* @__PURE__ */ React.createElement(
1040
+ /* @__PURE__ */ React2.createElement("div", { className: "sbflash__modal-head" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__modal-title" }, merged.helpTitle), /* @__PURE__ */ React2.createElement(
701
1041
  "button",
702
1042
  {
703
1043
  type: "button",
@@ -707,16 +1047,21 @@ function FlashTool({
707
1047
  },
708
1048
  "\xD7"
709
1049
  )),
710
- /* @__PURE__ */ React.createElement("ol", { className: "sbflash__modal-steps" }, [
1050
+ /* @__PURE__ */ React2.createElement("ol", { className: "sbflash__modal-steps" }, [
711
1051
  merged.helpStep1,
712
1052
  merged.helpStep2,
713
1053
  merged.helpStep3,
714
1054
  merged.helpStep4
715
- ].map((step, index) => /* @__PURE__ */ React.createElement("li", { key: index, className: "sbflash__modal-step" }, /* @__PURE__ */ React.createElement("span", { className: "sbflash__modal-step-index" }, index + 1), /* @__PURE__ */ React.createElement("span", null, step))))
1055
+ ].map((step, index) => /* @__PURE__ */ React2.createElement("li", { key: index, className: "sbflash__modal-step" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__modal-step-index" }, index + 1), /* @__PURE__ */ React2.createElement("span", null, step))))
716
1056
  )
717
1057
  ),
718
1058
  document.body
719
- ), !connected && /* @__PURE__ */ React.createElement(
1059
+ ), /* @__PURE__ */ React2.createElement("ol", { className: "sbflash__stepper" }, /* @__PURE__ */ React2.createElement("li", { className: `sbflash__step sbflash__step--${step1Status}` }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-marker" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-index" }, step1Status === "done" ? /* @__PURE__ */ React2.createElement(IconCheck2, null) : 1), /* @__PURE__ */ React2.createElement(
1060
+ "span",
1061
+ {
1062
+ className: `sbflash__step-line${step1Status === "done" ? " sbflash__step-line--done" : ""}`
1063
+ }
1064
+ )), /* @__PURE__ */ React2.createElement("div", { className: "sbflash__step-body" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-title" }, merged.step1Title), !connected && !flashComplete ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-hint" }, merged.step1Hint), /* @__PURE__ */ React2.createElement(
720
1065
  "button",
721
1066
  {
722
1067
  type: "button",
@@ -725,7 +1070,12 @@ function FlashTool({
725
1070
  disabled: busy === "connect"
726
1071
  },
727
1072
  busy === "connect" ? merged.connecting : merged.connect
728
- ), connected && needsBootloaderPermission && /* @__PURE__ */ React.createElement(
1073
+ )) : /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-detail", title: deviceLabel }, deviceLabel || merged.connected))), /* @__PURE__ */ React2.createElement("li", { className: `sbflash__step sbflash__step--${step2Status}` }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-marker" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-index" }, step2Status === "done" ? /* @__PURE__ */ React2.createElement(IconCheck2, null) : 2), /* @__PURE__ */ React2.createElement(
1074
+ "span",
1075
+ {
1076
+ className: `sbflash__step-line${step2Status === "done" ? " sbflash__step-line--done" : ""}`
1077
+ }
1078
+ )), /* @__PURE__ */ React2.createElement("div", { className: "sbflash__step-body" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-title" }, merged.step2Title), step2Status === "active" && needsBootloaderPermission && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-hint" }, merged.step2HintPermission), /* @__PURE__ */ React2.createElement(
729
1079
  "button",
730
1080
  {
731
1081
  type: "button",
@@ -734,16 +1084,7 @@ function FlashTool({
734
1084
  disabled: busy === "grant"
735
1085
  },
736
1086
  merged.grantPermission
737
- ), connected && !needsBootloaderPermission && !bootloaderReady && /* @__PURE__ */ React.createElement(
738
- "button",
739
- {
740
- type: "button",
741
- className: "sbflash__button sbflash__button--secondary",
742
- onClick: handlePrepare,
743
- disabled: busy === "prepare" || status === "flashing"
744
- },
745
- busy === "prepare" ? merged.preparing : merged.prepare
746
- ), connected && bootloaderReady && status !== "flashing" && status !== "error" && /* @__PURE__ */ React.createElement(
1087
+ )), step2Status === "active" && !needsBootloaderPermission && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-hint" }, merged.step2Hint)), step2Status === "done" && /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-detail" }, merged.step2Done), step2Status === "pending" && /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-hint" }, merged.step2Hint))), /* @__PURE__ */ React2.createElement("li", { className: `sbflash__step sbflash__step--${step3Status}` }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-marker" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-index" }, step3Status === "done" ? /* @__PURE__ */ React2.createElement(IconCheck2, null) : 3)), /* @__PURE__ */ React2.createElement("div", { className: "sbflash__step-body" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-title" }, merged.step3Title), step3Status === "pending" && /* @__PURE__ */ React2.createElement("span", { className: "sbflash__step-hint" }, merged.step3Hint), step3Status === "active" && status !== "flashing" && /* @__PURE__ */ React2.createElement(
747
1088
  "button",
748
1089
  {
749
1090
  type: "button",
@@ -752,13 +1093,21 @@ function FlashTool({
752
1093
  disabled: !resolvedBinary
753
1094
  },
754
1095
  resolvedBinary ? merged.flash : builtinLoading ? merged.loadingFirmware : merged.waitingForBinary
755
- ), status === "flashing" && (progress > 0 ? /* @__PURE__ */ React.createElement("div", { className: "sbflash__progress-wrap" }, /* @__PURE__ */ React.createElement("div", { className: "sbflash__progress" }, /* @__PURE__ */ React.createElement("div", { className: "sbflash__progress-track" }, /* @__PURE__ */ React.createElement(
1096
+ ), status === "flashing" && (progress > 0 ? /* @__PURE__ */ React2.createElement("div", { className: "sbflash__progress-wrap" }, /* @__PURE__ */ React2.createElement("div", { className: "sbflash__progress" }, /* @__PURE__ */ React2.createElement("div", { className: "sbflash__progress-track" }, /* @__PURE__ */ React2.createElement(
756
1097
  "div",
757
1098
  {
758
1099
  className: "sbflash__progress-fill",
759
1100
  style: { width: `${Math.min(100, progress)}%` }
760
1101
  }
761
- )), /* @__PURE__ */ React.createElement("span", { className: "sbflash__progress-value" }, progress, "%")), latestLogLine && /* @__PURE__ */ React.createElement("span", { className: "sbflash__progress-detail" }, latestLogLine)) : /* @__PURE__ */ React.createElement("div", { className: "sbflash__task" }, /* @__PURE__ */ React.createElement("span", { className: "sbflash__task-spinner" }), /* @__PURE__ */ React.createElement("span", { className: "sbflash__task-label" }, latestLogLine || merged.flashing))), status === "done" && /* @__PURE__ */ React.createElement("div", { className: "sbflash__status sbflash__status--ok" }, merged.done), status === "error" && /* @__PURE__ */ React.createElement("div", { className: "sbflash__status sbflash__status--error" }, merged.error, /* @__PURE__ */ React.createElement(
1102
+ )), /* @__PURE__ */ React2.createElement("span", { className: "sbflash__progress-value" }, progress, "%")), latestLogLine && /* @__PURE__ */ React2.createElement("span", { className: "sbflash__progress-detail" }, latestLogLine)) : /* @__PURE__ */ React2.createElement("div", { className: "sbflash__task" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__task-spinner" }), /* @__PURE__ */ React2.createElement("span", { className: "sbflash__task-label" }, latestLogLine || merged.flashing))), status === "done" && /* @__PURE__ */ React2.createElement("div", { className: "sbflash__done-banner", role: "status" }, /* @__PURE__ */ React2.createElement("span", { className: "sbflash__done-icon" }, /* @__PURE__ */ React2.createElement(IconCheck2, null)), /* @__PURE__ */ React2.createElement("span", { className: "sbflash__done-text" }, merged.done), /* @__PURE__ */ React2.createElement(
1103
+ "button",
1104
+ {
1105
+ type: "button",
1106
+ className: "sbflash__button sbflash__button--secondary",
1107
+ onClick: handleStartNew
1108
+ },
1109
+ merged.newUpload
1110
+ )), status === "error" && /* @__PURE__ */ React2.createElement("div", { className: "sbflash__status sbflash__status--error" }, merged.error, /* @__PURE__ */ React2.createElement(
762
1111
  "button",
763
1112
  {
764
1113
  type: "button",
@@ -766,10 +1115,10 @@ function FlashTool({
766
1115
  onClick: handleRetry
767
1116
  },
768
1117
  merged.retry
769
- )), log && logOpen && /* @__PURE__ */ React.createElement("pre", { className: "sbflash__log" }, log));
1118
+ ))))), logOpen && /* @__PURE__ */ React2.createElement("pre", { className: "sbflash__log", ref: logRef }, log || merged.logPlaceholder));
770
1119
  }
771
1120
  function IconTerminal() {
772
- return /* @__PURE__ */ React.createElement(
1121
+ return /* @__PURE__ */ React2.createElement(
773
1122
  "svg",
774
1123
  {
775
1124
  viewBox: "0 0 16 16",
@@ -782,13 +1131,30 @@ function IconTerminal() {
782
1131
  strokeLinejoin: "round",
783
1132
  "aria-hidden": "true"
784
1133
  },
785
- /* @__PURE__ */ React.createElement("rect", { x: "1.5", y: "2.5", width: "13", height: "11", rx: "2" }),
786
- /* @__PURE__ */ React.createElement("path", { d: "M4.25 6.25 6.75 8.5l-2.5 2.25" }),
787
- /* @__PURE__ */ React.createElement("path", { d: "M8.5 11h3" })
1134
+ /* @__PURE__ */ React2.createElement("rect", { x: "1.5", y: "2.5", width: "13", height: "11", rx: "2" }),
1135
+ /* @__PURE__ */ React2.createElement("path", { d: "M4.25 6.25 6.75 8.5l-2.5 2.25" }),
1136
+ /* @__PURE__ */ React2.createElement("path", { d: "M8.5 11h3" })
1137
+ );
1138
+ }
1139
+ function IconCheck2() {
1140
+ return /* @__PURE__ */ React2.createElement(
1141
+ "svg",
1142
+ {
1143
+ viewBox: "0 0 16 16",
1144
+ width: "11",
1145
+ height: "11",
1146
+ fill: "none",
1147
+ stroke: "currentColor",
1148
+ strokeWidth: "2",
1149
+ strokeLinecap: "round",
1150
+ strokeLinejoin: "round",
1151
+ "aria-hidden": "true"
1152
+ },
1153
+ /* @__PURE__ */ React2.createElement("path", { d: "M3 8.5 6.5 12 13 4.5" })
788
1154
  );
789
1155
  }
790
1156
  function IconDisconnect() {
791
- return /* @__PURE__ */ React.createElement(
1157
+ return /* @__PURE__ */ React2.createElement(
792
1158
  "svg",
793
1159
  {
794
1160
  viewBox: "0 0 16 16",
@@ -801,12 +1167,12 @@ function IconDisconnect() {
801
1167
  strokeLinejoin: "round",
802
1168
  "aria-hidden": "true"
803
1169
  },
804
- /* @__PURE__ */ React.createElement("path", { d: "M8 1.5v4.25" }),
805
- /* @__PURE__ */ React.createElement("path", { d: "M4.5 5.5a5 5 0 1 0 7 0" })
1170
+ /* @__PURE__ */ React2.createElement("path", { d: "M8 1.5v4.25" }),
1171
+ /* @__PURE__ */ React2.createElement("path", { d: "M4.5 5.5a5 5 0 1 0 7 0" })
806
1172
  );
807
1173
  }
808
1174
  function IconHelp() {
809
- return /* @__PURE__ */ React.createElement(
1175
+ return /* @__PURE__ */ React2.createElement(
810
1176
  "svg",
811
1177
  {
812
1178
  viewBox: "0 0 16 16",
@@ -819,42 +1185,138 @@ function IconHelp() {
819
1185
  strokeLinejoin: "round",
820
1186
  "aria-hidden": "true"
821
1187
  },
822
- /* @__PURE__ */ React.createElement("circle", { cx: "8", cy: "8", r: "6.5" }),
823
- /* @__PURE__ */ React.createElement("path", { d: "M6 6.25a2 2 0 1 1 2.75 1.85c-.6.25-.95.7-.95 1.4v.25" }),
824
- /* @__PURE__ */ React.createElement("circle", { cx: "8", cy: "11.5", r: "0.9", fill: "currentColor", stroke: "none" })
1188
+ /* @__PURE__ */ React2.createElement("circle", { cx: "8", cy: "8", r: "6.5" }),
1189
+ /* @__PURE__ */ React2.createElement("path", { d: "M6 6.25a2 2 0 1 1 2.75 1.85c-.6.25-.95.7-.95 1.4v.25" }),
1190
+ /* @__PURE__ */ React2.createElement("circle", { cx: "8", cy: "11.5", r: "0.9", fill: "currentColor", stroke: "none" })
825
1191
  );
826
1192
  }
1193
+ function IconChip() {
1194
+ return /* @__PURE__ */ React2.createElement(
1195
+ "svg",
1196
+ {
1197
+ viewBox: "0 0 16 16",
1198
+ width: "14",
1199
+ height: "14",
1200
+ fill: "none",
1201
+ stroke: "currentColor",
1202
+ strokeWidth: "1.4",
1203
+ strokeLinecap: "round",
1204
+ strokeLinejoin: "round",
1205
+ "aria-hidden": "true"
1206
+ },
1207
+ /* @__PURE__ */ React2.createElement("rect", { x: "4", y: "4", width: "8", height: "8", rx: "1.2" }),
1208
+ /* @__PURE__ */ React2.createElement("path", { d: "M6 1.5v2M10 1.5v2M6 12.5v2M10 12.5v2M1.5 6v-.01M1.5 10v-.01M14.5 6v-.01M14.5 10v-.01M1.5 6h2M1.5 10h2M12.5 6h2M12.5 10h2" })
1209
+ );
1210
+ }
1211
+ function IconCloud() {
1212
+ return /* @__PURE__ */ React2.createElement(
1213
+ "svg",
1214
+ {
1215
+ viewBox: "0 0 16 16",
1216
+ width: "14",
1217
+ height: "14",
1218
+ fill: "none",
1219
+ stroke: "currentColor",
1220
+ strokeWidth: "1.4",
1221
+ strokeLinecap: "round",
1222
+ strokeLinejoin: "round",
1223
+ "aria-hidden": "true"
1224
+ },
1225
+ /* @__PURE__ */ React2.createElement("path", { d: "M4.5 12.5A3 3 0 0 1 4 6.53 3.5 3.5 0 0 1 11 5.5a2.75 2.75 0 0 1 .5 5.46" }),
1226
+ /* @__PURE__ */ React2.createElement("path", { d: "M6.5 12.5h5" }),
1227
+ /* @__PURE__ */ React2.createElement("path", { d: "M8 9.5v5M6.25 12.25 8 14.5l1.75-2.25" })
1228
+ );
1229
+ }
1230
+ function IconBolt() {
1231
+ return /* @__PURE__ */ React2.createElement(
1232
+ "svg",
1233
+ {
1234
+ viewBox: "0 0 16 16",
1235
+ width: "14",
1236
+ height: "14",
1237
+ fill: "none",
1238
+ stroke: "currentColor",
1239
+ strokeWidth: "1.4",
1240
+ strokeLinecap: "round",
1241
+ strokeLinejoin: "round",
1242
+ "aria-hidden": "true"
1243
+ },
1244
+ /* @__PURE__ */ React2.createElement("path", { d: "M8.5 1.5 3 9h4l-1 5.5L13 7H9l-0.5-5.5Z" })
1245
+ );
1246
+ }
1247
+ function IconLayers() {
1248
+ return /* @__PURE__ */ React2.createElement(
1249
+ "svg",
1250
+ {
1251
+ viewBox: "0 0 16 16",
1252
+ width: "14",
1253
+ height: "14",
1254
+ fill: "none",
1255
+ stroke: "currentColor",
1256
+ strokeWidth: "1.4",
1257
+ strokeLinecap: "round",
1258
+ strokeLinejoin: "round",
1259
+ "aria-hidden": "true"
1260
+ },
1261
+ /* @__PURE__ */ React2.createElement("path", { d: "M8 1.75 14 5 8 8.25 2 5Z" }),
1262
+ /* @__PURE__ */ React2.createElement("path", { d: "M2 8 8 11.25 14 8" }),
1263
+ /* @__PURE__ */ React2.createElement("path", { d: "M2 11 8 14.25 14 11" })
1264
+ );
1265
+ }
1266
+ var FIRMWARE_ICONS = {
1267
+ circuitpy: /* @__PURE__ */ React2.createElement(IconChip, null),
1268
+ ota: /* @__PURE__ */ React2.createElement(IconCloud, null),
1269
+ basic: /* @__PURE__ */ React2.createElement(IconBolt, null),
1270
+ standard: /* @__PURE__ */ React2.createElement(IconLayers, null)
1271
+ };
827
1272
  FlashTool.propTypes = {
828
1273
  /** Already-compiled firmware image to flash. Ignored if `firmwares` is set. */
829
- binary: PropTypes.instanceOf(Uint8Array),
1274
+ binary: PropTypes2.instanceOf(Uint8Array),
830
1275
  /** Map of sketch options to their compiled firmware image: { circuitpy, ota, basic, standard }. */
831
- firmwares: PropTypes.shape({
832
- circuitpy: PropTypes.instanceOf(Uint8Array),
833
- ota: PropTypes.instanceOf(Uint8Array),
834
- basic: PropTypes.instanceOf(Uint8Array),
835
- standard: PropTypes.instanceOf(Uint8Array)
1276
+ firmwares: PropTypes2.shape({
1277
+ circuitpy: PropTypes2.instanceOf(Uint8Array),
1278
+ ota: PropTypes2.instanceOf(Uint8Array),
1279
+ basic: PropTypes2.instanceOf(Uint8Array),
1280
+ standard: PropTypes2.instanceOf(Uint8Array)
836
1281
  }),
1282
+ /** Same as `firmwareType`, but also accepts friendlier aliases (e.g. "circuitpython" for "circuitpy"). */
1283
+ firmware: PropTypes2.oneOf([
1284
+ ...FIRMWARE_TYPES,
1285
+ ...Object.keys(FIRMWARE_ALIASES)
1286
+ ]),
837
1287
  /** Locks the sketch dropdown to this value and disables it. One of "circuitpy" | "ota" | "basic" | "standard". */
838
- firmwareType: PropTypes.oneOf(FIRMWARE_TYPES),
1288
+ firmwareType: PropTypes2.oneOf(FIRMWARE_TYPES),
839
1289
  /** Initial dropdown selection when `firmwareType` isn't set (dropdown stays interactive). */
840
- defaultFirmwareType: PropTypes.oneOf(FIRMWARE_TYPES),
1290
+ defaultFirmwareType: PropTypes2.oneOf(FIRMWARE_TYPES),
841
1291
  /** Per-sketch overrides for the esptool flash step, keyed by firmware type. Merges over (and can override) the built-in CircuitPython defaults. */
842
- firmwareFlashOptions: PropTypes.objectOf(PropTypes.object),
1292
+ firmwareFlashOptions: PropTypes2.objectOf(PropTypes2.object),
1293
+ /** Per-sketch description shown under the sketch picker, keyed by firmware type: { text, linkText, linkUrl }. Merges over (and can override) the built-in defaults. */
1294
+ firmwareDescriptions: PropTypes2.objectOf(
1295
+ PropTypes2.shape({
1296
+ text: PropTypes2.string,
1297
+ linkText: PropTypes2.string,
1298
+ linkUrl: PropTypes2.string
1299
+ })
1300
+ ),
843
1301
  /** USB vendor id used to filter the device picker. Defaults to senseBox (0x303a). */
844
- usbVendorId: PropTypes.number,
1302
+ usbVendorId: PropTypes2.number,
845
1303
  /** Overrides for the esptool flash step (address, baudrate, flashMode, flashFreq, flashSize, eraseAll, usingUsbOtg). */
846
- flashOptions: PropTypes.object,
1304
+ flashOptions: PropTypes2.object,
847
1305
  /** Start flashing automatically once the selected firmware and a connected device are both ready. */
848
- autoFlash: PropTypes.bool,
849
- /** Overrides for the built-in (English) UI strings. */
850
- strings: PropTypes.object,
851
- className: PropTypes.string,
852
- onConnected: PropTypes.func,
853
- onDisconnected: PropTypes.func,
854
- onFlashed: PropTypes.func,
855
- onError: PropTypes.func,
1306
+ autoFlash: PropTypes2.bool,
1307
+ /** Keep the log/terminal view expanded by default and disable the button to collapse it. */
1308
+ expand: PropTypes2.bool,
1309
+ /** UI language for the built-in strings. "de" uses the built-in German translation. */
1310
+ language: PropTypes2.oneOf(["en", "de"]),
1311
+ /** Overrides for the built-in UI strings (applied on top of `language`). */
1312
+ strings: PropTypes2.object,
1313
+ className: PropTypes2.string,
1314
+ onConnected: PropTypes2.func,
1315
+ onDisconnected: PropTypes2.func,
1316
+ onFlashed: PropTypes2.func,
1317
+ onError: PropTypes2.func,
856
1318
  /** Called with the newly selected sketch type when the dropdown changes. */
857
- onFirmwareTypeChange: PropTypes.func
1319
+ onFirmwareTypeChange: PropTypes2.func
858
1320
  };
859
1321
  export {
860
1322
  DEFAULT_USB_VENDOR_ID,