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;
|
|
|
|
|
background: #252526;
|
|
|
|
|
border-bottom: 1px solid #333;
|
|
|
|
|
min-height: 38px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unified-toolbar-explorer-toggle {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unified-toolbar-editor {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unified-toolbar-editor .editor-toolbar-wrapper,
|
|
|
|
|
.unified-toolbar-editor .editor-toolbar {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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 {
|
|
|
|
|
background: #007acc;
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
background: #007acc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
border-bottom: 2px solid #007acc;
|
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 {
|
|
|
|
|
color: #007acc;
|
2026-03-24 03:15:29 +07:00
|
|
|
border-bottom-color: #007acc;
|
|
|
|
|
background: rgba(0, 122, 204, 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); }
|
|
|
|
|
}
|