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>
|
|
@ -90,6 +90,12 @@
|
|||
<link rel="dns-prefetch" href="https://github.com" />
|
||||
<link rel="dns-prefetch" href="https://discord.com" />
|
||||
|
||||
<!-- ═══════════════════════════════════════════════
|
||||
WEBFONTS — self-hosted, preloaded
|
||||
═══════════════════════════════════════════════ -->
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/Inter.var.woff2" crossorigin />
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/JetBrainsMono.var.woff2" crossorigin />
|
||||
|
||||
<!-- ═══════════════════════════════════════════════
|
||||
JSON-LD STRUCTURED DATA
|
||||
Helps Google understand the page type and show
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"idb-keyval": "^6.2.2",
|
||||
"jszip": "^3.10.1",
|
||||
"littlefs": "^0.1.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
# Self-hosted webfonts
|
||||
|
||||
Velxio's design system depends on two variable fonts shipped from this directory:
|
||||
|
||||
- `Inter.var.woff2` — Inter variable, 100-900 weight axis. Source: https://github.com/rsms/inter/releases (download the latest, copy `Inter.var.woff2`).
|
||||
- `JetBrainsMono.var.woff2` — JetBrains Mono variable, 100-800 weight axis. Source: https://github.com/JetBrains/JetBrainsMono/releases (latest tag, file is `webfonts/JetBrainsMono[wght].woff2` — rename to `JetBrainsMono.var.woff2`).
|
||||
|
||||
Both are SIL Open Font License 1.1 — include the LICENSE files alongside the woff2 if redistributing.
|
||||
|
||||
## Why self-hosted
|
||||
|
||||
We previously used `-apple-system, SF Pro` which renders correctly on macOS only. Inter renders identically across Mac, Windows, Linux, Android. JetBrains Mono ships every glyph the editor needs (box-drawing, ligatures off by default — Monaco handles its own font separately, but our `<pre>` and inline `<code>` use this).
|
||||
|
||||
## Preload
|
||||
|
||||
Both fonts are preloaded in `index.html`:
|
||||
|
||||
```html
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/Inter.var.woff2" crossorigin>
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/JetBrainsMono.var.woff2" crossorigin>
|
||||
```
|
||||
|
||||
The `crossorigin` attribute is required — without it the browser fetches twice.
|
||||
|
||||
## Verifying
|
||||
|
||||
After deploy, in DevTools Network tab filter by "Font". Should show two requests, both with `(memory cache)` after the first page load. If you see Roboto / Segoe UI / Helvetica fall through, the font failed to load — check the woff2 path and CORS headers from your CDN.
|
||||
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 181 KiB |
|
|
@ -5,11 +5,8 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
background-color: #000;
|
||||
color: #f5f5f7;
|
||||
background-color: var(--color-bg-canvas);
|
||||
color: var(--color-fg-default);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
.ui-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
border: none;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--duration-base) var(--ease-out),
|
||||
color var(--duration-base) var(--ease-out),
|
||||
filter var(--duration-base) var(--ease-out),
|
||||
transform var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.ui-btn--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-btn:disabled,
|
||||
.ui-btn--loading {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ui-btn__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-btn__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.ui-btn--sm { padding: 8px 14px; font-size: 13px; border-radius: var(--radius-pill); }
|
||||
.ui-btn--md { padding: 11px 18px; font-size: 15px; border-radius: var(--radius-pill); }
|
||||
.ui-btn--lg { padding: 14px 22px; font-size: 17px; border-radius: var(--radius-pill); }
|
||||
|
||||
/* Variants */
|
||||
.ui-btn--primary {
|
||||
background: var(--color-action-primary);
|
||||
color: var(--color-action-primary-fg);
|
||||
}
|
||||
.ui-btn--primary:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.ui-btn--secondary {
|
||||
background: var(--color-action-secondary);
|
||||
color: var(--color-fg-default);
|
||||
}
|
||||
.ui-btn--secondary:hover:not(:disabled) {
|
||||
background: var(--color-action-secondary-hover);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.ui-btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--color-fg-default);
|
||||
}
|
||||
.ui-btn--ghost:hover:not(:disabled) {
|
||||
background: var(--color-bg-surface-2);
|
||||
}
|
||||
|
||||
.ui-btn--white {
|
||||
background: #ffffff;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
.ui-btn--white:hover:not(:disabled) {
|
||||
filter: brightness(0.96);
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.ui-btn__spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
animation: ui-btn-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ui-btn-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
||||
import './Button.css';
|
||||
|
||||
type Variant = 'primary' | 'secondary' | 'ghost' | 'white';
|
||||
type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
loading?: boolean;
|
||||
iconLeft?: ReactNode;
|
||||
iconRight?: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
|
||||
{
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
iconLeft,
|
||||
iconRight,
|
||||
fullWidth = false,
|
||||
disabled,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const classes = [
|
||||
'ui-btn',
|
||||
`ui-btn--${variant}`,
|
||||
`ui-btn--${size}`,
|
||||
fullWidth ? 'ui-btn--full' : '',
|
||||
loading ? 'ui-btn--loading' : '',
|
||||
className ?? '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classes}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={loading || undefined}
|
||||
{...rest}
|
||||
>
|
||||
{loading ? <span className="ui-btn__spinner" aria-hidden="true" /> : null}
|
||||
{!loading && iconLeft ? <span className="ui-btn__icon">{iconLeft}</span> : null}
|
||||
<span className="ui-btn__label">{children}</span>
|
||||
{!loading && iconRight ? <span className="ui-btn__icon">{iconRight}</span> : null}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
.ui-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.ui-card--default {
|
||||
background: var(--color-bg-surface-1);
|
||||
}
|
||||
|
||||
.ui-card--elevated {
|
||||
background: var(--color-bg-surface-2);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.ui-card--outlined {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.ui-card--p-sm { padding: var(--space-6); }
|
||||
.ui-card--p-md { padding: var(--space-9); }
|
||||
.ui-card--p-lg { padding: var(--space-11) var(--space-10); }
|
||||
|
||||
.ui-card__header {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.ui-card__footer {
|
||||
margin-top: var(--space-6);
|
||||
padding-top: var(--space-6);
|
||||
border-top: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import './Card.css';
|
||||
|
||||
type Variant = 'default' | 'elevated' | 'outlined';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: Variant;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
padding?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function Card({
|
||||
variant = 'default',
|
||||
header,
|
||||
footer,
|
||||
padding = 'md',
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}: Props) {
|
||||
const classes = [
|
||||
'ui-card',
|
||||
`ui-card--${variant}`,
|
||||
`ui-card--p-${padding}`,
|
||||
className ?? '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<div className={classes} {...rest}>
|
||||
{header ? <div className="ui-card__header">{header}</div> : null}
|
||||
<div className="ui-card__body">{children}</div>
|
||||
{footer ? <div className="ui-card__footer">{footer}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
.ui-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.ui-field__label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-fg-muted);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.ui-input {
|
||||
background: var(--color-bg-input);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 11px 14px;
|
||||
color: var(--color-fg-default);
|
||||
font-size: 15px;
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
transition: border-color var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
.ui-input:focus {
|
||||
border-color: var(--color-action-primary);
|
||||
}
|
||||
|
||||
.ui-input--error {
|
||||
border-color: var(--color-feedback-error);
|
||||
}
|
||||
|
||||
.ui-field__helper {
|
||||
font-size: 12px;
|
||||
color: var(--color-fg-subtle);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui-field__error {
|
||||
font-size: 12px;
|
||||
color: var(--color-feedback-error);
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { forwardRef, useId, type InputHTMLAttributes } from 'react';
|
||||
import './Input.css';
|
||||
|
||||
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
helper?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, Props>(function Input(
|
||||
{ label, helper, error, id: providedId, className, ...rest },
|
||||
ref,
|
||||
) {
|
||||
const generatedId = useId();
|
||||
const id = providedId ?? generatedId;
|
||||
const helperId = helper ? `${id}-helper` : undefined;
|
||||
const errorId = error ? `${id}-error` : undefined;
|
||||
|
||||
return (
|
||||
<div className={['ui-field', className ?? ''].filter(Boolean).join(' ')}>
|
||||
{label ? (
|
||||
<label htmlFor={id} className="ui-field__label">
|
||||
{label}
|
||||
</label>
|
||||
) : null}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={['ui-input', error ? 'ui-input--error' : ''].filter(Boolean).join(' ')}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
aria-describedby={errorId ?? helperId}
|
||||
{...rest}
|
||||
/>
|
||||
{helper && !error ? (
|
||||
<p id={helperId} className="ui-field__helper">
|
||||
{helper}
|
||||
</p>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p id={errorId} className="ui-field__error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { Button } from './Button';
|
||||
export { Card } from './Card';
|
||||
export { Input } from './Input';
|
||||
|
|
@ -1,39 +1,20 @@
|
|||
/* ── Global Apple Design System ────────────────────────── */
|
||||
/* Velxio global styles.
|
||||
*
|
||||
* Tokens live in ./tokens/. Component utilities (.ap-*) below depend on
|
||||
* those tokens. Legacy aliases (--accent, --bg, etc.) are kept inside
|
||||
* tokens/colors.css so existing components keep working until they migrate
|
||||
* to the typed primitives in ./components/ui/.
|
||||
*/
|
||||
|
||||
@import './tokens/index.css';
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--accent: #0071e3;
|
||||
--accent-hover: #0077ed;
|
||||
--bg: #000000;
|
||||
--bg-card: #141416;
|
||||
--bg-elevated: #2c2c2e;
|
||||
--bg-input: #1c1c1e;
|
||||
--border: rgba(255, 255, 255, 0.09);
|
||||
--border-hi: rgba(255, 255, 255, 0.16);
|
||||
--text: #f5f5f7;
|
||||
--text-muted: #86868b;
|
||||
--text-dim: #6e6e73;
|
||||
|
||||
/* Typography */
|
||||
--font:
|
||||
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
--mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
|
||||
/* Radii */
|
||||
--radius: 18px;
|
||||
--radius-sm: 10px;
|
||||
--radius-pill: 100px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow: 0 4px 32px rgba(0, 0, 0, 0.45);
|
||||
--shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
|
||||
font-family: var(--font);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: dark;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
color: var(--color-fg-default);
|
||||
background-color: var(--color-bg-canvas);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
@ -51,9 +32,9 @@ body {
|
|||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
background: var(--color-bg-canvas);
|
||||
color: var(--color-fg-default);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
#root {
|
||||
|
|
@ -61,24 +42,21 @@ body {
|
|||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Shared auth / page utility classes ─────────────────── */
|
||||
/* ── Auth / page utility classes (legacy — migrate to <Button>, <Card>, <Input>) ─ */
|
||||
|
||||
/* Full-page centered layout */
|
||||
.ap-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg);
|
||||
padding: 32px 16px;
|
||||
font-family: var(--font);
|
||||
background: var(--color-bg-canvas);
|
||||
padding: var(--space-9) var(--space-6);
|
||||
}
|
||||
|
||||
/* Auth card */
|
||||
.ap-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 44px 40px;
|
||||
background: var(--color-bg-surface-1);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-11) var(--space-10);
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
display: flex;
|
||||
|
|
@ -87,79 +65,77 @@ body {
|
|||
}
|
||||
|
||||
.ap-card-title {
|
||||
margin: 0 0 8px;
|
||||
margin: 0 0 var(--space-4);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.8px;
|
||||
color: var(--color-fg-default);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ap-card-sub {
|
||||
margin: 0 0 32px;
|
||||
margin: 0 0 var(--space-9);
|
||||
font-size: 15px;
|
||||
color: var(--text-muted);
|
||||
color: var(--color-fg-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.ap-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
gap: var(--space-5);
|
||||
margin-bottom: var(--space-7);
|
||||
}
|
||||
|
||||
.ap-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.ap-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
color: var(--color-fg-muted);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.ap-input {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-hi);
|
||||
border-radius: 10px;
|
||||
background: var(--color-bg-input);
|
||||
border: 1px solid var(--color-border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 11px 14px;
|
||||
color: var(--text);
|
||||
color: var(--color-fg-default);
|
||||
font-size: 15px;
|
||||
font-family: var(--font);
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
.ap-input:focus {
|
||||
border-color: var(--accent);
|
||||
border-color: var(--color-action-primary);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.ap-btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-4);
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
background: var(--color-action-primary);
|
||||
color: var(--color-action-primary-fg);
|
||||
border: none;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
filter 0.2s,
|
||||
transform 0.15s;
|
||||
filter var(--duration-base) var(--ease-out),
|
||||
transform var(--duration-fast) var(--ease-out);
|
||||
letter-spacing: -0.1px;
|
||||
margin-top: 6px;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.ap-btn-primary:hover:not(:disabled) {
|
||||
|
|
@ -176,25 +152,25 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-4);
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
background: var(--color-action-secondary);
|
||||
color: var(--color-fg-default);
|
||||
border: none;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s,
|
||||
transform 0.15s;
|
||||
background var(--duration-base) var(--ease-out),
|
||||
transform var(--duration-fast) var(--ease-out);
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.ap-btn-secondary:hover {
|
||||
background: #3a3a3c;
|
||||
background: var(--color-action-secondary-hover);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +178,7 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
gap: var(--space-5);
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
background: #ffffff;
|
||||
|
|
@ -211,22 +187,21 @@ body {
|
|||
border-radius: var(--radius-pill);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
transition: filter 0.2s;
|
||||
transition: filter var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
.ap-btn-white:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.ap-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
color: var(--text-dim);
|
||||
gap: var(--space-5);
|
||||
margin: var(--space-6) 0;
|
||||
color: var(--color-fg-subtle);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
|
@ -235,30 +210,28 @@ body {
|
|||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-hi);
|
||||
background: var(--color-border-strong);
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.ap-error {
|
||||
background: #2d0e0e;
|
||||
border: 1px solid #7f1d1d;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #f87171;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
/* Footer link */
|
||||
.ap-footer {
|
||||
color: var(--text-muted);
|
||||
color: var(--color-fg-muted);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin: 20px 0 0;
|
||||
margin: var(--space-7) 0 0;
|
||||
}
|
||||
|
||||
.ap-link {
|
||||
color: var(--accent);
|
||||
color: var(--color-action-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Cpu as IcoChip,
|
||||
CircuitBoard as IcoCpu,
|
||||
Code2 as IcoCode,
|
||||
Zap as IcoZap,
|
||||
Layers as IcoLayers,
|
||||
Monitor as IcoMonitor,
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import { trackVisitGitHub, trackClickCTA } from '../utils/analytics';
|
||||
import { AppHeader } from '../components/layout/AppHeader';
|
||||
|
|
@ -12,94 +20,7 @@ const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
|
|||
const PAYPAL_URL = 'https://paypal.me/odoonext';
|
||||
const GITHUB_SPONSORS_URL = 'https://github.com/sponsors/davidmonterocrespo24';
|
||||
|
||||
/* ── Icons ───────────────────────────────────────────── */
|
||||
const IcoChip = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="5" y="5" width="14" height="14" rx="2" />
|
||||
<rect x="9" y="9" width="6" height="6" />
|
||||
<path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcoCpu = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<rect x="8" y="8" width="8" height="8" />
|
||||
<path d="M10 2v2M14 2v2M10 20v2M14 20v2M2 10h2M2 14h2M20 10h2M20 14h2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcoCode = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="16 18 22 12 16 6" />
|
||||
<polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcoZap = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcoLayers = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
||||
<polyline points="2 17 12 22 22 17" />
|
||||
<polyline points="2 12 12 17 22 12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcoMonitor = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* GitHub keeps a custom (filled) glyph — Lucide's outline doesn't match the brand. */
|
||||
const IcoGitHub = () => (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.295 24 12c0-6.63-5.37-12-12-12z" />
|
||||
|
|
@ -962,25 +883,66 @@ export const LandingPage: React.FC = () => {
|
|||
<div className="boards-row">
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img src="/boards/arduino-uno.svg" alt="Arduino Uno" className="board-img-sm" />
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/arduino-uno.webp 1x, /boards/arduino-uno@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/arduino-uno.png 1x, /boards/arduino-uno@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/arduino-uno.svg"
|
||||
alt="Arduino Uno"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">Arduino Uno</span>
|
||||
<span className="board-chip-sm">ATmega328p · 32 KB</span>
|
||||
</div>
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img src="/boards/arduino-nano.svg" alt="Arduino Nano" className="board-img-sm" />
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/arduino-nano.webp 1x, /boards/arduino-nano@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/arduino-nano.png 1x, /boards/arduino-nano@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/arduino-nano.svg"
|
||||
alt="Arduino Nano"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">Arduino Nano</span>
|
||||
<span className="board-chip-sm">ATmega328p · 32 KB</span>
|
||||
</div>
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img
|
||||
src="/boards/arduino-mega.svg"
|
||||
alt="Arduino Mega 2560"
|
||||
className="board-img-sm"
|
||||
/>
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/arduino-mega.webp 1x, /boards/arduino-mega@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/arduino-mega.png 1x, /boards/arduino-mega@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/arduino-mega.svg"
|
||||
alt="Arduino Mega 2560"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">Arduino Mega 2560</span>
|
||||
<span className="board-chip-sm">ATmega2560 · 256 KB</span>
|
||||
|
|
@ -1007,18 +969,44 @@ export const LandingPage: React.FC = () => {
|
|||
<div className="boards-row">
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img src="/boards/pi-pico.svg" alt="Raspberry Pi Pico" className="board-img-sm" />
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/pi-pico.webp 1x, /boards/pi-pico@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/pi-pico.png 1x, /boards/pi-pico@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/pi-pico.svg"
|
||||
alt="Raspberry Pi Pico"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">Raspberry Pi Pico</span>
|
||||
<span className="board-chip-sm">RP2040 · 264 KB RAM</span>
|
||||
</div>
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img
|
||||
src="/boards/pi-pico-w.svg"
|
||||
alt="Raspberry Pi Pico W"
|
||||
className="board-img-sm"
|
||||
/>
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/pi-pico-w.webp 1x, /boards/pi-pico-w@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/pi-pico-w.png 1x, /boards/pi-pico-w@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/pi-pico-w.svg"
|
||||
alt="Raspberry Pi Pico W"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">Raspberry Pi Pico W</span>
|
||||
<span className="board-chip-sm">RP2040 + WiFi</span>
|
||||
|
|
@ -1038,7 +1026,22 @@ export const LandingPage: React.FC = () => {
|
|||
<div className="boards-row">
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img src="/boards/esp32-c3.svg" alt="ESP32-C3" className="board-img-sm" />
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/esp32-c3.webp 1x, /boards/esp32-c3@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/esp32-c3.png 1x, /boards/esp32-c3@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/esp32-c3.svg"
|
||||
alt="ESP32-C3"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">ESP32-C3 DevKit</span>
|
||||
<span className="board-chip-sm">RV32IMC · 4 MB flash</span>
|
||||
|
|
@ -1083,11 +1086,22 @@ export const LandingPage: React.FC = () => {
|
|||
<div className="boards-row">
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img
|
||||
src="/boards/esp32-devkit-c-v4.svg"
|
||||
alt="ESP32 DevKit V1"
|
||||
className="board-img-sm"
|
||||
/>
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/esp32-devkit-v1.webp 1x, /boards/esp32-devkit-v1@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/esp32-devkit-v1.png 1x, /boards/esp32-devkit-v1@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/esp32-devkit-v1.svg"
|
||||
alt="ESP32 DevKit V1"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">ESP32 DevKit V1</span>
|
||||
<span className="board-chip-sm">LX6 · 4 MB flash</span>
|
||||
|
|
@ -1119,7 +1133,22 @@ export const LandingPage: React.FC = () => {
|
|||
</div>
|
||||
<div className="board-card-sm">
|
||||
<div className="board-img-box">
|
||||
<img src="/boards/xiao-esp32-s3.svg" alt="XIAO ESP32-S3" className="board-img-sm" />
|
||||
<picture>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcSet="/boards/xiao-esp32-s3.webp 1x, /boards/xiao-esp32-s3@2x.webp 2x"
|
||||
/>
|
||||
<source
|
||||
type="image/png"
|
||||
srcSet="/boards/xiao-esp32-s3.png 1x, /boards/xiao-esp32-s3@2x.png 2x"
|
||||
/>
|
||||
<img
|
||||
src="/boards/xiao-esp32-s3.svg"
|
||||
alt="XIAO ESP32-S3"
|
||||
className="board-img-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<span className="board-name-sm">XIAO ESP32-S3</span>
|
||||
<span className="board-chip-sm">LX7 · compact</span>
|
||||
|
|
|
|||
|
|
@ -443,15 +443,27 @@ export const Velxio2Page: React.FC = () => {
|
|||
</div>
|
||||
<div className="v2-boards-row">
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/arduino-uno.svg" alt="Arduino Uno" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/arduino-uno.webp 1x, /boards/arduino-uno@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/arduino-uno.png 1x, /boards/arduino-uno@2x.png 2x" />
|
||||
<img src="/boards/arduino-uno.svg" alt="Arduino Uno" loading="lazy" />
|
||||
</picture>
|
||||
<span>Arduino Uno</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/arduino-nano.svg" alt="Arduino Nano" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/arduino-nano.webp 1x, /boards/arduino-nano@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/arduino-nano.png 1x, /boards/arduino-nano@2x.png 2x" />
|
||||
<img src="/boards/arduino-nano.svg" alt="Arduino Nano" loading="lazy" />
|
||||
</picture>
|
||||
<span>Arduino Nano</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/arduino-mega.svg" alt="Arduino Mega 2560" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/arduino-mega.webp 1x, /boards/arduino-mega@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/arduino-mega.png 1x, /boards/arduino-mega@2x.png 2x" />
|
||||
<img src="/boards/arduino-mega.svg" alt="Arduino Mega 2560" loading="lazy" />
|
||||
</picture>
|
||||
<span>Mega 2560</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -465,11 +477,19 @@ export const Velxio2Page: React.FC = () => {
|
|||
</div>
|
||||
<div className="v2-boards-row">
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/pi-pico.svg" alt="Raspberry Pi Pico" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/pi-pico.webp 1x, /boards/pi-pico@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/pi-pico.png 1x, /boards/pi-pico@2x.png 2x" />
|
||||
<img src="/boards/pi-pico.svg" alt="Raspberry Pi Pico" loading="lazy" />
|
||||
</picture>
|
||||
<span>Pi Pico</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/pi-pico-w.svg" alt="Raspberry Pi Pico W" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/pi-pico-w.webp 1x, /boards/pi-pico-w@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/pi-pico-w.png 1x, /boards/pi-pico-w@2x.png 2x" />
|
||||
<img src="/boards/pi-pico-w.svg" alt="Raspberry Pi Pico W" loading="lazy" />
|
||||
</picture>
|
||||
<span>Pi Pico W</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -483,7 +503,11 @@ export const Velxio2Page: React.FC = () => {
|
|||
</div>
|
||||
<div className="v2-boards-row">
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/esp32-c3.svg" alt="ESP32-C3" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/esp32-c3.webp 1x, /boards/esp32-c3@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/esp32-c3.png 1x, /boards/esp32-c3@2x.png 2x" />
|
||||
<img src="/boards/esp32-c3.svg" alt="ESP32-C3" loading="lazy" />
|
||||
</picture>
|
||||
<span>ESP32-C3</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
|
|
@ -517,7 +541,11 @@ export const Velxio2Page: React.FC = () => {
|
|||
<span>ESP32-CAM</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
<img src="/boards/xiao-esp32-s3.svg" alt="XIAO ESP32-S3" />
|
||||
<picture>
|
||||
<source type="image/webp" srcSet="/boards/xiao-esp32-s3.webp 1x, /boards/xiao-esp32-s3@2x.webp 2x" />
|
||||
<source type="image/png" srcSet="/boards/xiao-esp32-s3.png 1x, /boards/xiao-esp32-s3@2x.png 2x" />
|
||||
<img src="/boards/xiao-esp32-s3.svg" alt="XIAO ESP32-S3" loading="lazy" />
|
||||
</picture>
|
||||
<span>XIAO S3</span>
|
||||
</div>
|
||||
<div className="v2-board-card">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/* Color tokens — primitive palette + semantic mappings.
|
||||
*
|
||||
* Layers:
|
||||
* 1. Primitives (--gray-*, --blue-*, etc.) — raw scale, never used directly
|
||||
* in components.
|
||||
* 2. Semantics (--color-bg-*, --color-fg-*, --color-action-*) — used by
|
||||
* components. Swap themes by remapping semantics to different primitives.
|
||||
* 3. Legacy aliases (--accent, --bg, etc.) — kept temporarily so existing
|
||||
* components keep working. Remove after full migration.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Primitives — gray scale */
|
||||
--gray-0: #ffffff;
|
||||
--gray-50: #fafafa;
|
||||
--gray-100: #f5f5f7;
|
||||
--gray-200: #e5e5ea;
|
||||
--gray-300: #d1d1d6;
|
||||
--gray-400: #aeaeb2;
|
||||
--gray-500: #86868b;
|
||||
--gray-600: #6e6e73;
|
||||
--gray-700: #48484a;
|
||||
--gray-800: #2c2c2e;
|
||||
--gray-850: #1c1c1e;
|
||||
--gray-900: #141416;
|
||||
--gray-950: #0a0a0c;
|
||||
--gray-1000: #000000;
|
||||
|
||||
/* Primitives — accents */
|
||||
--blue-400: #4ea8ff;
|
||||
--blue-500: #0a84ff;
|
||||
--blue-600: #0071e3;
|
||||
--blue-700: #0058b3;
|
||||
|
||||
--red-500: #ff453a;
|
||||
--red-600: #d70015;
|
||||
--green-500: #30d158;
|
||||
--green-600: #248a3d;
|
||||
--amber-500: #ff9f0a;
|
||||
--amber-600: #c93400;
|
||||
|
||||
/* Semantic — backgrounds (dark theme baseline) */
|
||||
--color-bg-canvas: var(--gray-1000);
|
||||
--color-bg-surface-1: var(--gray-900);
|
||||
--color-bg-surface-2: var(--gray-850);
|
||||
--color-bg-surface-3: var(--gray-800);
|
||||
--color-bg-input: var(--gray-850);
|
||||
--color-bg-overlay: rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Semantic — foreground */
|
||||
--color-fg-default: var(--gray-100);
|
||||
--color-fg-muted: var(--gray-500);
|
||||
--color-fg-subtle: var(--gray-600);
|
||||
--color-fg-on-action: var(--gray-0);
|
||||
|
||||
/* Semantic — borders */
|
||||
--color-border-default: rgba(255, 255, 255, 0.09);
|
||||
--color-border-strong: rgba(255, 255, 255, 0.16);
|
||||
|
||||
/* Semantic — actions */
|
||||
--color-action-primary: var(--blue-600);
|
||||
--color-action-primary-hover: var(--blue-500);
|
||||
--color-action-primary-fg: var(--gray-0);
|
||||
--color-action-secondary: var(--gray-800);
|
||||
--color-action-secondary-hover: var(--gray-700);
|
||||
|
||||
/* Semantic — feedback */
|
||||
--color-feedback-error: var(--red-500);
|
||||
--color-feedback-success: var(--green-500);
|
||||
--color-feedback-warning: var(--amber-500);
|
||||
|
||||
/* Focus ring — used by *:focus-visible across all interactives */
|
||||
--color-focus-ring: 0 0 0 2px var(--color-action-primary);
|
||||
|
||||
/* Tier badges (pro overlay reads these) */
|
||||
--color-tier-free: var(--gray-500);
|
||||
--color-tier-pro: var(--blue-600);
|
||||
--color-tier-team: var(--amber-500);
|
||||
|
||||
/* Legacy aliases — DO NOT use in new code. Remove after migration. */
|
||||
--accent: var(--color-action-primary);
|
||||
--accent-hover: var(--color-action-primary-hover);
|
||||
--bg: var(--color-bg-canvas);
|
||||
--bg-card: var(--color-bg-surface-1);
|
||||
--bg-elevated: var(--color-bg-surface-3);
|
||||
--bg-input: var(--color-bg-input);
|
||||
--border: var(--color-border-default);
|
||||
--border-hi: var(--color-border-strong);
|
||||
--text: var(--color-fg-default);
|
||||
--text-muted: var(--color-fg-muted);
|
||||
--text-dim: var(--color-fg-subtle);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/* Elevation tokens — 5-step shadow scale.
|
||||
*
|
||||
* shadow-flat — no elevation, used for inline content
|
||||
* shadow-1 — cards at rest (hover/static)
|
||||
* shadow-2 — dropdowns, popovers, raised cards
|
||||
* shadow-3 — modals, sheets
|
||||
* shadow-4 — critical popovers, attention-needed surfaces
|
||||
*
|
||||
* Tuned for dark theme. When light mode lands, override these inside
|
||||
* [data-theme=light] to use lower-opacity black.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--shadow-flat: none;
|
||||
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.30),
|
||||
0 2px 6px rgba(0, 0, 0, 0.20);
|
||||
--shadow-2: 0 2px 8px rgba(0, 0, 0, 0.35),
|
||||
0 8px 16px rgba(0, 0, 0, 0.25);
|
||||
--shadow-3: 0 4px 16px rgba(0, 0, 0, 0.40),
|
||||
0 16px 40px rgba(0, 0, 0, 0.35);
|
||||
--shadow-4: 0 8px 24px rgba(0, 0, 0, 0.45),
|
||||
0 24px 64px rgba(0, 0, 0, 0.50);
|
||||
|
||||
/* Legacy aliases */
|
||||
--shadow: var(--shadow-3);
|
||||
--shadow-sm: var(--shadow-1);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* Design tokens — single import surface for the foundation.
|
||||
*
|
||||
* Import this from src/index.css before any other CSS. Tokens cascade so
|
||||
* order matters: primitives must load before semantics that reference them.
|
||||
*/
|
||||
|
||||
@import './colors.css';
|
||||
@import './spacing.css';
|
||||
@import './typography.css';
|
||||
@import './radius.css';
|
||||
@import './elevation.css';
|
||||
@import './motion.css';
|
||||
@import './z-index.css';
|
||||
|
||||
/* Global focus ring — applied to every interactive element. WCAG 2.4.7 +
|
||||
* Apple HIG focus-state guidance. Override per-component if needed.
|
||||
*/
|
||||
*:focus-visible {
|
||||
box-shadow: var(--color-focus-ring);
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* Motion tokens — durations and easings.
|
||||
*
|
||||
* Three durations for three intents:
|
||||
* fast — micro-interactions (hover, click feedback)
|
||||
* base — standard transitions (color, bg, border)
|
||||
* slow — meaningful state changes (modal open/close, tab switch)
|
||||
*
|
||||
* Easings:
|
||||
* ease-out — entries (modal opening, tooltip showing)
|
||||
* ease-in — exits (modal closing)
|
||||
* ease-in-out — symmetric movements (tab switch, accordion)
|
||||
* spring — drag/drop in canvas, playful bounces
|
||||
*/
|
||||
|
||||
:root {
|
||||
--duration-fast: 120ms;
|
||||
--duration-base: 200ms;
|
||||
--duration-slow: 320ms;
|
||||
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in: cubic-bezier(0.7, 0, 0.84, 0);
|
||||
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
/* Common shortcuts */
|
||||
--transition-color: color var(--duration-base) var(--ease-out),
|
||||
background-color var(--duration-base) var(--ease-out),
|
||||
border-color var(--duration-base) var(--ease-out);
|
||||
--transition-transform: transform var(--duration-base) var(--ease-out);
|
||||
--transition-opacity: opacity var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--duration-fast: 1ms;
|
||||
--duration-base: 1ms;
|
||||
--duration-slow: 1ms;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* Radius tokens — corner radius scale.
|
||||
*
|
||||
* Use --radius-pill for fully-round buttons/badges. Use the others by
|
||||
* matching the component's role:
|
||||
* xs — code chips, tags
|
||||
* sm — inputs, small buttons
|
||||
* md — cards, dropdowns
|
||||
* lg — modals, large surfaces
|
||||
* xl — feature cards, hero panels
|
||||
* 2xl — full-bleed marketing tiles
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius-xs: 4px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
--radius-2xl: 28px;
|
||||
--radius-pill: 9999px;
|
||||
|
||||
/* Legacy aliases */
|
||||
--radius: var(--radius-xl); /* 18px ~ 20px in old system */
|
||||
--radius-pill-old: var(--radius-pill);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* Spacing tokens — 4-pt base scale.
|
||||
*
|
||||
* Use these for padding, margin, gap. Hard-coded pixel values in components
|
||||
* are a smell — replace with the closest token.
|
||||
*
|
||||
* Naming: --space-N where N maps to the rough order of magnitude. Not a
|
||||
* literal multiplier of 4 because the scale uses tighter steps at the small
|
||||
* end where visual differences matter most.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--space-0: 0;
|
||||
--space-1: 2px;
|
||||
--space-2: 4px;
|
||||
--space-3: 6px;
|
||||
--space-4: 8px;
|
||||
--space-5: 12px;
|
||||
--space-6: 16px;
|
||||
--space-7: 20px;
|
||||
--space-8: 24px;
|
||||
--space-9: 32px;
|
||||
--space-10: 40px;
|
||||
--space-11: 48px;
|
||||
--space-12: 64px;
|
||||
--space-13: 80px;
|
||||
--space-14: 96px;
|
||||
--space-15: 128px;
|
||||
--space-16: 160px;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* Typography tokens — type ramp inspired by Apple HIG.
|
||||
*
|
||||
* Self-hosted Inter (variable) + JetBrains Mono (variable) with system-stack
|
||||
* fallbacks. Mac users still get SF Pro because Inter is listed first but
|
||||
* -apple-system catches before Inter ever loads.
|
||||
*
|
||||
* Each --text-* token bundles size + line-height. Use `font: var(--text-body)`
|
||||
* or split with separate properties if needed.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/Inter.var.woff2') format('woff2-variations'),
|
||||
url('/fonts/Inter.var.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
src: url('/fonts/JetBrainsMono.var.woff2') format('woff2-variations'),
|
||||
url('/fonts/JetBrainsMono.var.woff2') format('woff2');
|
||||
font-weight: 100 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text',
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', Consolas,
|
||||
'Liberation Mono', monospace;
|
||||
|
||||
/* Type ramp (size / line-height pairs in the same token) */
|
||||
--text-caption2: 400 11px/13px var(--font-sans);
|
||||
--text-caption1: 400 12px/16px var(--font-sans);
|
||||
--text-footnote: 400 13px/18px var(--font-sans);
|
||||
--text-subheadline: 400 15px/20px var(--font-sans);
|
||||
--text-callout: 400 16px/21px var(--font-sans);
|
||||
--text-body: 400 17px/22px var(--font-sans);
|
||||
--text-headline: 600 17px/22px var(--font-sans);
|
||||
--text-title3: 600 20px/25px var(--font-sans);
|
||||
--text-title2: 600 22px/28px var(--font-sans);
|
||||
--text-title1: 700 28px/34px var(--font-sans);
|
||||
--text-largetitle: 700 34px/41px var(--font-sans);
|
||||
|
||||
/* Letter-spacing tokens for tight headlines */
|
||||
--tracking-tight: -0.02em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-loose: 0.02em;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/* Z-index tokens — explicit stacking layer scale.
|
||||
*
|
||||
* Always use a token, never a magic number. Add a new layer here if needed.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--z-base: 0;
|
||||
--z-dropdown: 10;
|
||||
--z-sticky: 20;
|
||||
--z-overlay: 30;
|
||||
--z-modal: 40;
|
||||
--z-popover: 50;
|
||||
--z-toast: 60;
|
||||
--z-tooltip: 70;
|
||||
}
|
||||