2026-03-03 10:20:49 +07:00
|
|
|
* {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body {
|
feat(design): tokens, webfonts, Lucide icons, board PNG/WebP picture strip
Foundation
- Add 7 token CSS files in src/tokens/ — semantic colors, 4-pt spacing,
Apple-HIG type ramp, radius/elevation/motion/z-index scales.
- Refactor src/index.css to import tokens and remap legacy aliases
(--accent, --bg, etc.) onto the new --color-* semantics so existing
components keep rendering during migration.
- Drop the duplicate font-family from src/App.css; body inherits from :root.
- Global *:focus-visible ring backed by --color-focus-ring (WCAG 2.4.7).
Webfonts (self-hosted)
- Add Inter.var.woff2 (variable, OFL) and JetBrainsMono.var.woff2 to
public/fonts/. Preloaded in index.html with crossorigin.
- Old stack -apple-system kept as fallback so Mac users still get SF Pro.
- Fixes cross-OS rendering inconsistency (Win/Linux/Android were falling
back to Segoe UI / Roboto, breaking the type grid).
Component primitives
- New src/components/ui/{Button,Card,Input}.tsx + .css. Built on the
semantic tokens, ready for incremental migration of .ap-* CSS classes.
Lucide icons
- Replace 6 inline SVG icon components in LandingPage (IcoChip / IcoCpu /
IcoCode / IcoZap / IcoLayers / IcoMonitor) with lucide-react imports.
Aliased so call sites are unchanged. ~80 lines of inline SVG removed.
- IcoGitHub kept bespoke (filled glyph, brand-correct).
Marketing assets
- Convert top 8 boards to transparent PNG + WebP at 1x / 2x:
Arduino Uno, Nano, Mega 2560, Pi Pico, Pi Pico W, ESP32-C3,
ESP32-DevKit-V1, XIAO ESP32-S3.
- Migrate matching cards in LandingPage and Velxio2Page to <picture>
with WebP > PNG > SVG fallback. Other 8 boards keep <img src=*.svg>
for now (Raspberry Pi 3B, ESP32-CAM, etc.).
- Refresh og-image.png — same canonical URL, new content (4 hero boards
+ branding instead of generic logo card).
- Fix latent bug in LandingPage: ESP32 DevKit V1 card was loading
esp32-devkit-c-v4.svg; now uses esp32-devkit-v1.{webp,png,svg}.
Build verified: npm run build:docker succeeds, 246 SEO pages prerender.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:45:44 +07:00
|
|
|
background-color: var(--color-bg-canvas);
|
|
|
|
|
color: var(--color-fg-default);
|
2026-03-16 10:12:33 +07:00
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
-moz-osx-font-smoothing: grayscale;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
/* ── App shell ──────────────────────────────────── */
|
2026-03-03 10:20:49 +07:00
|
|
|
.app {
|
|
|
|
|
height: 100vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-03-05 08:05:23 +07:00
|
|
|
overflow: hidden;
|
2026-03-07 12:25:59 +07:00
|
|
|
overscroll-behavior: none;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
/* ── Header ─────────────────────────────────────── */
|
2026-03-03 10:20:49 +07:00
|
|
|
.app-header {
|
2026-03-16 10:12:33 +07:00
|
|
|
background-color: rgba(0, 0, 0, 0.88);
|
|
|
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
|
2026-03-05 08:05:23 +07:00
|
|
|
padding: 0 16px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-03-12 18:17:29 +07:00
|
|
|
position: relative;
|
|
|
|
|
z-index: 100;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 08:25:52 +07:00
|
|
|
.header-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-03-05 08:05:23 +07:00
|
|
|
width: 100%;
|
2026-03-04 08:25:52 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 09:15:38 +07:00
|
|
|
.header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
.header-brand {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
.header-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
2026-03-16 10:12:33 +07:00
|
|
|
color: #f5f5f7;
|
|
|
|
|
letter-spacing: -0.2px;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 18:17:29 +07:00
|
|
|
/* Kept for backward compatibility */
|
2026-03-04 08:25:52 +07:00
|
|
|
.examples-link {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-03-05 08:05:23 +07:00
|
|
|
gap: 6px;
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: #9d9d9d;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border: 1px solid #3a3a3a;
|
|
|
|
|
transition: all 0.15s;
|
2026-03-04 08:25:52 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.examples-link:hover {
|
2026-03-05 08:05:23 +07:00
|
|
|
background: #2a2d2e;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-color: #555;
|
2026-03-04 08:25:52 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 18:17:29 +07:00
|
|
|
/* ── Unified nav links ──────────────────────────── */
|
|
|
|
|
.header-nav-links {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-link {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
2026-03-16 10:12:33 +07:00
|
|
|
padding: 5px 12px;
|
|
|
|
|
color: #86868b;
|
2026-03-12 18:17:29 +07:00
|
|
|
text-decoration: none;
|
2026-03-16 10:12:33 +07:00
|
|
|
border-radius: 100px;
|
2026-03-12 18:17:29 +07:00
|
|
|
font-size: 13px;
|
2026-03-16 10:12:33 +07:00
|
|
|
font-weight: 400;
|
2026-04-22 02:45:45 +07:00
|
|
|
transition:
|
|
|
|
|
color 0.2s,
|
|
|
|
|
background 0.2s;
|
2026-03-12 18:17:29 +07:00
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-link:hover {
|
2026-03-16 10:12:33 +07:00
|
|
|
color: #f5f5f7;
|
|
|
|
|
background: rgba(255, 255, 255, 0.08);
|
2026-03-12 18:17:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-link-active {
|
2026-03-16 10:12:33 +07:00
|
|
|
color: #f5f5f7;
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
2026-03-12 18:17:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-discord {
|
|
|
|
|
color: #7289da;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-discord:hover {
|
|
|
|
|
color: #8fa8f5;
|
2026-03-16 10:12:33 +07:00
|
|
|
background: rgba(114, 137, 218, 0.1);
|
2026-03-12 18:17:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-right {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2026-03-16 09:15:38 +07:00
|
|
|
margin-left: auto;
|
2026-03-12 18:17:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile hamburger */
|
|
|
|
|
.header-hamburger {
|
|
|
|
|
display: none;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-hamburger span {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 2px;
|
|
|
|
|
background: #9d9d9d;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-hamburger:hover span {
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 10:21:26 +07:00
|
|
|
/* ── Unified top toolbar (desktop) ───────────────
|
|
|
|
|
Editor controls + canvas controls share a single full-width row that does
|
|
|
|
|
NOT reflow when the editor/canvas splitter below is dragged. The canvas
|
|
|
|
|
side renders into the empty `.unified-toolbar-canvas` slot via a React
|
|
|
|
|
portal from inside SimulatorCanvas. */
|
|
|
|
|
.unified-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
flex-shrink: 0;
|
2026-07-30 21:43:46 +07:00
|
|
|
/* When the row genuinely cannot fit (narrow window + docked chat), wrap
|
|
|
|
|
the canvas controls onto a second line instead of letting flexbox
|
|
|
|
|
squeeze fixed blocks below their content — that used to crush the
|
|
|
|
|
Code/Both/Circuit toggle to a third of its width, clipping two of its
|
|
|
|
|
buttons into unreachable slivers. Two short rows beat hidden buttons. */
|
|
|
|
|
flex-wrap: wrap;
|
2026-04-29 10:21:26 +07:00
|
|
|
background: #252526;
|
|
|
|
|
border-bottom: 1px solid #333;
|
|
|
|
|
min-height: 38px;
|
fix(editor): stop unified toolbar controls overlapping on a narrow bar
The top bar packs three zones onto one row: the view-mode toggle, the editor
actions (Compile/Run/Stop/...), and the canvas controls (board selector,
Serial, Scope, zoom, Add) portaled in from SimulatorCanvas. The editor zone
was flex:1 min-width:0 while the canvas zone was fixed-width, so when the bar
narrowed - mainly when the right-docked AI chat opens - the editor zone shrank
below its own buttons and painted them over the board selector / Serial /
Scope. The existing collapse logic was keyed to the viewport (@media 768px),
so it never fired on a wide screen with the chat open.
Make the shared bar a container-query context and collapse every zone by the
bar's own width instead of the viewport: view-mode labels drop to icons first,
then Serial/Scope/Add labels and the board selector ellipsizes, then the
component count and finally the zoom buttons (wheel-zoom still works). Floor
the editor zone at its collapsed content width so it can never underflow and
overlap; past that the lower-priority canvas controls yield toward the right
edge instead. Verified across bar widths 660-1140px with the AI chat open:
overlap eliminated, dropdown menus still render un-clipped.
2026-07-03 12:32:14 +07:00
|
|
|
/* Query container so every zone (view-mode toggle, editor toolbar, canvas
|
|
|
|
|
controls) can collapse based on the *shared bar's own width* — which is
|
|
|
|
|
what shrinks when the right-docked AI chat opens — instead of the viewport
|
|
|
|
|
width. Prevents the canvas controls (board selector, Serial, Scope) from
|
|
|
|
|
overlapping the editor Run/Compile buttons on a narrow bar. `layout`
|
|
|
|
|
containment (implied) does not clip: the overflow/run dropdowns still
|
|
|
|
|
render below the bar. */
|
|
|
|
|
container-type: inline-size;
|
|
|
|
|
container-name: unified-toolbar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Collapse the Code/Both/Circuit labels to icon-only first (biggest fixed
|
|
|
|
|
block in the bar) as the shared bar narrows. */
|
2026-08-18 05:17:38 +07:00
|
|
|
.view-mode-toggle {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
fix(editor): stop unified toolbar controls overlapping on a narrow bar
The top bar packs three zones onto one row: the view-mode toggle, the editor
actions (Compile/Run/Stop/...), and the canvas controls (board selector,
Serial, Scope, zoom, Add) portaled in from SimulatorCanvas. The editor zone
was flex:1 min-width:0 while the canvas zone was fixed-width, so when the bar
narrowed - mainly when the right-docked AI chat opens - the editor zone shrank
below its own buttons and painted them over the board selector / Serial /
Scope. The existing collapse logic was keyed to the viewport (@media 768px),
so it never fired on a wide screen with the chat open.
Make the shared bar a container-query context and collapse every zone by the
bar's own width instead of the viewport: view-mode labels drop to icons first,
then Serial/Scope/Add labels and the board selector ellipsizes, then the
component count and finally the zoom buttons (wheel-zoom still works). Floor
the editor zone at its collapsed content width so it can never underflow and
overlap; past that the lower-priority canvas controls yield toward the right
edge instead. Verified across bar widths 660-1140px with the AI chat open:
overlap eliminated, dropdown menus still render un-clipped.
2026-07-03 12:32:14 +07:00
|
|
|
@container unified-toolbar (max-width: 1140px) {
|
|
|
|
|
.view-mode-toggle .vm-label {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
2026-04-29 10:21:26 +07:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 05:17:38 +07:00
|
|
|
/* Narrow bar (small window with the AI chat docked): drop the explorer /
|
|
|
|
|
Code / Both / Circuit toggle altogether. Every one of its switches is a
|
|
|
|
|
checkmarked row in the header's View menu, so nothing is lost — and the
|
|
|
|
|
~140px it frees is what keeps the Run group and the canvas controls on
|
|
|
|
|
one line instead of overlapping. */
|
|
|
|
|
@container unified-toolbar (max-width: 760px) {
|
|
|
|
|
.view-mode-toggle {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 10:21:26 +07:00
|
|
|
.unified-toolbar-editor {
|
|
|
|
|
flex: 1 1 auto;
|
2026-07-31 05:46:08 +07:00
|
|
|
/* Floor the editor zone at its own content width so it can never shrink
|
|
|
|
|
*under* its buttons and paint them over the adjacent canvas controls
|
|
|
|
|
(a hardcoded 280px floor sat below the real content width, letting the
|
|
|
|
|
console button overlap the board selector with the chat docked). When
|
|
|
|
|
the row genuinely cannot fit, the bar's flex-wrap moves the canvas
|
|
|
|
|
controls to a second line instead — never an overlap. */
|
|
|
|
|
min-width: max-content;
|
2026-04-29 10:21:26 +07:00
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unified-toolbar-editor .editor-toolbar-wrapper,
|
|
|
|
|
.unified-toolbar-editor .editor-toolbar {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 05:17:38 +07:00
|
|
|
/* The wrapper is an inline-size query container for its standalone (mobile)
|
|
|
|
|
mount. Inside the shared bar that containment is exactly what let the
|
|
|
|
|
editor zone shrink under its buttons: size containment makes the wrapper
|
|
|
|
|
contribute ZERO intrinsic width, so the zone's `min-width: max-content`
|
|
|
|
|
above resolved to nothing, flexbox squeezed it, and Libraries / Output
|
|
|
|
|
painted over the board selector instead of the bar wrapping. Turn it off
|
|
|
|
|
here — the label collapses are keyed to the `unified-toolbar` container
|
|
|
|
|
in EditorToolbar.css for this mount. */
|
|
|
|
|
.unified-toolbar-editor .editor-toolbar-wrapper {
|
|
|
|
|
container-type: normal;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 10:21:26 +07:00
|
|
|
.unified-toolbar-editor .editor-toolbar {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unified-toolbar-canvas {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
border-left: 1px solid #333;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
/* ── Main panels container ──────────────────────── */
|
2026-03-03 10:20:49 +07:00
|
|
|
.app-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
overflow: hidden;
|
2026-03-05 08:05:23 +07:00
|
|
|
position: relative;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-panel {
|
|
|
|
|
display: flex;
|
2026-03-06 20:14:50 +07:00
|
|
|
flex-direction: row;
|
2026-03-05 08:05:23 +07:00
|
|
|
flex-shrink: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-width: 20%;
|
|
|
|
|
max-width: 80%;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:14:50 +07:00
|
|
|
/* Toggle button for the file explorer sidebar */
|
|
|
|
|
.explorer-toggle-btn {
|
|
|
|
|
width: 36px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
background: #252526;
|
|
|
|
|
border: none;
|
|
|
|
|
border-right: 1px solid #333;
|
|
|
|
|
border-bottom: 1px solid #333;
|
|
|
|
|
color: #7a7a7a;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-04-22 02:45:45 +07:00
|
|
|
transition:
|
|
|
|
|
background 0.1s,
|
|
|
|
|
color 0.1s;
|
2026-03-06 20:14:50 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.explorer-toggle-btn:hover {
|
|
|
|
|
background: #2a2d2e;
|
|
|
|
|
color: #ccc;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 21:20:47 +07:00
|
|
|
/* Resize handle for the file explorer sidebar */
|
|
|
|
|
.explorer-resize-handle {
|
|
|
|
|
width: 4px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
cursor: col-resize;
|
|
|
|
|
background: #2a2a2a;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.explorer-resize-handle:hover,
|
|
|
|
|
.explorer-resize-handle:active {
|
style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 10:34:25 +07:00
|
|
|
background: var(--color-action-primary);
|
2026-03-06 21:20:47 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
.editor-wrapper {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.simulator-panel {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
2026-03-05 08:05:23 +07:00
|
|
|
min-width: 20%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Resize handle ──────────────────────────────── */
|
|
|
|
|
.resize-handle {
|
|
|
|
|
width: 5px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
background: #2a2a2a;
|
|
|
|
|
cursor: col-resize;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 10;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
.resize-handle:hover,
|
|
|
|
|
.resize-handle:active {
|
style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 10:34:25 +07:00
|
|
|
background: var(--color-action-primary);
|
2026-03-05 08:05:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.resize-handle-grip {
|
|
|
|
|
width: 2px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
background: #444;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.resize-handle:hover .resize-handle-grip,
|
|
|
|
|
.resize-handle:active .resize-handle-grip {
|
|
|
|
|
background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Responsive ─────────────────────────────────── */
|
2026-03-03 10:20:49 +07:00
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.app-container {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
.editor-panel,
|
|
|
|
|
.simulator-panel {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
min-width: unset;
|
|
|
|
|
max-width: unset;
|
2026-03-10 23:09:32 +07:00
|
|
|
flex: 1;
|
2026-03-05 08:05:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.resize-handle {
|
2026-03-10 23:09:32 +07:00
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Mobile tab bar ──────────────────────────────── */
|
|
|
|
|
.mobile-tab-bar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.mobile-tab-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-shrink: 0;
|
2026-03-24 03:15:29 +07:00
|
|
|
height: 44px;
|
|
|
|
|
background: #1e1e1e;
|
style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 10:34:25 +07:00
|
|
|
border-bottom: 2px solid var(--color-action-primary);
|
2026-03-10 23:09:32 +07:00
|
|
|
z-index: 50;
|
2026-03-24 03:15:29 +07:00
|
|
|
/* Sits between header and panels */
|
2026-03-10 23:09:32 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-tab-btn {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
2026-03-24 03:15:29 +07:00
|
|
|
flex-direction: row;
|
2026-03-10 23:09:32 +07:00
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-03-24 03:15:29 +07:00
|
|
|
gap: 6px;
|
2026-03-10 23:09:32 +07:00
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
2026-03-24 03:15:29 +07:00
|
|
|
border-bottom: 2px solid transparent;
|
2026-03-10 23:09:32 +07:00
|
|
|
color: #7a7a7a;
|
2026-03-24 03:15:29 +07:00
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
2026-03-10 23:09:32 +07:00
|
|
|
cursor: pointer;
|
2026-04-22 02:45:45 +07:00
|
|
|
transition:
|
|
|
|
|
color 0.15s,
|
|
|
|
|
background 0.15s,
|
|
|
|
|
border-color 0.15s;
|
2026-03-24 03:15:29 +07:00
|
|
|
padding: 8px 0;
|
|
|
|
|
margin-bottom: -2px;
|
2026-03-10 23:09:32 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-tab-btn--active {
|
style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 10:34:25 +07:00
|
|
|
color: var(--color-action-primary);
|
|
|
|
|
border-bottom-color: var(--color-action-primary);
|
|
|
|
|
background: rgba(0, 113, 227, 0.1);
|
2026-03-10 23:09:32 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-tab-btn:active {
|
|
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 10:53:27 +07:00
|
|
|
/* ── View-mode toggle: desktop only (mobile uses bottom-nav) ─────── */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.view-mode-toggle {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:09:32 +07:00
|
|
|
/* ── Header responsive ───────────────────────────── */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.app-header {
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.examples-link span,
|
|
|
|
|
.header-github-text,
|
|
|
|
|
.header-username-text {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.examples-link {
|
|
|
|
|
padding: 6px 8px;
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
2026-03-12 18:17:29 +07:00
|
|
|
|
|
|
|
|
/* Mobile nav: hide links, show hamburger, dropdown on open */
|
|
|
|
|
.header-nav-links {
|
|
|
|
|
display: none;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 44px;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
2026-03-16 10:12:33 +07:00
|
|
|
background: rgba(0, 0, 0, 0.96);
|
|
|
|
|
backdrop-filter: blur(20px);
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
|
2026-03-12 18:17:29 +07:00
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
gap: 0;
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
z-index: 200;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-links.header-nav-open {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-nav-link {
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-hamburger {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
2026-03-03 10:20:49 +07:00
|
|
|
}
|
feat: persist multi-board projects + add auto-save
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
|
|
|
|
|
|
|
|
@keyframes velxio-pulse {
|
|
|
|
|
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
|
|
|
50% { opacity: 1.0; transform: scale(1.0); }
|
|
|
|
|
}
|
2026-07-27 23:27:30 +07:00
|
|
|
|
|
|
|
|
/* Marching-ants attention border (CameraToggle while it wants a click):
|
|
|
|
|
four thin repeating-gradient strips, one per edge, sliding one dash
|
|
|
|
|
period per cycle. Drawn as backgrounds rather than a border because a
|
|
|
|
|
CSS border cannot animate its dash offset. Uses currentColor so the
|
2026-08-04 21:12:50 +07:00
|
|
|
ants inherit the button's state colour (grey idle, red on error).
|
|
|
|
|
NOT named .velxio-ants: that class is the canvas selection outline
|
|
|
|
|
(SimulatorCanvas.css) whose ::after is position:absolute — on a
|
|
|
|
|
non-positioned button it anchored to the toolbar and drew a
|
|
|
|
|
toolbar-wide ant line. */
|
2026-07-27 23:27:30 +07:00
|
|
|
@keyframes velxio-marching-ants {
|
|
|
|
|
to { background-position: 12px 0, -12px 100%, 0 -12px, 100% 12px; }
|
|
|
|
|
}
|
2026-08-04 21:12:50 +07:00
|
|
|
.velxio-btn-ants {
|
2026-07-27 23:27:30 +07:00
|
|
|
background-image:
|
|
|
|
|
repeating-linear-gradient(90deg, currentColor 0 6px, transparent 6px 12px),
|
|
|
|
|
repeating-linear-gradient(90deg, currentColor 0 6px, transparent 6px 12px),
|
|
|
|
|
repeating-linear-gradient(0deg, currentColor 0 6px, transparent 6px 12px),
|
|
|
|
|
repeating-linear-gradient(0deg, currentColor 0 6px, transparent 6px 12px);
|
|
|
|
|
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
|
|
|
|
background-size: 12px 1px, 12px 1px, 1px 12px, 1px 12px;
|
|
|
|
|
background-position: 0 0, 0 100%, 0 0, 100% 0;
|
|
|
|
|
animation: velxio-marching-ants 0.7s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
2026-08-04 21:12:50 +07:00
|
|
|
.velxio-btn-ants { animation: none; }
|
2026-07-27 23:27:30 +07:00
|
|
|
}
|
2026-07-30 23:56:24 +07:00
|
|
|
|
|
|
|
|
/* ── Editor: toolbar riding inside the header ─────────────────────────────
|
|
|
|
|
With the marketing nav gone from the editor header, its middle was dead
|
|
|
|
|
space while the toolbar occupied a whole second bar. The strip now fills
|
|
|
|
|
that middle: one 44px row instead of 44+38 — a full row returned to the
|
|
|
|
|
code/canvas. The strip keeps its own class names (container queries, the
|
|
|
|
|
docked-chat padding-right) and wraps internally when it truly cannot
|
|
|
|
|
fit; the header then grows instead of clipping or overlapping. */
|
|
|
|
|
.app-header--with-toolbar {
|
|
|
|
|
height: auto;
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-editor-toolbar {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-self: stretch;
|
|
|
|
|
margin: 0 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-editor-toolbar .unified-toolbar {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
/* Skin: it is part of the header now, not a bar of its own. */
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Keep the brand block and the right-side controls pinned to the first
|
|
|
|
|
line when the strip wraps the header taller. */
|
|
|
|
|
.app-header--with-toolbar .header-left,
|
|
|
|
|
.app-header--with-toolbar .header-right {
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 05:46:08 +07:00
|
|
|
/* Logo and the File menu read as one menubar — the marketing-header 16px
|
|
|
|
|
gap left a hole between them in the editor. */
|
|
|
|
|
.app-header--with-toolbar .header-left {
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 23:56:24 +07:00
|
|
|
.app-header--with-toolbar .header-content {
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The editor zone / toolbar inside the strip: no own chrome, blend into
|
|
|
|
|
the header instead of painting a nested dark box with its own border. */
|
|
|
|
|
.header-editor-toolbar .editor-toolbar,
|
|
|
|
|
.header-editor-toolbar .unified-toolbar-editor .editor-toolbar {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
height: 44px;
|
|
|
|
|
}
|
2026-07-31 00:43:31 +07:00
|
|
|
|
2026-08-18 10:21:21 +07:00
|
|
|
/* ── Fallback for the in-header strip: its own full-width bar ───────────
|
2026-07-31 00:43:31 +07:00
|
|
|
Between "fits in one row" and "phone" there is an awkward middle (e.g.
|
|
|
|
|
1440px with the chat docked) where flex-wrap produced an orphan group of
|
|
|
|
|
canvas controls floating inside a tall transparent header, next to a
|
2026-08-18 10:21:21 +07:00
|
|
|
dead hole before the right edge. When one row GENUINELY cannot hold the
|
|
|
|
|
strip, it drops back to being its own full-width second bar with its old
|
|
|
|
|
skin: exactly the familiar pre-merge layout, which reads as intentional
|
|
|
|
|
instead of broken.
|
|
|
|
|
|
|
|
|
|
Whether it fits is MEASURED by AppHeader (headerStripFit.ts), which sets
|
|
|
|
|
this class — not guessed from media queries. The old thresholds
|
|
|
|
|
(1040 / 1060 / 1400px by chat state) were right for one board only: a
|
|
|
|
|
XIAO with display + camera + mic + scope controls needs a wider strip
|
|
|
|
|
than an Uno, and at 1440px with the chat docked it wrapped inside the
|
|
|
|
|
header again. */
|
|
|
|
|
.app-header--with-toolbar.app-header--strip-below .header-content {
|
|
|
|
|
flex-wrap: wrap;
|
2026-07-31 00:43:31 +07:00
|
|
|
}
|
2026-08-18 10:21:21 +07:00
|
|
|
.app-header--with-toolbar.app-header--strip-below .header-editor-toolbar {
|
|
|
|
|
flex-basis: 100%;
|
|
|
|
|
order: 10;
|
|
|
|
|
margin: 0 -16px;
|
2026-07-31 00:43:31 +07:00
|
|
|
}
|
2026-08-18 10:21:21 +07:00
|
|
|
.app-header--with-toolbar.app-header--strip-below .header-editor-toolbar .unified-toolbar {
|
|
|
|
|
background: #252526;
|
|
|
|
|
border-top: 1px solid #333;
|
|
|
|
|
min-height: 38px;
|
2026-07-31 00:43:31 +07:00
|
|
|
}
|
2026-07-31 00:52:58 +07:00
|
|
|
|
|
|
|
|
/* ── Editor corner box (bottom-left) ─────────────────────────────────────
|
|
|
|
|
Language switcher + account button, IDE style. Moving them out of the
|
|
|
|
|
header is what buys the toolbar the last ~150px it needed to keep the
|
|
|
|
|
single-row layout on 1440px screens with the chat docked. */
|
|
|
|
|
.editor-corner-box {
|
|
|
|
|
position: fixed;
|
|
|
|
|
left: 8px;
|
|
|
|
|
bottom: 8px;
|
|
|
|
|
z-index: 7000; /* above the canvas, below the chat (8000) and modals */
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
background: rgba(23, 26, 33, 0.95);
|
|
|
|
|
border: 1px solid #2f3542;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* With the chat rising to the very top (agent CSS sets the header's
|
|
|
|
|
padding-right), the strip must NOT also pad itself — that would double
|
|
|
|
|
the reservation and squeeze the toolbar by another panel-width. */
|
|
|
|
|
.app-header--with-toolbar .header-editor-toolbar .unified-toolbar {
|
|
|
|
|
padding-right: 6px !important;
|
|
|
|
|
}
|
2026-07-31 01:54:24 +07:00
|
|
|
|
|
|
|
|
/* ── Explorer account footer ─────────────────────────────────────────────
|
|
|
|
|
The account + language block fused to the bottom of the file explorer:
|
|
|
|
|
a long tree scrolls above it instead of hiding under a floating box.
|
|
|
|
|
(.editor-corner-box remains as the fallback when the explorer is
|
|
|
|
|
collapsed.) The standalone globe is a logged-out affordance only — once
|
|
|
|
|
the pro auth slot renders its signed-in marker, language moves inside
|
|
|
|
|
the account menu and the globe hides. */
|
|
|
|
|
.explorer-account-footer {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
border-top: 1px solid #2f3542;
|
|
|
|
|
background: #171a21;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 03:56:19 +07:00
|
|
|
.explorer-account-footer {
|
|
|
|
|
container-type: inline-size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Tight footer (narrow explorer): the avatar alone identifies the account;
|
2026-08-18 09:54:49 +07:00
|
|
|
the name is a luxury that costs exactly the width we do not have. The
|
|
|
|
|
threshold sits under the explorer's 150px default so the first name shows
|
|
|
|
|
out of the box and only goes when the user drags the pane narrower. */
|
|
|
|
|
@container (max-width: 135px) {
|
2026-07-31 03:56:19 +07:00
|
|
|
.explorer-account-footer .header-username-text {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Collapsed explorer: the floating fallback box shows the circle only. */
|
|
|
|
|
.editor-corner-box .header-username-text {
|
2026-07-31 01:54:24 +07:00
|
|
|
display: none;
|
|
|
|
|
}
|
flash: compile before flashing, stale-build warning, i18n dialog, entitlement seam (#207)
"Flash to real board" no longer needs a prior Compile. The menu item is
enabled for every Arduino/ESP-IDF board (MicroPython keeps its own
gating) and the Flash dialog compiles first when the board has no program
or its code changed since the last build — compiler output streams into
the dialog's console, a red build ends in an error banner there with a
"Recompile & flash" retry, and only a green build reaches the flasher.
Web Serial's port picker needs a user gesture, so the web flow asks the
overlay to grant the port at the click (new WebFlashImpl.preparePort)
before the compile that may take minutes.
Stale detection: compileBoardProgram() now records a fingerprint of the
sources + language + build options (BoardInstance.compiledSourceHash,
utils/sourceFingerprint.ts); the dialog warns and rebuilds when the live
sources differ. utils/boardCompile.ts holds the compile request builder
shared with the toolbar (same files, same options).
The whole dialog and the menu hints are translated (editor.flash.* and
editor.canvas.flashHint.* in the 9 locales); before, every string was
hard-coded English.
Entitlement seam for #207 (desktop "upload to board"): lib/proWebFlash.ts
gains installHardwareFlashGate(); OSS default allows, and when a gate
blocks, the dialog shows a paid-license panel with a "See plans" link
instead of the port picker.
2026-08-19 01:21:57 +07:00
|
|
|
|
|
|
|
|
/* Flash dialog: indeterminate bar while the sketch compiles before a flash. */
|
|
|
|
|
@keyframes flash-indeterminate {
|
|
|
|
|
0% { transform: translateX(-100%); }
|
|
|
|
|
100% { transform: translateX(300%); }
|
|
|
|
|
}
|
|
|
|
|
.flash-indeterminate {
|
|
|
|
|
animation: flash-indeterminate 1.4s ease-in-out infinite;
|
|
|
|
|
}
|