velxio/frontend/src/App.css

640 lines
17 KiB
CSS
Raw Normal View History

* {
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);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ── App shell ──────────────────────────────────── */
.app {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
overscroll-behavior: none;
}
/* ── Header ─────────────────────────────────────── */
.app-header {
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);
padding: 0 16px;
height: 44px;
flex-shrink: 0;
display: flex;
align-items: center;
position: relative;
z-index: 100;
}
.header-content {
display: flex;
align-items: center;
width: 100%;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-brand {
display: flex;
align-items: center;
gap: 10px;
}
.header-title {
font-size: 15px;
font-weight: 600;
color: #f5f5f7;
letter-spacing: -0.2px;
}
/* Kept for backward compatibility */
.examples-link {
display: flex;
align-items: center;
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;
}
.examples-link:hover {
background: #2a2d2e;
color: #fff;
border-color: #555;
}
/* ── Unified nav links ──────────────────────────── */
.header-nav-links {
display: flex;
align-items: center;
gap: 2px;
}
.header-nav-link {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
color: #86868b;
text-decoration: none;
border-radius: 100px;
font-size: 13px;
font-weight: 400;
transition:
color 0.2s,
background 0.2s;
white-space: nowrap;
}
.header-nav-link:hover {
color: #f5f5f7;
background: rgba(255, 255, 255, 0.08);
}
.header-nav-link-active {
color: #f5f5f7;
background: rgba(255, 255, 255, 0.1);
}
.header-nav-discord {
color: #7289da;
}
.header-nav-discord:hover {
color: #8fa8f5;
background: rgba(114, 137, 218, 0.1);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
/* 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;
}
/* ── 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;
/* 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;
background: #252526;
border-bottom: 1px solid #333;
min-height: 38px;
/* 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. */
@container unified-toolbar (max-width: 1140px) {
.view-mode-toggle .vm-label {
display: none;
}
}
.unified-toolbar-explorer-toggle {
border-bottom: none;
}
.unified-toolbar-editor {
flex: 1 1 auto;
/* Floor the editor zone at its fully-collapsed content width so it can never
shrink *under* its own buttons and paint them over the adjacent canvas
controls. Below this floor the (lower-priority) canvas controls get pushed
toward the right edge instead of overlapping — never the other way. */
min-width: 280px;
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;
}
/* ── Main panels container ──────────────────────── */
.app-container {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.editor-panel {
display: flex;
flex-direction: row;
flex-shrink: 0;
overflow: hidden;
min-width: 20%;
max-width: 80%;
}
/* 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;
transition:
background 0.1s,
color 0.1s;
}
.explorer-toggle-btn:hover {
background: #2a2d2e;
color: #ccc;
}
/* 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: var(--color-action-primary);
}
.editor-wrapper {
flex: 1;
overflow: hidden;
}
.simulator-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
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;
}
.resize-handle:hover,
.resize-handle:active {
background: var(--color-action-primary);
}
.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 ─────────────────────────────────── */
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.editor-panel,
.simulator-panel {
width: 100% !important;
min-width: unset;
max-width: unset;
flex: 1;
}
.resize-handle {
display: none;
}
}
/* ── Mobile tab bar ──────────────────────────────── */
.mobile-tab-bar {
display: none;
}
@media (max-width: 768px) {
.mobile-tab-bar {
display: flex;
flex-shrink: 0;
height: 44px;
background: #1e1e1e;
border-bottom: 2px solid var(--color-action-primary);
z-index: 50;
/* Sits between header and panels */
}
.mobile-tab-btn {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: #7a7a7a;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition:
color 0.15s,
background 0.15s,
border-color 0.15s;
padding: 8px 0;
margin-bottom: -2px;
}
.mobile-tab-btn--active {
color: var(--color-action-primary);
border-bottom-color: var(--color-action-primary);
background: rgba(0, 113, 227, 0.1);
}
.mobile-tab-btn:active {
background: rgba(255, 255, 255, 0.06);
}
}
feat(editor): view-mode toggle, agent-chat slot, toolbar polish Changes that ship to OSS — all benign for self-hosters, but most are extension points the velxio-prod overlay (and any private fork) needs to plug an in-editor AI chat into the page. Editor: - 3-way view-mode toggle (code / both / circuit) in the unified toolbar. Lets users hide a pane to give a right-docked sidebar (e.g. the AI chat overlay) more breathing room. Persisted in useEditorStore. - Default file explorer narrower (210 → 165 px); min 110. - Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip); the BoardSelector dropdown elsewhere already shows the active board. - Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow menu gave up too much discoverability. Removed dead overflow state. Simulator: - Fix: global Delete/Backspace handler in SimulatorCanvas no longer fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable — affected any in-page text field, not just the chat overlay. Overlay extensibility: - New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so pro overlays can portal a chat panel into the editor without forking the page. - vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set. Lets local-dev junctions (overlay tree → frontend/src/pro) resolve bare imports back to the OSS node_modules without resolving symlinks. Deps: - Added react-markdown + remark-gfm (rendered chat output) and @google/genai + zod (overlay agent loop). Tree-shaken from the OSS bundle when no pro code imports them. gitignore: - Ignore backend/app/pro/ and frontend/src/pro/ junctions used by developers running a private overlay against the OSS dev server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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;
}
}
/* ── 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;
}
/* Mobile nav: hide links, show hamburger, dropdown on open */
.header-nav-links {
display: none;
position: absolute;
top: 44px;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.96);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
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;
}
}
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); }
}
/* 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
ants inherit the button's state colour (grey idle, red on error). */
@keyframes velxio-marching-ants {
to { background-position: 12px 0, -12px 100%, 0 -12px, 100% 12px; }
}
.velxio-ants {
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) {
.velxio-ants { animation: none; }
}
/* ── 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;
}
.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;
}
/* ── Tight fallback for the in-header strip ──────────────────────────────
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
dead hole before the right-side controls. Below the width where one row
GENUINELY fits — thresholds account for the docked chat's reserved
380px / 36px — the strip 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. Three blocks because a media
query cannot test a body class in its condition. */
@media (max-width: 1039.9px) {
.app-header--with-toolbar .header-content {
flex-wrap: wrap;
}
.app-header--with-toolbar .header-editor-toolbar {
flex-basis: 100%;
order: 10;
margin: 0 -16px;
}
.app-header--with-toolbar .header-editor-toolbar .unified-toolbar {
background: #252526;
border-top: 1px solid #333;
min-height: 38px;
}
}
@media (max-width: 1059.9px) {
body.velxio-agent-collapsed .app-header--with-toolbar .header-content {
flex-wrap: wrap;
}
body.velxio-agent-collapsed .app-header--with-toolbar .header-editor-toolbar {
flex-basis: 100%;
order: 10;
margin: 0 -16px;
}
body.velxio-agent-collapsed .app-header--with-toolbar .header-editor-toolbar .unified-toolbar {
background: #252526;
border-top: 1px solid #333;
min-height: 38px;
}
}
@media (max-width: 1399.9px) {
body.velxio-agent-open .app-header--with-toolbar .header-content {
flex-wrap: wrap;
}
body.velxio-agent-open .app-header--with-toolbar .header-editor-toolbar {
flex-basis: 100%;
order: 10;
margin: 0 -16px;
}
body.velxio-agent-open .app-header--with-toolbar .header-editor-toolbar .unified-toolbar {
background: #252526;
border-top: 1px solid #333;
min-height: 38px;
}
}
/* ── 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;
}
/* ── 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;
}
.explorer-account-footer:has([data-auth-user]) .corner-lang,
.editor-corner-box:has([data-auth-user]) .corner-lang {
display: none;
}