668 lines
18 KiB
CSS
668 lines
18 KiB
CSS
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
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. */
|
|
.view-mode-toggle {
|
|
display: flex;
|
|
}
|
|
|
|
@container unified-toolbar (max-width: 1140px) {
|
|
.view-mode-toggle .vm-label {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
.unified-toolbar-editor {
|
|
flex: 1 1 auto;
|
|
/* 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;
|
|
display: flex;
|
|
}
|
|
|
|
.unified-toolbar-editor .editor-toolbar-wrapper,
|
|
.unified-toolbar-editor .editor-toolbar {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
}
|
|
|
|
@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).
|
|
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. */
|
|
@keyframes velxio-marching-ants {
|
|
to { background-position: 12px 0, -12px 100%, 0 -12px, 100% 12px; }
|
|
}
|
|
.velxio-btn-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-btn-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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* ── Fallback for the in-header strip: its own full-width bar ───────────
|
|
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 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;
|
|
}
|
|
.app-header--with-toolbar.app-header--strip-below .header-editor-toolbar {
|
|
flex-basis: 100%;
|
|
order: 10;
|
|
margin: 0 -16px;
|
|
}
|
|
.app-header--with-toolbar.app-header--strip-below .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 {
|
|
container-type: inline-size;
|
|
}
|
|
|
|
/* Tight footer (narrow explorer): the avatar alone identifies the account;
|
|
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) {
|
|
.explorer-account-footer .header-username-text {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* Collapsed explorer: the floating fallback box shows the circle only. */
|
|
.editor-corner-box .header-username-text {
|
|
display: none;
|
|
}
|
|
|
|
/* 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;
|
|
}
|