Browse Source

Implemented dual-nozzle AMS wiring visualization

- Added AMSSectionDual component with CSS border-based wiring
- Wiring connects AMS slots to hub to extruder with proper 90° corners
- Supports future dynamic filament color display
- Added TemperatureColumn with dual nozzle L/R display
- Added JogPad, BedControls, ExtruderControls components
- Added various control page icons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Martin Ziegler 5 months ago
parent
commit
2f103d607d
73 changed files with 1705 additions and 212 deletions
  1. BIN
      frontend/public/icons/ams-ht.png
  2. 1 0
      frontend/public/icons/ams-settings.svg
  3. 9 0
      frontend/public/icons/ams-wiring-center.svg
  4. 17 0
      frontend/public/icons/ams-wiring-left.svg
  5. 17 0
      frontend/public/icons/ams-wiring-right.svg
  6. BIN
      frontend/public/icons/ams.png
  7. 0 0
      frontend/public/icons/chamber.svg
  8. BIN
      frontend/public/icons/dual-extruder.png
  9. BIN
      frontend/public/icons/extruder-left-right.png
  10. 51 0
      frontend/public/icons/eye.svg
  11. 1 0
      frontend/public/icons/heatbed.svg
  12. 44 0
      frontend/public/icons/home.svg
  13. 1 0
      frontend/public/icons/hotend.svg
  14. BIN
      frontend/public/icons/jogpad.png
  15. 5 0
      frontend/public/icons/jogpad.svg
  16. 1 0
      frontend/public/icons/lamp.svg
  17. 0 0
      frontend/public/icons/micro-sd.svg
  18. 1 0
      frontend/public/icons/reload.svg
  19. 0 0
      frontend/public/icons/settings.svg
  20. BIN
      frontend/public/icons/single-extruder1.png
  21. BIN
      frontend/public/icons/single-extruder2.png
  22. 1 0
      frontend/public/icons/skip-objects.svg
  23. 53 0
      frontend/public/icons/snowflake.svg
  24. 0 0
      frontend/public/icons/speed.svg
  25. 1 0
      frontend/public/icons/temperature.svg
  26. 0 0
      frontend/public/icons/ventilation.svg
  27. 1 0
      frontend/public/icons/video-camera.svg
  28. 2 0
      frontend/public/icons/water.svg
  29. 73 0
      frontend/public/icons/webcam.svg
  30. 337 0
      frontend/src/components/control/AMSSectionDual.tsx
  31. 99 0
      frontend/src/components/control/BedControls.tsx
  32. 63 71
      frontend/src/components/control/CameraFeed.tsx
  33. 98 0
      frontend/src/components/control/ExtruderControls.tsx
  34. 160 0
      frontend/src/components/control/JogPad.tsx
  35. 1 1
      frontend/src/components/control/PrintControls.tsx
  36. 169 69
      frontend/src/components/control/PrintStatus.tsx
  37. 93 0
      frontend/src/components/control/TemperatureColumn.tsx
  38. 22 1
      frontend/src/index.css
  39. 102 68
      frontend/src/pages/ControlPage.tsx
  40. 1 0
      frontend/tailwind.config.js
  41. 0 0
      static/assets/index-BdU-188w.js
  42. 0 0
      static/assets/index-Ca04bhbd.css
  43. 0 0
      static/assets/index-DiPSi5MU.css
  44. BIN
      static/icons/ams-ht.png
  45. 1 0
      static/icons/ams-settings.svg
  46. 9 0
      static/icons/ams-wiring-center.svg
  47. 17 0
      static/icons/ams-wiring-left.svg
  48. 17 0
      static/icons/ams-wiring-right.svg
  49. BIN
      static/icons/ams.png
  50. 0 0
      static/icons/chamber.svg
  51. BIN
      static/icons/dual-extruder.png
  52. BIN
      static/icons/extruder-left-right.png
  53. 51 0
      static/icons/eye.svg
  54. 1 0
      static/icons/heatbed.svg
  55. 44 0
      static/icons/home.svg
  56. 1 0
      static/icons/hotend.svg
  57. BIN
      static/icons/jogpad.png
  58. 5 0
      static/icons/jogpad.svg
  59. 1 0
      static/icons/lamp.svg
  60. 0 0
      static/icons/micro-sd.svg
  61. 1 0
      static/icons/reload.svg
  62. 0 0
      static/icons/settings.svg
  63. BIN
      static/icons/single-extruder1.png
  64. BIN
      static/icons/single-extruder2.png
  65. 1 0
      static/icons/skip-objects.svg
  66. 53 0
      static/icons/snowflake.svg
  67. 0 0
      static/icons/speed.svg
  68. 1 0
      static/icons/temperature.svg
  69. 0 0
      static/icons/ventilation.svg
  70. 1 0
      static/icons/video-camera.svg
  71. 2 0
      static/icons/water.svg
  72. 73 0
      static/icons/webcam.svg
  73. 2 2
      static/index.html

BIN
frontend/public/icons/ams-ht.png


+ 1 - 0
frontend/public/icons/ams-settings.svg

@@ -0,0 +1 @@
+<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill="#000" fill-rule="evenodd"><path d="m2.25 6c0-.41421.33579-.75.75-.75h3.5c.41421 0 .75.33579.75.75s-.33579.75-.75.75h-3.5c-.41421 0-.75-.33579-.75-.75zm8.5 0c0-.41421.3358-.75.75-.75h9.5c.4142 0 .75.33579.75.75s-.3358.75-.75.75h-9.5c-.4142 0-.75-.33579-.75-.75z"/><path d="m9 4.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.79493 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.45507 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.45507-3.25-3.25z"/><path d="m21.75 12c0-.4142-.3358-.75-.75-.75h-2c-.4142 0-.75.3358-.75.75s.3358.75.75.75h2c.4142 0 .75-.3358.75-.75zm-7 0c0-.4142-.3358-.75-.75-.75h-5c-.41421 0-.75.3358-.75.75s.33579.75.75.75h5c.4142 0 .75-.3358.75-.75zm-9 0c0-.4142-.33579-.75-.75-.75h-2c-.41421 0-.75.3358-.75.75s.33579.75.75.75h2c.41421 0 .75-.3358.75-.75z"/><path d="m16.5 10.25c.9665 0 1.75.7835 1.75 1.75s-.7835 1.75-1.75 1.75-1.75-.7835-1.75-1.75.7835-1.75 1.75-1.75zm3.25 1.75c0-1.7949-1.4551-3.25-3.25-3.25s-3.25 1.4551-3.25 3.25 1.4551 3.25 3.25 3.25 3.25-1.4551 3.25-3.25z"/><path d="m2.25 18c0-.4142.33579-.75.75-.75h5c.41421 0 .75.3358.75.75s-.33579.75-.75.75h-5c-.41421 0-.75-.3358-.75-.75zm10 0c0-.4142.3358-.75.75-.75h8c.4142 0 .75.3358.75.75s-.3358.75-.75.75h-8c-.4142 0-.75-.3358-.75-.75z"/><path d="m10.5 16.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.7949 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.4551 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.4551-3.25-3.25z"/></g></svg>

+ 9 - 0
frontend/public/icons/ams-wiring-center.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50">
+  <!-- Left wire: horizontal from left edge, then down to extruder left inlet -->
+  <line x1="0" y1="0" x2="10" y2="0" stroke="#909090" stroke-width="2" />
+  <line x1="10" y1="0" x2="10" y2="50" stroke="#909090" stroke-width="2" />
+
+  <!-- Right wire: horizontal from right edge, then down to extruder right inlet -->
+  <line x1="40" y1="0" x2="30" y2="0" stroke="#909090" stroke-width="2" />
+  <line x1="30" y1="0" x2="30" y2="50" stroke="#909090" stroke-width="2" />
+</svg>

+ 17 - 0
frontend/public/icons/ams-wiring-left.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
+  <!-- Vertical lines from slots down to horizontal bar -->
+  <line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Horizontal bar across all slots -->
+  <line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Center hub box -->
+  <rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
+
+  <!-- Wire from hub: down, then right to edge (at same level as hub horizontal bar) -->
+  <line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+  <line x1="110" y1="35" x2="220" y2="35" stroke="#909090" stroke-width="2" />
+</svg>

+ 17 - 0
frontend/public/icons/ams-wiring-right.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
+  <!-- Vertical lines from slots down to horizontal bar -->
+  <line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Horizontal bar across all slots -->
+  <line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Center hub box -->
+  <rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
+
+  <!-- Wire from hub: down, then left to edge (at same level as hub horizontal bar) -->
+  <line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+  <line x1="0" y1="35" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+</svg>

BIN
frontend/public/icons/ams.png


File diff suppressed because it is too large
+ 0 - 0
frontend/public/icons/chamber.svg


BIN
frontend/public/icons/dual-extruder.png


BIN
frontend/public/icons/extruder-left-right.png


+ 51 - 0
frontend/public/icons/eye.svg

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M508.745,246.041c-4.574-6.257-113.557-153.206-252.748-153.206S7.818,239.784,3.249,246.035
+			c-4.332,5.936-4.332,13.987,0,19.923c4.569,6.257,113.557,153.206,252.748,153.206s248.174-146.95,252.748-153.201
+			C513.083,260.028,513.083,251.971,508.745,246.041z M255.997,385.406c-102.529,0-191.33-97.533-217.617-129.418
+			c26.253-31.913,114.868-129.395,217.617-129.395c102.524,0,191.319,97.516,217.617,129.418
+			C447.361,287.923,358.746,385.406,255.997,385.406z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M255.997,154.725c-55.842,0-101.275,45.433-101.275,101.275s45.433,101.275,101.275,101.275
+			s101.275-45.433,101.275-101.275S311.839,154.725,255.997,154.725z M255.997,323.516c-37.23,0-67.516-30.287-67.516-67.516
+			s30.287-67.516,67.516-67.516s67.516,30.287,67.516,67.516S293.227,323.516,255.997,323.516z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 1 - 0
frontend/public/icons/heatbed.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><g fill="rgb(0,0,0)"><path d="m6.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m12.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m18.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m22 13.5c-1.07 0-1.61.65-2.05 1.18-.44.52-.71.82-1.29.82s-.85-.3-1.29-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.85-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.84-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18-.28 0-.5.22-.5.5s.22.5.5.5c.57 0 .84.3 1.28.82.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.28.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.29.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.29-.82.28 0 .5-.22.5-.5s-.22-.5-.5-.5z"/><path d="m21 18.5h-18c-.83 0-1.5.67-1.5 1.5v1c0 .83.67 1.5 1.5 1.5h18c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5zm.5 2.5c0 .28-.22.5-.5.5h-18c-.28 0-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5h18c.28 0 .5.22.5.5z"/></g></svg>

+ 44 - 0
frontend/public/icons/home.svg

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 476.912 476.912" style="enable-background:new 0 0 476.912 476.912;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M461.776,209.408L249.568,4.52c-6.182-6.026-16.042-6.026-22.224,0L15.144,209.4c-3.124,3.015-4.888,7.17-4.888,11.512
+			c0,8.837,7.164,16,16,16h28.2v224c0,8.837,7.163,16,16,16h112c8.837,0,16-7.163,16-16v-128h80v128c0,8.837,7.163,16,16,16h112
+			c8.837,0,16-7.163,16-16v-224h28.2c4.338,0,8.489-1.761,11.504-4.88C468.301,225.678,468.129,215.549,461.776,209.408z
+			 M422.456,220.912c-8.837,0-16,7.163-16,16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837,0-16,7.163-16,16v128h-112v-224
+			c0-8.837-7.163-16-16-16h-28.2l212.2-204.88l212.28,204.88H422.456z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 1 - 0
frontend/public/icons/hotend.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><g id="Front_Heater" data-name="Front Heater"><path d="m33.14 29h2.86l-4-7-4 7h3l1.5 3.7a8.38 8.38 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m23.22 29h2.78l-4-7-4 7h3.06l1.5 3.7a8.35 8.35 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .24-1.39 8.4 8.4 0 0 1 .2-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m57.88 20.09a3 3 0 0 0 -1.49-1.84l-7.73-4.1a35.48 35.48 0 0 0 -33.32 0l-7.72 4.1a3 3 0 0 0 -1.22 4.2l9.72 17a3 3 0 0 0 2.6 1.55 3 3 0 0 0 1.39-.34 1 1 0 0 0 .42-1.35 1 1 0 0 0 -1.34-.42 1 1 0 0 1 -1.33-.39l-9.72-17a1 1 0 0 1 -.1-.81 1 1 0 0 1 .52-.69l7.72-4.1a33.46 33.46 0 0 1 31.44 0l7.73 4.1a1 1 0 0 1 .51.63 1 1 0 0 1 -.1.81l-9.72 17a1 1 0 0 1 -1.33.39l-.2-.11a10.82 10.82 0 0 0 -.36-6.72l-1.19-3h2.94l-4-7-4 7h2.9l1.5 3.7a8.38 8.38 0 0 1 -.93 8l-.66.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.64-.78a8.27 8.27 0 0 0 .84-1.3 3 3 0 0 0 4-1.17l9.72-17a3 3 0 0 0 .32-2.44z"/></g></svg>

BIN
frontend/public/icons/jogpad.png


File diff suppressed because it is too large
+ 5 - 0
frontend/public/icons/jogpad.svg


+ 1 - 0
frontend/public/icons/lamp.svg

@@ -0,0 +1 @@
+<svg id="Ecommerce" enable-background="new 0 0 48 48" height="512" viewBox="0 0 48 48" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m25 7.03c0-.552-.447-1-1-1-5.514 0-10 4.486-10 10 0 .552.447 1 1 1s1-.448 1-1c0-4.411 3.589-8 8-8 .553 0 1-.448 1-1z"/><path d="m22.246 45.79h1.754 1.754c1.652 0 2.999-1.345 3-3v-1.099c1.032-.475 1.755-1.512 1.755-2.721v-3.72c.176-.382.281-.802.281-1.25 0-1.016.226-2 .671-2.927.441-.919 1.086-1.752 1.864-2.409 3.746-3.165 5.709-7.989 5.25-12.909-.7-7.375-6.813-13.189-14.265-13.525l-.31-.01-.354.011c-7.408.335-13.521 6.149-14.222 13.526-.458 4.917 1.505 9.742 5.251 12.906.778.658 1.423 1.491 1.864 2.41.445.927.671 1.911.671 2.927 0 .447.105.868.281 1.25v3.721c0 1.209.722 2.247 1.755 2.721v1.1c.001 1.653 1.348 2.998 3 2.998zm4.508-3c0 .552-.449 1-1 1h-1.754-1.754c-.551 0-1-.449-1-1v-.82h2.754 2.754zm1.755-3.819c0 .551-.448 1-1 1h-3.509-3.509c-.552 0-1-.449-1-1v-2.068c.232.058.47.097.719.097h3.79 3.79c.249 0 .487-.039.719-.097zm-9.299-4.971c0-1.318-.292-2.595-.868-3.793-.563-1.171-1.385-2.233-2.376-3.071-3.247-2.742-4.948-6.926-4.551-11.191.607-6.389 5.903-11.426 12.275-11.715l.31-.01.265.009c6.417.29 11.713 5.327 12.319 11.714.398 4.267-1.303 8.451-4.55 11.193-.991.838-1.813 1.9-2.376 3.071-.576 1.198-.868 2.475-.868 3.793 0 .551-.448 1-1 1h-3.79-3.79c-.552 0-1-.449-1-1z"/></g></svg>

File diff suppressed because it is too large
+ 0 - 0
frontend/public/icons/micro-sd.svg


+ 1 - 0
frontend/public/icons/reload.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22 11a1 1 0 0 0 -1 1 9 9 0 1 1 -9-9 8.9 8.9 0 0 1 4.42 1.166l-1.127 1.127a1 1 0 0 0 .707 1.707h4a1 1 0 0 0 1-1v-4a1 1 0 0 0 -1.707-.707l-1.411 1.407a10.9 10.9 0 0 0 -5.882-1.7 11 11 0 1 0 11 11 1 1 0 0 0 -1-1z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
frontend/public/icons/settings.svg


BIN
frontend/public/icons/single-extruder1.png


BIN
frontend/public/icons/single-extruder2.png


+ 1 - 0
frontend/public/icons/skip-objects.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" height="512" viewBox="0 0 32 32" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m30 17-3 3-3-3h2c0-6.0654-4.9355-11-11-11s-11 4.9346-11 11h-2c0-7.168 5.832-13 13-13s13 5.832 13 13zm0 5h-6v2h6zm-10 0h-2v2h2zm-16 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2z"/></svg>

+ 53 - 0
frontend/public/icons/snowflake.svg

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 412.8 412.8" style="enable-background:new 0 0 412.8 412.8;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M378.4,225.6L304,251.2L274,234v-27.6v-27.2l30-17.2l74.4,25.6c5.2,2,11.2-1.2,12.8-6.4c2-5.2-1.2-11.2-6.4-12.8
+			l-57.6-19.6l54-31.2c4.8-2.8,6.4-9.2,3.6-14c-2.8-4.8-9.2-6.4-14-3.6l-54,31.2l11.6-59.6c1.2-5.6-2.4-10.8-8-12
+			c-5.6-1.2-10.8,2.4-12,8l-15.2,77.2l-30,17.2l-22.8-13.2l-0.4-0.4l-23.2-13.6v-34.4L276,48.8c4.4-3.6,4.8-10,0.8-14.4
+			c-3.6-4.4-10-4.8-14.4-0.8l-45.6,40V10.4c0-5.6-4.4-10.4-10.4-10.4C200.8,0,196,4.4,196,10.4v62.4l-45.6-39.6
+			C146,29.6,139.6,30,136,34c-3.6,4.4-3.2,10.8,0.8,14.4L196,100v34.4L172.8,148l-23.2,13.6l-30-17.2l-15.2-77.2
+			c-1.2-5.6-6.4-9.2-12-8c-5.6,1.2-9.2,6.4-8,12L96,130.8L42,99.6c-4.8-2.8-11.2-1.2-14,3.6s-1.2,11.2,3.6,14l54,31.2L28,168
+			c-5.2,2-8.4,7.6-6.4,12.8s7.6,8.4,12.8,6.4l74.4-25.6l30,17.2v27.6v27.2h0.4l-30,17.2l-74.4-25.6c-5.2-2-11.2,1.2-12.8,6.4
+			c-2,5.2,1.2,11.2,6.4,12.8L86,264l-54,31.2c-4.8,2.8-6.4,9.2-3.6,14c2.8,4.8,9.2,6.4,14,3.6l54-31.2l-11.6,59.6
+			c-1.2,5.6,2.4,10.8,8,12c5.6,1.2,10.8-2.4,12-8L120,268l30-17.2l23.6,13.6l23.2,13.6v34.4L137.6,364c-4.4,3.6-4.8,10-0.8,14.4
+			c3.6,4.4,10,4.8,14.4,0.8l45.6-40v63.2c0,5.6,4.4,10.4,10.4,10.4c5.6,0,10.4-4.4,10.4-10.4V340l45.6,40c4.4,3.6,10.8,3.2,14.4-0.8
+			c3.6-4.4,3.2-10.8-0.8-14.4l-60-52v-34.4l23.2-13.6l23.2-13.6l30,17.2l15.2,77.2c1.2,5.6,6.4,9.2,12,8c5.6-1.2,9.2-6.4,8-12
+			L316.8,282l54,31.2c4.8,2.8,11.2,1.2,14-3.6c2.8-4.8,1.2-11.2-3.6-14l-54-31.2l57.6-19.6c5.2-2,8.4-7.6,6.4-12.8
+			C389.2,226.8,383.6,223.6,378.4,225.6z M252.4,206.4v27.2l-23.2,13.6l-22.8,13.2l-23.6-13.6l-23.2-13.6v-26.8v-27.2l23.2-13.6
+			L206,152l23.2,13.6l0.4,0.4l22.8,13.2V206.4z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

File diff suppressed because it is too large
+ 0 - 0
frontend/public/icons/speed.svg


+ 1 - 0
frontend/public/icons/temperature.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m36 39.47v-32.47a7 7 0 1 0 -14 0v32.47a13.26 13.26 0 1 0 14 0zm-7 22.59a11.32 11.32 0 0 1 -5.53-21.19 1 1 0 0 0 .53-.87v-33a5 5 0 1 1 10 0v33a1 1 0 0 0 .49.84 11.32 11.32 0 0 1 -5.49 21.22z"/><path d="m30 44.89v-30.25a1 1 0 1 0 -1.94 0v30.25a5.94 5.94 0 1 0 1.94 0zm-1 9.85a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m40.32 9.64h7a1 1 0 0 0 0-1.94h-7a1 1 0 1 0 0 1.94z"/><path d="m40.32 16.06h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 20.55h-7a1 1 0 1 0 0 1.93h7a1 1 0 1 0 0-1.93z"/><path d="m40.32 28.91h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 33.39h-7a1 1 0 0 0 0 1.94h7a1 1 0 1 0 0-1.94z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
frontend/public/icons/ventilation.svg


+ 1 - 0
frontend/public/icons/video-camera.svg

@@ -0,0 +1 @@
+<svg height="472pt" viewBox="0 -87 472 472" width="472pt" xmlns="http://www.w3.org/2000/svg"><path d="m467.101562 26.527344c-3.039062-1.800782-6.796874-1.871094-9.898437-.179688l-108.296875 59.132813v-35.480469c-.03125-27.601562-22.398438-49.96875-50-50h-248.90625c-27.601562.03125-49.96875 22.398438-50 50v197.421875c.03125 27.601563 22.398438 49.96875 50 50h248.90625c27.601562-.03125 49.96875-22.398437 50-50v-34.835937l108.300781 59.132812c3.097657 1.691406 6.859375 1.625 9.894531-.175781 3.039063-1.804688 4.898438-5.074219 4.898438-8.601563v-227.816406c0-3.53125-1.863281-6.796875-4.898438-8.597656zm-138.203124 220.898437c-.015626 16.5625-13.4375 29.980469-30 30h-248.898438c-16.5625-.019531-29.980469-13.4375-30-30v-197.425781c.019531-16.558594 13.4375-29.980469 30-30h248.90625c16.558594.019531 29.980469 13.441406 30 30zm123.101562-1.335937-103.09375-56.289063v-81.535156l103.09375-56.285156zm0 0"/></svg>

+ 2 - 0
frontend/public/icons/water.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="512" height="512"><g id="Water"><path d="M24,46A16.0183,16.0183,0,0,1,8,30C8,16.0942,22.708,2.8125,23.3345,2.2539a.9983.9983,0,0,1,1.331,0C25.292,2.8125,40,16.0942,40,30A16.0183,16.0183,0,0,1,24,46ZM24,4.3721C21.1333,7.1372,10,18.6118,10,30a14,14,0,0,0,28,0C38,18.6118,26.8667,7.1372,24,4.3721Z"/><path d="M18.4976,40.5273a.9946.9946,0,0,1-.5-.1342A12.0449,12.0449,0,0,1,12,30a1,1,0,0,1,2,0,10.0373,10.0373,0,0,0,5,8.6616,1,1,0,0,1-.5019,1.8657Z"/></g></svg>

+ 73 - 0
frontend/public/icons/webcam.svg

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M256,40c-5.52,0-10,4.48-10,10s4.48,10,10,10s10-4.48,10-10S261.52,40,256,40z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M466,210C466,94.206,371.794,0,256,0S46,94.206,46,210c0,96.488,66.579,180.855,159.516,203.859
+			c-1.591,14.119-6.958,31.441-13.568,38.051l-0.131,0.131c-18.899,0.353-32.638,3.149-42.999,8.73
+			C133.677,468.949,126,482.82,126,502c0,5.522,4.478,10,10,10h240c5.522,0,10-4.478,10-10c0-19.187-7.68-33.058-22.824-41.229
+			c-10.344-5.58-24.082-8.378-42.992-8.731l-0.132-0.132c-6.61-6.609-11.977-23.931-13.568-38.05
+			C399.423,390.853,466,306.486,466,210z M316,472c33.23,0,45.303,7.689,48.794,20H147.226c2.172-7.762,6.862-11.345,11.087-13.626
+			C166.274,474.085,178.603,472,196,472H316z M215.517,452c5.068-10.601,8.238-23.466,9.638-34.27
+			C235.326,419.232,245.658,420,256,420c10.342,0,20.674-0.768,30.845-2.27c1.401,10.804,4.57,23.67,9.638,34.27H215.517z
+			 M294.015,396.179c-0.019,0.004-0.037,0.007-0.056,0.011c-24.788,5.056-51.127,5.057-75.922-0.001
+			c-0.017-0.004-0.035-0.007-0.052-0.01C129.918,378.227,66,299.929,66,210c0-104.767,85.233-190,190-190s190,85.233,190,190
+			C446,299.929,382.082,378.227,294.015,396.179z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M389.606,104.994c-23.072-29.303-55.544-50.505-91.434-59.701c-5.355-1.374-10.799,1.855-12.17,7.205
+			c-1.37,5.35,1.855,10.798,7.205,12.169c31.66,8.112,60.314,26.828,80.686,52.7c3.426,4.352,9.716,5.077,14.043,1.67
+			C392.275,115.621,393.023,109.333,389.606,104.994z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M256,100c-60.654,0-110,49.346-110,110s49.346,110,110,110s110-49.346,110-110S316.654,100,256,100z M256,300
+			c-49.626,0-90-40.374-90-90c0-49.626,40.374-90,90-90c49.626,0,90,40.374,90,90C346,259.626,305.626,300,256,300z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M256,140c-38.598,0-70,31.402-70,70c0,38.598,31.402,70,70,70c38.598,0,70-31.402,70-70C326,171.402,294.598,140,256,140z
+			 M256,260c-27.57,0-50-22.43-50-50s22.43-50,50-50s50,22.43,50,50S283.57,260,256,260z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 337 - 0
frontend/src/components/control/AMSSectionDual.tsx

@@ -0,0 +1,337 @@
+import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { api } from '../../api/client';
+import type { PrinterStatus, AMSUnit } from '../../api/client';
+import { Loader2 } from 'lucide-react';
+
+interface AMSSectionDualProps {
+  printerId: number;
+  status: PrinterStatus | null | undefined;
+  nozzleCount: number;
+}
+
+function hexToRgb(hex: string | null): string {
+  if (!hex) return 'rgb(128, 128, 128)';
+  const cleanHex = hex.replace('#', '').substring(0, 6);
+  const r = parseInt(cleanHex.substring(0, 2), 16) || 128;
+  const g = parseInt(cleanHex.substring(2, 4), 16) || 128;
+  const b = parseInt(cleanHex.substring(4, 6), 16) || 128;
+  return `rgb(${r}, ${g}, ${b})`;
+}
+
+function isLightColor(hex: string | null): boolean {
+  if (!hex) return false;
+  const cleanHex = hex.replace('#', '').substring(0, 6);
+  const r = parseInt(cleanHex.substring(0, 2), 16) || 0;
+  const g = parseInt(cleanHex.substring(2, 4), 16) || 0;
+  const b = parseInt(cleanHex.substring(4, 6), 16) || 0;
+  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+  return luminance > 0.5;
+}
+
+interface AMSPanelContentProps {
+  units: AMSUnit[];
+  side: 'left' | 'right';
+  isPrinting: boolean;
+  selectedAmsIndex: number;
+  onSelectAms: (index: number) => void;
+  selectedTray: number | null;
+  onSelectTray: (trayId: number | null) => void;
+}
+
+function AMSPanelContent({
+  units,
+  side,
+  isPrinting,
+  selectedAmsIndex,
+  onSelectAms,
+  selectedTray,
+  onSelectTray,
+}: AMSPanelContentProps) {
+  const selectedUnit = units[selectedAmsIndex];
+  const slotPrefix = side === 'left' ? 'A' : 'B';
+
+  return (
+    <div className="flex-1 min-w-0 overflow-visible">
+      <div className="text-center text-[11px] font-semibold text-bambu-gray uppercase mb-2">
+        {side === 'left' ? 'Left Nozzle' : 'Right Nozzle'}
+      </div>
+
+      {/* AMS Tab Selectors - only show connected units */}
+      <div className="flex gap-1.5 mb-2.5 p-1.5 bg-bambu-dark/50 rounded-lg w-fit">
+        {units.map((unit, index) => (
+          <button
+            key={unit.id}
+            onClick={() => onSelectAms(index)}
+            className={`flex items-center p-1.5 rounded border-2 transition-colors bg-bambu-dark ${
+              selectedAmsIndex === index
+                ? 'border-bambu-green'
+                : 'border-transparent hover:border-bambu-gray'
+            }`}
+          >
+            <div className="flex gap-0.5">
+              {unit.tray.map((tray) => (
+                <div
+                  key={tray.id}
+                  className="w-2.5 h-2.5 rounded-full"
+                  style={{
+                    backgroundColor: tray.tray_color ? hexToRgb(tray.tray_color) : '#808080',
+                  }}
+                />
+              ))}
+            </div>
+          </button>
+        ))}
+      </div>
+
+      {/* AMS Content */}
+      {selectedUnit && (
+        <div className="bg-bambu-dark-secondary rounded-[10px] p-2.5 pb-0 overflow-visible">
+          {/* AMS Header - Humidity & Temp */}
+          <div className="flex items-center gap-2.5 text-xs text-bambu-gray mb-2">
+            {selectedUnit.humidity !== null && (
+              <span className="flex items-center gap-1">
+                <img src="/icons/water.svg" alt="" className="w-3.5 icon-theme" />
+                {selectedUnit.humidity} %
+              </span>
+            )}
+            {selectedUnit.temp !== null && (
+              <span className="flex items-center gap-1">
+                <img src="/icons/temperature.svg" alt="" className="w-3.5 icon-theme" />
+                {selectedUnit.temp}°C
+              </span>
+            )}
+            <span className="text-yellow-500 text-sm">☀</span>
+          </div>
+
+          {/* Slot Labels */}
+          <div className="flex justify-center gap-1.5 mb-1.5">
+            {selectedUnit.tray.map((tray, index) => (
+              <div
+                key={tray.id}
+                className="w-12 flex items-center justify-center gap-0.5 text-[10px] text-bambu-gray px-1.5 py-[3px] bg-bambu-dark rounded-full border border-bambu-dark-tertiary"
+              >
+                {slotPrefix}{index + 1}
+                <img src="/icons/reload.svg" alt="" className="w-2.5 h-2.5 icon-theme" />
+              </div>
+            ))}
+          </div>
+
+          {/* AMS Slots with integrated wiring */}
+          <div className="flex justify-center gap-1.5 mb-0">
+            {selectedUnit.tray.map((tray) => {
+              const globalTrayId = selectedUnit.id * 4 + tray.id;
+              const isSelected = selectedTray === globalTrayId;
+              const isEmpty = !tray.tray_type || tray.tray_type === '' || tray.tray_type === 'NONE';
+              const isLight = isLightColor(tray.tray_color);
+
+              return (
+                <div key={tray.id} className="flex flex-col items-center">
+                  <button
+                    onClick={() => !isEmpty && onSelectTray(isSelected ? null : globalTrayId)}
+                    disabled={isEmpty || isPrinting}
+                    className={`w-12 h-[70px] rounded-md border-2 overflow-hidden transition-all bg-bambu-dark ${
+                      isSelected
+                        ? 'border-[#d4a84b]'
+                        : 'border-bambu-dark-tertiary hover:border-bambu-gray'
+                    } ${isEmpty ? 'opacity-50' : ''} disabled:cursor-not-allowed`}
+                  >
+                    <div
+                      className="w-full h-full flex flex-col items-center justify-end pb-[5px]"
+                      style={{
+                        backgroundColor: isEmpty ? undefined : hexToRgb(tray.tray_color),
+                      }}
+                    >
+                      <span
+                        className={`text-[11px] font-semibold mb-1 ${
+                          isLight ? 'text-gray-800' : 'text-white'
+                        } ${isLight ? '' : 'drop-shadow-sm'}`}
+                      >
+                        {isEmpty ? '--' : tray.tray_type}
+                      </span>
+                      {!isEmpty && (
+                        <img
+                          src="/icons/eye.svg"
+                          alt=""
+                          className={`w-3.5 h-3.5 ${isLight ? '' : 'invert'}`}
+                          style={{ opacity: 0.8 }}
+                        />
+                      )}
+                    </div>
+                  </button>
+                  {/* Vertical wire from slot center down */}
+                  <div className="w-[2px] h-[14px] bg-[#909090]" />
+                </div>
+              );
+            })}
+          </div>
+
+          {/* Wiring visualization - horizontal bar and hub */}
+          <div className="flex justify-center">
+            <div className="relative h-[50px]" style={{ width: '210px' }}>
+              {/* Horizontal bar connecting all slots (spans from first to last slot center) */}
+              <div className="absolute left-[24px] right-[24px] top-0 border-t-2 border-[#909090]" />
+
+            {/* Center hub box on the horizontal bar */}
+            <div className="absolute left-1/2 -translate-x-1/2 top-[-6px] w-[28px] h-[14px] bg-[#c0c0c0] border border-[#909090] rounded-sm" />
+
+            {/* Vertical wire from hub going down */}
+            <div className="absolute left-1/2 -translate-x-[1px] top-[8px] h-[14px] border-l-2 border-[#909090]" />
+
+            {/* Horizontal wire from hub toward the center of the panel (extends beyond panel edge) */}
+            {side === 'left' && (
+              <div className="absolute left-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
+            )}
+            {side === 'right' && (
+              <div className="absolute right-1/2 top-[21px] w-[calc(50%+30px)] border-t-2 border-[#909090]" />
+            )}
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* No AMS message */}
+      {units.length === 0 && (
+        <div className="bg-bambu-dark-secondary rounded-[10px] p-6 text-center text-bambu-gray text-sm">
+          No AMS connected to {side} nozzle
+        </div>
+      )}
+    </div>
+  );
+}
+
+export function AMSSectionDual({ printerId, status, nozzleCount }: AMSSectionDualProps) {
+  const isConnected = status?.connected ?? false;
+  const isPrinting = status?.state === 'RUNNING';
+  const isDualNozzle = nozzleCount > 1;
+  const amsUnits: AMSUnit[] = status?.ams ?? [];
+
+  // For dual nozzle, split AMS units between left and right
+  // In real implementation, this would be based on actual nozzle assignment
+  const leftUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 0) : amsUnits;
+  const rightUnits = isDualNozzle ? amsUnits.filter((_, i) => i % 2 === 1) : [];
+
+  const [leftAmsIndex, setLeftAmsIndex] = useState(0);
+  const [rightAmsIndex, setRightAmsIndex] = useState(0);
+  const [selectedTray, setSelectedTray] = useState<number | null>(null);
+
+  const loadMutation = useMutation({
+    mutationFn: (trayId: number) => api.amsLoadFilament(printerId, trayId),
+  });
+
+  const unloadMutation = useMutation({
+    mutationFn: () => api.amsUnloadFilament(printerId),
+  });
+
+  const handleLoad = () => {
+    if (selectedTray !== null) {
+      loadMutation.mutate(selectedTray);
+    }
+  };
+
+  const handleUnload = () => {
+    unloadMutation.mutate();
+  };
+
+  const isLoading = loadMutation.isPending || unloadMutation.isPending;
+
+  return (
+    <div className="bg-bambu-dark-tertiary rounded-[10px] p-3 relative overflow-visible">
+      {/* Center wiring and Extruder - absolutely centered between the two AMS panels */}
+      {isDualNozzle && (
+        <>
+          {/* Center wiring: two vertical lines going down to extruder inlets */}
+          {/* Positioned to connect with horizontal wires from AMS panels */}
+          <div className="absolute left-1/2 -translate-x-1/2 bottom-[62px] pointer-events-none" style={{ width: '24px', height: '30px' }}>
+            {/* Left vertical line - connects to left AMS horizontal wire, goes to left extruder inlet */}
+            <div className="absolute left-0 top-0 h-full border-l-2 border-[#909090]" />
+            {/* Right vertical line - connects to right AMS horizontal wire, goes to right extruder inlet */}
+            <div className="absolute right-0 top-0 h-full border-l-2 border-[#909090]" />
+          </div>
+          {/* Extruder */}
+          <img
+            src="/icons/extruder-left-right.png"
+            alt="Extruder"
+            className="absolute h-[50px] left-1/2 -translate-x-1/2 bottom-[12px]"
+          />
+        </>
+      )}
+
+      {/* Dual Panel Layout */}
+      <div className="flex gap-5 overflow-visible">
+        {/* Left Nozzle Panel */}
+        <AMSPanelContent
+          units={leftUnits}
+          side="left"
+          isPrinting={isPrinting}
+          selectedAmsIndex={leftAmsIndex}
+          onSelectAms={setLeftAmsIndex}
+          selectedTray={selectedTray}
+          onSelectTray={setSelectedTray}
+        />
+
+        {/* Right Nozzle Panel - only for dual nozzle */}
+        {isDualNozzle && (
+          <AMSPanelContent
+            units={rightUnits}
+            side="right"
+            isPrinting={isPrinting}
+            selectedAmsIndex={rightAmsIndex}
+            onSelectAms={setRightAmsIndex}
+            selectedTray={selectedTray}
+            onSelectTray={setSelectedTray}
+          />
+        )}
+      </div>
+
+      {/* Action Buttons Row with Extruder */}
+      <div className="flex items-start pt-2">
+        {/* Left buttons */}
+        <div className="flex items-center gap-2">
+          <button className="w-10 h-10 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary flex items-center justify-center">
+            <img src="/icons/ams-settings.svg" alt="Settings" className="w-5 icon-theme" />
+          </button>
+          <button className="px-[18px] py-2.5 rounded-lg bg-bambu-dark-secondary hover:bg-bambu-dark border border-bambu-dark-tertiary text-sm text-bambu-gray flex items-center gap-1.5">
+            Auto-refill
+          </button>
+        </div>
+
+        {/* Spacer */}
+        <div className="flex-1" />
+
+        {/* Right buttons */}
+        <div className="flex items-center gap-2">
+          <button
+            onClick={handleUnload}
+            disabled={!isConnected || isPrinting || isLoading}
+            className="px-7 py-2.5 rounded-lg bg-bambu-dark hover:bg-bambu-dark-secondary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            {unloadMutation.isPending ? (
+              <Loader2 className="w-4 h-4 animate-spin" />
+            ) : (
+              'Unload'
+            )}
+          </button>
+          <button
+            onClick={handleLoad}
+            disabled={!isConnected || isPrinting || selectedTray === null || isLoading}
+            className="px-7 py-2.5 rounded-lg bg-bambu-dark hover:bg-bambu-dark-secondary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            {loadMutation.isPending ? (
+              <Loader2 className="w-4 h-4 animate-spin" />
+            ) : (
+              'Load'
+            )}
+          </button>
+        </div>
+      </div>
+
+      {/* Error messages */}
+      {(loadMutation.error || unloadMutation.error) && (
+        <p className="mt-2 text-sm text-red-500 text-center">
+          {(loadMutation.error || unloadMutation.error)?.message}
+        </p>
+      )}
+    </div>
+  );
+}

+ 99 - 0
frontend/src/components/control/BedControls.tsx

@@ -0,0 +1,99 @@
+import { useMutation } from '@tanstack/react-query';
+import { api, isConfirmationRequired } from '../../api/client';
+import type { PrinterStatus } from '../../api/client';
+import { useState } from 'react';
+import { ConfirmModal } from '../ConfirmModal';
+
+interface BedControlsProps {
+  printerId: number;
+  status: PrinterStatus | null | undefined;
+}
+
+export function BedControls({ printerId, status }: BedControlsProps) {
+  const isConnected = status?.connected ?? false;
+
+  const [confirmModal, setConfirmModal] = useState<{
+    token: string;
+    warning: string;
+    distance: number;
+  } | null>(null);
+
+  const moveMutation = useMutation({
+    mutationFn: ({ distance, token }: { distance: number; token?: string }) =>
+      api.moveAxis(printerId, 'Z', distance, 1000, token),
+    onSuccess: (result, variables) => {
+      if (isConfirmationRequired(result)) {
+        setConfirmModal({
+          token: result.token,
+          warning: result.warning,
+          distance: variables.distance,
+        });
+      }
+    },
+  });
+
+  const handleMove = (distance: number) => {
+    moveMutation.mutate({ distance });
+  };
+
+  const handleConfirm = () => {
+    if (confirmModal) {
+      moveMutation.mutate({ distance: confirmModal.distance, token: confirmModal.token });
+      setConfirmModal(null);
+    }
+  };
+
+  const isDisabled = !isConnected || moveMutation.isPending;
+
+  return (
+    <>
+      <div className="flex items-center gap-2">
+        <button
+          onClick={() => handleMove(10)}
+          disabled={isDisabled}
+          className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          title="Z+10"
+        >
+          ↑10
+        </button>
+        <button
+          onClick={() => handleMove(1)}
+          disabled={isDisabled}
+          className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          title="Z+1"
+        >
+          ↑1
+        </button>
+        <span className="px-2 py-2 text-sm text-bambu-gray">Bed</span>
+        <button
+          onClick={() => handleMove(-1)}
+          disabled={isDisabled}
+          className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          title="Z-1"
+        >
+          ↓1
+        </button>
+        <button
+          onClick={() => handleMove(-10)}
+          disabled={isDisabled}
+          className="px-3.5 py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary text-sm text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+          title="Z-10"
+        >
+          ↓10
+        </button>
+      </div>
+
+      {/* Confirmation Modal */}
+      {confirmModal && (
+        <ConfirmModal
+          title="Confirm Bed Movement"
+          message={confirmModal.warning}
+          confirmText="Continue"
+          variant="warning"
+          onConfirm={handleConfirm}
+          onCancel={() => setConfirmModal(null)}
+        />
+      )}
+    </>
+  );
+}

+ 63 - 71
frontend/src/components/control/CameraFeed.tsx

@@ -55,84 +55,76 @@ export function CameraFeed({ printerId, isConnected }: CameraFeedProps) {
   };
 
   return (
-    <div className="bg-bambu-dark-secondary rounded-lg overflow-hidden">
-      <div className="flex items-center justify-between px-4 py-2 border-b border-bambu-dark-tertiary">
-        <div className="flex items-center gap-2">
-          <Camera className="w-4 h-4 text-bambu-gray" />
-          <span className="text-sm font-medium">Camera</span>
+    <div className="relative w-full h-full bg-black">
+      {!streamEnabled ? (
+        <div className="absolute inset-0 flex flex-col items-center justify-center text-bambu-gray">
+          <div className="bg-bambu-dark-secondary rounded-lg p-6 flex flex-col items-center">
+            <Camera className="w-8 h-8 mb-2" />
+            <span className="text-sm mb-3">
+              {isConnected ? 'Click Start to view camera' : 'Printer not connected'}
+            </span>
+            <button
+              onClick={handleToggleStream}
+              disabled={!isConnected}
+              className="px-4 py-1.5 rounded text-sm bg-bambu-green text-white hover:bg-bambu-green-dark disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              Start
+            </button>
+          </div>
         </div>
-        <div className="flex items-center gap-2">
-          {streamEnabled && (
-            <>
+      ) : (
+        <>
+          {isLoading && (
+            <div className="absolute inset-0 flex items-center justify-center">
+              <Loader2 className="w-8 h-8 animate-spin text-bambu-green" />
+            </div>
+          )}
+          {error ? (
+            <div className="absolute inset-0 flex flex-col items-center justify-center text-red-400">
+              <CameraOff className="w-12 h-12 mb-2" />
+              <span className="text-sm">{error}</span>
               <button
                 onClick={handleRefresh}
-                className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
-                title="Refresh stream"
+                className="mt-2 text-xs text-bambu-green hover:underline"
               >
-                <RefreshCw className="w-4 h-4" />
+                Retry
               </button>
-              <button
-                onClick={handleFullscreen}
-                className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
-                title="Fullscreen"
-              >
-                <Maximize2 className="w-4 h-4" />
-              </button>
-            </>
+            </div>
+          ) : (
+            <img
+              ref={imgRef}
+              src={streamUrl}
+              alt="Camera stream"
+              className="w-full h-full object-contain"
+              onLoad={handleImageLoad}
+              onError={handleImageError}
+            />
           )}
-          <button
-            onClick={handleToggleStream}
-            disabled={!isConnected}
-            className={`px-3 py-1 rounded text-sm transition-colors ${
-              streamEnabled
-                ? 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
-                : 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
-            } disabled:opacity-50 disabled:cursor-not-allowed`}
-          >
-            {streamEnabled ? 'Stop' : 'Start'}
-          </button>
-        </div>
-      </div>
-
-      <div className="relative aspect-video bg-bambu-dark">
-        {!streamEnabled ? (
-          <div className="absolute inset-0 flex flex-col items-center justify-center text-bambu-gray">
-            <CameraOff className="w-12 h-12 mb-2" />
-            <span className="text-sm">
-              {isConnected ? 'Click Start to view camera' : 'Printer not connected'}
-            </span>
+          {/* Overlay controls */}
+          <div className="absolute top-2 right-2 flex gap-2">
+            <button
+              onClick={handleRefresh}
+              className="p-2 rounded bg-black/50 hover:bg-black/70 text-white transition-colors"
+              title="Refresh stream"
+            >
+              <RefreshCw className="w-4 h-4" />
+            </button>
+            <button
+              onClick={handleFullscreen}
+              className="p-2 rounded bg-black/50 hover:bg-black/70 text-white transition-colors"
+              title="Fullscreen"
+            >
+              <Maximize2 className="w-4 h-4" />
+            </button>
+            <button
+              onClick={handleToggleStream}
+              className="px-3 py-1.5 rounded bg-red-500/80 hover:bg-red-500 text-white text-sm transition-colors"
+            >
+              Stop
+            </button>
           </div>
-        ) : (
-          <>
-            {isLoading && (
-              <div className="absolute inset-0 flex items-center justify-center bg-bambu-dark">
-                <Loader2 className="w-8 h-8 animate-spin text-bambu-green" />
-              </div>
-            )}
-            {error ? (
-              <div className="absolute inset-0 flex flex-col items-center justify-center text-red-400">
-                <CameraOff className="w-12 h-12 mb-2" />
-                <span className="text-sm">{error}</span>
-                <button
-                  onClick={handleRefresh}
-                  className="mt-2 text-xs text-bambu-green hover:underline"
-                >
-                  Retry
-                </button>
-              </div>
-            ) : (
-              <img
-                ref={imgRef}
-                src={streamUrl}
-                alt="Camera stream"
-                className="w-full h-full object-contain"
-                onLoad={handleImageLoad}
-                onError={handleImageError}
-              />
-            )}
-          </>
-        )}
-      </div>
+        </>
+      )}
     </div>
   );
 }

+ 98 - 0
frontend/src/components/control/ExtruderControls.tsx

@@ -0,0 +1,98 @@
+import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import type { PrinterStatus } from '../../api/client';
+import { ChevronUp, ChevronDown } from 'lucide-react';
+
+interface ExtruderControlsProps {
+  printerId: number;
+  status: PrinterStatus | null | undefined;
+  nozzleCount: number;
+}
+
+export function ExtruderControls({ status, nozzleCount }: ExtruderControlsProps) {
+  const isConnected = status?.connected ?? false;
+  const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
+  const isDualNozzle = nozzleCount > 1;
+
+  const [selectedNozzle, setSelectedNozzle] = useState<'left' | 'right'>('left');
+
+  // TODO: Add extrude/retract API calls when available
+  const extrudeMutation = useMutation({
+    mutationFn: async ({ distance }: { distance: number }) => {
+      // Placeholder - implement when API is ready
+      console.log(`Extrude ${distance}mm on ${selectedNozzle} nozzle`);
+    },
+  });
+
+  const handleExtrude = (distance: number) => {
+    extrudeMutation.mutate({ distance });
+  };
+
+  const isDisabled = !isConnected || isPrinting || extrudeMutation.isPending;
+
+  return (
+    <div className="flex flex-col items-center gap-1.5 flex-1 justify-center">
+      {/* Left/Right Toggle - only for dual nozzle */}
+      {isDualNozzle && (
+        <div className="flex rounded-md overflow-hidden border border-bambu-dark-tertiary mb-1 flex-shrink-0">
+          <button
+            onClick={() => setSelectedNozzle('left')}
+            className={`px-3 py-1.5 text-sm border-r border-bambu-dark-tertiary transition-colors ${
+              selectedNozzle === 'left'
+                ? 'bg-bambu-green text-white'
+                : 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
+            }`}
+          >
+            Left
+          </button>
+          <button
+            onClick={() => setSelectedNozzle('right')}
+            className={`px-3 py-1.5 text-sm transition-colors ${
+              selectedNozzle === 'right'
+                ? 'bg-bambu-green text-white'
+                : 'bg-bambu-dark-secondary text-bambu-gray hover:bg-bambu-dark-tertiary'
+            }`}
+          >
+            Right
+          </button>
+        </div>
+      )}
+
+      {/* Extrude Up Button */}
+      <button
+        onClick={() => handleExtrude(5)}
+        disabled={isDisabled}
+        className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+        title="Extrude 5mm"
+      >
+        <ChevronUp className="w-4 h-4" />
+      </button>
+
+      {/* Extruder Image */}
+      <div className="h-[120px] flex items-center justify-center">
+        <img
+          src={isDualNozzle ? "/icons/dual-extruder.png" : "/icons/single-extruder1.png"}
+          alt={isDualNozzle ? "Dual Extruder" : "Single Extruder"}
+          className="h-full object-contain"
+          onError={(e) => {
+            // Fallback if image doesn't load
+            (e.target as HTMLImageElement).style.display = 'none';
+          }}
+        />
+      </div>
+
+      {/* Retract Down Button */}
+      <button
+        onClick={() => handleExtrude(-5)}
+        disabled={isDisabled}
+        className="w-9 h-[30px] rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+        title="Retract 5mm"
+      >
+        <ChevronDown className="w-4 h-4" />
+      </button>
+
+      {/* Label */}
+      <span className="text-xs text-bambu-gray mt-0.5">Extruder</span>
+    </div>
+  );
+}

+ 160 - 0
frontend/src/components/control/JogPad.tsx

@@ -0,0 +1,160 @@
+import { useMutation } from '@tanstack/react-query';
+import { api, isConfirmationRequired } from '../../api/client';
+import type { PrinterStatus } from '../../api/client';
+import { useState } from 'react';
+import { ConfirmModal } from '../ConfirmModal';
+
+interface JogPadProps {
+  printerId: number;
+  status: PrinterStatus | null | undefined;
+}
+
+export function JogPad({ printerId, status }: JogPadProps) {
+  const isConnected = status?.connected ?? false;
+
+  const [confirmModal, setConfirmModal] = useState<{
+    action: string;
+    token: string;
+    warning: string;
+    onConfirm: () => void;
+  } | null>(null);
+
+  const homeMutation = useMutation({
+    mutationFn: ({ axes, token }: { axes: string; token?: string }) =>
+      api.homeAxes(printerId, axes, token),
+    onSuccess: (result) => {
+      if (isConfirmationRequired(result)) {
+        setConfirmModal({
+          action: 'home',
+          token: result.token,
+          warning: result.warning,
+          onConfirm: () => homeMutation.mutate({ axes: 'XY', token: result.token }),
+        });
+      }
+    },
+  });
+
+  const moveMutation = useMutation({
+    mutationFn: ({ axis, distance, token }: { axis: string; distance: number; token?: string }) =>
+      api.moveAxis(printerId, axis, distance, 3000, token),
+    onSuccess: (result, variables) => {
+      if (isConfirmationRequired(result)) {
+        setConfirmModal({
+          action: 'move',
+          token: result.token,
+          warning: result.warning,
+          onConfirm: () =>
+            moveMutation.mutate({
+              axis: variables.axis,
+              distance: variables.distance,
+              token: result.token,
+            }),
+        });
+      }
+    },
+  });
+
+  const handleHome = () => {
+    homeMutation.mutate({ axes: 'XY' });
+  };
+
+  const handleMove = (axis: string, distance: number) => {
+    moveMutation.mutate({ axis, distance });
+  };
+
+  const handleConfirm = () => {
+    if (confirmModal) {
+      confirmModal.onConfirm();
+      setConfirmModal(null);
+    }
+  };
+
+  const isLoading = homeMutation.isPending || moveMutation.isPending;
+  const isDisabled = !isConnected || isLoading;
+
+  return (
+    <>
+      <div className="relative w-[220px] h-[220px] mb-3.5">
+        {/* Use the actual jogpad.svg from mockup */}
+        <img
+          src="/icons/jogpad.svg"
+          alt="Jog Pad"
+          className="w-full h-full jogpad-theme"
+        />
+
+        {/* Invisible clickable areas overlaid on the SVG */}
+        {/* Outer ring - 10mm moves */}
+        <button
+          onClick={() => handleMove('Y', 10)}
+          disabled={isDisabled}
+          className="absolute top-[8px] left-1/2 -translate-x-1/2 w-[40px] h-[30px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="Y+10"
+        />
+        <button
+          onClick={() => handleMove('Y', -10)}
+          disabled={isDisabled}
+          className="absolute bottom-[8px] left-1/2 -translate-x-1/2 w-[40px] h-[30px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="Y-10"
+        />
+        <button
+          onClick={() => handleMove('X', -10)}
+          disabled={isDisabled}
+          className="absolute left-[8px] top-1/2 -translate-y-1/2 w-[30px] h-[40px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="X-10"
+        />
+        <button
+          onClick={() => handleMove('X', 10)}
+          disabled={isDisabled}
+          className="absolute right-[8px] top-1/2 -translate-y-1/2 w-[30px] h-[40px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="X+10"
+        />
+
+        {/* Inner ring - 1mm moves */}
+        <button
+          onClick={() => handleMove('Y', 1)}
+          disabled={isDisabled}
+          className="absolute top-[42px] left-1/2 -translate-x-1/2 w-[35px] h-[25px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="Y+1"
+        />
+        <button
+          onClick={() => handleMove('Y', -1)}
+          disabled={isDisabled}
+          className="absolute bottom-[42px] left-1/2 -translate-x-1/2 w-[35px] h-[25px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="Y-1"
+        />
+        <button
+          onClick={() => handleMove('X', -1)}
+          disabled={isDisabled}
+          className="absolute left-[42px] top-1/2 -translate-y-1/2 w-[25px] h-[35px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="X-1"
+        />
+        <button
+          onClick={() => handleMove('X', 1)}
+          disabled={isDisabled}
+          className="absolute right-[42px] top-1/2 -translate-y-1/2 w-[25px] h-[35px] opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="X+1"
+        />
+
+        {/* Home button in center - clickable overlay */}
+        <button
+          onClick={handleHome}
+          disabled={isDisabled}
+          className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[50px] h-[50px] rounded-full opacity-0 hover:opacity-10 hover:bg-white disabled:cursor-not-allowed"
+          title="Home XY"
+        />
+      </div>
+
+      {/* Confirmation Modal */}
+      {confirmModal && (
+        <ConfirmModal
+          title="Confirm Action"
+          message={confirmModal.warning}
+          confirmText="Continue"
+          variant="warning"
+          onConfirm={handleConfirm}
+          onCancel={() => setConfirmModal(null)}
+        />
+      )}
+    </>
+  );
+}

+ 1 - 1
frontend/src/components/control/PrintControls.tsx

@@ -66,7 +66,7 @@ export function PrintControls({ printerId, status }: PrintControlsProps) {
 
   return (
     <>
-      <div className="bg-bambu-dark-secondary rounded-lg p-4">
+      <div>
         <h3 className="text-sm font-medium text-bambu-gray mb-3">Print Controls</h3>
 
         <div className="flex gap-2">

+ 169 - 69
frontend/src/components/control/PrintStatus.tsx

@@ -1,90 +1,190 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useState } from 'react';
 import type { PrinterStatus } from '../../api/client';
-import { Clock, Layers, FileText } from 'lucide-react';
+import { api, isConfirmationRequired } from '../../api/client';
+import { Pause, Square, Loader2 } from 'lucide-react';
+import { ConfirmModal } from '../ConfirmModal';
 
 interface PrintStatusProps {
   printerId: number;
   status: PrinterStatus | null | undefined;
 }
 
-function formatTime(seconds: number | null | undefined): string {
-  if (!seconds) return '--:--';
-  const hours = Math.floor(seconds / 3600);
-  const mins = Math.floor((seconds % 3600) / 60);
-  if (hours > 0) {
-    return `${hours}h ${mins}m`;
-  }
-  return `${mins}m`;
+function formatFinishTime(seconds: number | null | undefined): string {
+  if (!seconds) return 'N/A';
+  const now = new Date();
+  const finish = new Date(now.getTime() + seconds * 1000);
+  return finish.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
 }
 
-export function PrintStatus({ status }: PrintStatusProps) {
-  const isPrinting = status?.state === 'RUNNING' || status?.state === 'PAUSE';
+export function PrintStatus({ printerId, status }: PrintStatusProps) {
+  const queryClient = useQueryClient();
+  const [confirmModal, setConfirmModal] = useState<{
+    action: string;
+    token: string;
+    warning: string;
+  } | null>(null);
+
+  const isConnected = status?.connected ?? false;
+  const isPrinting = status?.state === 'RUNNING';
+  const isPaused = status?.state === 'PAUSE';
   const progress = status?.progress ?? 0;
 
-  return (
-    <div className="bg-bambu-dark-secondary rounded-lg p-4">
-      <div className="flex items-center justify-between mb-3">
-        <h3 className="text-sm font-medium text-bambu-gray">Print Status</h3>
-        <span
-          className={`px-2 py-0.5 rounded text-xs font-medium ${
-            status?.state === 'RUNNING'
-              ? 'bg-bambu-green/20 text-bambu-green'
-              : status?.state === 'PAUSE'
-              ? 'bg-yellow-500/20 text-yellow-500'
-              : status?.state === 'FINISH'
-              ? 'bg-blue-500/20 text-blue-500'
-              : status?.state === 'FAILED'
-              ? 'bg-red-500/20 text-red-500'
-              : 'bg-bambu-dark-tertiary text-bambu-gray'
-          }`}
-        >
-          {status?.state || 'IDLE'}
-        </span>
-      </div>
+  const pauseMutation = useMutation({
+    mutationFn: () => api.pausePrint(printerId),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
+    },
+  });
 
-      {isPrinting && status?.subtask_name && (
-        <div className="flex items-center gap-2 mb-3 text-sm">
-          <FileText className="w-4 h-4 text-bambu-gray" />
-          <span className="truncate" title={status.subtask_name}>
-            {status.subtask_name}
-          </span>
-        </div>
-      )}
+  const resumeMutation = useMutation({
+    mutationFn: () => api.resumePrint(printerId),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
+    },
+  });
 
-      {/* Progress Bar */}
-      <div className="mb-3">
-        <div className="flex justify-between text-xs text-bambu-gray mb-1">
-          <span>Progress</span>
-          <span>{Math.round(progress)}%</span>
-        </div>
-        <div className="h-2 bg-bambu-dark-tertiary rounded-full overflow-hidden">
-          <div
-            className={`h-full transition-all duration-300 ${
-              status?.state === 'PAUSE' ? 'bg-yellow-500' : 'bg-bambu-green'
-            }`}
-            style={{ width: `${progress}%` }}
-          />
+  const stopMutation = useMutation({
+    mutationFn: (token?: string) => api.stopPrint(printerId, token),
+    onSuccess: (result) => {
+      if (isConfirmationRequired(result)) {
+        setConfirmModal({
+          action: 'stop',
+          token: result.token,
+          warning: result.warning,
+        });
+      } else {
+        queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
+      }
+    },
+  });
+
+  const handlePauseResume = () => {
+    if (isPrinting) {
+      pauseMutation.mutate();
+    } else if (isPaused) {
+      resumeMutation.mutate();
+    }
+  };
+
+  const handleStop = () => {
+    stopMutation.mutate(undefined);
+  };
+
+  const handleConfirmStop = () => {
+    if (confirmModal) {
+      stopMutation.mutate(confirmModal.token);
+      setConfirmModal(null);
+    }
+  };
+
+  const isLoading = pauseMutation.isPending || resumeMutation.isPending || stopMutation.isPending;
+  const canControl = isConnected && (isPrinting || isPaused);
+
+  return (
+    <>
+      <div className="text-xs text-bambu-gray mb-3">Printing Progress</div>
+      <div className="flex gap-4 items-center">
+        {/* Thumbnail */}
+        <div className="w-20 h-20 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
+          {status?.subtask_name ? (
+            <img
+              src={`/api/v1/archives/thumbnail/${encodeURIComponent(status.subtask_name)}`}
+              alt=""
+              className="w-full h-full object-contain"
+              onError={(e) => {
+                (e.target as HTMLImageElement).style.display = 'none';
+                (e.target as HTMLImageElement).parentElement!.innerHTML = '<span class="text-xs text-bambu-gray">Bambu<br/>Lab</span>';
+              }}
+            />
+          ) : (
+            <span className="text-xs text-bambu-gray text-center">Bambu<br/>Lab</span>
+          )}
         </div>
-      </div>
 
-      {/* Stats */}
-      <div className="grid grid-cols-2 gap-3 text-sm">
-        <div className="flex items-center gap-2">
-          <Clock className="w-4 h-4 text-bambu-gray" />
-          <div>
-            <div className="text-xs text-bambu-gray">Remaining</div>
-            <div className="font-medium">{formatTime(status?.remaining_time)}</div>
+        {/* Info */}
+        <div className="flex-1 min-w-0">
+          <div className="font-medium text-sm text-white truncate mb-0.5">
+            {status?.subtask_name || 'N/A'}
           </div>
-        </div>
-        <div className="flex items-center gap-2">
-          <Layers className="w-4 h-4 text-bambu-gray" />
-          <div>
-            <div className="text-xs text-bambu-gray">Layer</div>
-            <div className="font-medium">
-              {status?.layer_num ?? 0} / {status?.total_layers ?? 0}
-            </div>
+          <div className={`text-sm mb-2 ${
+            status?.state === 'RUNNING' || status?.state === 'PAUSE'
+              ? 'text-bambu-green'
+              : 'text-bambu-gray'
+          }`}>
+            {status?.state || 'N/A'}
+          </div>
+          {/* Progress Bar */}
+          <div className="h-1 bg-bambu-dark-tertiary rounded-full overflow-hidden mb-2">
+            <div
+              className={`h-full transition-all duration-300 ${
+                status?.state === 'PAUSE' ? 'bg-yellow-500' : 'bg-bambu-green'
+              }`}
+              style={{ width: `${progress}%` }}
+            />
+          </div>
+          <div className="text-xs text-bambu-gray mb-1">
+            Layer: {status?.layer_num ?? 'N/A'} / {status?.total_layers ?? 'N/A'} &nbsp;&nbsp; {Math.round(progress)}%
+          </div>
+          <div className="text-xs text-bambu-gray">
+            Estimated finish time: {formatFinishTime(status?.remaining_time)}
           </div>
         </div>
+
+        {/* Control Buttons */}
+        <div className="flex gap-1.5 flex-shrink-0">
+          <button
+            onClick={handlePauseResume}
+            disabled={!canControl || isLoading}
+            className="w-8 h-8 rounded-md border border-bambu-dark-tertiary bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+            title={isPrinting ? 'Pause' : 'Resume'}
+          >
+            {pauseMutation.isPending || resumeMutation.isPending ? (
+              <Loader2 className="w-4 h-4 animate-spin" />
+            ) : (
+              <Pause className="w-4 h-4" />
+            )}
+          </button>
+          <button
+            onClick={handleStop}
+            disabled={!canControl || isLoading}
+            className="w-8 h-8 rounded-md border border-bambu-dark-tertiary bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+            title="Stop"
+          >
+            {stopMutation.isPending ? (
+              <Loader2 className="w-4 h-4 animate-spin" />
+            ) : (
+              <Square className="w-4 h-4" />
+            )}
+          </button>
+          <button
+            disabled={!canControl || isLoading}
+            className="w-8 h-8 rounded-md border border-bambu-dark-tertiary bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary flex items-center justify-center text-bambu-gray disabled:opacity-50 disabled:cursor-not-allowed"
+            title="Skip Objects"
+          >
+            <img src="/icons/skip-objects.svg" alt="Skip Objects" className="w-4 h-4 icon-theme" />
+          </button>
+        </div>
       </div>
-    </div>
+
+      {/* Error Message */}
+      {(pauseMutation.error || resumeMutation.error || stopMutation.error) && (
+        <p className="mt-2 text-xs text-red-500">
+          {(pauseMutation.error || resumeMutation.error || stopMutation.error)?.message}
+        </p>
+      )}
+
+      {/* Confirmation Modal */}
+      {confirmModal && (
+        <ConfirmModal
+          title="Confirm Stop"
+          message={confirmModal.warning}
+          confirmText="Stop Print"
+          variant="danger"
+          onConfirm={handleConfirmStop}
+          onCancel={() => setConfirmModal(null)}
+        />
+      )}
+    </>
   );
 }

+ 93 - 0
frontend/src/components/control/TemperatureColumn.tsx

@@ -0,0 +1,93 @@
+import type { PrinterStatus } from '../../api/client';
+
+interface Temperatures {
+  bed?: number;
+  bed_target?: number;
+  nozzle?: number;
+  nozzle_target?: number;
+  nozzle_2?: number;
+  nozzle_2_target?: number;
+  chamber?: number;
+}
+
+interface TemperatureColumnProps {
+  printerId?: number;
+  status: PrinterStatus | null | undefined;
+  nozzleCount: number;
+}
+
+export function TemperatureColumn({ status, nozzleCount }: TemperatureColumnProps) {
+  const temps = (status?.temperatures ?? {}) as Temperatures;
+  const isDualNozzle = nozzleCount > 1;
+
+  return (
+    <div className="flex flex-col justify-evenly min-w-[150px] pr-5 border-r border-bambu-dark-tertiary">
+      {/* Nozzle 1 (Left) */}
+      <div className="flex items-center gap-1.5">
+        <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+          <img src="/icons/hotend.svg" alt="" className="w-5 icon-theme" />
+        </div>
+        {isDualNozzle && (
+          <span className="text-[11px] font-semibold text-bambu-green bg-bambu-green/20 px-1.5 py-0.5 rounded min-w-[18px] text-center flex-shrink-0">
+            L
+          </span>
+        )}
+        <span className="text-lg font-medium text-white">{Math.round(temps.nozzle ?? 0)}</span>
+        <span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_target ?? 0)} °C</span>
+      </div>
+
+      {/* Nozzle 2 (Right) - only for dual nozzle */}
+      {isDualNozzle && (
+        <div className="flex items-center gap-1.5">
+          <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+            <img src="/icons/hotend.svg" alt="" className="w-5 icon-theme" />
+          </div>
+          <span className="text-[11px] font-semibold text-bambu-green bg-bambu-green/20 px-1.5 py-0.5 rounded min-w-[18px] text-center flex-shrink-0">
+            R
+          </span>
+          <span className="text-lg font-medium text-white">{Math.round(temps.nozzle_2 ?? 0)}</span>
+          <span className="text-sm text-bambu-gray">/{Math.round(temps.nozzle_2_target ?? 0)} °C</span>
+        </div>
+      )}
+
+      {/* Bed */}
+      <div className="flex items-center gap-1.5">
+        <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+          <img src="/icons/heatbed.svg" alt="" className="w-5 icon-theme" />
+        </div>
+        {/* Spacer to align with L/R badge (min-w-[18px]) */}
+        {isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
+        <span className="text-lg font-medium text-white">{Math.round(temps.bed ?? 0)}</span>
+        <span className="text-sm text-bambu-gray">/{Math.round(temps.bed_target ?? 0)} °C</span>
+      </div>
+
+      {/* Chamber */}
+      <div className="flex items-center gap-1.5">
+        <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+          <img src="/icons/chamber.svg" alt="" className="w-5 icon-theme" />
+        </div>
+        {/* Spacer to align with L/R badge */}
+        {isDualNozzle && <span className="min-w-[18px] flex-shrink-0" />}
+        <span className="text-lg font-medium text-white">{Math.round(temps.chamber ?? 0)}</span>
+        <span className="text-sm text-bambu-gray">/{0} °C</span>
+      </div>
+
+      {/* Air Condition - button */}
+      <button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
+        <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+          <img src="/icons/ventilation.svg" alt="" className="w-5 icon-theme" />
+        </div>
+        <span className="text-sm text-bambu-gray">Air Condition</span>
+      </button>
+
+      {/* Lamp - button */}
+      <button className="flex items-center gap-2 hover:opacity-80 transition-opacity">
+        <div className="w-5 h-5 flex items-center justify-center flex-shrink-0">
+          <img src="/icons/ventilation.svg" alt="" className="w-4 icon-theme" />
+        </div>
+        <span className="text-sm text-bambu-gray">Lamp</span>
+        <div className="w-3.5 h-3.5 rounded-full bg-bambu-green" />
+      </button>
+    </div>
+  );
+}

+ 22 - 1
frontend/src/index.css

@@ -1,7 +1,9 @@
 @import "tailwindcss";
-
 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
 
+/* Enable class-based dark mode for Tailwind v4 */
+@custom-variant dark (&:where(.dark, .dark *));
+
 @theme {
   /* Bambu Lab brand colors - always the same */
   --color-bambu-green: #00ae42;
@@ -89,3 +91,22 @@ body {
 .animate-slide-in {
   animation: slide-in 0.2s ease-out;
 }
+
+/* Theme-aware icon inversion - only invert in dark mode */
+.icon-theme {
+  opacity: 0.5;
+}
+
+.dark .icon-theme {
+  filter: invert(1);
+  opacity: 0.4;
+}
+
+/* Jogpad theme styling - darken background in dark mode */
+.jogpad-theme {
+  /* Light mode - normal */
+}
+
+.dark .jogpad-theme {
+  filter: brightness(0.7) contrast(1.1);
+}

+ 102 - 68
frontend/src/pages/ControlPage.tsx

@@ -5,14 +5,12 @@ import { api } from '../api/client';
 import type { PrinterStatus } from '../api/client';
 import { CameraFeed } from '../components/control/CameraFeed';
 import { PrintStatus } from '../components/control/PrintStatus';
-import { PrintControls } from '../components/control/PrintControls';
-import { TemperaturePanel } from '../components/control/TemperaturePanel';
-import { SpeedControl } from '../components/control/SpeedControl';
-import { FanControls } from '../components/control/FanControls';
-import { LightToggle } from '../components/control/LightToggle';
-import { MovementControls } from '../components/control/MovementControls';
-import { AMSPanel } from '../components/control/AMSPanel';
-import { Loader2, WifiOff } from 'lucide-react';
+import { TemperatureColumn } from '../components/control/TemperatureColumn';
+import { JogPad } from '../components/control/JogPad';
+import { BedControls } from '../components/control/BedControls';
+import { ExtruderControls } from '../components/control/ExtruderControls';
+import { AMSSectionDual } from '../components/control/AMSSectionDual';
+import { Loader2, WifiOff, Video, Webcam, HardDrive, Settings } from 'lucide-react';
 
 export function ControlPage() {
   const [searchParams, setSearchParams] = useSearchParams();
@@ -89,7 +87,7 @@ export function ControlPage() {
   }
 
   return (
-    <div className="h-screen flex flex-col">
+    <div className="h-screen flex flex-col bg-bambu-dark">
       {/* Printer Tabs */}
       <div className="bg-bambu-dark-secondary border-b border-bambu-dark-tertiary">
         <div className="flex overflow-x-auto">
@@ -125,79 +123,115 @@ export function ControlPage() {
         </div>
       </div>
 
-      {/* Main Content */}
+      {/* Main Content - Bambu Studio Layout */}
       {selectedPrinter && (
-        <div className="flex-1 overflow-auto p-4">
-          <div className="max-w-7xl mx-auto">
+        <div className="flex-1 flex overflow-hidden">
+          {/* Left Panel - Camera & Print Progress */}
+          <div className="flex-1 flex flex-col bg-bambu-dark">
+            {/* Camera Header Icons - same height as Control header */}
+            <div className="flex items-center justify-end gap-2 px-3 py-2.5 bg-bambu-dark-secondary border-b border-bambu-dark-tertiary min-h-[44px]">
+              <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
+                <HardDrive className="w-4 h-4" />
+              </button>
+              <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
+                <Video className="w-4 h-4" />
+              </button>
+              <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
+                <Webcam className="w-4 h-4" />
+              </button>
+              <button className="p-1.5 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white">
+                <Settings className="w-4 h-4" />
+              </button>
+            </div>
+
+            {/* Camera Feed - Embedded directly */}
+            <div className="flex-1 bg-black">
+              <CameraFeed
+                printerId={selectedPrinter.id}
+                isConnected={selectedStatus?.connected ?? false}
+              />
+            </div>
+
+            {/* Status Bar */}
+            <div className="h-1 bg-bambu-green" />
+
+            {/* Print Progress with integrated controls */}
+            <div className="bg-bambu-dark-secondary p-4 px-5">
+              <PrintStatus
+                printerId={selectedPrinter.id}
+                status={selectedStatus}
+              />
+            </div>
+          </div>
+
+          {/* Right Panel - Control */}
+          <div className="w-[620px] flex flex-col bg-bambu-dark-secondary border-l border-bambu-dark-tertiary overflow-y-auto">
+            {/* Control Header - same height as Camera header */}
+            <div className="flex items-center justify-between px-3 py-2.5 border-b border-bambu-dark-tertiary min-h-[44px]">
+              <span className="text-sm text-bambu-gray">Control</span>
+              <div className="flex gap-2">
+                <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
+                  Printer Parts
+                </button>
+                <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
+                  Print Options
+                </button>
+                <button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
+                  Calibration
+                </button>
+              </div>
+            </div>
+
             {/* Connection Warning */}
             {!selectedStatus?.connected && (
-              <div className="mb-4 p-4 bg-red-500/20 border border-red-500/50 rounded-lg flex items-center gap-3">
-                <WifiOff className="w-5 h-5 text-red-500" />
-                <span className="text-red-400">
+              <div className="m-3 p-3 bg-red-500/20 border border-red-500/50 rounded-lg flex items-center gap-3">
+                <WifiOff className="w-4 h-4 text-red-500" />
+                <span className="text-sm text-red-400">
                   Printer is not connected. Controls are disabled.
                 </span>
               </div>
             )}
 
-            <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
-              {/* Left Column: Camera + Print Status */}
-              <div className="space-y-4">
-                {/* Camera Feed */}
-                <CameraFeed
-                  printerId={selectedPrinter.id}
-                  isConnected={selectedStatus?.connected ?? false}
-                />
-
-                {/* Print Status & Controls */}
-                <PrintStatus
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-                <PrintControls
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-
-                {/* AMS Panel */}
-                <AMSPanel
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-              </div>
-
-              {/* Right Column: Controls */}
-              <div className="space-y-4">
-                {/* Temperature Panel */}
-                <TemperaturePanel
+            {/* Control Body */}
+            <div className="flex-1 p-4 bg-bambu-dark">
+              {/* Top Section: Temp + Movement + Extruder */}
+              <div className="flex gap-6 mb-4" style={{ minHeight: '300px' }}>
+                {/* Temperature Column */}
+                <TemperatureColumn
                   printerId={selectedPrinter.id}
                   status={selectedStatus}
                   nozzleCount={selectedPrinter.nozzle_count}
                 />
 
-                {/* Speed Control */}
-                <SpeedControl
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-
-                {/* Fan Controls */}
-                <FanControls
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-
-                {/* Light Toggle */}
-                <LightToggle
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
-
-                {/* Movement Controls */}
-                <MovementControls
-                  printerId={selectedPrinter.id}
-                  status={selectedStatus}
-                />
+                {/* Movement Column */}
+                <div className="flex-1 flex gap-6 items-center justify-center">
+                  {/* Jog Section */}
+                  <div className="flex flex-col items-center">
+                    <JogPad
+                      printerId={selectedPrinter.id}
+                      status={selectedStatus}
+                    />
+                    <BedControls
+                      printerId={selectedPrinter.id}
+                      status={selectedStatus}
+                    />
+                  </div>
+
+                  {/* Extruder Section */}
+                  <ExtruderControls
+                    printerId={selectedPrinter.id}
+                    status={selectedStatus}
+                    nozzleCount={selectedPrinter.nozzle_count}
+                  />
+                </div>
               </div>
+
+              {/* AMS Section */}
+              <AMSSectionDual
+                printerId={selectedPrinter.id}
+                status={selectedStatus}
+                nozzleCount={selectedPrinter.nozzle_count}
+              />
             </div>
           </div>
         </div>

+ 1 - 0
frontend/tailwind.config.js

@@ -1,5 +1,6 @@
 /** @type {import('tailwindcss').Config} */
 export default {
+  darkMode: 'class',
   content: [
     "./index.html",
     "./src/**/*.{js,ts,jsx,tsx}",

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BdU-188w.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Ca04bhbd.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DiPSi5MU.css


BIN
static/icons/ams-ht.png


+ 1 - 0
static/icons/ams-settings.svg

@@ -0,0 +1 @@
+<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill="#000" fill-rule="evenodd"><path d="m2.25 6c0-.41421.33579-.75.75-.75h3.5c.41421 0 .75.33579.75.75s-.33579.75-.75.75h-3.5c-.41421 0-.75-.33579-.75-.75zm8.5 0c0-.41421.3358-.75.75-.75h9.5c.4142 0 .75.33579.75.75s-.3358.75-.75.75h-9.5c-.4142 0-.75-.33579-.75-.75z"/><path d="m9 4.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.79493 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.45507 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.45507-3.25-3.25z"/><path d="m21.75 12c0-.4142-.3358-.75-.75-.75h-2c-.4142 0-.75.3358-.75.75s.3358.75.75.75h2c.4142 0 .75-.3358.75-.75zm-7 0c0-.4142-.3358-.75-.75-.75h-5c-.41421 0-.75.3358-.75.75s.33579.75.75.75h5c.4142 0 .75-.3358.75-.75zm-9 0c0-.4142-.33579-.75-.75-.75h-2c-.41421 0-.75.3358-.75.75s.33579.75.75.75h2c.41421 0 .75-.3358.75-.75z"/><path d="m16.5 10.25c.9665 0 1.75.7835 1.75 1.75s-.7835 1.75-1.75 1.75-1.75-.7835-1.75-1.75.7835-1.75 1.75-1.75zm3.25 1.75c0-1.7949-1.4551-3.25-3.25-3.25s-3.25 1.4551-3.25 3.25 1.4551 3.25 3.25 3.25 3.25-1.4551 3.25-3.25z"/><path d="m2.25 18c0-.4142.33579-.75.75-.75h5c.41421 0 .75.3358.75.75s-.33579.75-.75.75h-5c-.41421 0-.75-.3358-.75-.75zm10 0c0-.4142.3358-.75.75-.75h8c.4142 0 .75.3358.75.75s-.3358.75-.75.75h-8c-.4142 0-.75-.3358-.75-.75z"/><path d="m10.5 16.25c-.9665 0-1.75.7835-1.75 1.75s.7835 1.75 1.75 1.75 1.75-.7835 1.75-1.75-.7835-1.75-1.75-1.75zm-3.25 1.75c0-1.7949 1.45507-3.25 3.25-3.25 1.7949 0 3.25 1.4551 3.25 3.25s-1.4551 3.25-3.25 3.25c-1.79493 0-3.25-1.4551-3.25-3.25z"/></g></svg>

+ 9 - 0
static/icons/ams-wiring-center.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50">
+  <!-- Left wire: horizontal from left edge, then down to extruder left inlet -->
+  <line x1="0" y1="0" x2="10" y2="0" stroke="#909090" stroke-width="2" />
+  <line x1="10" y1="0" x2="10" y2="50" stroke="#909090" stroke-width="2" />
+
+  <!-- Right wire: horizontal from right edge, then down to extruder right inlet -->
+  <line x1="40" y1="0" x2="30" y2="0" stroke="#909090" stroke-width="2" />
+  <line x1="30" y1="0" x2="30" y2="50" stroke="#909090" stroke-width="2" />
+</svg>

+ 17 - 0
static/icons/ams-wiring-left.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
+  <!-- Vertical lines from slots down to horizontal bar -->
+  <line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Horizontal bar across all slots -->
+  <line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Center hub box -->
+  <rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
+
+  <!-- Wire from hub: down, then right to edge (at same level as hub horizontal bar) -->
+  <line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+  <line x1="110" y1="35" x2="220" y2="35" stroke="#909090" stroke-width="2" />
+</svg>

+ 17 - 0
static/icons/ams-wiring-right.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
+  <!-- Vertical lines from slots down to horizontal bar -->
+  <line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
+  <line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Horizontal bar across all slots -->
+  <line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
+
+  <!-- Center hub box -->
+  <rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
+
+  <!-- Wire from hub: down, then left to edge (at same level as hub horizontal bar) -->
+  <line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+  <line x1="0" y1="35" x2="110" y2="35" stroke="#909090" stroke-width="2" />
+</svg>

BIN
static/icons/ams.png


File diff suppressed because it is too large
+ 0 - 0
static/icons/chamber.svg


BIN
static/icons/dual-extruder.png


BIN
static/icons/extruder-left-right.png


+ 51 - 0
static/icons/eye.svg

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M508.745,246.041c-4.574-6.257-113.557-153.206-252.748-153.206S7.818,239.784,3.249,246.035
+			c-4.332,5.936-4.332,13.987,0,19.923c4.569,6.257,113.557,153.206,252.748,153.206s248.174-146.95,252.748-153.201
+			C513.083,260.028,513.083,251.971,508.745,246.041z M255.997,385.406c-102.529,0-191.33-97.533-217.617-129.418
+			c26.253-31.913,114.868-129.395,217.617-129.395c102.524,0,191.319,97.516,217.617,129.418
+			C447.361,287.923,358.746,385.406,255.997,385.406z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M255.997,154.725c-55.842,0-101.275,45.433-101.275,101.275s45.433,101.275,101.275,101.275
+			s101.275-45.433,101.275-101.275S311.839,154.725,255.997,154.725z M255.997,323.516c-37.23,0-67.516-30.287-67.516-67.516
+			s30.287-67.516,67.516-67.516s67.516,30.287,67.516,67.516S293.227,323.516,255.997,323.516z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 1 - 0
static/icons/heatbed.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><g fill="rgb(0,0,0)"><path d="m6.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m12.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m18.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m22 13.5c-1.07 0-1.61.65-2.05 1.18-.44.52-.71.82-1.29.82s-.85-.3-1.29-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.85-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.84-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18-.28 0-.5.22-.5.5s.22.5.5.5c.57 0 .84.3 1.28.82.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.28.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.29.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.29-.82.28 0 .5-.22.5-.5s-.22-.5-.5-.5z"/><path d="m21 18.5h-18c-.83 0-1.5.67-1.5 1.5v1c0 .83.67 1.5 1.5 1.5h18c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5zm.5 2.5c0 .28-.22.5-.5.5h-18c-.28 0-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5h18c.28 0 .5.22.5.5z"/></g></svg>

+ 44 - 0
static/icons/home.svg

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 476.912 476.912" style="enable-background:new 0 0 476.912 476.912;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M461.776,209.408L249.568,4.52c-6.182-6.026-16.042-6.026-22.224,0L15.144,209.4c-3.124,3.015-4.888,7.17-4.888,11.512
+			c0,8.837,7.164,16,16,16h28.2v224c0,8.837,7.163,16,16,16h112c8.837,0,16-7.163,16-16v-128h80v128c0,8.837,7.163,16,16,16h112
+			c8.837,0,16-7.163,16-16v-224h28.2c4.338,0,8.489-1.761,11.504-4.88C468.301,225.678,468.129,215.549,461.776,209.408z
+			 M422.456,220.912c-8.837,0-16,7.163-16,16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837,0-16,7.163-16,16v128h-112v-224
+			c0-8.837-7.163-16-16-16h-28.2l212.2-204.88l212.28,204.88H422.456z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 1 - 0
static/icons/hotend.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><g id="Front_Heater" data-name="Front Heater"><path d="m33.14 29h2.86l-4-7-4 7h3l1.5 3.7a8.38 8.38 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m23.22 29h2.78l-4-7-4 7h3.06l1.5 3.7a8.35 8.35 0 0 1 -.92 8l-.67.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .24-1.39 8.4 8.4 0 0 1 .2-9.32l.67-.82a10.32 10.32 0 0 0 1.22-10z"/><path d="m57.88 20.09a3 3 0 0 0 -1.49-1.84l-7.73-4.1a35.48 35.48 0 0 0 -33.32 0l-7.72 4.1a3 3 0 0 0 -1.22 4.2l9.72 17a3 3 0 0 0 2.6 1.55 3 3 0 0 0 1.39-.34 1 1 0 0 0 .42-1.35 1 1 0 0 0 -1.34-.42 1 1 0 0 1 -1.33-.39l-9.72-17a1 1 0 0 1 -.1-.81 1 1 0 0 1 .52-.69l7.72-4.1a33.46 33.46 0 0 1 31.44 0l7.73 4.1a1 1 0 0 1 .51.63 1 1 0 0 1 -.1.81l-9.72 17a1 1 0 0 1 -1.33.39l-.2-.11a10.82 10.82 0 0 0 -.36-6.72l-1.19-3h2.94l-4-7-4 7h2.9l1.5 3.7a8.38 8.38 0 0 1 -.93 8l-.66.82a10.43 10.43 0 0 0 -.29 11.74 1 1 0 0 0 .82.42 1 1 0 0 0 .58-.19 1 1 0 0 0 .23-1.39 8.4 8.4 0 0 1 .21-9.32l.64-.78a8.27 8.27 0 0 0 .84-1.3 3 3 0 0 0 4-1.17l9.72-17a3 3 0 0 0 .32-2.44z"/></g></svg>

BIN
static/icons/jogpad.png


File diff suppressed because it is too large
+ 5 - 0
static/icons/jogpad.svg


+ 1 - 0
static/icons/lamp.svg

@@ -0,0 +1 @@
+<svg id="Ecommerce" enable-background="new 0 0 48 48" height="512" viewBox="0 0 48 48" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m25 7.03c0-.552-.447-1-1-1-5.514 0-10 4.486-10 10 0 .552.447 1 1 1s1-.448 1-1c0-4.411 3.589-8 8-8 .553 0 1-.448 1-1z"/><path d="m22.246 45.79h1.754 1.754c1.652 0 2.999-1.345 3-3v-1.099c1.032-.475 1.755-1.512 1.755-2.721v-3.72c.176-.382.281-.802.281-1.25 0-1.016.226-2 .671-2.927.441-.919 1.086-1.752 1.864-2.409 3.746-3.165 5.709-7.989 5.25-12.909-.7-7.375-6.813-13.189-14.265-13.525l-.31-.01-.354.011c-7.408.335-13.521 6.149-14.222 13.526-.458 4.917 1.505 9.742 5.251 12.906.778.658 1.423 1.491 1.864 2.41.445.927.671 1.911.671 2.927 0 .447.105.868.281 1.25v3.721c0 1.209.722 2.247 1.755 2.721v1.1c.001 1.653 1.348 2.998 3 2.998zm4.508-3c0 .552-.449 1-1 1h-1.754-1.754c-.551 0-1-.449-1-1v-.82h2.754 2.754zm1.755-3.819c0 .551-.448 1-1 1h-3.509-3.509c-.552 0-1-.449-1-1v-2.068c.232.058.47.097.719.097h3.79 3.79c.249 0 .487-.039.719-.097zm-9.299-4.971c0-1.318-.292-2.595-.868-3.793-.563-1.171-1.385-2.233-2.376-3.071-3.247-2.742-4.948-6.926-4.551-11.191.607-6.389 5.903-11.426 12.275-11.715l.31-.01.265.009c6.417.29 11.713 5.327 12.319 11.714.398 4.267-1.303 8.451-4.55 11.193-.991.838-1.813 1.9-2.376 3.071-.576 1.198-.868 2.475-.868 3.793 0 .551-.448 1-1 1h-3.79-3.79c-.552 0-1-.449-1-1z"/></g></svg>

File diff suppressed because it is too large
+ 0 - 0
static/icons/micro-sd.svg


+ 1 - 0
static/icons/reload.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22 11a1 1 0 0 0 -1 1 9 9 0 1 1 -9-9 8.9 8.9 0 0 1 4.42 1.166l-1.127 1.127a1 1 0 0 0 .707 1.707h4a1 1 0 0 0 1-1v-4a1 1 0 0 0 -1.707-.707l-1.411 1.407a10.9 10.9 0 0 0 -5.882-1.7 11 11 0 1 0 11 11 1 1 0 0 0 -1-1z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
static/icons/settings.svg


BIN
static/icons/single-extruder1.png


BIN
static/icons/single-extruder2.png


+ 1 - 0
static/icons/skip-objects.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" height="512" viewBox="0 0 32 32" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m30 17-3 3-3-3h2c0-6.0654-4.9355-11-11-11s-11 4.9346-11 11h-2c0-7.168 5.832-13 13-13s13 5.832 13 13zm0 5h-6v2h6zm-10 0h-2v2h2zm-16 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2z"/></svg>

+ 53 - 0
static/icons/snowflake.svg

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 412.8 412.8" style="enable-background:new 0 0 412.8 412.8;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M378.4,225.6L304,251.2L274,234v-27.6v-27.2l30-17.2l74.4,25.6c5.2,2,11.2-1.2,12.8-6.4c2-5.2-1.2-11.2-6.4-12.8
+			l-57.6-19.6l54-31.2c4.8-2.8,6.4-9.2,3.6-14c-2.8-4.8-9.2-6.4-14-3.6l-54,31.2l11.6-59.6c1.2-5.6-2.4-10.8-8-12
+			c-5.6-1.2-10.8,2.4-12,8l-15.2,77.2l-30,17.2l-22.8-13.2l-0.4-0.4l-23.2-13.6v-34.4L276,48.8c4.4-3.6,4.8-10,0.8-14.4
+			c-3.6-4.4-10-4.8-14.4-0.8l-45.6,40V10.4c0-5.6-4.4-10.4-10.4-10.4C200.8,0,196,4.4,196,10.4v62.4l-45.6-39.6
+			C146,29.6,139.6,30,136,34c-3.6,4.4-3.2,10.8,0.8,14.4L196,100v34.4L172.8,148l-23.2,13.6l-30-17.2l-15.2-77.2
+			c-1.2-5.6-6.4-9.2-12-8c-5.6,1.2-9.2,6.4-8,12L96,130.8L42,99.6c-4.8-2.8-11.2-1.2-14,3.6s-1.2,11.2,3.6,14l54,31.2L28,168
+			c-5.2,2-8.4,7.6-6.4,12.8s7.6,8.4,12.8,6.4l74.4-25.6l30,17.2v27.6v27.2h0.4l-30,17.2l-74.4-25.6c-5.2-2-11.2,1.2-12.8,6.4
+			c-2,5.2,1.2,11.2,6.4,12.8L86,264l-54,31.2c-4.8,2.8-6.4,9.2-3.6,14c2.8,4.8,9.2,6.4,14,3.6l54-31.2l-11.6,59.6
+			c-1.2,5.6,2.4,10.8,8,12c5.6,1.2,10.8-2.4,12-8L120,268l30-17.2l23.6,13.6l23.2,13.6v34.4L137.6,364c-4.4,3.6-4.8,10-0.8,14.4
+			c3.6,4.4,10,4.8,14.4,0.8l45.6-40v63.2c0,5.6,4.4,10.4,10.4,10.4c5.6,0,10.4-4.4,10.4-10.4V340l45.6,40c4.4,3.6,10.8,3.2,14.4-0.8
+			c3.6-4.4,3.2-10.8-0.8-14.4l-60-52v-34.4l23.2-13.6l23.2-13.6l30,17.2l15.2,77.2c1.2,5.6,6.4,9.2,12,8c5.6-1.2,9.2-6.4,8-12
+			L316.8,282l54,31.2c4.8,2.8,11.2,1.2,14-3.6c2.8-4.8,1.2-11.2-3.6-14l-54-31.2l57.6-19.6c5.2-2,8.4-7.6,6.4-12.8
+			C389.2,226.8,383.6,223.6,378.4,225.6z M252.4,206.4v27.2l-23.2,13.6l-22.8,13.2l-23.6-13.6l-23.2-13.6v-26.8v-27.2l23.2-13.6
+			L206,152l23.2,13.6l0.4,0.4l22.8,13.2V206.4z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

File diff suppressed because it is too large
+ 0 - 0
static/icons/speed.svg


+ 1 - 0
static/icons/temperature.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m36 39.47v-32.47a7 7 0 1 0 -14 0v32.47a13.26 13.26 0 1 0 14 0zm-7 22.59a11.32 11.32 0 0 1 -5.53-21.19 1 1 0 0 0 .53-.87v-33a5 5 0 1 1 10 0v33a1 1 0 0 0 .49.84 11.32 11.32 0 0 1 -5.49 21.22z"/><path d="m30 44.89v-30.25a1 1 0 1 0 -1.94 0v30.25a5.94 5.94 0 1 0 1.94 0zm-1 9.85a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m40.32 9.64h7a1 1 0 0 0 0-1.94h-7a1 1 0 1 0 0 1.94z"/><path d="m40.32 16.06h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 20.55h-7a1 1 0 1 0 0 1.93h7a1 1 0 1 0 0-1.93z"/><path d="m40.32 28.91h4.06a1 1 0 0 0 0-1.94h-4.06a1 1 0 0 0 0 1.94z"/><path d="m47.29 33.39h-7a1 1 0 0 0 0 1.94h7a1 1 0 1 0 0-1.94z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
static/icons/ventilation.svg


+ 1 - 0
static/icons/video-camera.svg

@@ -0,0 +1 @@
+<svg height="472pt" viewBox="0 -87 472 472" width="472pt" xmlns="http://www.w3.org/2000/svg"><path d="m467.101562 26.527344c-3.039062-1.800782-6.796874-1.871094-9.898437-.179688l-108.296875 59.132813v-35.480469c-.03125-27.601562-22.398438-49.96875-50-50h-248.90625c-27.601562.03125-49.96875 22.398438-50 50v197.421875c.03125 27.601563 22.398438 49.96875 50 50h248.90625c27.601562-.03125 49.96875-22.398437 50-50v-34.835937l108.300781 59.132812c3.097657 1.691406 6.859375 1.625 9.894531-.175781 3.039063-1.804688 4.898438-5.074219 4.898438-8.601563v-227.816406c0-3.53125-1.863281-6.796875-4.898438-8.597656zm-138.203124 220.898437c-.015626 16.5625-13.4375 29.980469-30 30h-248.898438c-16.5625-.019531-29.980469-13.4375-30-30v-197.425781c.019531-16.558594 13.4375-29.980469 30-30h248.90625c16.558594.019531 29.980469 13.441406 30 30zm123.101562-1.335937-103.09375-56.289063v-81.535156l103.09375-56.285156zm0 0"/></svg>

+ 2 - 0
static/icons/water.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="512" height="512"><g id="Water"><path d="M24,46A16.0183,16.0183,0,0,1,8,30C8,16.0942,22.708,2.8125,23.3345,2.2539a.9983.9983,0,0,1,1.331,0C25.292,2.8125,40,16.0942,40,30A16.0183,16.0183,0,0,1,24,46ZM24,4.3721C21.1333,7.1372,10,18.6118,10,30a14,14,0,0,0,28,0C38,18.6118,26.8667,7.1372,24,4.3721Z"/><path d="M18.4976,40.5273a.9946.9946,0,0,1-.5-.1342A12.0449,12.0449,0,0,1,12,30a1,1,0,0,1,2,0,10.0373,10.0373,0,0,0,5,8.6616,1,1,0,0,1-.5019,1.8657Z"/></g></svg>

+ 73 - 0
static/icons/webcam.svg

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M256,40c-5.52,0-10,4.48-10,10s4.48,10,10,10s10-4.48,10-10S261.52,40,256,40z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M466,210C466,94.206,371.794,0,256,0S46,94.206,46,210c0,96.488,66.579,180.855,159.516,203.859
+			c-1.591,14.119-6.958,31.441-13.568,38.051l-0.131,0.131c-18.899,0.353-32.638,3.149-42.999,8.73
+			C133.677,468.949,126,482.82,126,502c0,5.522,4.478,10,10,10h240c5.522,0,10-4.478,10-10c0-19.187-7.68-33.058-22.824-41.229
+			c-10.344-5.58-24.082-8.378-42.992-8.731l-0.132-0.132c-6.61-6.609-11.977-23.931-13.568-38.05
+			C399.423,390.853,466,306.486,466,210z M316,472c33.23,0,45.303,7.689,48.794,20H147.226c2.172-7.762,6.862-11.345,11.087-13.626
+			C166.274,474.085,178.603,472,196,472H316z M215.517,452c5.068-10.601,8.238-23.466,9.638-34.27
+			C235.326,419.232,245.658,420,256,420c10.342,0,20.674-0.768,30.845-2.27c1.401,10.804,4.57,23.67,9.638,34.27H215.517z
+			 M294.015,396.179c-0.019,0.004-0.037,0.007-0.056,0.011c-24.788,5.056-51.127,5.057-75.922-0.001
+			c-0.017-0.004-0.035-0.007-0.052-0.01C129.918,378.227,66,299.929,66,210c0-104.767,85.233-190,190-190s190,85.233,190,190
+			C446,299.929,382.082,378.227,294.015,396.179z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M389.606,104.994c-23.072-29.303-55.544-50.505-91.434-59.701c-5.355-1.374-10.799,1.855-12.17,7.205
+			c-1.37,5.35,1.855,10.798,7.205,12.169c31.66,8.112,60.314,26.828,80.686,52.7c3.426,4.352,9.716,5.077,14.043,1.67
+			C392.275,115.621,393.023,109.333,389.606,104.994z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M256,100c-60.654,0-110,49.346-110,110s49.346,110,110,110s110-49.346,110-110S316.654,100,256,100z M256,300
+			c-49.626,0-90-40.374-90-90c0-49.626,40.374-90,90-90c49.626,0,90,40.374,90,90C346,259.626,305.626,300,256,300z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M256,140c-38.598,0-70,31.402-70,70c0,38.598,31.402,70,70,70c38.598,0,70-31.402,70-70C326,171.402,294.598,140,256,140z
+			 M256,260c-27.57,0-50-22.43-50-50s22.43-50,50-50s50,22.43,50,50S283.57,260,256,260z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 2 - 2
static/index.html

@@ -7,8 +7,8 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-BarB10XI.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-DiPSi5MU.css">
+    <script type="module" crossorigin src="/assets/index-BdU-188w.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-Ca04bhbd.css">
   </head>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff