Spaces:
Running
Running
v0.7.0
Browse files- The logo comes alive: it winks, blinks, looks around, dozes off, squints and even puts on glasses
- The winking head becomes the logo everywhere, at every size, with an HD version for releases
- Community apps get real store cards: emoji, description, tags and thumbnail
- Update settings fit on one line, with a clear green 'Everything is up to date'
- The mic waveform moves right on page load and stays flat in a silent room
- The dashboard remembers your robot's address and reconnects faster
Source: d2ec4fb35b96f37b4adf7b89702a49857a877201
Auto-sync: reachy_web
- VERSION +1 -1
- changelog.json +70 -0
- index.html +4 -4
- pyproject.toml +1 -1
- reachy_web/static/assets/favicon-180.png +2 -2
- reachy_web/static/assets/favicon-64.png +2 -2
- reachy_web/static/assets/favicon.svg +2 -2
- reachy_web/static/assets/logo-192.png +2 -2
- reachy_web/static/assets/logo-2048.png +2 -2
- reachy_web/static/assets/logo-512.png +2 -2
- reachy_web/static/assets/logo-64.png +2 -2
- reachy_web/static/assets/logo.png +2 -2
- reachy_web/static/assets/logo.svg +70 -18
- reachy_web/static/assets/og.png +2 -2
- reachy_web/static/css/app.css +14 -4
- reachy_web/static/css/logo.css +155 -0
- reachy_web/static/js/locales.js +8 -8
- reachy_web/static/js/logo_anim.js +64 -0
- reachy_web/static/js/skins.js +21 -11
- reachy_web/static/js/update.js +41 -27
- reachy_web/static/js/waveform.js +17 -4
- reachy_web/templates/base.html +41 -3
- reachy_web/templates/index.html +1 -1
- reachy_web/templates/sections/appearance.html +11 -10
- reachy_web/templates/sections/update.html +7 -6
- tailwindcss/styles/app.css +15 -1
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
0.
|
|
|
|
| 1 |
+
0.7.0
|
changelog.json
CHANGED
|
@@ -1,4 +1,74 @@
|
|
| 1 |
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
{
|
| 3 |
"version": "0.6.0",
|
| 4 |
"date": "2026-07-16",
|
|
|
|
| 1 |
[
|
| 2 |
+
{
|
| 3 |
+
"version": "0.7.0",
|
| 4 |
+
"date": "2026-07-17",
|
| 5 |
+
"changes": {
|
| 6 |
+
"en": [
|
| 7 |
+
"The logo comes alive: it winks, blinks, looks around, dozes off, squints and even puts on glasses",
|
| 8 |
+
"The winking head becomes the logo everywhere, at every size, with an HD version for releases",
|
| 9 |
+
"Community apps get real store cards: emoji, description, tags and thumbnail",
|
| 10 |
+
"Update settings fit on one line, with a clear green 'Everything is up to date'",
|
| 11 |
+
"The mic waveform moves right on page load and stays flat in a silent room",
|
| 12 |
+
"The dashboard remembers your robot's address and reconnects faster"
|
| 13 |
+
],
|
| 14 |
+
"fr": [
|
| 15 |
+
"Le logo prend vie : clin d'œil, clignements, regards, somnolence, il louche et met même ses lunettes",
|
| 16 |
+
"La tête qui cligne devient le logo partout, à toutes les tailles, avec une version HD pour les releases",
|
| 17 |
+
"Les apps communautaires ont de vraies fiches : emoji, description, tags et vignette",
|
| 18 |
+
"Les réglages de mise à jour tiennent sur une ligne, avec un vrai 'Tout est à jour' en vert",
|
| 19 |
+
"L'onde du micro bouge dès le chargement de la page et reste plate dans une pièce silencieuse",
|
| 20 |
+
"Le dashboard retient l'adresse du robot et se reconnecte plus vite"
|
| 21 |
+
],
|
| 22 |
+
"es": [
|
| 23 |
+
"El logo cobra vida: guiña, parpadea, mira alrededor, se adormece, bizquea y hasta se pone gafas",
|
| 24 |
+
"La cabeza que guiña es ahora el logo en todas partes y en todos los tamaños, con versión HD para releases",
|
| 25 |
+
"Las apps de la comunidad tienen fichas completas: emoji, descripción, etiquetas y miniatura",
|
| 26 |
+
"Los ajustes de actualización caben en una línea, con un claro 'Todo está actualizado' en verde",
|
| 27 |
+
"La onda del micrófono se mueve al cargar la página y queda plana en una sala en silencio",
|
| 28 |
+
"El panel recuerda la dirección del robot y se reconecta más rápido"
|
| 29 |
+
],
|
| 30 |
+
"de": [
|
| 31 |
+
"Das Logo wird lebendig: es zwinkert, blinzelt, schaut sich um, döst ein, schielt und setzt sogar eine Brille auf",
|
| 32 |
+
"Der zwinkernde Kopf ist jetzt überall das Logo, in jeder Größe, mit einer HD-Version für Releases",
|
| 33 |
+
"Community-Apps bekommen echte Karten: Emoji, Beschreibung, Tags und Vorschaubild",
|
| 34 |
+
"Die Update-Einstellungen passen in eine Zeile, mit einem klaren grünen 'Alles ist aktuell'",
|
| 35 |
+
"Die Mikrofon-Welle bewegt sich direkt beim Laden und bleibt in einem stillen Raum flach",
|
| 36 |
+
"Das Dashboard merkt sich die Adresse des Roboters und verbindet sich schneller"
|
| 37 |
+
],
|
| 38 |
+
"it": [
|
| 39 |
+
"Il logo prende vita: strizza l'occhio, sbatte le palpebre, si guarda intorno, si assopisce, incrocia gli occhi e mette persino gli occhiali",
|
| 40 |
+
"La testa che strizza l'occhio è ora il logo ovunque, in ogni dimensione, con una versione HD per le release",
|
| 41 |
+
"Le app della community hanno schede complete: emoji, descrizione, tag e miniatura",
|
| 42 |
+
"Le impostazioni di aggiornamento stanno su una riga, con un chiaro 'Tutto è aggiornato' in verde",
|
| 43 |
+
"L'onda del microfono si muove già al caricamento e resta piatta in una stanza silenziosa",
|
| 44 |
+
"La dashboard ricorda l'indirizzo del robot e si riconnette più in fretta"
|
| 45 |
+
],
|
| 46 |
+
"pt": [
|
| 47 |
+
"O logo ganha vida: pisca o olho, pestaneja, olha em volta, cochila, fica vesgo e até põe óculos",
|
| 48 |
+
"A cabeça que pisca é agora o logo em todo lado e em todos os tamanhos, com versão HD para releases",
|
| 49 |
+
"As apps da comunidade têm fichas completas: emoji, descrição, tags e miniatura",
|
| 50 |
+
"As definições de atualização cabem numa linha, com um claro 'Tudo está atualizado' em verde",
|
| 51 |
+
"A onda do microfone mexe logo ao carregar a página e fica plana numa sala em silêncio",
|
| 52 |
+
"O painel lembra o endereço do robô e reconecta-se mais depressa"
|
| 53 |
+
],
|
| 54 |
+
"ja": [
|
| 55 |
+
"ロゴが生きています。ウインク、まばたき、よそ見、居眠り、寄り目、メガネまでかけます",
|
| 56 |
+
"ウインクする顔があらゆる場所・あらゆるサイズでロゴになり、リリース用の HD 版も用意",
|
| 57 |
+
"コミュニティアプリに絵文字・説明・タグ・サムネイル付きの完全なカード",
|
| 58 |
+
"アップデート設定を 1 行に整理し、緑の「すべて最新です」を表示",
|
| 59 |
+
"マイク波形はページ読み込み直後から動き、静かな部屋では平らなまま",
|
| 60 |
+
"ダッシュボードがロボットのアドレスを記憶し、再接続が速くなりました"
|
| 61 |
+
],
|
| 62 |
+
"ar": [
|
| 63 |
+
"الشعار ينبض بالحياة: يغمز ويرمش وينظر حوله ويغفو ويحوّل عينيه بل ويرتدي نظارة",
|
| 64 |
+
"الرأس الغامز أصبح الشعار في كل مكان وبكل الأحجام، مع نسخة HD للإصدارات",
|
| 65 |
+
"تطبيقات المجتمع صارت لها بطاقات كاملة: إيموجي ووصف ووسوم وصورة مصغّرة",
|
| 66 |
+
"إعدادات التحديث في سطر واحد، مع عبارة خضراء واضحة: كل شيء محدّث",
|
| 67 |
+
"موجة الميكروفون تتحرك فور تحميل الصفحة وتبقى مستقيمة في غرفة هادئة",
|
| 68 |
+
"لوحة التحكم تتذكر عنوان الروبوت وتعيد الاتصال أسرع"
|
| 69 |
+
]
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
{
|
| 73 |
"version": "0.6.0",
|
| 74 |
"date": "2026-07-16",
|
index.html
CHANGED
|
@@ -17,13 +17,13 @@
|
|
| 17 |
<meta property="og:title" content="Reachy Web: the dashboard is back">
|
| 18 |
<meta property="og:description" content="Control your Reachy Mini from any browser again. The removed web dashboard, back as an installable app.">
|
| 19 |
<meta property="og:url" content="https://domotick-reachy-web.static.hf.space/">
|
| 20 |
-
<meta property="og:image" content="https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png">
|
| 21 |
<meta property="og:image:width" content="512">
|
| 22 |
<meta property="og:image:height" content="512">
|
| 23 |
<meta name="twitter:card" content="summary">
|
| 24 |
<meta name="twitter:title" content="Reachy Web: the dashboard is back">
|
| 25 |
<meta name="twitter:description" content="Control your Reachy Mini from any browser again. The removed web dashboard, back as an installable app.">
|
| 26 |
-
<meta name="twitter:image" content="https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png">
|
| 27 |
<script type="application/ld+json">
|
| 28 |
{
|
| 29 |
"@context": "https://schema.org",
|
|
@@ -33,7 +33,7 @@
|
|
| 33 |
"operatingSystem": "Any (web browser)",
|
| 34 |
"description": "The Reachy Mini web dashboard, back as an installable app. Control your Reachy Mini Lite or Wireless from any browser: live camera, 3D digital twin, moves, app store, volume, no desktop app needed.",
|
| 35 |
"url": "https://domotick-reachy-web.static.hf.space/",
|
| 36 |
-
"image": "https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png",
|
| 37 |
"author": { "@type": "Organization", "name": "Domotick", "url": "https://huggingface.co/Domotick" },
|
| 38 |
"keywords": "Reachy Mini, Reachy Mini Lite, Reachy Mini Wireless, dashboard, web dashboard, 3D digital twin, Reachy Mirror, Reachy Eyes, Domotick, Pollen Robotics",
|
| 39 |
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
|
|
@@ -49,7 +49,7 @@
|
|
| 49 |
<div class="flex items-center justify-center gap-2.5 whitespace-nowrap font-display text-lg font-semibold md:justify-start">
|
| 50 |
<img src="reachy_web/static/assets/logo.png" alt="Reachy Web logo" class="h-9 w-9 rounded-xl">
|
| 51 |
<span>Reachy Web</span>
|
| 52 |
-
<span class="version-pill" id="version-pill">v0.
|
| 53 |
</div>
|
| 54 |
<div class="flex flex-wrap items-center justify-center gap-3.5 md:justify-end">
|
| 55 |
<div class="flex flex-wrap items-center gap-1.5" id="palettes" title="Palette"></div>
|
|
|
|
| 17 |
<meta property="og:title" content="Reachy Web: the dashboard is back">
|
| 18 |
<meta property="og:description" content="Control your Reachy Mini from any browser again. The removed web dashboard, back as an installable app.">
|
| 19 |
<meta property="og:url" content="https://domotick-reachy-web.static.hf.space/">
|
| 20 |
+
<meta property="og:image" content="https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png?v=2">
|
| 21 |
<meta property="og:image:width" content="512">
|
| 22 |
<meta property="og:image:height" content="512">
|
| 23 |
<meta name="twitter:card" content="summary">
|
| 24 |
<meta name="twitter:title" content="Reachy Web: the dashboard is back">
|
| 25 |
<meta name="twitter:description" content="Control your Reachy Mini from any browser again. The removed web dashboard, back as an installable app.">
|
| 26 |
+
<meta name="twitter:image" content="https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png?v=2">
|
| 27 |
<script type="application/ld+json">
|
| 28 |
{
|
| 29 |
"@context": "https://schema.org",
|
|
|
|
| 33 |
"operatingSystem": "Any (web browser)",
|
| 34 |
"description": "The Reachy Mini web dashboard, back as an installable app. Control your Reachy Mini Lite or Wireless from any browser: live camera, 3D digital twin, moves, app store, volume, no desktop app needed.",
|
| 35 |
"url": "https://domotick-reachy-web.static.hf.space/",
|
| 36 |
+
"image": "https://domotick-reachy-web.static.hf.space/reachy_web/static/assets/og.png?v=2",
|
| 37 |
"author": { "@type": "Organization", "name": "Domotick", "url": "https://huggingface.co/Domotick" },
|
| 38 |
"keywords": "Reachy Mini, Reachy Mini Lite, Reachy Mini Wireless, dashboard, web dashboard, 3D digital twin, Reachy Mirror, Reachy Eyes, Domotick, Pollen Robotics",
|
| 39 |
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
|
|
|
|
| 49 |
<div class="flex items-center justify-center gap-2.5 whitespace-nowrap font-display text-lg font-semibold md:justify-start">
|
| 50 |
<img src="reachy_web/static/assets/logo.png" alt="Reachy Web logo" class="h-9 w-9 rounded-xl">
|
| 51 |
<span>Reachy Web</span>
|
| 52 |
+
<span class="version-pill" id="version-pill">v0.7.0</span>
|
| 53 |
</div>
|
| 54 |
<div class="flex flex-wrap items-center justify-center gap-3.5 md:justify-end">
|
| 55 |
<div class="flex flex-wrap items-center gap-1.5" id="palettes" title="Palette"></div>
|
pyproject.toml
CHANGED
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
| 5 |
|
| 6 |
[project]
|
| 7 |
name = "reachy_web"
|
| 8 |
-
version = "0.
|
| 9 |
description = "The web dashboard for your Reachy Mini, in any browser"
|
| 10 |
readme = "README.md"
|
| 11 |
requires-python = ">=3.10"
|
|
|
|
| 5 |
|
| 6 |
[project]
|
| 7 |
name = "reachy_web"
|
| 8 |
+
version = "0.7.0"
|
| 9 |
description = "The web dashboard for your Reachy Mini, in any browser"
|
| 10 |
readme = "README.md"
|
| 11 |
requires-python = ">=3.10"
|
reachy_web/static/assets/favicon-180.png
CHANGED
|
|
Git LFS Details
|
|
|
Git LFS Details
|
reachy_web/static/assets/favicon-64.png
CHANGED
|
|
Git LFS Details
|
|
|
Git LFS Details
|
reachy_web/static/assets/favicon.svg
CHANGED
|
|
|
|
reachy_web/static/assets/logo-192.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/assets/logo-2048.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/assets/logo-512.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/assets/logo-64.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/assets/logo.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/assets/logo.svg
CHANGED
|
|
|
|
reachy_web/static/assets/og.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
reachy_web/static/css/app.css
CHANGED
|
@@ -198,6 +198,9 @@
|
|
| 198 |
.pointer-events-none {
|
| 199 |
pointer-events: none;
|
| 200 |
}
|
|
|
|
|
|
|
|
|
|
| 201 |
.visible {
|
| 202 |
visibility: visible;
|
| 203 |
}
|
|
@@ -564,8 +567,8 @@
|
|
| 564 |
.gap-y-4 {
|
| 565 |
row-gap: calc(var(--spacing) * 4);
|
| 566 |
}
|
| 567 |
-
.justify-self-
|
| 568 |
-
justify-self: flex-
|
| 569 |
}
|
| 570 |
.truncate {
|
| 571 |
overflow: hidden;
|
|
@@ -852,6 +855,9 @@
|
|
| 852 |
.text-left {
|
| 853 |
text-align: left;
|
| 854 |
}
|
|
|
|
|
|
|
|
|
|
| 855 |
.font-mono {
|
| 856 |
font-family: var(--font-mono);
|
| 857 |
}
|
|
@@ -1695,6 +1701,9 @@
|
|
| 1695 |
:root[data-palette][data-theme=light] body {
|
| 1696 |
background: radial-gradient(1200px 600px at 50% -10%, #fff, var(--bg)) fixed;
|
| 1697 |
}
|
|
|
|
|
|
|
|
|
|
| 1698 |
html {
|
| 1699 |
-webkit-font-smoothing: antialiased;
|
| 1700 |
-moz-osx-font-smoothing: grayscale;
|
|
@@ -1744,10 +1753,11 @@ body {
|
|
| 1744 |
flex-wrap: wrap;
|
| 1745 |
}
|
| 1746 |
.palettes {
|
| 1747 |
-
display:
|
|
|
|
|
|
|
| 1748 |
align-items: center;
|
| 1749 |
gap: 6px;
|
| 1750 |
-
flex-wrap: wrap;
|
| 1751 |
}
|
| 1752 |
.pdot {
|
| 1753 |
width: 16px;
|
|
|
|
| 198 |
.pointer-events-none {
|
| 199 |
pointer-events: none;
|
| 200 |
}
|
| 201 |
+
.invisible {
|
| 202 |
+
visibility: hidden;
|
| 203 |
+
}
|
| 204 |
.visible {
|
| 205 |
visibility: visible;
|
| 206 |
}
|
|
|
|
| 567 |
.gap-y-4 {
|
| 568 |
row-gap: calc(var(--spacing) * 4);
|
| 569 |
}
|
| 570 |
+
.justify-self-end {
|
| 571 |
+
justify-self: flex-end;
|
| 572 |
}
|
| 573 |
.truncate {
|
| 574 |
overflow: hidden;
|
|
|
|
| 855 |
.text-left {
|
| 856 |
text-align: left;
|
| 857 |
}
|
| 858 |
+
.text-right {
|
| 859 |
+
text-align: right;
|
| 860 |
+
}
|
| 861 |
.font-mono {
|
| 862 |
font-family: var(--font-mono);
|
| 863 |
}
|
|
|
|
| 1701 |
:root[data-palette][data-theme=light] body {
|
| 1702 |
background: radial-gradient(1200px 600px at 50% -10%, #fff, var(--bg)) fixed;
|
| 1703 |
}
|
| 1704 |
+
.hidden {
|
| 1705 |
+
display: none !important;
|
| 1706 |
+
}
|
| 1707 |
html {
|
| 1708 |
-webkit-font-smoothing: antialiased;
|
| 1709 |
-moz-osx-font-smoothing: grayscale;
|
|
|
|
| 1753 |
flex-wrap: wrap;
|
| 1754 |
}
|
| 1755 |
.palettes {
|
| 1756 |
+
display: grid;
|
| 1757 |
+
grid-template-columns: repeat(7, auto);
|
| 1758 |
+
justify-items: center;
|
| 1759 |
align-items: center;
|
| 1760 |
gap: 6px;
|
|
|
|
| 1761 |
}
|
| 1762 |
.pdot {
|
| 1763 |
width: 16px;
|
reachy_web/static/css/logo.css
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Topbar logo animations.
|
| 2 |
+
The SVG is INLINE in base.html (not an <img>: that runs in the browser's secure
|
| 3 |
+
static mode, no scripts, so its timing could only ever be a fixed CSS loop).
|
| 4 |
+
logo_anim.js adds one v-* class at a time, waits out its duration, removes it —
|
| 5 |
+
so every keyframe set here runs ONCE and lands back on the resting pose.
|
| 6 |
+
Y axis only on the eyes (never X); each antenna pivots where it leaves the head. */
|
| 7 |
+
|
| 8 |
+
.lg-ant-l { transform-box: fill-box; transform-origin: 100% 100%; }
|
| 9 |
+
.lg-ant-r { transform-box: fill-box; transform-origin: 0% 100%; }
|
| 10 |
+
.lg-eye, .lg-wink { transform-box: fill-box; transform-origin: center; }
|
| 11 |
+
.lg-wink { opacity: 0; } /* the wink curve only shows while an eye is shut */
|
| 12 |
+
|
| 13 |
+
/* ===== the antennas never go still =====
|
| 14 |
+
A permanent, tiny breath on the PARENT group of each antenna. It has to be the
|
| 15 |
+
parent: a move (lg-ant-*) animates `transform` too, and two animations on one
|
| 16 |
+
element fight — nested, the two rotations simply compose. Both pivot on the
|
| 17 |
+
same point (where the antenna leaves the head), and their periods share no
|
| 18 |
+
factor (7.3s / 11.9s), so the pair never falls into a metronome beat. */
|
| 19 |
+
.lg-sway-l {
|
| 20 |
+
transform-box: fill-box; transform-origin: 100% 100%;
|
| 21 |
+
animation: lgSwayL 7.3s ease-in-out infinite;
|
| 22 |
+
}
|
| 23 |
+
.lg-sway-r {
|
| 24 |
+
transform-box: fill-box; transform-origin: 0% 100%;
|
| 25 |
+
animation: lgSwayR 11.9s ease-in-out infinite;
|
| 26 |
+
}
|
| 27 |
+
@keyframes lgSwayL { 0%, 100% { transform: rotate(-5deg) } 50% { transform: rotate(5deg) } }
|
| 28 |
+
@keyframes lgSwayR { 0%, 100% { transform: rotate(4deg) } 50% { transform: rotate(-4deg) } }
|
| 29 |
+
|
| 30 |
+
/* ================= 1. WINK — the signature (1.1s) ================= */
|
| 31 |
+
.v-wink .lg-eye-r { animation: lgWinkEye 1.1s ease-in-out 1 both; }
|
| 32 |
+
.v-wink .lg-wink { animation: lgWinkCurve 1.1s ease-in-out 1 both; }
|
| 33 |
+
.v-wink .lg-ant-r { animation: lgWinkAnt 1.1s ease-in-out 1 both; }
|
| 34 |
+
@keyframes lgWinkEye {
|
| 35 |
+
0% { transform: scaleY(1); opacity: 1 }
|
| 36 |
+
9% { transform: scaleY(.12); opacity: 0 } /* snaps shut, 100ms */
|
| 37 |
+
64% { transform: scaleY(.12); opacity: 0 } /* held 600ms as the curve */
|
| 38 |
+
77% { transform: scaleY(1.25); opacity: 1 } /* springs past rest */
|
| 39 |
+
86% { transform: scaleY(.8); opacity: 1 } /* bounces under it */
|
| 40 |
+
100% { transform: scaleY(1); opacity: 1 } /* settles */
|
| 41 |
+
}
|
| 42 |
+
@keyframes lgWinkCurve { 0%, 8% { opacity: 0 } 9%, 64% { opacity: 1 } 70%, 100% { opacity: 0 } }
|
| 43 |
+
@keyframes lgWinkAnt { 0% { transform: rotate(0) } 20% { transform: rotate(-6deg) } 60% { transform: rotate(3deg) } 100% { transform: rotate(0) } }
|
| 44 |
+
|
| 45 |
+
/* ================= 2. LOOK — follows someone across the room (2.6s) ================= */
|
| 46 |
+
.v-look .lg-eye-l, .v-look .lg-eye-r { animation: lgLookEye 2.6s ease-in-out 1 both; }
|
| 47 |
+
.v-look .lg-ant-l { animation: lgLookAntL 2.6s ease-in-out 1 both; }
|
| 48 |
+
.v-look .lg-ant-r { animation: lgLookAntR 2.6s ease-in-out 1 both; }
|
| 49 |
+
@keyframes lgLookEye {
|
| 50 |
+
0%, 8% { transform: translateX(0) }
|
| 51 |
+
24%, 38% { transform: translateX(3.2px) }
|
| 52 |
+
56%, 72% { transform: translateX(-3.2px) }
|
| 53 |
+
92%, 100% { transform: translateX(0) }
|
| 54 |
+
}
|
| 55 |
+
@keyframes lgLookAntL { 0%, 20% { transform: rotate(0) } 40% { transform: rotate(-5deg) } 70% { transform: rotate(2deg) } 100% { transform: rotate(0) } }
|
| 56 |
+
@keyframes lgLookAntR { 0%, 30% { transform: rotate(0) } 55% { transform: rotate(5deg) } 80% { transform: rotate(-2deg) } 100% { transform: rotate(0) } }
|
| 57 |
+
|
| 58 |
+
/* ================= 3. DOUBLE BLINK — both eyes, twice (0.9s) ================= */
|
| 59 |
+
.v-blink .lg-eye-l, .v-blink .lg-eye-r { animation: lgBlinkEye .9s ease-in-out 1 both; }
|
| 60 |
+
.v-blink .lg-ant-l { animation: lgBlinkAnt .9s ease-in-out 1 both; }
|
| 61 |
+
@keyframes lgBlinkEye {
|
| 62 |
+
0% { transform: scaleY(1) }
|
| 63 |
+
12% { transform: scaleY(.08) }
|
| 64 |
+
24% { transform: scaleY(1) }
|
| 65 |
+
40% { transform: scaleY(.08) }
|
| 66 |
+
55% { transform: scaleY(1.15) }
|
| 67 |
+
72%, 100% { transform: scaleY(1) }
|
| 68 |
+
}
|
| 69 |
+
@keyframes lgBlinkAnt { 0% { transform: rotate(0) } 30% { transform: rotate(-4deg) } 60% { transform: rotate(3deg) } 100% { transform: rotate(0) } }
|
| 70 |
+
|
| 71 |
+
/* ================= 5. SLEEPY — lids droop, then a startle (2.4s) ================= */
|
| 72 |
+
.v-sleepy .lg-eye-l, .v-sleepy .lg-eye-r { animation: lgSleepEye 2.4s ease-in 1 both; }
|
| 73 |
+
.v-sleepy .lg-ant-l { animation: lgSleepAntL 2.4s ease-in-out 1 both; }
|
| 74 |
+
.v-sleepy .lg-ant-r { animation: lgSleepAntR 2.4s ease-in-out 1 both; }
|
| 75 |
+
@keyframes lgSleepEye {
|
| 76 |
+
0% { transform: scaleY(1) }
|
| 77 |
+
45% { transform: scaleY(.45) } /* the lids slide down… */
|
| 78 |
+
62% { transform: scaleY(.1) }
|
| 79 |
+
68% { transform: scaleY(1.3) } /* …startle! */
|
| 80 |
+
76% { transform: scaleY(.9) }
|
| 81 |
+
86%, 100% { transform: scaleY(1) }
|
| 82 |
+
}
|
| 83 |
+
@keyframes lgSleepAntL { 0% { transform: rotate(0) } 55% { transform: rotate(9deg) } 68% { transform: rotate(-6deg) } 100% { transform: rotate(0) } }
|
| 84 |
+
@keyframes lgSleepAntR { 0% { transform: rotate(0) } 55% { transform: rotate(-9deg) } 68% { transform: rotate(6deg) } 100% { transform: rotate(0) } }
|
| 85 |
+
|
| 86 |
+
/* ================= 6. EXCITED — antennas buzz, eyes go wide (1.8s) ================= */
|
| 87 |
+
.v-excited .lg-ant-l { animation: lgExAntL 1.8s ease-in-out 1 both; }
|
| 88 |
+
.v-excited .lg-ant-r { animation: lgExAntR 1.8s ease-in-out 1 both; }
|
| 89 |
+
.v-excited .lg-eye-l, .v-excited .lg-eye-r { animation: lgExEye 1.8s ease-in-out 1 both; }
|
| 90 |
+
@keyframes lgExAntL {
|
| 91 |
+
0% { transform: rotate(0) }
|
| 92 |
+
8% { transform: rotate(-7deg) } 16% { transform: rotate(7deg) }
|
| 93 |
+
24% { transform: rotate(-6deg) } 32% { transform: rotate(6deg) }
|
| 94 |
+
40% { transform: rotate(-5deg) } 48% { transform: rotate(5deg) }
|
| 95 |
+
56% { transform: rotate(-4deg) } 64% { transform: rotate(4deg) }
|
| 96 |
+
72% { transform: rotate(-2deg) } 80% { transform: rotate(2deg) }
|
| 97 |
+
90%, 100% { transform: rotate(0) }
|
| 98 |
+
}
|
| 99 |
+
@keyframes lgExAntR {
|
| 100 |
+
0% { transform: rotate(0) }
|
| 101 |
+
8% { transform: rotate(7deg) } 16% { transform: rotate(-7deg) }
|
| 102 |
+
24% { transform: rotate(6deg) } 32% { transform: rotate(-6deg) }
|
| 103 |
+
40% { transform: rotate(5deg) } 48% { transform: rotate(-5deg) }
|
| 104 |
+
56% { transform: rotate(4deg) } 64% { transform: rotate(-4deg) }
|
| 105 |
+
72% { transform: rotate(2deg) } 80% { transform: rotate(-2deg) }
|
| 106 |
+
90%, 100% { transform: rotate(0) }
|
| 107 |
+
}
|
| 108 |
+
@keyframes lgExEye { 0% { transform: scale(1) } 18% { transform: scale(1.28) } 70% { transform: scale(1.28) } 100% { transform: scale(1) } }
|
| 109 |
+
|
| 110 |
+
/* ================= 9. BOOT — the eyes flicker on, CRT style (1.6s) ================= */
|
| 111 |
+
.v-boot .lg-eye-l { animation: lgBootL 1.6s steps(1) 1 both; }
|
| 112 |
+
.v-boot .lg-eye-r { animation: lgBootR 1.6s steps(1) 1 both; }
|
| 113 |
+
.v-boot .lg-ant-l { animation: lgBootAnt 1.6s ease-out 1 both; }
|
| 114 |
+
@keyframes lgBootL {
|
| 115 |
+
0%, 10% { opacity: 0 }
|
| 116 |
+
14% { opacity: 1 } 18% { opacity: 0 } 24% { opacity: 1 } 28% { opacity: 0 }
|
| 117 |
+
34%, 100% { opacity: 1 }
|
| 118 |
+
}
|
| 119 |
+
@keyframes lgBootR {
|
| 120 |
+
0%, 22% { opacity: 0 }
|
| 121 |
+
26% { opacity: 1 } 30% { opacity: 0 } 38% { opacity: 1 } 42% { opacity: 0 }
|
| 122 |
+
48%, 100% { opacity: 1 }
|
| 123 |
+
}
|
| 124 |
+
@keyframes lgBootAnt { 0%, 40% { transform: rotate(0) } 52% { transform: rotate(-11deg) } 70% { transform: rotate(4deg) } 100% { transform: rotate(0) } }
|
| 125 |
+
|
| 126 |
+
@media (prefers-reduced-motion: reduce) {
|
| 127 |
+
.topbar-logo * { animation: none !important; }
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* ================= 10. SQUINT — goes cross-eyed a moment (2s) ================= */
|
| 131 |
+
.v-squint .lg-eye-l { animation: lgSquintL 2s ease-in-out 1 both; }
|
| 132 |
+
.v-squint .lg-eye-r { animation: lgSquintR 2s ease-in-out 1 both; }
|
| 133 |
+
@keyframes lgSquintL {
|
| 134 |
+
0%, 8% { transform: translateX(0) }
|
| 135 |
+
20%, 72% { transform: translateX(4.5px) } /* both eyes dive toward the nose */
|
| 136 |
+
88%, 100% { transform: translateX(0) }
|
| 137 |
+
}
|
| 138 |
+
@keyframes lgSquintR {
|
| 139 |
+
0%, 8% { transform: translateX(0) }
|
| 140 |
+
20%, 72% { transform: translateX(-4.5px) }
|
| 141 |
+
88%, 100% { transform: translateX(0) }
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* ================= 11. GLASSES — puts its glasses on (2.8s) ================= */
|
| 145 |
+
/* the .lg-glasses group sits in the SVG, invisible at rest; the move drops it
|
| 146 |
+
onto the eyes with a tiny overshoot, wears it a moment, lifts it away */
|
| 147 |
+
.lg-glasses { transform-box: fill-box; transform-origin: center; opacity: 0; }
|
| 148 |
+
.v-glasses .lg-glasses { animation: lgGlasses 2.8s ease-in-out 1 both; }
|
| 149 |
+
@keyframes lgGlasses {
|
| 150 |
+
0% { opacity: 0; transform: translateY(-28px) }
|
| 151 |
+
10% { opacity: 1; transform: translateY(2.5px) } /* lands, slight overshoot */
|
| 152 |
+
16% { transform: translateY(0) }
|
| 153 |
+
72% { opacity: 1; transform: translateY(0) } /* worn ~1.6s */
|
| 154 |
+
86%, 100% { opacity: 0; transform: translateY(-28px) } /* lifted off */
|
| 155 |
+
}
|
reachy_web/static/js/locales.js
CHANGED
|
@@ -276,7 +276,7 @@ window.LOCALES = {
|
|
| 276 |
"update.ref_placeholder": "e.g. v1.2.7 or develop",
|
| 277 |
"update.start": "Update",
|
| 278 |
"update.title": "Update",
|
| 279 |
-
"update.up_to_date": "
|
| 280 |
"update.updating": "Updating...",
|
| 281 |
"update.version_unknown": "Version unknown",
|
| 282 |
"volume.microphone": "Microphone level",
|
|
@@ -678,7 +678,7 @@ window.LOCALES = {
|
|
| 678 |
"update.ref_placeholder": "ex. v1.2.7 ou develop",
|
| 679 |
"update.start": "Mettre à jour",
|
| 680 |
"update.title": "Mise à jour",
|
| 681 |
-
"update.up_to_date": "
|
| 682 |
"update.updating": "Mise à jour...",
|
| 683 |
"update.version_unknown": "Version inconnue",
|
| 684 |
"volume.microphone": "Niveau du micro",
|
|
@@ -1080,7 +1080,7 @@ window.LOCALES = {
|
|
| 1080 |
"update.ref_placeholder": "p. ej. v1.2.7 o develop",
|
| 1081 |
"update.start": "Actualizar",
|
| 1082 |
"update.title": "Actualización",
|
| 1083 |
-
"update.up_to_date": "
|
| 1084 |
"update.updating": "Actualizando...",
|
| 1085 |
"update.version_unknown": "Versión desconocida",
|
| 1086 |
"volume.microphone": "Nivel del micrófono",
|
|
@@ -1482,7 +1482,7 @@ window.LOCALES = {
|
|
| 1482 |
"update.ref_placeholder": "z. B. v1.2.7 oder develop",
|
| 1483 |
"update.start": "Aktualisieren",
|
| 1484 |
"update.title": "Update",
|
| 1485 |
-
"update.up_to_date": "
|
| 1486 |
"update.updating": "Aktualisiere…",
|
| 1487 |
"update.version_unknown": "Version unbekannt",
|
| 1488 |
"volume.microphone": "Mikrofonpegel",
|
|
@@ -1884,7 +1884,7 @@ window.LOCALES = {
|
|
| 1884 |
"update.ref_placeholder": "es. v1.2.7 o develop",
|
| 1885 |
"update.start": "Aggiorna",
|
| 1886 |
"update.title": "Aggiornamento",
|
| 1887 |
-
"update.up_to_date": "
|
| 1888 |
"update.updating": "Aggiornamento...",
|
| 1889 |
"update.version_unknown": "Versione sconosciuta",
|
| 1890 |
"volume.microphone": "Livello del microfono",
|
|
@@ -2286,7 +2286,7 @@ window.LOCALES = {
|
|
| 2286 |
"update.ref_placeholder": "ex.: v1.2.7 ou develop",
|
| 2287 |
"update.start": "Atualizar",
|
| 2288 |
"update.title": "Atualização",
|
| 2289 |
-
"update.up_to_date": "
|
| 2290 |
"update.updating": "A atualizar...",
|
| 2291 |
"update.version_unknown": "Versão desconhecida",
|
| 2292 |
"volume.microphone": "Nível do microfone",
|
|
@@ -2688,7 +2688,7 @@ window.LOCALES = {
|
|
| 2688 |
"update.ref_placeholder": "例: v1.2.7 または develop",
|
| 2689 |
"update.start": "アップデート",
|
| 2690 |
"update.title": "アップデート",
|
| 2691 |
-
"update.up_to_date": "最新
|
| 2692 |
"update.updating": "更新中...",
|
| 2693 |
"update.version_unknown": "バージョン不明",
|
| 2694 |
"volume.microphone": "マイクレベル",
|
|
@@ -3090,7 +3090,7 @@ window.LOCALES = {
|
|
| 3090 |
"update.ref_placeholder": "مثلاً v1.2.7 أو develop",
|
| 3091 |
"update.start": "تحديث",
|
| 3092 |
"update.title": "تحديث",
|
| 3093 |
-
"update.up_to_date": "محدّث
|
| 3094 |
"update.updating": "جارٍ التحديث...",
|
| 3095 |
"update.version_unknown": "الإصدار غير معروف",
|
| 3096 |
"volume.microphone": "مستوى الميكروفون",
|
|
|
|
| 276 |
"update.ref_placeholder": "e.g. v1.2.7 or develop",
|
| 277 |
"update.start": "Update",
|
| 278 |
"update.title": "Update",
|
| 279 |
+
"update.up_to_date": "Everything is up to date",
|
| 280 |
"update.updating": "Updating...",
|
| 281 |
"update.version_unknown": "Version unknown",
|
| 282 |
"volume.microphone": "Microphone level",
|
|
|
|
| 678 |
"update.ref_placeholder": "ex. v1.2.7 ou develop",
|
| 679 |
"update.start": "Mettre à jour",
|
| 680 |
"update.title": "Mise à jour",
|
| 681 |
+
"update.up_to_date": "Tout est à jour",
|
| 682 |
"update.updating": "Mise à jour...",
|
| 683 |
"update.version_unknown": "Version inconnue",
|
| 684 |
"volume.microphone": "Niveau du micro",
|
|
|
|
| 1080 |
"update.ref_placeholder": "p. ej. v1.2.7 o develop",
|
| 1081 |
"update.start": "Actualizar",
|
| 1082 |
"update.title": "Actualización",
|
| 1083 |
+
"update.up_to_date": "Todo está actualizado",
|
| 1084 |
"update.updating": "Actualizando...",
|
| 1085 |
"update.version_unknown": "Versión desconocida",
|
| 1086 |
"volume.microphone": "Nivel del micrófono",
|
|
|
|
| 1482 |
"update.ref_placeholder": "z. B. v1.2.7 oder develop",
|
| 1483 |
"update.start": "Aktualisieren",
|
| 1484 |
"update.title": "Update",
|
| 1485 |
+
"update.up_to_date": "Alles ist aktuell",
|
| 1486 |
"update.updating": "Aktualisiere…",
|
| 1487 |
"update.version_unknown": "Version unbekannt",
|
| 1488 |
"volume.microphone": "Mikrofonpegel",
|
|
|
|
| 1884 |
"update.ref_placeholder": "es. v1.2.7 o develop",
|
| 1885 |
"update.start": "Aggiorna",
|
| 1886 |
"update.title": "Aggiornamento",
|
| 1887 |
+
"update.up_to_date": "Tutto è aggiornato",
|
| 1888 |
"update.updating": "Aggiornamento...",
|
| 1889 |
"update.version_unknown": "Versione sconosciuta",
|
| 1890 |
"volume.microphone": "Livello del microfono",
|
|
|
|
| 2286 |
"update.ref_placeholder": "ex.: v1.2.7 ou develop",
|
| 2287 |
"update.start": "Atualizar",
|
| 2288 |
"update.title": "Atualização",
|
| 2289 |
+
"update.up_to_date": "Tudo está atualizado",
|
| 2290 |
"update.updating": "A atualizar...",
|
| 2291 |
"update.version_unknown": "Versão desconhecida",
|
| 2292 |
"volume.microphone": "Nível do microfone",
|
|
|
|
| 2688 |
"update.ref_placeholder": "例: v1.2.7 または develop",
|
| 2689 |
"update.start": "アップデート",
|
| 2690 |
"update.title": "アップデート",
|
| 2691 |
+
"update.up_to_date": "すべて最新です",
|
| 2692 |
"update.updating": "更新中...",
|
| 2693 |
"update.version_unknown": "バージョン不明",
|
| 2694 |
"volume.microphone": "マイクレベル",
|
|
|
|
| 3090 |
"update.ref_placeholder": "مثلاً v1.2.7 أو develop",
|
| 3091 |
"update.start": "تحديث",
|
| 3092 |
"update.title": "تحديث",
|
| 3093 |
+
"update.up_to_date": "كل شيء محدّث",
|
| 3094 |
"update.updating": "جارٍ التحديث...",
|
| 3095 |
"update.version_unknown": "الإصدار غير معروف",
|
| 3096 |
"volume.microphone": "مستوى الميكروفون",
|
reachy_web/static/js/logo_anim.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The topbar logo, alive.
|
| 2 |
+
//
|
| 3 |
+
// WHY the SVG is inline in base.html rather than <img src="logo.svg">: an SVG
|
| 4 |
+
// loaded through <img> runs in the browser's secure static mode — scripts never
|
| 5 |
+
// execute — so its timing could only ever be a fixed CSS loop, and every visit
|
| 6 |
+
// would replay the exact same beat. Inline, we pick the next move here, at random.
|
| 7 |
+
//
|
| 8 |
+
// One move at a time: add its v-* class, let the (one-shot) CSS play out, remove
|
| 9 |
+
// the class, wait a random pause, pick again. The WINK opens the show on load and
|
| 10 |
+
// keeps the heaviest weight afterwards — it is the logo's signature; the rest is
|
| 11 |
+
// seasoning that should surprise, not compete.
|
| 12 |
+
const logoAnim = {
|
| 13 |
+
// w = how often a move comes up (relative). The wink stays the most frequent
|
| 14 |
+
// single move (~1 round in 3) without drowning the others out. It is played at
|
| 15 |
+
// load regardless of weight — see init().
|
| 16 |
+
MOVES: [
|
| 17 |
+
{ cls: 'v-wink', ms: 1100, w: 5 },
|
| 18 |
+
{ cls: 'v-look', ms: 2600, w: 3 },
|
| 19 |
+
{ cls: 'v-blink', ms: 900, w: 3 },
|
| 20 |
+
{ cls: 'v-sleepy', ms: 2400, w: 2 },
|
| 21 |
+
{ cls: 'v-squint', ms: 2000, w: 2 },
|
| 22 |
+
{ cls: 'v-excited', ms: 1800, w: 1 },
|
| 23 |
+
{ cls: 'v-boot', ms: 1600, w: 1 },
|
| 24 |
+
{ cls: 'v-glasses', ms: 2800, w: 1 },
|
| 25 |
+
],
|
| 26 |
+
PAUSE_MIN: 4000,
|
| 27 |
+
PAUSE_MAX: 11000,
|
| 28 |
+
|
| 29 |
+
el: null,
|
| 30 |
+
timer: null,
|
| 31 |
+
|
| 32 |
+
init: () => {
|
| 33 |
+
logoAnim.el = document.querySelector('.topbar-logo');
|
| 34 |
+
if (!logoAnim.el) return;
|
| 35 |
+
// the whole point of the animation is delight; someone who asked the OS for
|
| 36 |
+
// less motion is not asking for delight
|
| 37 |
+
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
| 38 |
+
logoAnim.play(logoAnim.MOVES[0]); // the wink opens the show, always
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
+
// weighted pick: walk the moves subtracting weights until the dart lands
|
| 42 |
+
pick: () => {
|
| 43 |
+
const total = logoAnim.MOVES.reduce((s, m) => s + m.w, 0);
|
| 44 |
+
let r = Math.random() * total;
|
| 45 |
+
for (const m of logoAnim.MOVES) {
|
| 46 |
+
if ((r -= m.w) < 0) return m;
|
| 47 |
+
}
|
| 48 |
+
return logoAnim.MOVES[0];
|
| 49 |
+
},
|
| 50 |
+
|
| 51 |
+
play: (move) => {
|
| 52 |
+
const el = logoAnim.el;
|
| 53 |
+
if (!el) return;
|
| 54 |
+
el.classList.add(move.cls);
|
| 55 |
+
clearTimeout(logoAnim.timer);
|
| 56 |
+
logoAnim.timer = setTimeout(() => {
|
| 57 |
+
el.classList.remove(move.cls); // back to the resting pose
|
| 58 |
+
const pause = logoAnim.PAUSE_MIN + Math.random() * (logoAnim.PAUSE_MAX - logoAnim.PAUSE_MIN);
|
| 59 |
+
logoAnim.timer = setTimeout(() => logoAnim.play(logoAnim.pick()), pause);
|
| 60 |
+
}, move.ms);
|
| 61 |
+
},
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
document.addEventListener('DOMContentLoaded', logoAnim.init);
|
reachy_web/static/js/skins.js
CHANGED
|
@@ -301,24 +301,34 @@ const skins = {
|
|
| 301 |
spiderman: (c, p) => {
|
| 302 |
const ink = p.line || '#0a0a0a', white = p.white || '#f2f2f0';
|
| 303 |
skins.paint.spiderWeb(c, p); // red suit + black web
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
const outline = (k) => {
|
| 309 |
c.beginPath();
|
| 310 |
-
c.moveTo(
|
| 311 |
-
c.bezierCurveTo(
|
| 312 |
-
c.bezierCurveTo(-
|
| 313 |
-
c.bezierCurveTo(-
|
| 314 |
c.closePath();
|
| 315 |
};
|
| 316 |
-
outline(1); c.lineJoin = 'round'; c.lineWidth =
|
| 317 |
c.fillStyle = white; c.fill();
|
| 318 |
-
outline(0.
|
| 319 |
c.restore();
|
| 320 |
};
|
| 321 |
-
eye(185, 1);
|
|
|
|
| 322 |
const bx = 256, by = 300, s = 1.7; // on the chest/torso, above the web centre
|
| 323 |
c.fillStyle = ink;
|
| 324 |
c.beginPath(); c.ellipse(bx, by - 13 * s, 7 * s, 11 * s, 0, 0, Math.PI * 2); c.fill();
|
|
|
|
| 301 |
spiderman: (c, p) => {
|
| 302 |
const ink = p.line || '#0a0a0a', white = p.white || '#f2f2f0';
|
| 303 |
skins.paint.spiderWeb(c, p); // red suit + black web
|
| 304 |
+
// The mask's eye panel, drawn the way the suit actually reads: a POINT at the
|
| 305 |
+
// inner corner (towards the nose, sitting low), sweeping up and out over the top
|
| 306 |
+
// to a wide ROUNDED outer end that rides high, then back along the bottom. Thick
|
| 307 |
+
// black border, white fill, and the thin inner outline the mask always has.
|
| 308 |
+
//
|
| 309 |
+
// Reachy's two lenses are NOT the same size (88 px and 66 px across, see the
|
| 310 |
+
// landmarks), so each panel is scaled to its own lens. Drawn UNSQUASHED (uniform
|
| 311 |
+
// scale) so the border stays even, and centred at y=88 with a ±55 reach, which
|
| 312 |
+
// keeps the whole panel on the head (the body starts around y=150).
|
| 313 |
+
const eye = (X, m, size) => {
|
| 314 |
+
c.save();
|
| 315 |
+
c.translate(X, 88);
|
| 316 |
+
c.scale(m * size, size);
|
| 317 |
const outline = (k) => {
|
| 318 |
c.beginPath();
|
| 319 |
+
c.moveTo(56 * k, 24 * k); // inner point, low, at the nose
|
| 320 |
+
c.bezierCurveTo(24 * k, -38 * k, -26 * k, -54 * k, -46 * k, -28 * k); // up and out over the top
|
| 321 |
+
c.bezierCurveTo(-62 * k, -4 * k, -54 * k, 32 * k, -28 * k, 46 * k); // the wide rounded outer end
|
| 322 |
+
c.bezierCurveTo(-4 * k, 56 * k, 34 * k, 44 * k, 56 * k, 24 * k); // back along the bottom
|
| 323 |
c.closePath();
|
| 324 |
};
|
| 325 |
+
outline(1); c.lineJoin = 'round'; c.lineWidth = 13; c.strokeStyle = ink; c.stroke();
|
| 326 |
c.fillStyle = white; c.fill();
|
| 327 |
+
outline(0.72); c.lineWidth = 4; c.strokeStyle = ink; c.stroke();
|
| 328 |
c.restore();
|
| 329 |
};
|
| 330 |
+
eye(185, 1, 1.0); // the BIG lens (88 px across)
|
| 331 |
+
eye(327, -1, 0.78); // the smaller lens (66 px across)
|
| 332 |
const bx = 256, by = 300, s = 1.7; // on the chest/torso, above the web centre
|
| 333 |
c.fillStyle = ink;
|
| 334 |
c.beginPath(); c.ellipse(bx, by - 13 * s, 7 * s, 11 * s, 0, 0, Math.PI * 2); c.fill();
|
reachy_web/static/js/update.js
CHANGED
|
@@ -46,6 +46,18 @@ const updateManager = {
|
|
| 46 |
}
|
| 47 |
},
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
// Never leave "Checking for updates…" spinning forever: the daemon can be
|
| 50 |
// away (502) or the request can fail. Say so plainly, so the line resolves.
|
| 51 |
checkFailed: () => {
|
|
@@ -113,38 +125,39 @@ const updateManager = {
|
|
| 113 |
}
|
| 114 |
},
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
connectLogsWebSocket: (jobId) => {
|
| 117 |
-
|
| 118 |
-
const updateModalTitle = document.getElementById('update-modal-title');
|
| 119 |
-
const logsDiv = document.getElementById('update-logs');
|
| 120 |
-
const closeButton = document.getElementById('update-modal-close-button');
|
| 121 |
-
|
| 122 |
-
updateModalTitle.textContent = t('update.updating');
|
| 123 |
-
|
| 124 |
-
closeButton.onclick = () => {
|
| 125 |
-
updateModal.classList.add('hidden');
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
updateModal.classList.remove('hidden');
|
| 129 |
|
| 130 |
const ws = new WebSocket(`ws://${location.host}/update/ws/logs?job_id=${jobId}`);
|
| 131 |
-
|
| 132 |
-
ws.onmessage = (event) => {
|
| 133 |
-
// console.log('Update log:', event);
|
| 134 |
-
logsDiv.innerHTML += event.data + '<br>';
|
| 135 |
-
logsDiv.scrollTop = logsDiv.scrollHeight;
|
| 136 |
-
};
|
| 137 |
ws.onclose = async () => {
|
| 138 |
-
|
| 139 |
-
closeButton.classList.remove('hidden');
|
| 140 |
-
closeButton.textContent = t('common.close');
|
| 141 |
-
updateModalTitle.textContent = t('update.completed');
|
| 142 |
updateManager.busy = false;
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
| 145 |
};
|
| 146 |
},
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
updateUI: async (update) => {
|
| 149 |
// updateManager.updateMainPage(isUpdateAvailable);
|
| 150 |
updateManager.updateUpdatePage(update);
|
|
@@ -174,13 +187,14 @@ const updateManager = {
|
|
| 174 |
|
| 175 |
const updateInfo = data.update.reachy_mini;
|
| 176 |
if (updateInfo.is_available) {
|
| 177 |
-
// the newer version, as its own accent pill (just the number)
|
|
|
|
| 178 |
if (availableVersionElem) availableVersionElem.textContent = updateInfo.available_version || '';
|
| 179 |
-
statusElem.textContent =
|
| 180 |
startUpdateBtn.classList.remove('hidden');
|
| 181 |
} else {
|
| 182 |
if (availableVersionElem) availableVersionElem.textContent = ''; // up to date → no second pill
|
| 183 |
-
|
| 184 |
startUpdateBtn.classList.add('hidden');
|
| 185 |
}
|
| 186 |
}
|
|
|
|
| 46 |
}
|
| 47 |
},
|
| 48 |
|
| 49 |
+
// "Everything is up to date" with a green check — the clean, positive verdict
|
| 50 |
+
// when there is nothing to install.
|
| 51 |
+
sayUpToDate: (statusElem) => {
|
| 52 |
+
if (!statusElem) return;
|
| 53 |
+
statusElem.textContent = '';
|
| 54 |
+
const ico = document.createElement('span');
|
| 55 |
+
ico.className = 'ic i-check text-ok';
|
| 56 |
+
const txt = document.createElement('span');
|
| 57 |
+
txt.textContent = t('update.up_to_date');
|
| 58 |
+
statusElem.append(ico, txt);
|
| 59 |
+
},
|
| 60 |
+
|
| 61 |
// Never leave "Checking for updates…" spinning forever: the daemon can be
|
| 62 |
// away (502) or the request can fail. Say so plainly, so the line resolves.
|
| 63 |
checkFailed: () => {
|
|
|
|
| 125 |
}
|
| 126 |
},
|
| 127 |
|
| 128 |
+
// The OTA update streams into the SAME slide-in drawer as an app install —
|
| 129 |
+
// not a mini popin floating inside the settings panel. installPanel.open()
|
| 130 |
+
// closes the settings drawer itself (they share the right edge) and gives us
|
| 131 |
+
// the classified log lines + a completion footer for free.
|
| 132 |
connectLogsWebSocket: (jobId) => {
|
| 133 |
+
installPanel.open(t('update.updating'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
const ws = new WebSocket(`ws://${location.host}/update/ws/logs?job_id=${jobId}`);
|
| 136 |
+
ws.onmessage = (event) => installPanel.line(event.data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
ws.onclose = async () => {
|
| 138 |
+
installPanel.done(true, t('update.completed'));
|
|
|
|
|
|
|
|
|
|
| 139 |
updateManager.busy = false;
|
| 140 |
+
// The app has just been reinstalled, so it may need a beat before it can
|
| 141 |
+
// answer again. Asking once landed on "Version unknown" + a still-armed
|
| 142 |
+
// Update button — a lie. Retry until it answers, then the line settles
|
| 143 |
+
// on the real state ("Up to date").
|
| 144 |
+
await updateManager.refreshAfterUpdate();
|
| 145 |
};
|
| 146 |
},
|
| 147 |
|
| 148 |
+
// re-read the version and re-check, retrying while the app comes back up
|
| 149 |
+
refreshAfterUpdate: async (tries = 6) => {
|
| 150 |
+
for (let i = 0; i < tries; i++) {
|
| 151 |
+
try {
|
| 152 |
+
const r = await fetch('/update/install-source');
|
| 153 |
+
if (r.ok) break;
|
| 154 |
+
} catch (e) { /* not back yet */ }
|
| 155 |
+
await new Promise((res) => setTimeout(res, 1500));
|
| 156 |
+
}
|
| 157 |
+
await updateManager.loadInstallSource();
|
| 158 |
+
await updateManager.checkForUpdate();
|
| 159 |
+
},
|
| 160 |
+
|
| 161 |
updateUI: async (update) => {
|
| 162 |
// updateManager.updateMainPage(isUpdateAvailable);
|
| 163 |
updateManager.updateUpdatePage(update);
|
|
|
|
| 187 |
|
| 188 |
const updateInfo = data.update.reachy_mini;
|
| 189 |
if (updateInfo.is_available) {
|
| 190 |
+
// the newer version, as its own accent pill (just the number). The pill
|
| 191 |
+
// and the button say it all — no sentence.
|
| 192 |
if (availableVersionElem) availableVersionElem.textContent = updateInfo.available_version || '';
|
| 193 |
+
statusElem.textContent = '';
|
| 194 |
startUpdateBtn.classList.remove('hidden');
|
| 195 |
} else {
|
| 196 |
if (availableVersionElem) availableVersionElem.textContent = ''; // up to date → no second pill
|
| 197 |
+
updateManager.sayUpToDate(statusElem);
|
| 198 |
startUpdateBtn.classList.add('hidden');
|
| 199 |
}
|
| 200 |
}
|
reachy_web/static/js/waveform.js
CHANGED
|
@@ -11,7 +11,20 @@
|
|
| 11 |
// The speaker is a plain slider now: no waveform.
|
| 12 |
const waveform = {
|
| 13 |
BARS: 48,
|
| 14 |
-
MIC_GAIN: 4,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
mic: { el: null, bars: [], ctx: null, analyser: null, data: null, raf: null, session: null, retryTimer: null, gestureHooked: false },
|
| 17 |
|
|
@@ -78,7 +91,7 @@ const waveform = {
|
|
| 78 |
s.ws.onmessage = (ev) => {
|
| 79 |
let peaks; try { peaks = JSON.parse(ev.data); } catch { return; }
|
| 80 |
if (!Array.isArray(peaks)) return;
|
| 81 |
-
waveform.mic.bars = waveform.mic.bars.concat(peaks).slice(-waveform.BARS);
|
| 82 |
waveform.draw(waveform.mic.el, waveform.mic.bars);
|
| 83 |
};
|
| 84 |
s.ws.onclose = () => {
|
|
@@ -278,7 +291,7 @@ const waveform = {
|
|
| 278 |
});
|
| 279 |
if (level == null) return; // no audioLevel in this browser → analyser stays
|
| 280 |
m.statsDriving = true;
|
| 281 |
-
const peak =
|
| 282 |
m.bars = m.bars.concat(peak).slice(-waveform.BARS);
|
| 283 |
waveform.draw(m.el, m.bars);
|
| 284 |
}).catch(() => {});
|
|
@@ -300,7 +313,7 @@ const waveform = {
|
|
| 300 |
for (let i = 0; i < m.data.length; i++) {
|
| 301 |
raw = Math.max(raw, Math.abs(m.data[i] - 128) / 128);
|
| 302 |
}
|
| 303 |
-
const peak =
|
| 304 |
m.bars = m.bars.concat(peak).slice(-waveform.BARS); // scroll left
|
| 305 |
waveform.draw(m.el, m.bars);
|
| 306 |
},
|
|
|
|
| 11 |
// The speaker is a plain slider now: no waveform.
|
| 12 |
const waveform = {
|
| 13 |
BARS: 48,
|
| 14 |
+
MIC_GAIN: 4, // a raw mic peak is tiny (~0.05); lift it so it visibly MOVES
|
| 15 |
+
NOISE_FLOOR: 0.03, // at or under this, it is the room's hiss — not sound
|
| 16 |
+
|
| 17 |
+
// raw level (0..1) → bar factor (0..1). A noise GATE comes FIRST: a silent room
|
| 18 |
+
// still feeds the mic a constant low hiss, and since the drawing curve lifts
|
| 19 |
+
// small values hard (to keep quiet speech visible), that hiss was drawn as a
|
| 20 |
+
// permanent wave — "sound" in a silent room. Anything at or under the floor is
|
| 21 |
+
// flat silence; what is above is rescaled from zero, THEN gained, so real sound
|
| 22 |
+
// still stands out.
|
| 23 |
+
shape: (raw) => {
|
| 24 |
+
const f = waveform.NOISE_FLOOR;
|
| 25 |
+
const v = raw <= f ? 0 : (raw - f) / (1 - f);
|
| 26 |
+
return Math.min(1, v * waveform.MIC_GAIN);
|
| 27 |
+
},
|
| 28 |
|
| 29 |
mic: { el: null, bars: [], ctx: null, analyser: null, data: null, raf: null, session: null, retryTimer: null, gestureHooked: false },
|
| 30 |
|
|
|
|
| 91 |
s.ws.onmessage = (ev) => {
|
| 92 |
let peaks; try { peaks = JSON.parse(ev.data); } catch { return; }
|
| 93 |
if (!Array.isArray(peaks)) return;
|
| 94 |
+
waveform.mic.bars = waveform.mic.bars.concat(peaks.map(waveform.shape)).slice(-waveform.BARS);
|
| 95 |
waveform.draw(waveform.mic.el, waveform.mic.bars);
|
| 96 |
};
|
| 97 |
s.ws.onclose = () => {
|
|
|
|
| 291 |
});
|
| 292 |
if (level == null) return; // no audioLevel in this browser → analyser stays
|
| 293 |
m.statsDriving = true;
|
| 294 |
+
const peak = waveform.shape(level);
|
| 295 |
m.bars = m.bars.concat(peak).slice(-waveform.BARS);
|
| 296 |
waveform.draw(m.el, m.bars);
|
| 297 |
}).catch(() => {});
|
|
|
|
| 313 |
for (let i = 0; i < m.data.length; i++) {
|
| 314 |
raw = Math.max(raw, Math.abs(m.data[i] - 128) / 128);
|
| 315 |
}
|
| 316 |
+
const peak = waveform.shape(raw);
|
| 317 |
m.bars = m.bars.concat(peak).slice(-waveform.BARS); // scroll left
|
| 318 |
waveform.draw(m.el, m.bars);
|
| 319 |
},
|
reachy_web/templates/base.html
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
<link rel="stylesheet" href="/static/css/icons.css">
|
| 14 |
<link rel="stylesheet" href="/static/css/logs.css">
|
| 15 |
<link rel="stylesheet" href="/static/css/audio_eq.css">
|
|
|
|
| 16 |
|
| 17 |
<!-- where the robot actually is: the page may be served from a laptop -->
|
| 18 |
<script>window.REACHY_DAEMON_NAME = "{{ daemon_name }}";</script>
|
|
@@ -25,6 +26,8 @@
|
|
| 25 |
<script src="/static/js/locales.js"></script>
|
| 26 |
<script src="/static/js/i18n.js"></script>
|
| 27 |
<script src="/static/js/loader.js"></script>
|
|
|
|
|
|
|
| 28 |
<!-- the controller button lives in the topbar, so it must exist on every page -->
|
| 29 |
<script src="/static/js/controller.js"></script>
|
| 30 |
<script src="/static/js/robot_name.js"></script>
|
|
@@ -49,7 +52,42 @@
|
|
| 49 |
<div class="topbar-inner container px-4 sm:px-8 md:px-8 lg:px-8 xl:px-8 mx-auto w-full md:w-2/3">
|
| 50 |
<!-- the logo spans the three rows; it and the title go home -->
|
| 51 |
<a href="/" class="topbar-home" title="Home" data-i18n-title="topbar.home">
|
| 52 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</a>
|
| 54 |
<button class="burger topbar-iconbtn" id="burgerBtn" aria-label="Menu" data-i18n-title="topbar.menu" onclick="topbar.toggleMenu()">
|
| 55 |
<span class="ic i-menu"></span>
|
|
@@ -64,8 +102,8 @@
|
|
| 64 |
<nav class="navmenu-list">
|
| 65 |
<button onclick="nav.show('reachy')">
|
| 66 |
<svg class="tab-bot" viewBox="0 0 120 124" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 67 |
-
<path d="M45 46 L37
|
| 68 |
-
<path d="M75 46 L83
|
| 69 |
<rect x="34" y="44" width="52" height="36" rx="13"/>
|
| 70 |
<circle cx="48.98" cy="62.21" r="8.14" fill="currentColor" stroke="none"/>
|
| 71 |
<circle cx="72.67" cy="62.5" r="6.98" fill="currentColor" stroke="none"/>
|
|
|
|
| 13 |
<link rel="stylesheet" href="/static/css/icons.css">
|
| 14 |
<link rel="stylesheet" href="/static/css/logs.css">
|
| 15 |
<link rel="stylesheet" href="/static/css/audio_eq.css">
|
| 16 |
+
<link rel="stylesheet" href="/static/css/logo.css">
|
| 17 |
|
| 18 |
<!-- where the robot actually is: the page may be served from a laptop -->
|
| 19 |
<script>window.REACHY_DAEMON_NAME = "{{ daemon_name }}";</script>
|
|
|
|
| 26 |
<script src="/static/js/locales.js"></script>
|
| 27 |
<script src="/static/js/i18n.js"></script>
|
| 28 |
<script src="/static/js/loader.js"></script>
|
| 29 |
+
<!-- the topbar logo lives on every page, and so must the thing that animates it -->
|
| 30 |
+
<script src="/static/js/logo_anim.js"></script>
|
| 31 |
<!-- the controller button lives in the topbar, so it must exist on every page -->
|
| 32 |
<script src="/static/js/controller.js"></script>
|
| 33 |
<script src="/static/js/robot_name.js"></script>
|
|
|
|
| 52 |
<div class="topbar-inner container px-4 sm:px-8 md:px-8 lg:px-8 xl:px-8 mx-auto w-full md:w-2/3">
|
| 53 |
<!-- the logo spans the three rows; it and the title go home -->
|
| 54 |
<a href="/" class="topbar-home" title="Home" data-i18n-title="topbar.home">
|
| 55 |
+
<!-- INLINE, not <img src="logo.svg">: an <img>-loaded SVG runs in
|
| 56 |
+
the browser's secure static mode (no scripts), so its timing
|
| 57 |
+
could only ever be a fixed CSS loop. Inline, logo_anim.js picks
|
| 58 |
+
each move at random. Asymmetric on purpose: the right eye and
|
| 59 |
+
the right antenna are smaller than their left twin. -->
|
| 60 |
+
<svg class="topbar-logo" viewBox="24 13 72 72" role="img" aria-label="Reachy Web">
|
| 61 |
+
<!-- two nested groups per antenna: the outer one never stops
|
| 62 |
+
breathing (lg-sway), the inner one carries whatever move
|
| 63 |
+
logo_anim.js is playing. Same property on two elements =
|
| 64 |
+
the rotations compose instead of fighting. -->
|
| 65 |
+
<g class="lg-sway lg-sway-l">
|
| 66 |
+
<g class="lg-ant lg-ant-l">
|
| 67 |
+
<path d="M45 46 L37 22" fill="none" stroke="#1b2330" stroke-width="3" stroke-linecap="round"/>
|
| 68 |
+
<circle class="lg-ball lg-ball-l" cx="37" cy="22" r="4" fill="#1b2330"/>
|
| 69 |
+
</g>
|
| 70 |
+
</g>
|
| 71 |
+
<g class="lg-sway lg-sway-r">
|
| 72 |
+
<g class="lg-ant lg-ant-r">
|
| 73 |
+
<path d="M75 46 L81.4 26.4" fill="none" stroke="#1b2330" stroke-width="3" stroke-linecap="round"/>
|
| 74 |
+
<circle class="lg-ball lg-ball-r" cx="81.4" cy="26.4" r="3.4" fill="#1b2330"/>
|
| 75 |
+
</g>
|
| 76 |
+
</g>
|
| 77 |
+
<rect x="34" y="44" width="52" height="36" rx="13" fill="#fff" stroke="#1b2330" stroke-width="3"/>
|
| 78 |
+
<circle class="lg-eye lg-eye-l" cx="48.98" cy="62.21" r="8.14" fill="#1b2330"/>
|
| 79 |
+
<circle class="lg-eye lg-eye-r" cx="71.02" cy="62.21" r="7" fill="#1b2330"/>
|
| 80 |
+
<path class="lg-wink" d="M63.9 61.5 Q71 68.5 78.1 61.5" fill="none" stroke="#1b2330" stroke-width="3.6" stroke-linecap="round"/>
|
| 81 |
+
<!-- the glasses, invisible at rest — the v-glasses move drops
|
| 82 |
+
them on. Two round lenses that touch at the bridge, and
|
| 83 |
+
two little temples running to the head's edges. -->
|
| 84 |
+
<g class="lg-glasses" fill="none" stroke="#1b2330" stroke-width="3" stroke-linecap="round">
|
| 85 |
+
<circle cx="48.98" cy="62.21" r="11.5"/>
|
| 86 |
+
<circle cx="71.02" cy="62.21" r="10.5"/>
|
| 87 |
+
<path d="M37.48 62.21 L34 59.5"/>
|
| 88 |
+
<path d="M81.52 62.21 L86 59.5"/>
|
| 89 |
+
</g>
|
| 90 |
+
</svg>
|
| 91 |
</a>
|
| 92 |
<button class="burger topbar-iconbtn" id="burgerBtn" aria-label="Menu" data-i18n-title="topbar.menu" onclick="topbar.toggleMenu()">
|
| 93 |
<span class="ic i-menu"></span>
|
|
|
|
| 102 |
<nav class="navmenu-list">
|
| 103 |
<button onclick="nav.show('reachy')">
|
| 104 |
<svg class="tab-bot" viewBox="0 0 120 124" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 105 |
+
<path d="M45 46 L37 22"/><circle cx="37" cy="22" r="4.2" fill="currentColor" stroke="none"/>
|
| 106 |
+
<path d="M75 46 L83 22"/><circle cx="83" cy="22" r="4.2" fill="currentColor" stroke="none"/>
|
| 107 |
<rect x="34" y="44" width="52" height="36" rx="13"/>
|
| 108 |
<circle cx="48.98" cy="62.21" r="8.14" fill="currentColor" stroke="none"/>
|
| 109 |
<circle cx="72.67" cy="62.5" r="6.98" fill="currentColor" stroke="none"/>
|
reachy_web/templates/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
base — its face traced from the assets, only monochrome and still here -->
|
| 12 |
<svg class="tab-bot" viewBox="0 0 120 124" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 13 |
<path d="M45 46 L37 24"/><circle cx="37" cy="22" r="4.2" fill="currentColor" stroke="none"/>
|
| 14 |
-
<path d="M75 46 L83
|
| 15 |
<rect x="34" y="44" width="52" height="36" rx="13"/>
|
| 16 |
<circle cx="48.98" cy="62.21" r="8.14" fill="currentColor" stroke="none"/>
|
| 17 |
<circle cx="72.67" cy="62.5" r="6.98" fill="currentColor" stroke="none"/>
|
|
|
|
| 11 |
base — its face traced from the assets, only monochrome and still here -->
|
| 12 |
<svg class="tab-bot" viewBox="0 0 120 124" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 13 |
<path d="M45 46 L37 24"/><circle cx="37" cy="22" r="4.2" fill="currentColor" stroke="none"/>
|
| 14 |
+
<path d="M75 46 L83 22"/><circle cx="83" cy="22" r="4.2" fill="currentColor" stroke="none"/>
|
| 15 |
<rect x="34" y="44" width="52" height="36" rx="13"/>
|
| 16 |
<circle cx="48.98" cy="62.21" r="8.14" fill="currentColor" stroke="none"/>
|
| 17 |
<circle cx="72.67" cy="62.5" r="6.98" fill="currentColor" stroke="none"/>
|
reachy_web/templates/sections/appearance.html
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
<div class="app-section">
|
| 2 |
<div class="app-section-title"><span class="ic i-palette"></span> <span data-i18n="settings.appearance">Appearance</span></div>
|
| 3 |
-
<!-- a 2-column grid: labels in one column (aligned left), controls in the next
|
| 4 |
-
|
|
|
|
| 5 |
<div class="grid grid-cols-[max-content_1fr] items-center gap-x-6 gap-y-4 p-4">
|
| 6 |
<span class="text-sm font-medium" data-i18n="settings.theme">Theme</span>
|
| 7 |
-
<div class="theme-ctl justify-self-
|
| 8 |
<span class="ic i-weather-night"></span>
|
| 9 |
<button class="ios" id="themeSw" data-theme-switch role="switch" aria-label="Light theme" data-i18n-aria="settings.light_theme"
|
| 10 |
data-i18n-title="settings.light_theme"></button>
|
|
@@ -12,20 +13,20 @@
|
|
| 12 |
</div>
|
| 13 |
|
| 14 |
<span class="text-sm font-medium" data-i18n="settings.palette">Palette</span>
|
| 15 |
-
<div class="palettes justify-self-
|
| 16 |
|
| 17 |
<span class="text-sm font-medium" data-i18n="settings.random_skin">Random skin</span>
|
| 18 |
-
<div class="flex items-center gap-2 justify-self-
|
| 19 |
-
<
|
|
|
|
| 20 |
data-i18n-aria="settings.random_skin_hint" data-i18n-title="settings.random_skin_hint"></button>
|
| 21 |
-
<span class="text-xs text-mut" data-i18n="settings.random_skin_hint">A different skin on every start</span>
|
| 22 |
</div>
|
| 23 |
|
| 24 |
<span class="text-sm font-medium" data-i18n="settings.debug">Debug mode</span>
|
| 25 |
-
<div class="flex items-center gap-2 justify-self-
|
| 26 |
-
<
|
|
|
|
| 27 |
data-i18n-aria="settings.debug_hint" data-i18n-title="settings.debug_hint"></button>
|
| 28 |
-
<span class="text-xs text-mut" data-i18n="settings.debug_hint">Mirror the browser console into the logs panel</span>
|
| 29 |
</div>
|
| 30 |
</div>
|
| 31 |
</div>
|
|
|
|
| 1 |
<div class="app-section">
|
| 2 |
<div class="app-section-title"><span class="ic i-palette"></span> <span data-i18n="settings.appearance">Appearance</span></div>
|
| 3 |
+
<!-- a 2-column grid: labels in one column (aligned left), controls in the next,
|
| 4 |
+
all flushed RIGHT — so every switch lands on the same right edge. Where a
|
| 5 |
+
row carries a hint, the hint sits before the switch to keep it rightmost. -->
|
| 6 |
<div class="grid grid-cols-[max-content_1fr] items-center gap-x-6 gap-y-4 p-4">
|
| 7 |
<span class="text-sm font-medium" data-i18n="settings.theme">Theme</span>
|
| 8 |
+
<div class="theme-ctl justify-self-end">
|
| 9 |
<span class="ic i-weather-night"></span>
|
| 10 |
<button class="ios" id="themeSw" data-theme-switch role="switch" aria-label="Light theme" data-i18n-aria="settings.light_theme"
|
| 11 |
data-i18n-title="settings.light_theme"></button>
|
|
|
|
| 13 |
</div>
|
| 14 |
|
| 15 |
<span class="text-sm font-medium" data-i18n="settings.palette">Palette</span>
|
| 16 |
+
<div class="palettes justify-self-end" id="palettes" title="Palette" data-i18n-title="settings.palette"></div>
|
| 17 |
|
| 18 |
<span class="text-sm font-medium" data-i18n="settings.random_skin">Random skin</span>
|
| 19 |
+
<div class="flex items-center gap-2 justify-self-end min-w-0">
|
| 20 |
+
<span class="text-xs text-mut text-right" data-i18n="settings.random_skin_hint">A different skin on every start</span>
|
| 21 |
+
<button class="ios flex-none" id="skinRandomSw" role="switch"
|
| 22 |
data-i18n-aria="settings.random_skin_hint" data-i18n-title="settings.random_skin_hint"></button>
|
|
|
|
| 23 |
</div>
|
| 24 |
|
| 25 |
<span class="text-sm font-medium" data-i18n="settings.debug">Debug mode</span>
|
| 26 |
+
<div class="flex items-center gap-2 justify-self-end min-w-0">
|
| 27 |
+
<span class="text-xs text-mut text-right" data-i18n="settings.debug_hint">Mirror the browser console into the logs panel</span>
|
| 28 |
+
<button class="ios flex-none" id="debugSw" role="switch"
|
| 29 |
data-i18n-aria="settings.debug_hint" data-i18n-title="settings.debug_hint"></button>
|
|
|
|
| 30 |
</div>
|
| 31 |
</div>
|
| 32 |
</div>
|
reachy_web/templates/sections/update.html
CHANGED
|
@@ -17,21 +17,22 @@
|
|
| 17 |
<span id="available-version"
|
| 18 |
class="inline-flex items-center px-2.5 py-0.5 rounded-full bg-acc text-bg text-sm font-semibold tabular-nums empty:hidden"></span>
|
| 19 |
|
| 20 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
<button type="button" class="ios" id="pre-release-switch" role="switch" aria-checked="false"
|
| 22 |
data-i18n-title="update.pre_release"></button>
|
| 23 |
<span data-i18n="update.pre_release">pre-release</span>
|
| 24 |
</label>
|
| 25 |
|
| 26 |
<button id="start-update-btn" onclick="updateManager.startUpdate()"
|
| 27 |
-
class="hidden
|
| 28 |
data-i18n-title="update.start">
|
| 29 |
<span class="ic i-arrow-up-bold"></span> <span data-i18n="update.start">Update</span>
|
| 30 |
</button>
|
| 31 |
-
|
| 32 |
-
<!-- kept for update.js (it returns early if this is missing) but never
|
| 33 |
-
shown: the "update available" sentence was redundant with the pill -->
|
| 34 |
-
<span id="update-status" class="hidden"></span>
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
|
|
|
|
| 17 |
<span id="available-version"
|
| 18 |
class="inline-flex items-center px-2.5 py-0.5 rounded-full bg-acc text-bg text-sm font-semibold tabular-nums empty:hidden"></span>
|
| 19 |
|
| 20 |
+
<!-- the verdict, on the left: a green check + "Everything is up to date"
|
| 21 |
+
when there is nothing to do; emptied when an update IS available (the
|
| 22 |
+
pill and the button already say it) -->
|
| 23 |
+
<span id="update-status" class="flex items-center gap-1.5 text-sm text-mut"></span>
|
| 24 |
+
|
| 25 |
+
<label class="flex items-center gap-2 text-sm text-mut cursor-pointer ml-auto">
|
| 26 |
<button type="button" class="ios" id="pre-release-switch" role="switch" aria-checked="false"
|
| 27 |
data-i18n-title="update.pre_release"></button>
|
| 28 |
<span data-i18n="update.pre_release">pre-release</span>
|
| 29 |
</label>
|
| 30 |
|
| 31 |
<button id="start-update-btn" onclick="updateManager.startUpdate()"
|
| 32 |
+
class="hidden inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg shadow bg-green-400 hover:bg-green-500 text-white text-sm font-semibold"
|
| 33 |
data-i18n-title="update.start">
|
| 34 |
<span class="ic i-arrow-up-bold"></span> <span data-i18n="update.start">Update</span>
|
| 35 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
</div>
|
| 37 |
</div>
|
| 38 |
|
tailwindcss/styles/app.css
CHANGED
|
@@ -63,6 +63,12 @@
|
|
| 63 |
/* in light mode every palette fades from white (the font stays the palette's) */
|
| 64 |
:root[data-palette][data-theme=light] body { background: radial-gradient(1200px 600px at 50% -10%, #fff, var(--bg)) fixed; }
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
/* ================= utilities → tokens ================= */
|
| 67 |
/* The recovered UI is written in white/gray/blue/green utilities. Remapping the
|
| 68 |
scales onto the tokens makes every existing class — templates AND markup
|
|
@@ -165,7 +171,15 @@ body {
|
|
| 165 |
flex-wrap: wrap;
|
| 166 |
}
|
| 167 |
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
.pdot {
|
| 171 |
width: 16px;
|
|
|
|
| 63 |
/* in light mode every palette fades from white (the font stays the palette's) */
|
| 64 |
:root[data-palette][data-theme=light] body { background: radial-gradient(1200px 600px at 50% -10%, #fff, var(--bg)) fixed; }
|
| 65 |
|
| 66 |
+
/* Tailwind emits the display utilities in a fixed order, and .flex / .inline-flex
|
| 67 |
+
land AFTER .hidden — same specificity, so the later one wins and any element
|
| 68 |
+
carrying BOTH (every toggled button, pill or disclosure in this app) simply
|
| 69 |
+
never hid. Make .hidden win outright, whatever it is combined with. */
|
| 70 |
+
.hidden { display: none !important; }
|
| 71 |
+
|
| 72 |
/* ================= utilities → tokens ================= */
|
| 73 |
/* The recovered UI is written in white/gray/blue/green utilities. Remapping the
|
| 74 |
scales onto the tokens makes every existing class — templates AND markup
|
|
|
|
| 171 |
flex-wrap: wrap;
|
| 172 |
}
|
| 173 |
|
| 174 |
+
/* the 13 palettes on two balanced rows (7 + 6) rather than one long wrapping
|
| 175 |
+
line — a grid, so the dots line up in columns instead of drifting */
|
| 176 |
+
.palettes {
|
| 177 |
+
display: grid;
|
| 178 |
+
grid-template-columns: repeat(7, auto);
|
| 179 |
+
justify-items: center;
|
| 180 |
+
align-items: center;
|
| 181 |
+
gap: 6px;
|
| 182 |
+
}
|
| 183 |
|
| 184 |
.pdot {
|
| 185 |
width: 16px;
|