first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
---
|
||||
name: tweaks
|
||||
description: |
|
||||
Wrap any HTML artifact with a side panel of live, parameterized
|
||||
controls — accent color, type scale, density, motion, theme — that
|
||||
rewrite CSS custom properties in real time and persist to
|
||||
localStorage. Lets the user explore variants of a design without
|
||||
re-prompting the agent. Use when the brief asks for "variants",
|
||||
"side-by-side options", "tweak this", "let me adjust", "live
|
||||
knobs", or "实时调参".
|
||||
triggers:
|
||||
- "tweaks"
|
||||
- "variants"
|
||||
- "tweak panel"
|
||||
- "live controls"
|
||||
- "adjust on the fly"
|
||||
- "实时调参"
|
||||
- "可调参数面板"
|
||||
- "side panel"
|
||||
- "knobs"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: design
|
||||
upstream: "https://github.com/alchaincyf/huashu-design"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
example_prompt: "Wrap this landing page with a tweak panel — accent color, type scale, density, light/dark — persist to localStorage so the user can refresh without losing their choice."
|
||||
---
|
||||
|
||||
# Tweaks Skill · 参数化变体面板
|
||||
|
||||
Wrap any HTML artifact with a side panel of live controls that rewrite
|
||||
CSS custom properties in real time and persist to `localStorage`.
|
||||
Inspired by the *huashu-design* tweak pattern.
|
||||
|
||||
## What you produce
|
||||
|
||||
A single self-contained HTML file with two layers:
|
||||
|
||||
1. **Stage** — the original artifact (landing page / deck / dashboard)
|
||||
re-keyed so all visual decisions read from CSS custom properties:
|
||||
`--accent`, `--scale`, `--density`, `--mode`, `--motion`.
|
||||
2. **Panel** — a fixed sidebar (or drawer on small viewports) with
|
||||
form controls bound to those custom properties via a tiny
|
||||
vanilla-JS bridge. Persists every change to `localStorage` keyed
|
||||
by the artifact identifier.
|
||||
|
||||
The user can:
|
||||
|
||||
- Open the artifact and see the stage rendered with their saved
|
||||
preferences (or sensible defaults).
|
||||
- Adjust accent / scale / density / mode / motion in the panel and
|
||||
watch the stage update instantly — no rerender.
|
||||
- Press <kbd>T</kbd> to hide / reveal the panel; <kbd>R</kbd> to
|
||||
reset to defaults.
|
||||
- Refresh the page — every choice is persisted.
|
||||
|
||||
## When to use
|
||||
|
||||
- The user generated something they like 80% of, and wants to dial
|
||||
in the last 20% themselves.
|
||||
- You're presenting a design system / brand and want the audience to
|
||||
feel the variants live (instead of you re-running the agent).
|
||||
- You're shipping a stand-alone demo (e.g. a portfolio piece) and
|
||||
want viewers to play.
|
||||
|
||||
## When *not* to use
|
||||
|
||||
- One-shot artifacts that won't be iterated on (e.g. a runbook —
|
||||
parameters don't help).
|
||||
- When the artifact's value is in fixed ratios (e.g. an infographic
|
||||
with carefully balanced data viz — knobs would degrade it).
|
||||
|
||||
## The 5 standard knobs
|
||||
|
||||
> Pick a subset that suits the artifact. Don't ship all 5 if only 2
|
||||
> matter — clutter is a regression.
|
||||
|
||||
### 1. `--accent` — Accent color
|
||||
|
||||
A select with 5–8 curated swatches (don't ship a free color picker —
|
||||
the user will pick a bad color and blame you).
|
||||
|
||||
```js
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442', label: 'Rust' },
|
||||
{ id: 'cobalt', val: '#2c4d8e', label: 'Cobalt' },
|
||||
{ id: 'sage', val: '#4a7a3f', label: 'Sage' },
|
||||
{ id: 'plum', val: '#7a3f6a', label: 'Plum' },
|
||||
{ id: 'graphite',val: '#3a3a3a', label: 'Graphite' },
|
||||
];
|
||||
```
|
||||
|
||||
The artifact uses `var(--accent)` everywhere it had a hard-coded
|
||||
accent before. Border / link / pull-quote rule / CTA all flip
|
||||
together.
|
||||
|
||||
### 2. `--scale` — Type scale (0.85 / 1.0 / 1.15)
|
||||
|
||||
Three settings: *Compact* (0.85), *Normal* (1.0), *Generous* (1.15).
|
||||
All `font-size` declarations multiply by `var(--scale)` via
|
||||
`calc(... * var(--scale))`.
|
||||
|
||||
Don't go beyond ±15% — beyond that the layout breaks (column flow,
|
||||
breakpoints, line counts).
|
||||
|
||||
### 3. `--density` — Layout density (Tight / Normal / Roomy)
|
||||
|
||||
Three settings that swap the spacing scale: *Tight* (0.75) /
|
||||
*Normal* (1.0) / *Roomy* (1.4). All `padding` / `gap` / `margin`
|
||||
declarations multiply by `var(--density)`.
|
||||
|
||||
This is the highest-impact knob — it's also the most fragile, so
|
||||
**every layout-critical container must declare its base spacing in
|
||||
custom properties** before you wrap.
|
||||
|
||||
### 4. `--mode` — Light / Dark
|
||||
|
||||
A 2-state toggle. Sets `data-mode="light"` vs `"dark"` on the
|
||||
`<html>` element and the artifact's `:root` selector responds with
|
||||
two color sets.
|
||||
|
||||
If the artifact already has a media-query-based dark mode, *replace*
|
||||
it with the data-attr version — the user's choice should win over
|
||||
their OS.
|
||||
|
||||
### 5. `--motion` — Off / Subtle / Lively
|
||||
|
||||
Three settings. Maps to a CSS variable `--motion-mult` that scales
|
||||
all `transition-duration` / `animation-duration` declarations:
|
||||
|
||||
- *Off* — `0s` (also disables WebGL canvases / decorative animation).
|
||||
- *Subtle* — `1.0` (the artifact's authored timing).
|
||||
- *Lively* — `1.6` (slower transitions, more visible motion).
|
||||
|
||||
Respect `prefers-reduced-motion`: default to *Off* if the user has
|
||||
that set, regardless of stored preference.
|
||||
|
||||
## Implementation primitives
|
||||
|
||||
Read `assets/wrap.html` — it ships the panel + bridge as an
|
||||
inert template. Your job is to:
|
||||
|
||||
1. Take the user's existing artifact HTML.
|
||||
2. Lift its accent / mode / spacing / scale into custom properties
|
||||
(search for hard-coded `#hex` / `Npx` / `Nrem` and convert).
|
||||
3. Paste the contents into the marked region of `wrap.html`.
|
||||
4. Edit `assets/wrap.html`'s `KNOBS` array to keep only the knobs
|
||||
you decided are relevant to *this* artifact. Don't ship 5 if 2
|
||||
matter.
|
||||
5. Patch the `STORAGE_KEY` to a unique slug (`tweaks-<artifact-slug>`).
|
||||
|
||||
The bridge in `wrap.html`:
|
||||
- Loads `localStorage[STORAGE_KEY]` JSON on first paint.
|
||||
- Applies values as `document.documentElement.style.setProperty('--accent', ...)`.
|
||||
- Listens to every form control's `change` event and writes back.
|
||||
- Exposes <kbd>T</kbd> (toggle panel) and <kbd>R</kbd> (reset).
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Acquire the artifact
|
||||
|
||||
Same options as the critique skill:
|
||||
|
||||
1. Project file (`index.html` in the project folder).
|
||||
2. Pasted HTML in the chat.
|
||||
3. Generated by you in this turn.
|
||||
|
||||
### Step 2 — Decide which knobs apply
|
||||
|
||||
Read the artifact's CSS first. For each knob, decide *yes / no*:
|
||||
|
||||
- `--accent` — yes if the artifact has 1 accent color used ≥ 3 times.
|
||||
- `--scale` — yes if the artifact is type-driven (article, deck,
|
||||
pricing page).
|
||||
- `--density` — yes if the artifact has consistent gap / padding
|
||||
rhythm (deck, dashboard, landing). No for runbooks (already dense).
|
||||
- `--mode` — yes if the artifact has authored dark mode tokens, or
|
||||
you're willing to derive them.
|
||||
- `--motion` — yes if the artifact has any transition / animation
|
||||
worth scaling. No for static reports / critique reports.
|
||||
|
||||
Default: **3 knobs is the sweet spot.** Five is too busy, one is
|
||||
not worth a panel.
|
||||
|
||||
### Step 3 — Lift hard-coded values into custom properties
|
||||
|
||||
Open `assets/wrap.html`'s `<style>` block — copy its custom-property
|
||||
naming scheme (`--accent`, `--scale`, etc.). In the user's artifact,
|
||||
find every place those concerns live and rewrite:
|
||||
|
||||
- `color: #c96442` → `color: var(--accent)`
|
||||
- `font-size: 18px` → `font-size: calc(18px * var(--scale))`
|
||||
- `padding: 24px 32px` → `padding: calc(24px * var(--density)) calc(32px * var(--density))`
|
||||
- `transition: opacity 200ms` → `transition: opacity calc(200ms * var(--motion-mult))`
|
||||
|
||||
If the artifact uses `clamp()` or `vw` already, multiply the
|
||||
*outer* value by the custom property — don't tear apart `clamp(...)`.
|
||||
|
||||
### Step 4 — Paste into the wrap
|
||||
|
||||
Copy the artifact's `<style>` and `<body>` into the marked regions
|
||||
of `wrap.html`. Keep the panel + bridge intact.
|
||||
|
||||
### Step 5 — Test the loop
|
||||
|
||||
Open the result, click each knob at least once, refresh the page,
|
||||
confirm the choice persists. If a knob breaks the layout —
|
||||
*remove it*, don't ship it.
|
||||
|
||||
## Output contract
|
||||
|
||||
```
|
||||
<artifact identifier="tweaks-<artifact-slug>" type="text/html" title="<Artifact Title> · Tweaks">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
One sentence before the artifact ("Wrapped X with a 3-knob tweak
|
||||
panel — accent / scale / mode."). Stop after `</artifact>`.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Don't ship a free color picker** — only curated swatches. Users
|
||||
pick bad colors when given freedom; saving them from that is the
|
||||
whole point.
|
||||
- **Persist by artifact identifier** — `tweaks-<slug>`, not a global
|
||||
key. Two artifacts open in two tabs must not share state.
|
||||
- **Respect `prefers-reduced-motion`** — default to *Off* for motion
|
||||
if the user has that set, override only on explicit click.
|
||||
- **Single-file** — no external CSS / JS / fonts beyond the artifact's
|
||||
existing imports. Inline the panel + bridge.
|
||||
- **Panel hidden by default on viewports < 720px** — slide-in drawer
|
||||
via a "T" button at top-right.
|
||||
- **Don't ship more than 5 knobs.** Three is the sweet spot.
|
||||
@@ -0,0 +1,437 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-mode="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>[ARTIFACT TITLE] · Tweaks</title>
|
||||
<style>
|
||||
/* =========================================================
|
||||
TWEAKS · WRAP TEMPLATE
|
||||
=========================================================
|
||||
This file is a skeleton. Workflow:
|
||||
1. Replace [ARTIFACT TITLE] in <title>.
|
||||
2. Update STORAGE_KEY in the script (tweaks-<artifact-slug>).
|
||||
3. Decide which knobs apply (1-5 from KNOBS_LIBRARY).
|
||||
4. Paste artifact CSS into the [ARTIFACT_STYLE] region.
|
||||
5. Paste artifact body into the [ARTIFACT_BODY] region.
|
||||
6. Lift hard-coded #hex / Npx / Nrem values to custom
|
||||
properties so the knobs actually move.
|
||||
========================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------- Knob defaults (overridden by JS bridge) ---------- */
|
||||
--accent: #c96442;
|
||||
--scale: 1;
|
||||
--density: 1;
|
||||
--motion-mult: 1;
|
||||
|
||||
/* ---------- Light theme tokens ---------- */
|
||||
--bg: #f6f4ef;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1a1c;
|
||||
--muted: #6b6964;
|
||||
--rule: #e2dfd7;
|
||||
}
|
||||
[data-mode="dark"] {
|
||||
--bg: #0e0d0c;
|
||||
--paper: #181715;
|
||||
--ink: #f4f1ea;
|
||||
--muted: #8a857a;
|
||||
--rule: #2a2723;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root { --motion-mult: 0; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; min-height: 100%; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
font-size: calc(16px * var(--scale));
|
||||
line-height: 1.55;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
[ARTIFACT_STYLE] — paste artifact-specific CSS here
|
||||
========================================================= */
|
||||
|
||||
/* =========================================================
|
||||
PANEL · fixed sidebar with knobs
|
||||
========================================================= */
|
||||
.tw-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 280px;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
transition: transform calc(220ms * var(--motion-mult)) cubic-bezier(.2,.8,.2,1),
|
||||
opacity calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .tw-panel { box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
||||
.tw-panel.tw-hidden {
|
||||
transform: translateX(calc(100% + 32px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tw-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tw-head .ttl {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-head .toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
}
|
||||
.tw-head .toggle:hover { color: var(--ink); }
|
||||
|
||||
.tw-body { padding: 14px 18px 18px; }
|
||||
|
||||
.tw-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
.tw-row:last-child { margin-bottom: 0; }
|
||||
.tw-row .lbl {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Segmented control */
|
||||
.tw-seg {
|
||||
display: flex;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tw-seg button {
|
||||
flex: 1;
|
||||
padding: 7px 8px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--rule);
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
transition: color calc(180ms * var(--motion-mult)) ease,
|
||||
background calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-seg button:first-child { border-left: 0; }
|
||||
.tw-seg button:hover { color: var(--ink); }
|
||||
.tw-seg button[aria-pressed="true"] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
/* Swatch grid */
|
||||
.tw-swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
||||
.tw-swatch {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform calc(160ms * var(--motion-mult)) ease,
|
||||
border-color calc(160ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-swatch:hover { transform: scale(1.06); }
|
||||
.tw-swatch[aria-pressed="true"] { border-color: var(--ink); }
|
||||
|
||||
/* Mini toolbar in panel footer */
|
||||
.tw-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-foot button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.tw-foot button:hover { color: var(--ink); }
|
||||
kbd {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* When panel is hidden, expose a "T" button at top-right */
|
||||
.tw-restore {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-restore:hover { transform: scale(1.06); }
|
||||
.tw-restore.tw-show { display: flex; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tw-panel { left: 16px; right: 16px; width: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- =========================================================
|
||||
[ARTIFACT_BODY] — paste artifact-specific markup here
|
||||
========================================================= -->
|
||||
|
||||
<!-- =========================================================
|
||||
PANEL — knob controls (drop the rows you don't ship)
|
||||
========================================================= -->
|
||||
<aside class="tw-panel" id="tw-panel" aria-label="Tweak panel">
|
||||
<header class="tw-head">
|
||||
<span class="ttl">Tweaks</span>
|
||||
<button class="toggle" id="tw-close" aria-label="Hide panel" title="Hide (T)">×</button>
|
||||
</header>
|
||||
<div class="tw-body">
|
||||
|
||||
<!-- Accent -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Accent</span>
|
||||
<div class="tw-swatches" id="tw-accent" role="radiogroup" aria-label="Accent color"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mode -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Mode</span>
|
||||
<div class="tw-seg" id="tw-mode" role="radiogroup" aria-label="Color mode">
|
||||
<button data-val="light" aria-pressed="true">Light</button>
|
||||
<button data-val="dark" aria-pressed="false">Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scale -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Type scale</span>
|
||||
<div class="tw-seg" id="tw-scale" role="radiogroup" aria-label="Type scale">
|
||||
<button data-val="0.85" aria-pressed="false">Compact</button>
|
||||
<button data-val="1" aria-pressed="true">Normal</button>
|
||||
<button data-val="1.15" aria-pressed="false">Generous</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Density -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Density</span>
|
||||
<div class="tw-seg" id="tw-density" role="radiogroup" aria-label="Density">
|
||||
<button data-val="0.75" aria-pressed="false">Tight</button>
|
||||
<button data-val="1" aria-pressed="true">Normal</button>
|
||||
<button data-val="1.4" aria-pressed="false">Roomy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motion -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Motion</span>
|
||||
<div class="tw-seg" id="tw-motion" role="radiogroup" aria-label="Motion">
|
||||
<button data-val="0" aria-pressed="false">Off</button>
|
||||
<button data-val="1" aria-pressed="true">Subtle</button>
|
||||
<button data-val="1.6" aria-pressed="false">Lively</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="tw-foot">
|
||||
<span><kbd>T</kbd> hide · <kbd>R</kbd> reset</span>
|
||||
<button id="tw-reset" type="button">Reset</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<button class="tw-restore" id="tw-restore" aria-label="Show panel" title="Show panel (T)">T</button>
|
||||
|
||||
<script>
|
||||
// =========================================================
|
||||
// BRIDGE — binds knobs ↔ CSS custom props ↔ localStorage
|
||||
// =========================================================
|
||||
// CHANGE THIS to a unique slug per artifact you wrap.
|
||||
const STORAGE_KEY = 'tweaks-default';
|
||||
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442' },
|
||||
{ id: 'cobalt', val: '#2c4d8e' },
|
||||
{ id: 'sage', val: '#4a7a3f' },
|
||||
{ id: 'plum', val: '#7a3f6a' },
|
||||
{ id: 'graphite', val: '#3a3a3a' },
|
||||
];
|
||||
|
||||
const DEFAULTS = {
|
||||
accent: ACCENT_PRESETS[0].val,
|
||||
mode: 'light',
|
||||
scale: 1,
|
||||
density: 1,
|
||||
motion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 1,
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { ...DEFAULTS };
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch { return { ...DEFAULTS }; }
|
||||
}
|
||||
function save(state) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
|
||||
catch {}
|
||||
}
|
||||
|
||||
function applyState(s) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--accent', s.accent);
|
||||
root.style.setProperty('--scale', s.scale);
|
||||
root.style.setProperty('--density', s.density);
|
||||
root.style.setProperty('--motion-mult', s.motion);
|
||||
root.setAttribute('data-mode', s.mode);
|
||||
// Reflect to UI
|
||||
paintAccent(s.accent);
|
||||
paintSeg('tw-mode', s.mode);
|
||||
paintSeg('tw-scale', String(s.scale));
|
||||
paintSeg('tw-density', String(s.density));
|
||||
paintSeg('tw-motion', String(s.motion));
|
||||
}
|
||||
|
||||
function paintAccent(val) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
if (!host) return;
|
||||
host.querySelectorAll('button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
function paintSeg(id, val) {
|
||||
const host = document.getElementById(id);
|
||||
if (!host) return;
|
||||
host.querySelectorAll('button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
|
||||
function buildAccent(state) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
if (!host) return;
|
||||
host.innerHTML = '';
|
||||
for (const p of ACCENT_PRESETS) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'tw-swatch';
|
||||
b.dataset.val = p.val;
|
||||
b.setAttribute('aria-label', p.id);
|
||||
b.style.background = p.val;
|
||||
b.addEventListener('click', () => {
|
||||
state.accent = p.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
host.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function bindSeg(id, key, parser) {
|
||||
const host = document.getElementById(id);
|
||||
if (!host) return;
|
||||
host.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-val]');
|
||||
if (!btn) return;
|
||||
state[key] = parser ? parser(btn.dataset.val) : btn.dataset.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
}
|
||||
|
||||
const state = load();
|
||||
buildAccent(state);
|
||||
bindSeg('tw-mode', 'mode');
|
||||
bindSeg('tw-scale', 'scale', parseFloat);
|
||||
bindSeg('tw-density', 'density', parseFloat);
|
||||
bindSeg('tw-motion', 'motion', parseFloat);
|
||||
applyState(state);
|
||||
|
||||
// ---- Panel show/hide ----
|
||||
const panel = document.getElementById('tw-panel');
|
||||
const restore = document.getElementById('tw-restore');
|
||||
function setPanelVisible(v) {
|
||||
panel.classList.toggle('tw-hidden', !v);
|
||||
restore.classList.toggle('tw-show', !v);
|
||||
}
|
||||
document.getElementById('tw-close').addEventListener('click', () => setPanelVisible(false));
|
||||
restore.addEventListener('click', () => setPanelVisible(true));
|
||||
|
||||
// ---- Reset ----
|
||||
document.getElementById('tw-reset').addEventListener('click', () => {
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
});
|
||||
|
||||
// ---- Keyboard shortcuts ----
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.matches('input, textarea, select, [contenteditable]')) return;
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
e.preventDefault();
|
||||
setPanelVisible(panel.classList.contains('tw-hidden'));
|
||||
} else if (e.key === 'r' || e.key === 'R') {
|
||||
e.preventDefault();
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,750 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-mode="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Filebase · Tweaks demo</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #c96442;
|
||||
--scale: 1;
|
||||
--density: 1;
|
||||
--motion-mult: 1;
|
||||
|
||||
--bg: #f6f4ef;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1a1c;
|
||||
--muted: #6b6964;
|
||||
--rule: #e2dfd7;
|
||||
|
||||
--serif: 'Source Serif 4', Georgia, serif;
|
||||
--sans: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
[data-mode="dark"] {
|
||||
--bg: #0e0d0c;
|
||||
--paper: #181715;
|
||||
--ink: #f4f1ea;
|
||||
--muted: #8a857a;
|
||||
--rule: #2a2723;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root { --motion-mult: 0; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; min-height: 100%; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: calc(16px * var(--scale));
|
||||
line-height: 1.55;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
|
||||
/* ============ Layout ============ */
|
||||
.wrap {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: calc(28px * var(--density)) calc(40px * var(--density));
|
||||
}
|
||||
|
||||
/* ============ Header / nav ============ */
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: calc(20px * var(--density)) 0;
|
||||
gap: calc(32px * var(--density));
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(20px * var(--scale));
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
transition: background calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: calc(28px * var(--density));
|
||||
font-size: calc(14px * var(--scale));
|
||||
color: var(--muted);
|
||||
}
|
||||
.nav-links a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.nav-links a:hover { color: var(--ink); }
|
||||
.cta {
|
||||
display: inline-block;
|
||||
padding: calc(10px * var(--density)) calc(18px * var(--density));
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: calc(13px * var(--scale));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-decoration: none;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
transform calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.cta:hover { transform: translateY(-1px); }
|
||||
|
||||
/* ============ Hero ============ */
|
||||
.hero {
|
||||
padding: calc(96px * var(--density)) 0 calc(80px * var(--density));
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: calc(64px * var(--density));
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.hero { grid-template-columns: 1fr; }
|
||||
}
|
||||
.eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: calc(11px * var(--scale));
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: calc(22px * var(--density));
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.h1 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(58px * var(--scale));
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 calc(22px * var(--density));
|
||||
}
|
||||
.h1 em {
|
||||
font-style: italic;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.lede {
|
||||
font-size: calc(19px * var(--scale));
|
||||
color: var(--muted);
|
||||
max-width: 38ch;
|
||||
margin: 0 0 calc(36px * var(--density));
|
||||
line-height: 1.5;
|
||||
}
|
||||
.row { display: flex; gap: calc(14px * var(--density)); align-items: center; flex-wrap: wrap; }
|
||||
.secondary {
|
||||
font-size: calc(13px * var(--scale));
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Hero card preview */
|
||||
.hero-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 10px;
|
||||
padding: calc(20px * var(--density));
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.06);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(12px * var(--scale));
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
border-color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .hero-card { box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
|
||||
.hero-card .label {
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: calc(12px * var(--density));
|
||||
font-size: calc(10px * var(--scale));
|
||||
}
|
||||
.hero-card pre {
|
||||
margin: 0;
|
||||
padding: calc(14px * var(--density));
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(12px * var(--scale));
|
||||
line-height: 1.55;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.hero-card .k { color: var(--accent); }
|
||||
.hero-card .c { color: var(--muted); }
|
||||
.hero-card .ok { color: #4a7a3f; }
|
||||
[data-mode="dark"] .hero-card .ok { color: #8db876; }
|
||||
|
||||
/* ============ Stats strip ============ */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: calc(32px * var(--density));
|
||||
padding: calc(48px * var(--density)) 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(6px * var(--density));
|
||||
}
|
||||
.stat .num {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(40px * var(--scale));
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--accent);
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.stat .lbl {
|
||||
font-size: calc(13px * var(--scale));
|
||||
color: var(--muted);
|
||||
max-width: 22ch;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ============ Features grid ============ */
|
||||
.features {
|
||||
padding: calc(80px * var(--density)) 0 calc(40px * var(--density));
|
||||
}
|
||||
.section-title {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(34px * var(--scale));
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 0 calc(48px * var(--density));
|
||||
max-width: 22ch;
|
||||
}
|
||||
.feat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: calc(28px * var(--density));
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.feat-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.feat {
|
||||
padding: calc(28px * var(--density));
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 10px;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
border-color calc(220ms * var(--motion-mult)) ease,
|
||||
transform calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.feat:hover { transform: translateY(-2px); }
|
||||
.feat .ico {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklch, var(--accent) 18%, transparent);
|
||||
color: var(--accent);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(15px * var(--scale));
|
||||
margin-bottom: calc(20px * var(--density));
|
||||
}
|
||||
.feat h3 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(20px * var(--scale));
|
||||
margin: 0 0 calc(8px * var(--density));
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.feat p {
|
||||
color: var(--muted);
|
||||
font-size: calc(15px * var(--scale));
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ============ CTA banner ============ */
|
||||
.banner {
|
||||
margin: calc(60px * var(--density)) 0 calc(40px * var(--density));
|
||||
padding: calc(56px * var(--density)) calc(48px * var(--density));
|
||||
background: var(--ink);
|
||||
color: var(--bg);
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: calc(40px * var(--density));
|
||||
align-items: center;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.banner { grid-template-columns: 1fr; }
|
||||
}
|
||||
.banner h2 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(32px * var(--scale));
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 calc(8px * var(--density));
|
||||
max-width: 22ch;
|
||||
}
|
||||
.banner p {
|
||||
color: rgba(244,241,234,0.68);
|
||||
font-size: calc(15px * var(--scale));
|
||||
margin: 0;
|
||||
}
|
||||
[data-mode="dark"] .banner p { color: rgba(26,26,28,0.68); }
|
||||
|
||||
.ft {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: calc(28px * var(--density)) 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(11px * var(--scale));
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
PANEL · same primitives as assets/wrap.html
|
||||
========================================================= */
|
||||
.tw-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 280px;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
|
||||
font-family: var(--sans);
|
||||
transition: transform calc(220ms * var(--motion-mult)) cubic-bezier(.2,.8,.2,1),
|
||||
opacity calc(220ms * var(--motion-mult)) ease,
|
||||
background calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .tw-panel { box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
||||
.tw-panel.tw-hidden {
|
||||
transform: translateX(calc(100% + 32px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tw-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tw-head .ttl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-head .toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
}
|
||||
.tw-head .toggle:hover { color: var(--ink); }
|
||||
.tw-body { padding: 14px 18px 18px; }
|
||||
.tw-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
.tw-row:last-child { margin-bottom: 0; }
|
||||
.tw-row .lbl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-seg {
|
||||
display: flex;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tw-seg button {
|
||||
flex: 1;
|
||||
padding: 7px 8px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--rule);
|
||||
cursor: pointer;
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
transition: color calc(180ms * var(--motion-mult)) ease,
|
||||
background calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-seg button:first-child { border-left: 0; }
|
||||
.tw-seg button:hover { color: var(--ink); }
|
||||
.tw-seg button[aria-pressed='true'] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
.tw-swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
||||
.tw-swatch {
|
||||
width: 100%; aspect-ratio: 1;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform calc(160ms * var(--motion-mult)) ease,
|
||||
border-color calc(160ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-swatch:hover { transform: scale(1.06); }
|
||||
.tw-swatch[aria-pressed='true'] { border-color: var(--ink); }
|
||||
.tw-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-foot button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.tw-foot button:hover { color: var(--ink); }
|
||||
kbd {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.tw-restore {
|
||||
position: fixed;
|
||||
top: 16px; right: 16px;
|
||||
z-index: 100;
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-restore:hover { transform: scale(1.06); }
|
||||
.tw-restore.tw-show { display: flex; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tw-panel { left: 16px; right: 16px; width: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<!-- Nav -->
|
||||
<nav class="nav">
|
||||
<div class="brand">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
<span>Filebase</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#">Product</a>
|
||||
<a href="#">Pricing</a>
|
||||
<a href="#">Docs</a>
|
||||
<a href="#">Changelog</a>
|
||||
<a href="#">Customers</a>
|
||||
</div>
|
||||
<a class="cta" href="#">Start free</a>
|
||||
</nav>
|
||||
|
||||
<!-- Hero -->
|
||||
<header class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Series B · 2026</div>
|
||||
<h1 class="h1">The bandwidth bill is the <em>bug</em>.</h1>
|
||||
<p class="lede">A sync engine that ships only what changed. 38× less data over the wire on real customer workloads.</p>
|
||||
<div class="row">
|
||||
<a class="cta" href="#">Start free</a>
|
||||
<span class="secondary">no card required</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-card">
|
||||
<div class="label">filebase sync — typical run</div>
|
||||
<pre><span class="c">// hourly cron · feature/render-pass branch</span>
|
||||
$ filebase sync --watch
|
||||
<span class="k">→</span> diff: <span class="ok">12.4 MB</span> <span class="c">(of 4.7 GB)</span>
|
||||
<span class="k">→</span> upload: <span class="ok">8.2 MB</span> <span class="c">(deduplicated)</span>
|
||||
<span class="k">→</span> latency: <span class="ok">340 ms</span> <span class="c">p99</span>
|
||||
<span class="ok">✓ done in 2.1s</span></pre>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Stats -->
|
||||
<section class="stats">
|
||||
<div class="stat">
|
||||
<div class="num">38×</div>
|
||||
<div class="lbl">less data moved over the wire vs naive sync</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">3,184</div>
|
||||
<div class="lbl">paying teams across post-production, design, ML</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">99.99%</div>
|
||||
<div class="lbl">uptime over the last 12 rolling months</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">$2.1M</div>
|
||||
<div class="lbl">aggregate egress savings reported by Q1 cohort</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="features">
|
||||
<h2 class="section-title">Three reasons teams switch in the first month.</h2>
|
||||
<div class="feat-grid">
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">∂</div>
|
||||
<h3>Block-level diffs</h3>
|
||||
<p>Edit one frame in a 4 GB Final Cut project; sync 12 MB. The diff doesn't care how big your file is — it cares what changed.</p>
|
||||
</article>
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">≈</div>
|
||||
<h3>Cross-region dedup</h3>
|
||||
<p>Your team in Berlin uploads a checkpoint your team in Tokyo already pushed. We notice. Nothing transfers.</p>
|
||||
</article>
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">∇</div>
|
||||
<h3>Drop-in for S3 / GCS</h3>
|
||||
<p>Wire one env var, change zero application code. Existing buckets, existing IAM, new bandwidth bill.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA banner -->
|
||||
<section class="banner">
|
||||
<div>
|
||||
<h2>Pay for storage. Stop paying for movement.</h2>
|
||||
<p>Start free, no card required. Production teams in 14 days or fewer.</p>
|
||||
</div>
|
||||
<a class="cta" href="#" style="background: var(--accent); color: #fff;">Book a demo</a>
|
||||
</section>
|
||||
|
||||
<footer class="ft">
|
||||
<span>© 2026 Filebase, Inc.</span>
|
||||
<span>Privacy · Terms · Status</span>
|
||||
<span>built with the OD tweaks skill</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- ============ Tweak panel ============ -->
|
||||
<aside class="tw-panel" id="tw-panel" aria-label="Tweak panel">
|
||||
<header class="tw-head">
|
||||
<span class="ttl">Tweaks · Filebase</span>
|
||||
<button class="toggle" id="tw-close" aria-label="Hide panel" title="Hide (T)">×</button>
|
||||
</header>
|
||||
<div class="tw-body">
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Accent</span>
|
||||
<div class="tw-swatches" id="tw-accent" role="radiogroup" aria-label="Accent color"></div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Mode</span>
|
||||
<div class="tw-seg" id="tw-mode" role="radiogroup" aria-label="Color mode">
|
||||
<button data-val="light" aria-pressed="true">Light</button>
|
||||
<button data-val="dark" aria-pressed="false">Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Type scale</span>
|
||||
<div class="tw-seg" id="tw-scale" role="radiogroup" aria-label="Type scale">
|
||||
<button data-val="0.85" aria-pressed="false">Compact</button>
|
||||
<button data-val="1" aria-pressed="true">Normal</button>
|
||||
<button data-val="1.15" aria-pressed="false">Generous</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Density</span>
|
||||
<div class="tw-seg" id="tw-density" role="radiogroup" aria-label="Density">
|
||||
<button data-val="0.75" aria-pressed="false">Tight</button>
|
||||
<button data-val="1" aria-pressed="true">Normal</button>
|
||||
<button data-val="1.4" aria-pressed="false">Roomy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">Motion</span>
|
||||
<div class="tw-seg" id="tw-motion" role="radiogroup" aria-label="Motion">
|
||||
<button data-val="0" aria-pressed="false">Off</button>
|
||||
<button data-val="1" aria-pressed="true">Subtle</button>
|
||||
<button data-val="1.6" aria-pressed="false">Lively</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="tw-foot">
|
||||
<span><kbd>T</kbd> hide · <kbd>R</kbd> reset</span>
|
||||
<button id="tw-reset" type="button">Reset</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<button class="tw-restore" id="tw-restore" aria-label="Show panel" title="Show panel (T)">T</button>
|
||||
|
||||
<script>
|
||||
const STORAGE_KEY = 'tweaks-filebase-example';
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442' },
|
||||
{ id: 'cobalt', val: '#2c4d8e' },
|
||||
{ id: 'sage', val: '#4a7a3f' },
|
||||
{ id: 'plum', val: '#7a3f6a' },
|
||||
{ id: 'graphite', val: '#3a3a3a' },
|
||||
];
|
||||
const DEFAULTS = {
|
||||
accent: ACCENT_PRESETS[0].val,
|
||||
mode: 'light',
|
||||
scale: 1,
|
||||
density: 1,
|
||||
motion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 1,
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { ...DEFAULTS };
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch { return { ...DEFAULTS }; }
|
||||
}
|
||||
function save(s) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch {}
|
||||
}
|
||||
|
||||
function applyState(s) {
|
||||
const r = document.documentElement;
|
||||
r.style.setProperty('--accent', s.accent);
|
||||
r.style.setProperty('--scale', s.scale);
|
||||
r.style.setProperty('--density', s.density);
|
||||
r.style.setProperty('--motion-mult', s.motion);
|
||||
r.setAttribute('data-mode', s.mode);
|
||||
paintAccent(s.accent);
|
||||
paintSeg('tw-mode', s.mode);
|
||||
paintSeg('tw-scale', String(s.scale));
|
||||
paintSeg('tw-density', String(s.density));
|
||||
paintSeg('tw-motion', String(s.motion));
|
||||
}
|
||||
function paintAccent(val) {
|
||||
document.querySelectorAll('#tw-accent button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
function paintSeg(id, val) {
|
||||
document.querySelectorAll('#' + id + ' button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
|
||||
function buildAccent(state) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
host.innerHTML = '';
|
||||
for (const p of ACCENT_PRESETS) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'tw-swatch';
|
||||
b.dataset.val = p.val;
|
||||
b.setAttribute('aria-label', p.id);
|
||||
b.style.background = p.val;
|
||||
b.addEventListener('click', () => {
|
||||
state.accent = p.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
host.appendChild(b);
|
||||
}
|
||||
}
|
||||
function bindSeg(id, key, parser) {
|
||||
document.getElementById(id).addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-val]');
|
||||
if (!btn) return;
|
||||
state[key] = parser ? parser(btn.dataset.val) : btn.dataset.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
}
|
||||
|
||||
const state = load();
|
||||
buildAccent(state);
|
||||
bindSeg('tw-mode', 'mode');
|
||||
bindSeg('tw-scale', 'scale', parseFloat);
|
||||
bindSeg('tw-density', 'density', parseFloat);
|
||||
bindSeg('tw-motion', 'motion', parseFloat);
|
||||
applyState(state);
|
||||
|
||||
const panel = document.getElementById('tw-panel');
|
||||
const restore = document.getElementById('tw-restore');
|
||||
function setPanelVisible(v) {
|
||||
panel.classList.toggle('tw-hidden', !v);
|
||||
restore.classList.toggle('tw-show', !v);
|
||||
}
|
||||
document.getElementById('tw-close').addEventListener('click', () => setPanelVisible(false));
|
||||
restore.addEventListener('click', () => setPanelVisible(true));
|
||||
document.getElementById('tw-reset').addEventListener('click', () => {
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
});
|
||||
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.matches('input, textarea, select, [contenteditable]')) return;
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
e.preventDefault();
|
||||
setPanelVisible(panel.classList.contains('tw-hidden'));
|
||||
} else if (e.key === 'r' || e.key === 'R') {
|
||||
e.preventDefault();
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user