5 Achegas 9fe64d2cce ... 3857993ed6

Autor SHA1 Mensaxe Data
  tuanchris 3857993ed6 MAESTRO: frontend retheme pass 4 — tint pattern previews to theme foreground hai 5 días
  tuanchris 6acf161544 MAESTRO: frontend retheme pass 3 — palette-driven canvas sim, PlaylistsPage display type hai 5 días
  tuanchris 442245ec54 MAESTRO: frontend retheme pass 2 — semantic tokens, display type, outlined-ember destructive hai 5 días
  tuanchris ae8b32f614 MAESTRO: frontend design foundation — Basalt/Dune palette, Bricolage + Plex Mono type roles hai 5 días
  M8te 47b822a29b fix(mqtt): apply clear pattern when selecting a single pattern (#123) hai 6 días
Modificáronse 100 ficheiros con 140 adicións e 84 borrados
  1. 1 1
      frontend/index.html
  2. 20 0
      frontend/package-lock.json
  3. 2 0
      frontend/package.json
  4. 2 2
      frontend/src/components/UpdateDialog.tsx
  5. 1 1
      frontend/src/components/ui/button.tsx
  6. 1 1
      frontend/src/components/ui/card.tsx
  7. 1 1
      frontend/src/components/ui/dialog.tsx
  8. 1 1
      frontend/src/components/ui/sheet.tsx
  9. 71 44
      frontend/src/index.css
  10. 6 9
      frontend/src/main.tsx
  11. 20 13
      frontend/src/pages/BrowsePage.tsx
  12. 6 6
      frontend/src/pages/PlaylistsPage.tsx
  13. 4 4
      frontend/src/pages/WiFiSetupPage.tsx
  14. 4 1
      modules/mqtt/handler.py
  15. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-600-normal-Bw0J83-2.woff
  16. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-600-normal-nxTgbNFE.woff2
  17. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-700-normal-DvJDmrLu.woff
  18. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-700-normal-gtcctNPv.woff2
  19. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-ext-600-normal-CN5JKkBm.woff2
  20. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-ext-600-normal-DF26P3Q7.woff
  21. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-ext-700-normal-BXpPFNFj.woff
  22. BIN=BIN
      static/dist/assets/bricolage-grotesque-latin-ext-700-normal-fBkt-7fi.woff2
  23. BIN=BIN
      static/dist/assets/bricolage-grotesque-vietnamese-600-normal-BppAn7jL.woff
  24. BIN=BIN
      static/dist/assets/bricolage-grotesque-vietnamese-600-normal-Ipewt4hn.woff2
  25. BIN=BIN
      static/dist/assets/bricolage-grotesque-vietnamese-700-normal-BLYPkJbo.woff2
  26. BIN=BIN
      static/dist/assets/bricolage-grotesque-vietnamese-700-normal-CHrFuVHU.woff
  27. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2
  28. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff
  29. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff
  30. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2
  31. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff
  32. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2
  33. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff
  34. BIN=BIN
      static/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2
  35. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff
  36. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2
  37. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff
  38. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2
  39. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2
  40. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff
  41. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2
  42. BIN=BIN
      static/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff
  43. BIN=BIN
      static/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2
  44. BIN=BIN
      static/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff
  45. BIN=BIN
      static/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff
  46. BIN=BIN
      static/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2
  47. 0 0
      static/dist/assets/index-BbPVIrry.js
  48. 0 0
      static/dist/assets/index-CEWL2r_b.css
  49. 0 0
      static/dist/assets/index-DpUgdjlV.js
  50. 0 0
      static/dist/assets/index-RF2BgLvM.css
  51. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-400-normal-BDYvNhAR.woff
  52. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-400-normal-CHP_ranX.woff2
  53. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-500-normal-9zZ_jNuA.woff2
  54. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-500-normal-BxM0HQjg.woff
  55. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-600-normal-BRIw9PIU.woff
  56. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-600-normal-KpAl9xZA.woff2
  57. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-700-normal-D8UNalU-.woff
  58. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-700-normal-DYZmzPmX.woff2
  59. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-400-normal-BjDhGU6t.woff2
  60. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-400-normal-d9FrwbiD.woff
  61. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-500-normal-Bw4G4pNe.woff
  62. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-500-normal-CuwgPeWW.woff2
  63. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-600-normal-Cwz1867h.woff
  64. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-600-normal-DlWr7wnj.woff2
  65. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-700-normal-D83T7awq.woff
  66. BIN=BIN
      static/dist/assets/noto-sans-cyrillic-ext-700-normal-OK-fZO_i.woff2
  67. BIN=BIN
      static/dist/assets/noto-sans-devanagari-400-normal-C3FccbrF.woff2
  68. BIN=BIN
      static/dist/assets/noto-sans-devanagari-400-normal-g9fsM2jL.woff
  69. BIN=BIN
      static/dist/assets/noto-sans-devanagari-500-normal-B62tDw8r.woff
  70. BIN=BIN
      static/dist/assets/noto-sans-devanagari-500-normal-VG35fhMU.woff2
  71. BIN=BIN
      static/dist/assets/noto-sans-devanagari-600-normal-Bly84zfI.woff
  72. BIN=BIN
      static/dist/assets/noto-sans-devanagari-600-normal-Ewgvvq1j.woff2
  73. BIN=BIN
      static/dist/assets/noto-sans-devanagari-700-normal-CT12sGlc.woff
  74. BIN=BIN
      static/dist/assets/noto-sans-devanagari-700-normal-DVs0dmkg.woff2
  75. BIN=BIN
      static/dist/assets/noto-sans-greek-400-normal-Be2BcUUc.woff
  76. BIN=BIN
      static/dist/assets/noto-sans-greek-400-normal-DCESwnT1.woff2
  77. BIN=BIN
      static/dist/assets/noto-sans-greek-500-normal-BAAA_uK7.woff
  78. BIN=BIN
      static/dist/assets/noto-sans-greek-500-normal-D_0l3T9g.woff2
  79. BIN=BIN
      static/dist/assets/noto-sans-greek-600-normal-C0bz_iEd.woff
  80. BIN=BIN
      static/dist/assets/noto-sans-greek-600-normal-CT9U7UAD.woff2
  81. BIN=BIN
      static/dist/assets/noto-sans-greek-700-normal-DDNJsN3F.woff
  82. BIN=BIN
      static/dist/assets/noto-sans-greek-700-normal-x3kNWF-0.woff2
  83. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-400-normal-L11LEhi4.woff
  84. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-400-normal-i2oSBwXz.woff2
  85. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-500-normal-CbZNESfr.woff
  86. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-500-normal-D6bOGD5V.woff2
  87. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-600-normal-B4z4a2vi.woff2
  88. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-600-normal-BjvVOqxV.woff
  89. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-700-normal-BOmIwFkq.woff
  90. BIN=BIN
      static/dist/assets/noto-sans-greek-ext-700-normal-Dq1WA5-1.woff2
  91. BIN=BIN
      static/dist/assets/noto-sans-latin-400-normal-BTkUljjl.woff2
  92. BIN=BIN
      static/dist/assets/noto-sans-latin-400-normal-DPpKfCgK.woff
  93. BIN=BIN
      static/dist/assets/noto-sans-latin-500-normal-CDPngY5C.woff
  94. BIN=BIN
      static/dist/assets/noto-sans-latin-500-normal-DCA2Z9Cj.woff2
  95. BIN=BIN
      static/dist/assets/noto-sans-latin-600-normal-DVJMj_t0.woff
  96. BIN=BIN
      static/dist/assets/noto-sans-latin-600-normal-Dkgu5AE5.woff2
  97. BIN=BIN
      static/dist/assets/noto-sans-latin-700-normal-CrvTEWhy.woff2
  98. BIN=BIN
      static/dist/assets/noto-sans-latin-700-normal-ZyrKABVF.woff
  99. BIN=BIN
      static/dist/assets/noto-sans-latin-ext-400-normal-CNh9dRW_.woff
  100. BIN=BIN
      static/dist/assets/noto-sans-latin-ext-400-normal-CVf-LbaS.woff2

+ 1 - 1
frontend/index.html

@@ -6,7 +6,7 @@
     <meta name="description" content="Control your kinetic sand table" />
 
     <!-- PWA Meta Tags -->
-    <meta name="theme-color" content="#0a0a0a" />
+    <meta name="theme-color" content="#171310" />
     <meta name="apple-mobile-web-app-capable" content="yes" />
     <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
     <meta name="apple-mobile-web-app-title" content="Dune Weaver" />

+ 20 - 0
frontend/package-lock.json

@@ -9,6 +9,8 @@
       "version": "0.0.0",
       "license": "GPL-3.0-or-later",
       "dependencies": {
+        "@fontsource/bricolage-grotesque": "^5.2.10",
+        "@fontsource/ibm-plex-mono": "^5.2.7",
         "@fontsource/noto-sans": "^5.2.10",
         "@fontsource/plus-jakarta-sans": "^5.2.8",
         "@radix-ui/react-accordion": "^1.2.12",
@@ -2774,6 +2776,24 @@
       "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
       "license": "MIT"
     },
+    "node_modules/@fontsource/bricolage-grotesque": {
+      "version": "5.2.10",
+      "resolved": "https://registry.npmjs.org/@fontsource/bricolage-grotesque/-/bricolage-grotesque-5.2.10.tgz",
+      "integrity": "sha512-V2xS+1P7C8IrSypXLUx/bLtX/LsTlYtV2k2CsU+S/0t8qepZ2hvKSlyJIx7Ub/iY8Bbnj+IjAuUF9nvFz+BbIg==",
+      "license": "OFL-1.1",
+      "funding": {
+        "url": "https://github.com/sponsors/ayuhito"
+      }
+    },
+    "node_modules/@fontsource/ibm-plex-mono": {
+      "version": "5.2.7",
+      "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz",
+      "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==",
+      "license": "OFL-1.1",
+      "funding": {
+        "url": "https://github.com/sponsors/ayuhito"
+      }
+    },
     "node_modules/@fontsource/noto-sans": {
       "version": "5.2.10",
       "resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.2.10.tgz",

+ 2 - 0
frontend/package.json

@@ -17,6 +17,8 @@
     "test:e2e:ui": "playwright test --ui"
   },
   "dependencies": {
+    "@fontsource/bricolage-grotesque": "^5.2.10",
+    "@fontsource/ibm-plex-mono": "^5.2.7",
     "@fontsource/noto-sans": "^5.2.10",
     "@fontsource/plus-jakarta-sans": "^5.2.8",
     "@radix-ui/react-accordion": "^1.2.12",

+ 2 - 2
frontend/src/components/UpdateDialog.tsx

@@ -156,8 +156,8 @@ export function UpdateDialog({ open, onOpenChange, currentVersion, latestVersion
 
         {state === 'complete' && (
           <div className="flex flex-col items-center py-8 gap-4">
-            <div className="w-16 h-16 flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
-              <span className="material-icons text-green-600 dark:text-green-400 text-4xl">check_circle</span>
+            <div className="w-16 h-16 flex items-center justify-center rounded-full bg-success/15">
+              <span className="material-icons text-success text-4xl">check_circle</span>
             </div>
             <div className="text-center space-y-1">
               <p className="text-lg font-medium">Update complete!</p>

+ 1 - 1
frontend/src/components/ui/button.tsx

@@ -12,7 +12,7 @@ const buttonVariants = cva(
         default: "bg-card text-foreground border border-border shadow-sm hover:bg-accent",
         primary: "bg-primary text-primary-foreground hover:bg-primary/90",
         destructive:
-          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+          "border-[1.5px] border-destructive text-destructive bg-transparent hover:bg-destructive/10",
         outline:
           "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
         secondary:

+ 1 - 1
frontend/src/components/ui/card.tsx

@@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<
   <div
     ref={ref}
     className={cn(
-      "text-2xl font-semibold leading-none tracking-tight",
+      "font-display text-2xl font-semibold leading-none tracking-tight",
       className
     )}
     {...props}

+ 1 - 1
frontend/src/components/ui/dialog.tsx

@@ -86,7 +86,7 @@ const DialogTitle = React.forwardRef<
   <DialogPrimitive.Title
     ref={ref}
     className={cn(
-      "text-lg font-semibold leading-none tracking-tight",
+      "font-display text-lg font-semibold leading-none tracking-tight",
       className
     )}
     {...props}

+ 1 - 1
frontend/src/components/ui/sheet.tsx

@@ -106,7 +106,7 @@ const SheetTitle = React.forwardRef<
 >(({ className, ...props }, ref) => (
   <SheetPrimitive.Title
     ref={ref}
-    className={cn("text-lg font-semibold text-foreground", className)}
+    className={cn("font-display text-lg font-semibold text-foreground", className)}
     {...props}
   />
 ))

+ 71 - 44
frontend/src/index.css

@@ -2,74 +2,94 @@
 
 /* Fonts imported in main.tsx (self-hosted, no CDN dependency) */
 
-/* Theme configuration using CSS variables */
+/* Theme configuration using CSS variables.
+ * "Table at night" palette shared with dune-weaver-mobile —
+ * token source: dune-weaver-mobile/src/theme/index.ts.
+ * Light = day (fine-sand paper), dark = night (basalt ground). */
 @theme {
-  --color-border: hsl(214.3 31.8% 91.4%);
-  --color-input: hsl(214.3 31.8% 91.4%);
-  --color-ring: hsl(207 90% 50%);
-  --color-background: hsl(220 14% 98%);
-  --color-foreground: hsl(222.2 84% 4.9%);
+  --color-border: #E2D6C2;
+  --color-input: #E2D6C2;
+  --color-ring: #A87F45;
+  --color-background: #F5EFE6;
+  --color-foreground: #292219;
 
-  --color-primary: hsl(207 90% 50%);
-  --color-primary-foreground: hsl(210 40% 98%);
+  --color-primary: #A87F45;
+  --color-primary-foreground: #FFF9EE;
 
-  --color-secondary: hsl(210 40% 96.1%);
-  --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
+  --color-secondary: #EDE4D4;
+  --color-secondary-foreground: #292219;
 
-  --color-muted: hsl(220 14% 92%);
-  --color-muted-foreground: hsl(215.4 16.3% 46.9%);
+  --color-muted: #EDE4D4;
+  --color-muted-foreground: #8A7A63;
 
-  --color-accent: hsl(210 40% 96.1%);
-  --color-accent-foreground: hsl(222.2 47.4% 11.2%);
+  --color-accent: #EDE4D4;
+  --color-accent-foreground: #292219;
 
-  --color-destructive: hsl(0 84.2% 60.2%);
-  --color-destructive-foreground: hsl(210 40% 98%);
+  --color-destructive: #BC5843;
+  --color-destructive-foreground: #FFF9EE;
 
-  --color-card: hsl(0 0% 100%);
-  --color-card-foreground: hsl(222.2 84% 4.9%);
+  --color-card: #FDFAF4;
+  --color-card-foreground: #292219;
+  --color-card-elevated: #EDE4D4;
 
-  --color-popover: hsl(0 0% 100%);
-  --color-popover-foreground: hsl(222.2 84% 4.9%);
+  --color-popover: #FDFAF4;
+  --color-popover-foreground: #292219;
+
+  --color-input-background: #EDE4D4;
+  --color-success: #5E9950;
+  /* Things actually happening on the table right now (progress fills, the
+   * live ball, connect pulse). Distinct from primary so "go do this" (sand)
+   * never reads the same as "this is running" (patina). */
+  --color-live: #35836F;
 
   --radius: 0.5rem;
   --radius-lg: 0.75rem;
   --radius-md: calc(var(--radius) - 2px);
   --radius-sm: calc(var(--radius) - 4px);
 
-  --font-family-sans: 'Plus Jakarta Sans', 'Noto Sans', sans-serif;
+  /* Type roles (mirrors the app): body stays the system face so controls
+   * keep a native feel; display for titles/pattern names/the state word;
+   * mono for telemetry — elapsed/%, feed rate, θ/ρ, IPs, versions, logs. */
+  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  --font-display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, sans-serif;
+  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
+  --font-family-sans: var(--font-sans);
 }
 
-/* Dark mode */
+/* Dark mode — night: basalt ground, walnut cards, lit-sand ink, dune accent */
 .dark {
   color-scheme: dark;
-  --color-border: hsl(0 0% 32%);
-  --color-input: hsl(0 0% 32%);
-  --color-ring: hsl(207 90% 50%);
-  --color-background: hsl(0 0% 10%);
-  --color-foreground: hsl(210 40% 98%);
+  --color-border: #352D23;
+  --color-input: #352D23;
+  --color-ring: #D9B98A;
+  --color-background: #171310;
+  --color-foreground: #F2EAD9;
 
-  --color-primary: hsl(207 90% 50%);
-  --color-primary-foreground: hsl(210 40% 98%);
+  --color-primary: #D9B98A;
+  --color-primary-foreground: #221A0F;
 
-  --color-secondary: hsl(0 0% 25%);
-  --color-secondary-foreground: hsl(210 40% 98%);
+  --color-secondary: #2B241D;
+  --color-secondary-foreground: #F2EAD9;
 
-  --color-muted: hsl(0 0% 25%);
-  --color-muted-foreground: hsl(215 20.2% 65.1%);
+  --color-muted: #2B241D;
+  --color-muted-foreground: #A08F77;
 
-  --color-accent: hsl(0 0% 25%);
-  --color-accent-foreground: hsl(210 40% 98%);
+  --color-accent: #2B241D;
+  --color-accent-foreground: #F2EAD9;
 
-  --color-destructive: hsl(0 62.8% 50.6%);
-  --color-destructive-foreground: hsl(210 40% 98%);
+  --color-destructive: #D97A66;
+  --color-destructive-foreground: #221A0F;
 
-  --color-card: hsl(0 0% 18%);
-  --color-card-foreground: hsl(210 40% 98%);
+  --color-card: #211C17;
+  --color-card-foreground: #F2EAD9;
+  --color-card-elevated: #2B241D;
 
-  --color-input-background: hsl(0 0% 28%);
+  --color-input-background: #2B241D;
+  --color-success: #8FBF7F;
+  --color-live: #7BC4B0;
 
-  --color-popover: hsl(0 0% 15%);
-  --color-popover-foreground: hsl(210 40% 98%);
+  --color-popover: #211C17;
+  --color-popover-foreground: #F2EAD9;
 }
 
 /* Base styles */
@@ -143,9 +163,16 @@ body {
   font-family: 'Material Icons Outlined';
 }
 
-/* Invert pattern previews in dark mode */
+/* Tint pattern previews (black ink on transparent, from the backend renderer)
+   to the theme foreground — the web equivalent of the mobile app's tintColor
+   on its webp thumbnails. Chains are numerically solved to land exactly on
+   the foreground tokens: #292219 (day ink) / #F2EAD9 (lit sand, night). */
+.pattern-preview {
+  filter: brightness(0) saturate(100%) invert(9%) sepia(11%) saturate(1771%) hue-rotate(354deg) brightness(98%) contrast(88%);
+}
+
 .dark .pattern-preview {
-  filter: invert(1);
+  filter: brightness(0) saturate(100%) invert(89%) sepia(29%) saturate(179%) hue-rotate(335deg) brightness(104%) contrast(90%);
 }
 
 /* Marquee animation for scrolling text */

+ 6 - 9
frontend/src/main.tsx

@@ -3,15 +3,12 @@ import { createRoot } from 'react-dom/client'
 import { BrowserRouter } from 'react-router-dom'
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 
-// Self-hosted fonts (no CDN — works offline / hotspot mode)
-import '@fontsource/plus-jakarta-sans/400.css'
-import '@fontsource/plus-jakarta-sans/500.css'
-import '@fontsource/plus-jakarta-sans/600.css'
-import '@fontsource/plus-jakarta-sans/700.css'
-import '@fontsource/noto-sans/400.css'
-import '@fontsource/noto-sans/500.css'
-import '@fontsource/noto-sans/600.css'
-import '@fontsource/noto-sans/700.css'
+// Self-hosted fonts (no CDN — works offline / hotspot mode).
+// Body text stays the system face; these cover the display + telemetry roles.
+import '@fontsource/bricolage-grotesque/600.css'
+import '@fontsource/bricolage-grotesque/700.css'
+import '@fontsource/ibm-plex-mono/400.css'
+import '@fontsource/ibm-plex-mono/500.css'
 import 'material-icons/iconfont/filled.css'
 import 'material-icons/iconfont/outlined.css'
 

+ 20 - 13
frontend/src/pages/BrowsePage.tsx

@@ -466,16 +466,20 @@ export function BrowsePage() {
   const lastDrawnIndexRef = useRef<number>(-1)
   const lastThemeRef = useRef<boolean | null>(null)
 
-  // Get theme colors
+  // Get theme colors from the palette tokens (index.css @theme / .dark)
   const getThemeColors = useCallback(() => {
     const isDark = document.documentElement.classList.contains('dark')
+    const styles = getComputedStyle(document.documentElement)
+    const token = (name: string, fallback: string) =>
+      styles.getPropertyValue(name).trim() || fallback
     return {
       isDark,
-      bgOuter: isDark ? '#1a1a1a' : '#f5f5f5',
-      bgInner: isDark ? '#262626' : '#ffffff',
-      borderColor: isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(128, 128, 128, 0.3)',
-      lineColor: isDark ? '#e5e5e5' : '#333333',
-      markerBorder: isDark ? '#333333' : '#ffffff',
+      bgOuter: token('--color-background', isDark ? '#171310' : '#F5EFE6'),
+      bgInner: token('--color-card', isDark ? '#211C17' : '#FDFAF4'),
+      borderColor: token('--color-border', isDark ? '#352D23' : '#E2D6C2'),
+      lineColor: token('--color-foreground', isDark ? '#F2EAD9' : '#292219'),
+      markerColor: token('--color-live', isDark ? '#7BC4B0' : '#35836F'),
+      markerBorder: token('--color-card', isDark ? '#211C17' : '#FDFAF4'),
     }
   }, [])
 
@@ -580,8 +584,11 @@ export function BrowsePage() {
       const currentPoint = polarToCartesian(coords[upToIndex][0], coords[upToIndex][1], size)
       ctx.beginPath()
       ctx.arc(currentPoint.x, currentPoint.y, 5, 0, Math.PI * 2)
-      ctx.fillStyle = '#0b80ee'
+      ctx.fillStyle = colors.markerColor
+      ctx.shadowColor = colors.markerColor
+      ctx.shadowBlur = 8
       ctx.fill()
+      ctx.shadowBlur = 0
       ctx.strokeStyle = colors.markerBorder
       ctx.lineWidth = 1
       ctx.stroke()
@@ -874,7 +881,7 @@ export function BrowsePage() {
       {/* Page Header */}
       <div className="flex items-start justify-between gap-4 pl-1">
         <div className="space-y-0.5">
-          <h1 className="text-xl font-semibold tracking-tight">Browse Patterns</h1>
+          <h1 className="font-display text-xl font-semibold tracking-tight">Browse Patterns</h1>
           <p className="text-xs text-muted-foreground">
             {patterns.length} patterns available
           </p>
@@ -1017,7 +1024,7 @@ export function BrowsePage() {
             </span>
           </div>
           <div className="space-y-1">
-            <h2 className="text-xl font-semibold">No patterns found</h2>
+            <h2 className="font-display text-xl font-semibold">No patterns found</h2>
             <p className="text-muted-foreground">Try adjusting your search or filters</p>
           </div>
           {(searchQuery || selectedCategory !== 'all') && (
@@ -1067,7 +1074,7 @@ export function BrowsePage() {
                   role="button"
                   tabIndex={0}
                   className={`shrink-0 transition-colors cursor-pointer flex items-center ${
-                    favorites.has(selectedPattern.path) ? 'text-red-500 hover:text-red-600' : 'text-muted-foreground hover:text-red-500'
+                    favorites.has(selectedPattern.path) ? 'text-destructive hover:text-destructive/80' : 'text-muted-foreground hover:text-destructive'
                   }`}
                   onClick={(e) => toggleFavorite(selectedPattern.path, e)}
                   onKeyDown={(e) => {
@@ -1242,7 +1249,7 @@ export function BrowsePage() {
           >
             {/* Modal Header */}
             <div className="flex items-center justify-between p-6 border-b shrink-0">
-              <h3 className="text-xl font-semibold">
+              <h3 className="font-display text-xl font-semibold">
                 {selectedPattern?.name || 'Animated Preview'}
               </h3>
               <Button
@@ -1458,14 +1465,14 @@ function PatternCard({ pattern, isSelected, isFavorite, playTime, playCount, onT
 
       {/* Name and favorite row */}
       <div className="flex items-center justify-between w-full gap-1 px-0.5">
-        <span className="text-xs font-bold text-foreground truncate" title={pattern.name}>
+        <span className="font-display text-xs font-bold text-foreground truncate" title={pattern.name}>
           {pattern.name}
         </span>
         <span
           role="button"
           tabIndex={0}
           className={`shrink-0 transition-colors cursor-pointer ${
-            isFavorite ? 'text-red-500 hover:text-red-600' : 'text-muted-foreground hover:text-red-500'
+            isFavorite ? 'text-destructive hover:text-destructive/80' : 'text-muted-foreground hover:text-destructive'
           }`}
           onClick={(e) => {
             e.stopPropagation()

+ 6 - 6
frontend/src/pages/PlaylistsPage.tsx

@@ -556,7 +556,7 @@ export function PlaylistsPage() {
     <div className="flex flex-col w-full max-w-5xl mx-auto gap-4 sm:gap-6 py-3 sm:py-6 px-0 sm:px-4 overflow-hidden" style={{ height: 'calc(100dvh - 14rem - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))' }}>
       {/* Page Header */}
       <div className="space-y-0.5 sm:space-y-1 shrink-0 pl-1">
-        <h1 className="text-xl font-semibold tracking-tight">Playlists</h1>
+        <h1 className="font-display text-xl font-semibold tracking-tight">Playlists</h1>
         <p className="text-xs text-muted-foreground">
           Create and manage pattern playlists
         </p>
@@ -572,7 +572,7 @@ export function PlaylistsPage() {
         }`}>
           <div className="flex items-center justify-between px-3 py-2.5 border-b shrink-0">
             <div>
-              <h2 className="text-lg font-semibold">My Playlists</h2>
+              <h2 className="font-display text-lg font-semibold">My Playlists</h2>
               <p className="text-sm text-muted-foreground">{playlists.length} playlist{playlists.length !== 1 ? 's' : ''}</p>
             </div>
             {!isPlayOnlyActive && (
@@ -613,7 +613,7 @@ export function PlaylistsPage() {
               >
                 <div className="flex items-center gap-2 min-w-0">
                   <span className="material-icons-outlined text-lg">playlist_play</span>
-                  <span className="truncate text-sm font-medium">{name}</span>
+                  <span className="font-display truncate text-sm font-semibold">{name}</span>
                 </div>
                 {!isPlayOnlyActive && (
                   <div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
@@ -670,7 +670,7 @@ export function PlaylistsPage() {
                 <span className="material-icons-outlined">arrow_back</span>
               </Button>
               <div className="min-w-0">
-                <h2 className="text-lg font-semibold truncate">
+                <h2 className="font-display text-lg font-semibold truncate">
                   {selectedPlaylist || 'Select a Playlist'}
                 </h2>
                 {selectedPlaylist && playlistPatterns.length > 0 && (
@@ -747,7 +747,7 @@ export function PlaylistsPage() {
                         </button>
                       )}
                     </div>
-                    <p className="text-[10px] sm:text-xs truncate font-medium w-full text-center">{getPatternName(path)}</p>
+                    <p className="font-display font-bold text-[10px] sm:text-xs truncate w-full text-center">{getPatternName(path)}</p>
                   </div>
                 ))}
               </div>
@@ -1097,7 +1097,7 @@ export function PlaylistsPage() {
                           </div>
                         )}
                       </div>
-                      <p className={`text-xs truncate font-medium w-full text-center ${isSelected ? 'text-primary' : ''}`}>
+                      <p className={`font-display font-bold text-xs truncate w-full text-center ${isSelected ? 'text-primary' : ''}`}>
                         {pattern.name}
                       </p>
                     </div>

+ 4 - 4
frontend/src/pages/WiFiSetupPage.tsx

@@ -42,7 +42,7 @@ function SignalIcon({ signal }: { signal: number }) {
                signal >= 50 ? 'network_wifi_3_bar' :
                signal >= 25 ? 'network_wifi_2_bar' :
                'network_wifi_1_bar'
-  const color = signal >= 50 ? 'text-green-500' : signal >= 25 ? 'text-yellow-500' : 'text-red-500'
+  const color = signal >= 50 ? 'text-success' : signal >= 25 ? 'text-primary' : 'text-destructive'
   return <span className={`material-icons text-lg ${color}`}>{bars}</span>
 }
 
@@ -230,8 +230,8 @@ export function WiFiSetupPage() {
     <div className="container max-w-lg mx-auto px-3 py-4 space-y-3">
       {/* Hotspot Welcome Banner */}
       {isHotspotMode && (
-        <Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950/30 dark:border-blue-800">
-          <span className="material-icons text-blue-500 mr-2">wifi_tethering</span>
+        <Alert className="border-primary/30 bg-primary/10">
+          <span className="material-icons text-primary mr-2">wifi_tethering</span>
           <AlertDescription>
             <strong>Welcome to Dune Weaver!</strong>
             <br />
@@ -483,7 +483,7 @@ export function WiFiSetupPage() {
                 key={network.ssid}
                 onClick={() => openConnectDialog(network)}
                 className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left transition-colors hover:bg-muted/50
-                  ${network.active ? 'bg-green-50 dark:bg-green-950/20' : ''}`}
+                  ${network.active ? 'bg-success/10' : ''}`}
               >
                 <SignalIcon signal={network.signal} />
                 <div className="flex-1 min-w-0">

+ 4 - 1
modules/mqtt/handler.py

@@ -699,7 +699,10 @@ class MQTTHandler(BaseMQTTHandler):
                 if pattern_name in self.patterns:
                     # Schedule the coroutine to run in the main event loop
                     asyncio.run_coroutine_threadsafe(
-                        self.callback_registry['run_pattern'](file_path=f"{THETA_RHO_DIR}/{pattern_name}"),
+                        self.callback_registry['run_pattern'](
+                            file_path=f"{THETA_RHO_DIR}/{pattern_name}",
+                            clear_pattern=self.state.clear_pattern,
+                        ),
                         self.main_loop
                     ).add_done_callback(
                         lambda _: self._publish_pattern_state(None)  # Clear pattern after execution

BIN=BIN
static/dist/assets/bricolage-grotesque-latin-600-normal-Bw0J83-2.woff


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-600-normal-nxTgbNFE.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-700-normal-DvJDmrLu.woff


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-700-normal-gtcctNPv.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-ext-600-normal-CN5JKkBm.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-ext-600-normal-DF26P3Q7.woff


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-ext-700-normal-BXpPFNFj.woff


BIN=BIN
static/dist/assets/bricolage-grotesque-latin-ext-700-normal-fBkt-7fi.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-vietnamese-600-normal-BppAn7jL.woff


BIN=BIN
static/dist/assets/bricolage-grotesque-vietnamese-600-normal-Ipewt4hn.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-vietnamese-700-normal-BLYPkJbo.woff2


BIN=BIN
static/dist/assets/bricolage-grotesque-vietnamese-700-normal-CHrFuVHU.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2


BIN=BIN
static/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff


BIN=BIN
static/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/dist/assets/index-BbPVIrry.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/dist/assets/index-CEWL2r_b.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/dist/assets/index-DpUgdjlV.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/dist/assets/index-RF2BgLvM.css


BIN=BIN
static/dist/assets/noto-sans-cyrillic-400-normal-BDYvNhAR.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-400-normal-CHP_ranX.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-500-normal-9zZ_jNuA.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-500-normal-BxM0HQjg.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-600-normal-BRIw9PIU.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-600-normal-KpAl9xZA.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-700-normal-D8UNalU-.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-700-normal-DYZmzPmX.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-400-normal-BjDhGU6t.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-400-normal-d9FrwbiD.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-500-normal-Bw4G4pNe.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-500-normal-CuwgPeWW.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-600-normal-Cwz1867h.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-600-normal-DlWr7wnj.woff2


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-700-normal-D83T7awq.woff


BIN=BIN
static/dist/assets/noto-sans-cyrillic-ext-700-normal-OK-fZO_i.woff2


BIN=BIN
static/dist/assets/noto-sans-devanagari-400-normal-C3FccbrF.woff2


BIN=BIN
static/dist/assets/noto-sans-devanagari-400-normal-g9fsM2jL.woff


BIN=BIN
static/dist/assets/noto-sans-devanagari-500-normal-B62tDw8r.woff


BIN=BIN
static/dist/assets/noto-sans-devanagari-500-normal-VG35fhMU.woff2


BIN=BIN
static/dist/assets/noto-sans-devanagari-600-normal-Bly84zfI.woff


BIN=BIN
static/dist/assets/noto-sans-devanagari-600-normal-Ewgvvq1j.woff2


BIN=BIN
static/dist/assets/noto-sans-devanagari-700-normal-CT12sGlc.woff


BIN=BIN
static/dist/assets/noto-sans-devanagari-700-normal-DVs0dmkg.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-400-normal-Be2BcUUc.woff


BIN=BIN
static/dist/assets/noto-sans-greek-400-normal-DCESwnT1.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-500-normal-BAAA_uK7.woff


BIN=BIN
static/dist/assets/noto-sans-greek-500-normal-D_0l3T9g.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-600-normal-C0bz_iEd.woff


BIN=BIN
static/dist/assets/noto-sans-greek-600-normal-CT9U7UAD.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-700-normal-DDNJsN3F.woff


BIN=BIN
static/dist/assets/noto-sans-greek-700-normal-x3kNWF-0.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-ext-400-normal-L11LEhi4.woff


BIN=BIN
static/dist/assets/noto-sans-greek-ext-400-normal-i2oSBwXz.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-ext-500-normal-CbZNESfr.woff


BIN=BIN
static/dist/assets/noto-sans-greek-ext-500-normal-D6bOGD5V.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-ext-600-normal-B4z4a2vi.woff2


BIN=BIN
static/dist/assets/noto-sans-greek-ext-600-normal-BjvVOqxV.woff


BIN=BIN
static/dist/assets/noto-sans-greek-ext-700-normal-BOmIwFkq.woff


BIN=BIN
static/dist/assets/noto-sans-greek-ext-700-normal-Dq1WA5-1.woff2


BIN=BIN
static/dist/assets/noto-sans-latin-400-normal-BTkUljjl.woff2


BIN=BIN
static/dist/assets/noto-sans-latin-400-normal-DPpKfCgK.woff


BIN=BIN
static/dist/assets/noto-sans-latin-500-normal-CDPngY5C.woff


BIN=BIN
static/dist/assets/noto-sans-latin-500-normal-DCA2Z9Cj.woff2


BIN=BIN
static/dist/assets/noto-sans-latin-600-normal-DVJMj_t0.woff


BIN=BIN
static/dist/assets/noto-sans-latin-600-normal-Dkgu5AE5.woff2


BIN=BIN
static/dist/assets/noto-sans-latin-700-normal-CrvTEWhy.woff2


BIN=BIN
static/dist/assets/noto-sans-latin-700-normal-ZyrKABVF.woff


BIN=BIN
static/dist/assets/noto-sans-latin-ext-400-normal-CNh9dRW_.woff


BIN=BIN
static/dist/assets/noto-sans-latin-ext-400-normal-CVf-LbaS.woff2


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio