Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- README.md +87 -5
- app.css +498 -0
- app.js +507 -0
- assets/fonts/NordwandMono-Bold.woff2 +0 -0
- assets/fonts/NordwandMono-Medium.woff2 +0 -0
- assets/fonts/NordwandMono-Regular.woff2 +0 -0
- assets/icons/remixicon.css +0 -0
- assets/icons/remixicon.woff2 +3 -0
- index.html +157 -17
- styles.css +10 -0
- tokens/base.css +13 -0
- tokens/colors.css +34 -0
- tokens/fonts.css +5 -0
- tokens/motion.css +21 -0
- tokens/radius-shadow.css +26 -0
- tokens/spacing.css +10 -0
- tokens/theme-light.css +52 -0
- tokens/type-nordwand.css +23 -0
- tokens/typography.css +22 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/icons/remixicon.woff2 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,10 +1,92 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: html studio
|
| 3 |
+
emoji: ⬛
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
+
short_description: Describe a site, any model writes it, ship it to harvis.dev
|
| 9 |
+
hf_oauth: true
|
| 10 |
+
hf_oauth_scopes:
|
| 11 |
+
- inference-api
|
| 12 |
+
hf_oauth_expiration_minutes: 480
|
| 13 |
pinned: false
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# html studio
|
| 17 |
+
|
| 18 |
+
Describe a site. Any open-weights model on the Hugging Face Inference Providers
|
| 19 |
+
router writes it as one self-contained HTML file. Preview it, edit it, then
|
| 20 |
+
deploy it to [harvis.dev](https://harvis.dev) and get an immutable live URL in
|
| 21 |
+
about two seconds.
|
| 22 |
+
|
| 23 |
+
## How it works
|
| 24 |
+
|
| 25 |
+
There is no server. This is a Static Space — everything runs in your browser,
|
| 26 |
+
against two CORS-open APIs.
|
| 27 |
+
|
| 28 |
+
1. **Pick a model.** The list is fetched live from
|
| 29 |
+
`https://router.huggingface.co/v1/models`, so every open-weights chat model
|
| 30 |
+
the router serves is selectable.
|
| 31 |
+
2. **Describe the site.** The model streams back a complete `<!doctype html>`
|
| 32 |
+
document with all CSS and JS inlined. The preview renders as it arrives.
|
| 33 |
+
3. **Edit if you want.** The code pane is a live editor — edits re-render and
|
| 34 |
+
are what gets deployed.
|
| 35 |
+
4. **Deploy.** The document is posted to `https://harvis.dev/api/upload`.
|
| 36 |
+
|
| 37 |
+
Deploying again reuses the site's deploy token, so it updates the same URL in
|
| 38 |
+
place rather than creating a new site. Press **New site** to start a fresh one.
|
| 39 |
+
|
| 40 |
+
## Inference credits
|
| 41 |
+
|
| 42 |
+
Sign in with Hugging Face to generate against your own free monthly inference
|
| 43 |
+
credits — the OAuth token is requested with the `inference-api` scope. You can
|
| 44 |
+
paste a token from
|
| 45 |
+
[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 46 |
+
instead. Nothing leaves the browser except the request to the HF router; the
|
| 47 |
+
token is kept in `localStorage`.
|
| 48 |
+
|
| 49 |
+
## Deploy links
|
| 50 |
+
|
| 51 |
+
Every deploy returns two links.
|
| 52 |
+
|
| 53 |
+
- **Live URL** — public. Share it.
|
| 54 |
+
- **Claim link** — private. Open it and sign in to attach the site to a
|
| 55 |
+
harvis.dev account. It cannot be recovered if lost, and an unclaimed site
|
| 56 |
+
expires 24 hours after its last deploy.
|
| 57 |
+
|
| 58 |
+
## Branding
|
| 59 |
+
|
| 60 |
+
The interface uses the harvis.dev design system, vendored here so the Space is
|
| 61 |
+
self-contained and works offline:
|
| 62 |
+
|
| 63 |
+
| Path | What it is |
|
| 64 |
+
|---|---|
|
| 65 |
+
| `styles.css` | Design-system entry point — the `@import` list |
|
| 66 |
+
| `tokens/` | Colour, type, spacing, radius/shadow, motion, base reset, light theme |
|
| 67 |
+
| `assets/fonts/` | Nordwand Mono — the `[#]` logomark face |
|
| 68 |
+
| `assets/icons/` | Remix Icon v4.9.1 webfont, `-line` style |
|
| 69 |
+
| `app.css` | Application layer — every value resolves to a token |
|
| 70 |
+
|
| 71 |
+
Square corners, 9px crop-mark corner brackets, hard offset shadows with zero
|
| 72 |
+
blur, one accent (Flare `#FF4D17`), mono for every machine-generated value, and
|
| 73 |
+
no emoji anywhere in the interface. The contrast control in the nav toggles
|
| 74 |
+
`data-theme="paper"`, which inverts the whole app to the warm paper canvas with
|
| 75 |
+
no light-specific CSS branch.
|
| 76 |
+
|
| 77 |
+
## Notes
|
| 78 |
+
|
| 79 |
+
- The preview iframe is sandboxed without `allow-same-origin`, so origin-bound
|
| 80 |
+
APIs (`localStorage`, cookies) throw inside the preview. They work on the
|
| 81 |
+
deployed site.
|
| 82 |
+
- harvis.dev limits: 50 MB and 500 files per site, static files only.
|
| 83 |
+
|
| 84 |
+
## Run it locally
|
| 85 |
+
|
| 86 |
+
No build step.
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
python3 -m http.server 8000
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
OAuth only works when served as a Space; locally, paste a token instead.
|
app.css
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* html studio — application layer.
|
| 2 |
+
Every value below resolves to a design-system token. Nothing hardcodes a hex
|
| 3 |
+
except where a token does not exist, so `[data-theme="paper"]` inverts the
|
| 4 |
+
whole app with no light-specific branch. */
|
| 5 |
+
|
| 6 |
+
/* Several rules here set `display`, which would otherwise beat the UA's
|
| 7 |
+
`[hidden] { display:none }`. Keep the attribute authoritative. */
|
| 8 |
+
[hidden] { display: none !important; }
|
| 9 |
+
|
| 10 |
+
html, body { height: 100%; }
|
| 11 |
+
body {
|
| 12 |
+
margin: 0;
|
| 13 |
+
display: flex;
|
| 14 |
+
flex-direction: column;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
font: var(--type-ui);
|
| 17 |
+
letter-spacing: var(--tracking-body);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* ── corner brackets ─────────────────────────────────────────────
|
| 21 |
+
The edge signature: 9px crop marks sitting 1px outside the border,
|
| 22 |
+
echoing the brackets of the [#] logomark. */
|
| 23 |
+
.corners { position: absolute; inset: 0; pointer-events: none; }
|
| 24 |
+
.corners i {
|
| 25 |
+
position: absolute;
|
| 26 |
+
width: var(--corner-arm); height: var(--corner-arm);
|
| 27 |
+
border: 0 solid var(--corner-color);
|
| 28 |
+
transition: var(--t-color);
|
| 29 |
+
}
|
| 30 |
+
.corners i:nth-child(1) { top: var(--corner-inset); left: var(--corner-inset); border-top-width: var(--corner-weight); border-left-width: var(--corner-weight); }
|
| 31 |
+
.corners i:nth-child(2) { top: var(--corner-inset); right: var(--corner-inset); border-top-width: var(--corner-weight); border-right-width: var(--corner-weight); }
|
| 32 |
+
.corners i:nth-child(3) { bottom: var(--corner-inset); left: var(--corner-inset); border-bottom-width: var(--corner-weight); border-left-width: var(--corner-weight); }
|
| 33 |
+
.corners i:nth-child(4) { bottom: var(--corner-inset); right: var(--corner-inset); border-bottom-width: var(--corner-weight); border-right-width: var(--corner-weight); }
|
| 34 |
+
|
| 35 |
+
/* ── nav ─────────────────────────────────────────────────────────── */
|
| 36 |
+
.nav {
|
| 37 |
+
position: relative;
|
| 38 |
+
z-index: 3;
|
| 39 |
+
flex: none;
|
| 40 |
+
height: var(--nav-h);
|
| 41 |
+
display: flex;
|
| 42 |
+
align-items: center;
|
| 43 |
+
gap: var(--space-3);
|
| 44 |
+
padding: 0 var(--space-4);
|
| 45 |
+
border-bottom: 1px solid var(--border-hairline);
|
| 46 |
+
background: var(--surface-nav);
|
| 47 |
+
backdrop-filter: blur(12px);
|
| 48 |
+
}
|
| 49 |
+
.nav-spacer { flex: 1; }
|
| 50 |
+
.nav-div { width: 1px; height: 18px; background: var(--border-default); }
|
| 51 |
+
.nav-title {
|
| 52 |
+
font: var(--type-label);
|
| 53 |
+
letter-spacing: var(--tracking-label);
|
| 54 |
+
text-transform: uppercase;
|
| 55 |
+
color: var(--text-muted);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.lockup { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; }
|
| 59 |
+
.mark {
|
| 60 |
+
font-family: var(--font-logomark);
|
| 61 |
+
font-weight: 700;
|
| 62 |
+
font-size: 19px;
|
| 63 |
+
line-height: 1;
|
| 64 |
+
letter-spacing: -0.06em;
|
| 65 |
+
margin-right: -0.06em;
|
| 66 |
+
color: var(--accent);
|
| 67 |
+
}
|
| 68 |
+
.wordmark {
|
| 69 |
+
font: var(--weight-semibold) 19px/1 var(--font-sans);
|
| 70 |
+
letter-spacing: -0.035em;
|
| 71 |
+
color: var(--text-primary);
|
| 72 |
+
}
|
| 73 |
+
.lockup:hover .wordmark { color: var(--text-primary); }
|
| 74 |
+
|
| 75 |
+
.auth { display: flex; align-items: center; gap: var(--space-2); }
|
| 76 |
+
.whoami { display: flex; align-items: center; gap: var(--space-2); }
|
| 77 |
+
.whoami img { border-radius: 0; background: var(--bg-raised); }
|
| 78 |
+
#username {
|
| 79 |
+
max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 80 |
+
color: var(--text-secondary);
|
| 81 |
+
}
|
| 82 |
+
.mono { font: var(--weight-medium) var(--text-xs)/1 var(--font-mono); }
|
| 83 |
+
|
| 84 |
+
/* ── buttons ─────────────────────────────────────────────────────── */
|
| 85 |
+
.btn {
|
| 86 |
+
display: inline-flex;
|
| 87 |
+
align-items: center;
|
| 88 |
+
justify-content: center;
|
| 89 |
+
gap: var(--space-2);
|
| 90 |
+
height: var(--control-h);
|
| 91 |
+
padding: 0 14px;
|
| 92 |
+
border: 1px solid transparent;
|
| 93 |
+
border-radius: var(--radius-control);
|
| 94 |
+
font: var(--weight-medium) var(--text-sm)/1 var(--font-sans);
|
| 95 |
+
letter-spacing: var(--tracking-body);
|
| 96 |
+
white-space: nowrap;
|
| 97 |
+
cursor: pointer;
|
| 98 |
+
transition: var(--t-control);
|
| 99 |
+
background: transparent;
|
| 100 |
+
color: var(--text-primary);
|
| 101 |
+
}
|
| 102 |
+
.btn i { font-size: 15px; line-height: 1; }
|
| 103 |
+
.btn-sm { height: var(--control-h-sm); padding: 0 10px; font-size: var(--text-xs); gap: 6px; }
|
| 104 |
+
.btn-sm i { font-size: 14px; }
|
| 105 |
+
.btn-lg { height: var(--control-h-lg); padding: 0 20px; font-size: var(--text-md); }
|
| 106 |
+
.btn-full { flex: 1; }
|
| 107 |
+
.btn-icon { padding: 0; width: var(--control-h-sm); }
|
| 108 |
+
.btn-icon i { font-size: 15px; }
|
| 109 |
+
|
| 110 |
+
/* Press moves the control into its own hard shadow. No scale, no ripple. */
|
| 111 |
+
.btn:active:not(:disabled) { transform: translate(1px, 1px); }
|
| 112 |
+
.btn:disabled { opacity: .42; cursor: not-allowed; }
|
| 113 |
+
.btn:focus-visible { outline: none; box-shadow: var(--ring-focus); }
|
| 114 |
+
|
| 115 |
+
.btn-primary { background: var(--accent); color: var(--accent-fg); box-shadow: var(--shadow-inset-top); }
|
| 116 |
+
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
|
| 117 |
+
.btn-primary:active:not(:disabled) { background: var(--accent-press); }
|
| 118 |
+
|
| 119 |
+
.btn-secondary { background: var(--bg-raised); border-color: var(--border-default); }
|
| 120 |
+
.btn-secondary:hover:not(:disabled) { background: var(--ink-700); }
|
| 121 |
+
|
| 122 |
+
.btn-ghost { color: var(--text-secondary); }
|
| 123 |
+
.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
|
| 124 |
+
|
| 125 |
+
.btn-outline { border-color: var(--border-strong); }
|
| 126 |
+
.btn-outline:hover:not(:disabled) { background: var(--accent-quiet); border-color: var(--flare-a40); color: var(--text-accent); }
|
| 127 |
+
|
| 128 |
+
/* ── token row ───────────────────────────────────────────────────── */
|
| 129 |
+
.tokenrow {
|
| 130 |
+
position: relative;
|
| 131 |
+
z-index: 2;
|
| 132 |
+
flex: none;
|
| 133 |
+
display: flex;
|
| 134 |
+
align-items: center;
|
| 135 |
+
gap: var(--space-3);
|
| 136 |
+
padding: var(--space-3) var(--space-4);
|
| 137 |
+
border-bottom: 1px solid var(--border-hairline);
|
| 138 |
+
background: var(--bg-surface);
|
| 139 |
+
flex-wrap: wrap;
|
| 140 |
+
}
|
| 141 |
+
.tokenrow label {
|
| 142 |
+
font: var(--type-label);
|
| 143 |
+
letter-spacing: var(--tracking-label);
|
| 144 |
+
text-transform: uppercase;
|
| 145 |
+
color: var(--text-muted);
|
| 146 |
+
}
|
| 147 |
+
.inputwrap {
|
| 148 |
+
flex: 1 1 280px;
|
| 149 |
+
min-width: 0;
|
| 150 |
+
display: flex;
|
| 151 |
+
align-items: center;
|
| 152 |
+
gap: var(--space-2);
|
| 153 |
+
height: var(--control-h);
|
| 154 |
+
padding: 0 var(--space-3);
|
| 155 |
+
background: var(--bg-inset);
|
| 156 |
+
border: 1px solid var(--border-default);
|
| 157 |
+
border-radius: var(--radius-control);
|
| 158 |
+
box-shadow: var(--shadow-inset-well);
|
| 159 |
+
transition: var(--t-control);
|
| 160 |
+
}
|
| 161 |
+
.inputwrap:focus-within { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--flare-a24); }
|
| 162 |
+
.inputwrap i { color: var(--text-faint); font-size: 14px; }
|
| 163 |
+
.inputwrap input {
|
| 164 |
+
flex: 1; min-width: 0;
|
| 165 |
+
background: transparent; border: 0; outline: none;
|
| 166 |
+
color: var(--text-primary);
|
| 167 |
+
font: var(--weight-regular) var(--text-xs)/1 var(--font-mono);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* ── layout ──────────────────────────────────────────────────────── */
|
| 171 |
+
.layout {
|
| 172 |
+
position: relative;
|
| 173 |
+
flex: 1;
|
| 174 |
+
min-height: 0;
|
| 175 |
+
display: grid;
|
| 176 |
+
grid-template-columns: var(--sidebar-w-studio, 380px) 1fr;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.rail {
|
| 180 |
+
display: flex;
|
| 181 |
+
flex-direction: column;
|
| 182 |
+
gap: var(--space-5);
|
| 183 |
+
padding: var(--space-5);
|
| 184 |
+
border-right: 1px solid var(--border-hairline);
|
| 185 |
+
background: var(--bg-canvas);
|
| 186 |
+
overflow-y: auto;
|
| 187 |
+
min-height: 0;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.output { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg-canvas); }
|
| 191 |
+
|
| 192 |
+
/* ── fields ──────────────────────────────────────────────────────── */
|
| 193 |
+
.field { display: flex; flex-direction: column; gap: 6px; min-height: 0; }
|
| 194 |
+
.field-grow { flex: 1; min-height: 140px; }
|
| 195 |
+
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); align-items: end; }
|
| 196 |
+
|
| 197 |
+
label {
|
| 198 |
+
font: var(--type-label);
|
| 199 |
+
letter-spacing: var(--tracking-label);
|
| 200 |
+
text-transform: uppercase;
|
| 201 |
+
color: var(--text-muted);
|
| 202 |
+
}
|
| 203 |
+
.mono-val {
|
| 204 |
+
font: var(--weight-medium) var(--text-2xs)/1 var(--font-mono);
|
| 205 |
+
letter-spacing: var(--tracking-mono);
|
| 206 |
+
text-transform: none;
|
| 207 |
+
color: var(--text-secondary);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
select, textarea, input[type=number] {
|
| 211 |
+
width: 100%;
|
| 212 |
+
background: var(--bg-inset);
|
| 213 |
+
color: var(--text-primary);
|
| 214 |
+
border: 1px solid var(--border-default);
|
| 215 |
+
border-radius: var(--radius-control);
|
| 216 |
+
padding: 9px var(--space-3);
|
| 217 |
+
font: var(--weight-regular) var(--text-sm)/1.5 var(--font-sans);
|
| 218 |
+
box-shadow: var(--shadow-inset-well);
|
| 219 |
+
transition: var(--t-control);
|
| 220 |
+
}
|
| 221 |
+
select:focus, textarea:focus, input[type=number]:focus {
|
| 222 |
+
outline: none;
|
| 223 |
+
border-color: var(--border-focus);
|
| 224 |
+
box-shadow: 0 0 0 3px var(--flare-a24);
|
| 225 |
+
}
|
| 226 |
+
textarea { resize: none; }
|
| 227 |
+
#prompt { flex: 1; min-height: 120px; line-height: var(--leading-normal); }
|
| 228 |
+
#system { font: var(--type-code); resize: vertical; }
|
| 229 |
+
input[type=number] {
|
| 230 |
+
height: var(--control-h);
|
| 231 |
+
padding: 0 var(--space-3);
|
| 232 |
+
line-height: 1;
|
| 233 |
+
font-family: var(--font-mono);
|
| 234 |
+
font-size: var(--text-xs);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.selectwrap { position: relative; display: block; }
|
| 238 |
+
.selectwrap .caret {
|
| 239 |
+
position: absolute; right: var(--space-3); top: 50%; transform: translateY(-50%);
|
| 240 |
+
color: var(--text-faint); font-size: 12px; pointer-events: none;
|
| 241 |
+
}
|
| 242 |
+
/* Fixed-height controls: zero the block padding so the text is not clipped by
|
| 243 |
+
the border-box height coming from --control-h. */
|
| 244 |
+
select {
|
| 245 |
+
appearance: none;
|
| 246 |
+
height: var(--control-h);
|
| 247 |
+
padding: 0 30px 0 var(--space-3);
|
| 248 |
+
line-height: 1;
|
| 249 |
+
cursor: pointer;
|
| 250 |
+
}
|
| 251 |
+
select optgroup {
|
| 252 |
+
background: var(--bg-surface); color: var(--text-faint);
|
| 253 |
+
font: var(--weight-medium) var(--text-xs)/1 var(--font-mono); font-style: normal;
|
| 254 |
+
}
|
| 255 |
+
select option { background: var(--bg-surface); color: var(--text-primary); font-family: var(--font-mono); }
|
| 256 |
+
|
| 257 |
+
input[type=range] {
|
| 258 |
+
accent-color: var(--accent);
|
| 259 |
+
padding: 0; background: none; border: none; box-shadow: none; height: 18px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.hint {
|
| 263 |
+
margin: 0;
|
| 264 |
+
font: var(--weight-regular) var(--text-xs)/1.45 var(--font-sans);
|
| 265 |
+
color: var(--text-faint);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.advanced { border-top: 1px solid var(--border-hairline); padding-top: var(--space-3); }
|
| 269 |
+
.advanced summary {
|
| 270 |
+
cursor: pointer;
|
| 271 |
+
font: var(--type-label);
|
| 272 |
+
letter-spacing: var(--tracking-label);
|
| 273 |
+
text-transform: uppercase;
|
| 274 |
+
color: var(--text-muted);
|
| 275 |
+
list-style: none;
|
| 276 |
+
}
|
| 277 |
+
.advanced summary::-webkit-details-marker { display: none; }
|
| 278 |
+
.advanced summary::before {
|
| 279 |
+
content: "▸"; display: inline-block; margin-right: 7px;
|
| 280 |
+
transition: transform var(--dur-fast) var(--ease-out);
|
| 281 |
+
}
|
| 282 |
+
.advanced[open] summary::before { transform: rotate(90deg); }
|
| 283 |
+
.advanced[open] > .field, .advanced[open] > .field-row { margin-top: var(--space-3); }
|
| 284 |
+
|
| 285 |
+
.actions { display: flex; gap: var(--space-2); }
|
| 286 |
+
|
| 287 |
+
/* ── example tags ────────────────────────────────────────────────── */
|
| 288 |
+
.examples { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
| 289 |
+
.examples-label {
|
| 290 |
+
font: var(--type-label);
|
| 291 |
+
letter-spacing: var(--tracking-label);
|
| 292 |
+
text-transform: uppercase;
|
| 293 |
+
color: var(--text-faint);
|
| 294 |
+
margin-right: 2px;
|
| 295 |
+
}
|
| 296 |
+
.tag {
|
| 297 |
+
font: var(--weight-medium) var(--text-2xs)/1 var(--font-mono);
|
| 298 |
+
letter-spacing: var(--tracking-mono);
|
| 299 |
+
text-transform: uppercase;
|
| 300 |
+
padding: 6px 9px;
|
| 301 |
+
border: 1px solid var(--border-default);
|
| 302 |
+
border-radius: var(--radius-control);
|
| 303 |
+
background: transparent;
|
| 304 |
+
color: var(--text-muted);
|
| 305 |
+
cursor: pointer;
|
| 306 |
+
transition: var(--t-control);
|
| 307 |
+
}
|
| 308 |
+
.tag:hover { background: var(--accent-quiet); border-color: var(--flare-a40); color: var(--text-accent); }
|
| 309 |
+
.tag:active { transform: translate(1px, 1px); }
|
| 310 |
+
.tag:focus-visible { outline: none; box-shadow: var(--ring-focus); }
|
| 311 |
+
|
| 312 |
+
/* ── tabs ────────────────────────────────────────────────────────── */
|
| 313 |
+
.tabs {
|
| 314 |
+
flex: none;
|
| 315 |
+
display: flex;
|
| 316 |
+
align-items: center;
|
| 317 |
+
gap: var(--space-1);
|
| 318 |
+
height: var(--nav-h);
|
| 319 |
+
padding: 0 var(--space-3);
|
| 320 |
+
border-bottom: 1px solid var(--border-hairline);
|
| 321 |
+
background: var(--bg-canvas);
|
| 322 |
+
}
|
| 323 |
+
.tabs-spacer { flex: 1; }
|
| 324 |
+
.tab {
|
| 325 |
+
position: relative;
|
| 326 |
+
height: 100%;
|
| 327 |
+
display: inline-flex;
|
| 328 |
+
align-items: center;
|
| 329 |
+
gap: 7px;
|
| 330 |
+
padding: 0 var(--space-3);
|
| 331 |
+
border: 0;
|
| 332 |
+
background: transparent;
|
| 333 |
+
color: var(--text-faint);
|
| 334 |
+
font: var(--weight-medium) var(--text-xs)/1 var(--font-mono);
|
| 335 |
+
letter-spacing: var(--tracking-mono);
|
| 336 |
+
text-transform: uppercase;
|
| 337 |
+
cursor: pointer;
|
| 338 |
+
transition: var(--t-color);
|
| 339 |
+
}
|
| 340 |
+
.tab:hover { color: var(--text-secondary); }
|
| 341 |
+
.tab.is-active { color: var(--text-primary); }
|
| 342 |
+
/* Active marker is the accent, per the one-accent rule. */
|
| 343 |
+
.tab.is-active::after {
|
| 344 |
+
content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
|
| 345 |
+
height: 2px; background: var(--accent);
|
| 346 |
+
}
|
| 347 |
+
.tab:focus-visible { outline: none; box-shadow: var(--ring-focus); }
|
| 348 |
+
|
| 349 |
+
.meta {
|
| 350 |
+
font: var(--type-label);
|
| 351 |
+
letter-spacing: var(--tracking-mono);
|
| 352 |
+
padding: 3px 5px;
|
| 353 |
+
background: var(--w-a06);
|
| 354 |
+
color: var(--text-faint);
|
| 355 |
+
}
|
| 356 |
+
.meta:empty { display: none; }
|
| 357 |
+
|
| 358 |
+
.status {
|
| 359 |
+
display: flex; align-items: center; gap: var(--space-2);
|
| 360 |
+
font: var(--weight-medium) var(--text-xs)/1 var(--font-mono);
|
| 361 |
+
letter-spacing: var(--tracking-mono);
|
| 362 |
+
color: var(--text-muted);
|
| 363 |
+
}
|
| 364 |
+
.status.is-err { color: var(--status-error); }
|
| 365 |
+
.status.is-ok { color: var(--status-live); }
|
| 366 |
+
|
| 367 |
+
/* Status dots are squares — nothing in this system is circular. */
|
| 368 |
+
.dot { width: 8px; height: 8px; flex: none; background: var(--status-idle); border-radius: 0; }
|
| 369 |
+
.dot.is-live { background: var(--status-live); box-shadow: var(--glow-live); }
|
| 370 |
+
.dot.is-err { background: var(--status-error); }
|
| 371 |
+
.dot.is-building { background: var(--status-building); }
|
| 372 |
+
.dot.is-building::after {
|
| 373 |
+
content: ""; position: absolute; inset: 0; background: var(--status-building);
|
| 374 |
+
animation: harvis-pulse 1.8s var(--ease-out) infinite;
|
| 375 |
+
}
|
| 376 |
+
.status .dot { position: relative; }
|
| 377 |
+
@keyframes harvis-pulse {
|
| 378 |
+
0% { transform: scale(1); opacity: .6; }
|
| 379 |
+
70% { transform: scale(2.6); opacity: 0; }
|
| 380 |
+
100% { opacity: 0; }
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* ── panes ───────────────────────────────────────────────────────── */
|
| 384 |
+
.pane { flex: 1; min-height: 0; position: relative; }
|
| 385 |
+
.pane-preview { background: var(--bg-inset); }
|
| 386 |
+
#preview { width: 100%; height: 100%; border: 0; background: var(--white); display: block; }
|
| 387 |
+
|
| 388 |
+
#code {
|
| 389 |
+
width: 100%; height: 100%;
|
| 390 |
+
border: 0; border-radius: 0; box-shadow: none;
|
| 391 |
+
background: var(--surface-code);
|
| 392 |
+
color: var(--text-secondary);
|
| 393 |
+
font: var(--type-code);
|
| 394 |
+
padding: var(--space-4) var(--space-5);
|
| 395 |
+
resize: none;
|
| 396 |
+
tab-size: 2;
|
| 397 |
+
}
|
| 398 |
+
#code:focus { box-shadow: none; border: 0; }
|
| 399 |
+
|
| 400 |
+
/* Empty state carries the accent bloom and the corner brackets. */
|
| 401 |
+
.empty {
|
| 402 |
+
position: absolute;
|
| 403 |
+
inset: var(--space-6);
|
| 404 |
+
display: flex;
|
| 405 |
+
flex-direction: column;
|
| 406 |
+
align-items: center;
|
| 407 |
+
justify-content: center;
|
| 408 |
+
gap: var(--space-1);
|
| 409 |
+
text-align: center;
|
| 410 |
+
padding: var(--space-6);
|
| 411 |
+
border: 1px dashed var(--border-default);
|
| 412 |
+
border-radius: var(--radius-card);
|
| 413 |
+
background-image: var(--bg-wash);
|
| 414 |
+
background-size: cover;
|
| 415 |
+
}
|
| 416 |
+
.empty p { margin: 0; }
|
| 417 |
+
.empty-title { font: var(--weight-medium) var(--text-md)/1.4 var(--font-sans); color: var(--text-secondary); }
|
| 418 |
+
.empty-mark { font-size: 24px; color: var(--accent); margin-bottom: var(--space-3); }
|
| 419 |
+
|
| 420 |
+
/* ── deploy bar ──────────────────────────────────────────────────── */
|
| 421 |
+
.deploybar {
|
| 422 |
+
flex: none;
|
| 423 |
+
display: flex;
|
| 424 |
+
align-items: center;
|
| 425 |
+
gap: var(--space-3);
|
| 426 |
+
padding: var(--space-3) var(--space-4);
|
| 427 |
+
border-top: 1px solid var(--border-hairline);
|
| 428 |
+
background: var(--bg-surface);
|
| 429 |
+
flex-wrap: wrap;
|
| 430 |
+
}
|
| 431 |
+
.deployresult {
|
| 432 |
+
flex: 1 1 320px;
|
| 433 |
+
min-width: 0;
|
| 434 |
+
font: var(--weight-regular) var(--text-xs)/1.55 var(--font-mono);
|
| 435 |
+
letter-spacing: var(--tracking-mono);
|
| 436 |
+
color: var(--text-muted);
|
| 437 |
+
overflow-wrap: anywhere;
|
| 438 |
+
}
|
| 439 |
+
.deployresult a { color: var(--text-link); }
|
| 440 |
+
.deployresult a:hover { color: var(--text-link-hover); }
|
| 441 |
+
.deployresult .live { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text-secondary); }
|
| 442 |
+
.deployresult .claim-note { display: block; margin-top: 3px; color: var(--text-faint); font-size: var(--text-2xs); }
|
| 443 |
+
.deployresult.is-err { color: var(--status-error); }
|
| 444 |
+
|
| 445 |
+
.copy {
|
| 446 |
+
font: var(--weight-medium) var(--text-2xs)/1 var(--font-mono);
|
| 447 |
+
letter-spacing: var(--tracking-label);
|
| 448 |
+
text-transform: uppercase;
|
| 449 |
+
margin-left: 6px;
|
| 450 |
+
padding: 3px 6px;
|
| 451 |
+
border: 1px solid var(--border-default);
|
| 452 |
+
border-radius: var(--radius-control);
|
| 453 |
+
background: transparent;
|
| 454 |
+
color: var(--text-faint);
|
| 455 |
+
cursor: pointer;
|
| 456 |
+
transition: var(--t-control);
|
| 457 |
+
}
|
| 458 |
+
.copy:hover { background: var(--accent-quiet); border-color: var(--flare-a40); color: var(--text-accent); }
|
| 459 |
+
.copy:active { transform: translate(1px, 1px); }
|
| 460 |
+
|
| 461 |
+
/* ── toast — fixed bottom-right, 340px, hard offset shadow ───────── */
|
| 462 |
+
.toast {
|
| 463 |
+
position: fixed;
|
| 464 |
+
right: var(--space-4);
|
| 465 |
+
bottom: var(--space-4);
|
| 466 |
+
z-index: 50;
|
| 467 |
+
width: 340px;
|
| 468 |
+
max-width: calc(100vw - var(--space-8));
|
| 469 |
+
display: flex;
|
| 470 |
+
align-items: center;
|
| 471 |
+
gap: var(--space-3);
|
| 472 |
+
padding: var(--space-3) var(--space-4);
|
| 473 |
+
border: 1px solid var(--border-default);
|
| 474 |
+
border-radius: var(--radius-card);
|
| 475 |
+
background: var(--bg-raised);
|
| 476 |
+
box-shadow: var(--shadow-3);
|
| 477 |
+
color: var(--text-primary);
|
| 478 |
+
font: var(--weight-medium) var(--text-xs)/1.4 var(--font-sans);
|
| 479 |
+
animation: toast-in var(--dur-base) var(--ease-snap);
|
| 480 |
+
}
|
| 481 |
+
.toast .corners i { border-color: var(--corner-color-accent); }
|
| 482 |
+
@keyframes toast-in { from { opacity: 0; } }
|
| 483 |
+
|
| 484 |
+
/* ── scrollbars ──────────────────────────────────────────────────── */
|
| 485 |
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
| 486 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 487 |
+
::-webkit-scrollbar-thumb { background: var(--ink-700); border: 3px solid var(--bg-canvas); }
|
| 488 |
+
::-webkit-scrollbar-thumb:hover { background: var(--ink-600); }
|
| 489 |
+
|
| 490 |
+
/* ── responsive ──────────────────────────────────────────────────── */
|
| 491 |
+
@media (max-width: 940px) {
|
| 492 |
+
body { overflow: auto; }
|
| 493 |
+
.layout { grid-template-columns: 1fr; }
|
| 494 |
+
.rail { border-right: 0; border-bottom: 1px solid var(--border-hairline); overflow: visible; }
|
| 495 |
+
.output { min-height: 76vh; }
|
| 496 |
+
.nav-div, .nav-title { display: none; }
|
| 497 |
+
#signin span, #tokentoggle { display: none; }
|
| 498 |
+
}
|
app.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* html studio — harvis.dev
|
| 2 |
+
* Runs entirely in the browser. Two APIs, both CORS-open and called directly:
|
| 3 |
+
* - https://router.huggingface.co/v1 (OpenAI-compatible, needs an HF token)
|
| 4 |
+
* - https://harvis.dev/api/upload (static hosting, no auth)
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "https://esm.sh/@huggingface/hub@2.15.0";
|
| 8 |
+
|
| 9 |
+
const ROUTER = "https://router.huggingface.co/v1";
|
| 10 |
+
const HARVIS = "https://harvis.dev/api/upload";
|
| 11 |
+
|
| 12 |
+
const LS = { auth: "hs.auth", token: "hs.token", model: "hs.model", site: "hs.site", prompt: "hs.prompt", theme: "hs.theme" };
|
| 13 |
+
|
| 14 |
+
/* Models the router serves that are strong at writing a whole page in one
|
| 15 |
+
* shot. Everything else on the router is still selectable below this group. */
|
| 16 |
+
const RECOMMENDED = [
|
| 17 |
+
"Qwen/Qwen3-Coder-Next",
|
| 18 |
+
"moonshotai/Kimi-K2.7-Code",
|
| 19 |
+
"zai-org/GLM-5.2",
|
| 20 |
+
"deepseek-ai/DeepSeek-V4-Flash",
|
| 21 |
+
"openai/gpt-oss-120b",
|
| 22 |
+
"Qwen/Qwen3-Coder-30B-A3B-Instruct",
|
| 23 |
+
"MiniMaxAI/MiniMax-M3",
|
| 24 |
+
"google/gemma-4-31B-it",
|
| 25 |
+
];
|
| 26 |
+
|
| 27 |
+
const DEFAULT_SYSTEM = `You are an expert front-end engineer with a strong sense of visual design.
|
| 28 |
+
|
| 29 |
+
Return ONE complete, self-contained HTML document and NOTHING else.
|
| 30 |
+
|
| 31 |
+
Rules:
|
| 32 |
+
- Start with <!doctype html>. Include <html>, <head> with <meta charset> and a
|
| 33 |
+
responsive viewport meta, <title>, and <body>.
|
| 34 |
+
- Inline ALL CSS in a <style> tag and ALL JavaScript in a <script> tag. The file
|
| 35 |
+
must work when opened on its own, with no build step and no local assets.
|
| 36 |
+
- No markdown code fences, no explanation, no commentary before or after.
|
| 37 |
+
- External resources are allowed only from public CDNs (unpkg, jsdelivr,
|
| 38 |
+
fonts.googleapis.com) or picsum.photos for placeholder imagery. Prefer inline
|
| 39 |
+
SVG and CSS over dependencies.
|
| 40 |
+
- Make it genuinely well designed: a deliberate type scale, real spacing rhythm,
|
| 41 |
+
a coherent palette, hover and focus states, and a sensible responsive layout.
|
| 42 |
+
- Semantic HTML, accessible contrast, alt text, and keyboard-usable controls.`;
|
| 43 |
+
|
| 44 |
+
/* ── element handles ─────────────────────────────────────────────── */
|
| 45 |
+
const $ = (id) => document.getElementById(id);
|
| 46 |
+
const el = {
|
| 47 |
+
signin: $("signin"), signout: $("signout"), tokentoggle: $("tokentoggle"), theme: $("theme"),
|
| 48 |
+
whoami: $("whoami"), avatar: $("avatar"), username: $("username"),
|
| 49 |
+
tokenrow: $("tokenrow"), token: $("token"), tokensave: $("tokensave"),
|
| 50 |
+
model: $("model"), modelhint: $("modelhint"),
|
| 51 |
+
prompt: $("prompt"), system: $("system"),
|
| 52 |
+
temp: $("temp"), tempval: $("tempval"), maxtok: $("maxtok"),
|
| 53 |
+
generate: $("generate"), stop: $("stop"),
|
| 54 |
+
tabPreview: $("tab-preview"), tabCode: $("tab-code"),
|
| 55 |
+
panePreview: $("pane-preview"), paneCode: $("pane-code"),
|
| 56 |
+
preview: $("preview"), code: $("code"), empty: $("empty"), bytes: $("bytes"),
|
| 57 |
+
status: $("status"), reload: $("reload"), download: $("download"),
|
| 58 |
+
deploy: $("deploy"), deploylabel: $("deploylabel"), newsite: $("newsite"), deployresult: $("deployresult"),
|
| 59 |
+
toast: $("toast"), toastmsg: $("toastmsg"), toastdot: $("toastdot"),
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
/* ── state ───────────────────────────────────────────────────────── */
|
| 63 |
+
let auth = null; // { accessToken, expiresAt, user } — from HF OAuth
|
| 64 |
+
let rawToken = ""; // manually pasted token
|
| 65 |
+
let site = null; // { url, claimUrl, subdomain, deployToken }
|
| 66 |
+
let controller = null; // AbortController for the in-flight generation
|
| 67 |
+
let editTimer = 0;
|
| 68 |
+
|
| 69 |
+
const token = () => auth?.accessToken || rawToken || "";
|
| 70 |
+
|
| 71 |
+
/* ── helpers ─────────────────────────────────────────────────────── */
|
| 72 |
+
function toast(msg, tone = "live") {
|
| 73 |
+
el.toastmsg.textContent = msg;
|
| 74 |
+
el.toastdot.className = `dot is-${tone}`;
|
| 75 |
+
el.toast.hidden = false;
|
| 76 |
+
clearTimeout(toast._t);
|
| 77 |
+
toast._t = setTimeout(() => { el.toast.hidden = true; }, 4000);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function status(msg, kind = "") {
|
| 81 |
+
el.status.className = "status" + (kind ? ` is-${kind}` : "");
|
| 82 |
+
el.status.innerHTML = "";
|
| 83 |
+
if (!msg) return;
|
| 84 |
+
const dot = document.createElement("span");
|
| 85 |
+
dot.className = "dot " + (kind === "err" ? "is-err" : kind === "ok" ? "is-live" : kind === "busy" ? "is-building" : "");
|
| 86 |
+
if (kind) el.status.append(dot);
|
| 87 |
+
el.status.append(document.createTextNode(msg));
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
const store = {
|
| 91 |
+
get(k) { try { return JSON.parse(localStorage.getItem(k)); } catch { return null; } },
|
| 92 |
+
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
|
| 93 |
+
del(k) { try { localStorage.removeItem(k); } catch {} },
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
/* Machine values are always mono, and never given more precision than we have. */
|
| 97 |
+
function fmtBytes(n) {
|
| 98 |
+
return n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(2)} MB`;
|
| 99 |
+
}
|
| 100 |
+
const fmtSecs = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
| 101 |
+
|
| 102 |
+
/* ── theme ──��────────────────────────────────────────────────────── */
|
| 103 |
+
function applyTheme(t) {
|
| 104 |
+
if (t === "paper") document.documentElement.setAttribute("data-theme", "paper");
|
| 105 |
+
else document.documentElement.removeAttribute("data-theme");
|
| 106 |
+
store.set(LS.theme, t);
|
| 107 |
+
}
|
| 108 |
+
el.theme.addEventListener("click", () => {
|
| 109 |
+
applyTheme(document.documentElement.getAttribute("data-theme") === "paper" ? "ink" : "paper");
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
/* ── auth ────────────────────────────────────────────────────────── */
|
| 113 |
+
function renderAuth() {
|
| 114 |
+
const signedIn = !!auth?.accessToken;
|
| 115 |
+
el.whoami.hidden = !signedIn;
|
| 116 |
+
el.signin.hidden = signedIn;
|
| 117 |
+
el.tokentoggle.hidden = signedIn;
|
| 118 |
+
if (signedIn) {
|
| 119 |
+
el.username.textContent = auth.user?.name || "signed in";
|
| 120 |
+
if (auth.user?.avatarUrl) { el.avatar.src = auth.user.avatarUrl; el.avatar.hidden = false; }
|
| 121 |
+
else el.avatar.hidden = true;
|
| 122 |
+
}
|
| 123 |
+
syncGenerateEnabled();
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function syncGenerateEnabled() {
|
| 127 |
+
el.generate.disabled = !token() || !el.model.value || !!controller;
|
| 128 |
+
if (!token()) status("sign in to generate", "");
|
| 129 |
+
else if (!controller) status("");
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async function initAuth() {
|
| 133 |
+
try {
|
| 134 |
+
const res = await oauthHandleRedirectIfPresent();
|
| 135 |
+
if (res) {
|
| 136 |
+
auth = {
|
| 137 |
+
accessToken: res.accessToken,
|
| 138 |
+
expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : 0,
|
| 139 |
+
user: { name: res.userInfo?.name || res.userInfo?.preferred_username, avatarUrl: res.userInfo?.avatarUrl },
|
| 140 |
+
};
|
| 141 |
+
store.set(LS.auth, auth);
|
| 142 |
+
history.replaceState(null, "", location.pathname);
|
| 143 |
+
toast(`Signed in as ${auth.user.name}.`);
|
| 144 |
+
}
|
| 145 |
+
} catch (e) {
|
| 146 |
+
console.warn("[auth] redirect handling failed:", e);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if (!auth) {
|
| 150 |
+
const saved = store.get(LS.auth);
|
| 151 |
+
if (saved?.accessToken && (!saved.expiresAt || saved.expiresAt > Date.now() + 60_000)) auth = saved;
|
| 152 |
+
else store.del(LS.auth);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
rawToken = store.get(LS.token) || "";
|
| 156 |
+
if (rawToken) el.token.value = rawToken;
|
| 157 |
+
renderAuth();
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
el.signin.addEventListener("click", async () => {
|
| 161 |
+
try {
|
| 162 |
+
// Inside a Space with `hf_oauth: true`, client id and redirect are injected
|
| 163 |
+
// by the platform, so no arguments are needed.
|
| 164 |
+
location.href = await oauthLoginUrl({ scopes: "openid profile inference-api" });
|
| 165 |
+
} catch (e) {
|
| 166 |
+
console.warn("[auth] oauthLoginUrl failed:", e);
|
| 167 |
+
toast("OAuth needs this app to run as a Hugging Face Space — paste a token instead.", "err");
|
| 168 |
+
el.tokenrow.hidden = false;
|
| 169 |
+
el.token.focus();
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
el.signout.addEventListener("click", () => {
|
| 174 |
+
auth = null;
|
| 175 |
+
store.del(LS.auth);
|
| 176 |
+
renderAuth();
|
| 177 |
+
toast("Signed out.", "idle");
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
el.tokentoggle.addEventListener("click", () => {
|
| 181 |
+
el.tokenrow.hidden = !el.tokenrow.hidden;
|
| 182 |
+
if (!el.tokenrow.hidden) el.token.focus();
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
el.tokensave.addEventListener("click", () => {
|
| 186 |
+
rawToken = el.token.value.trim();
|
| 187 |
+
if (rawToken) { store.set(LS.token, rawToken); toast("Token saved to this browser."); el.tokenrow.hidden = true; }
|
| 188 |
+
else { store.del(LS.token); toast("Token cleared.", "idle"); }
|
| 189 |
+
syncGenerateEnabled();
|
| 190 |
+
});
|
| 191 |
+
el.token.addEventListener("keydown", (e) => { if (e.key === "Enter") el.tokensave.click(); });
|
| 192 |
+
|
| 193 |
+
/* ── models ──────────────────────────────────────────────────────── */
|
| 194 |
+
async function loadModels() {
|
| 195 |
+
let ids = [];
|
| 196 |
+
try {
|
| 197 |
+
const r = await fetch(`${ROUTER}/models`);
|
| 198 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 199 |
+
const j = await r.json();
|
| 200 |
+
ids = (j.data || j || []).map((m) => m.id).filter(Boolean);
|
| 201 |
+
} catch (e) {
|
| 202 |
+
console.warn("[models] router list unavailable, using fallback:", e);
|
| 203 |
+
ids = RECOMMENDED.slice();
|
| 204 |
+
el.modelhint.textContent = "Router model list unreachable — showing a fallback set.";
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
const available = new Set(ids);
|
| 208 |
+
const top = RECOMMENDED.filter((m) => available.has(m));
|
| 209 |
+
const rest = ids.filter((m) => !top.includes(m)).sort((a, b) => a.localeCompare(b));
|
| 210 |
+
|
| 211 |
+
el.model.innerHTML = "";
|
| 212 |
+
const group = (label, items) => {
|
| 213 |
+
if (!items.length) return;
|
| 214 |
+
const g = document.createElement("optgroup");
|
| 215 |
+
g.label = label;
|
| 216 |
+
for (const id of items) g.append(new Option(id, id));
|
| 217 |
+
el.model.append(g);
|
| 218 |
+
};
|
| 219 |
+
group("recommended for html", top);
|
| 220 |
+
group(`all models · ${rest.length}`, rest);
|
| 221 |
+
|
| 222 |
+
const saved = store.get(LS.model);
|
| 223 |
+
el.model.value = (saved && available.has(saved)) ? saved : (top[0] || rest[0] || "");
|
| 224 |
+
if (!el.modelhint.textContent.startsWith("Router model list")) {
|
| 225 |
+
el.modelhint.textContent = `${ids.length} open-weights chat models on the Hugging Face router.`;
|
| 226 |
+
}
|
| 227 |
+
syncGenerateEnabled();
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
el.model.addEventListener("change", () => { store.set(LS.model, el.model.value); syncGenerateEnabled(); });
|
| 231 |
+
|
| 232 |
+
/* ── generation ──────────────────────────────────────────────────── */
|
| 233 |
+
function extractHtml(raw) {
|
| 234 |
+
if (!raw) return "";
|
| 235 |
+
let t = raw;
|
| 236 |
+
const fence = t.match(/```(?:html|xml)?[ \t]*\r?\n([\s\S]*?)(?:\r?\n[ \t]*```|$)/i);
|
| 237 |
+
if (fence) t = fence[1];
|
| 238 |
+
else {
|
| 239 |
+
const i = t.search(/<!doctype html|<html[\s>]/i);
|
| 240 |
+
if (i > 0) t = t.slice(i);
|
| 241 |
+
}
|
| 242 |
+
return t.replace(/[ \t]*```[ \t]*$/, "").trim();
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
async function generate() {
|
| 246 |
+
const prompt = el.prompt.value.trim();
|
| 247 |
+
if (!prompt) { el.prompt.focus(); toast("Describe the site first.", "err"); return; }
|
| 248 |
+
if (!token()) { toast("Sign in with Hugging Face first.", "err"); return; }
|
| 249 |
+
|
| 250 |
+
controller = new AbortController();
|
| 251 |
+
el.generate.hidden = true;
|
| 252 |
+
el.stop.hidden = false;
|
| 253 |
+
el.deploy.disabled = true;
|
| 254 |
+
store.set(LS.prompt, prompt);
|
| 255 |
+
showTab("code");
|
| 256 |
+
el.code.value = "";
|
| 257 |
+
status("generating", "busy");
|
| 258 |
+
|
| 259 |
+
const started = performance.now();
|
| 260 |
+
let raw = "";
|
| 261 |
+
let lastPreview = 0;
|
| 262 |
+
|
| 263 |
+
try {
|
| 264 |
+
const res = await fetch(`${ROUTER}/chat/completions`, {
|
| 265 |
+
method: "POST",
|
| 266 |
+
signal: controller.signal,
|
| 267 |
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token()}` },
|
| 268 |
+
body: JSON.stringify({
|
| 269 |
+
model: el.model.value,
|
| 270 |
+
stream: true,
|
| 271 |
+
temperature: parseFloat(el.temp.value),
|
| 272 |
+
max_tokens: parseInt(el.maxtok.value, 10) || 16000,
|
| 273 |
+
messages: [
|
| 274 |
+
{ role: "system", content: el.system.value.trim() || DEFAULT_SYSTEM },
|
| 275 |
+
{ role: "user", content: prompt },
|
| 276 |
+
],
|
| 277 |
+
}),
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
if (!res.ok) {
|
| 281 |
+
const detail = (await res.text().catch(() => "")).slice(0, 400);
|
| 282 |
+
throw new Error(`Router returned ${res.status}. ${detail}`);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
const reader = res.body.getReader();
|
| 286 |
+
const dec = new TextDecoder();
|
| 287 |
+
let buf = "";
|
| 288 |
+
|
| 289 |
+
for (;;) {
|
| 290 |
+
const { done, value } = await reader.read();
|
| 291 |
+
if (done) break;
|
| 292 |
+
buf += dec.decode(value, { stream: true });
|
| 293 |
+
|
| 294 |
+
const lines = buf.split("\n");
|
| 295 |
+
buf = lines.pop();
|
| 296 |
+
for (const line of lines) {
|
| 297 |
+
const s = line.trim();
|
| 298 |
+
if (!s.startsWith("data:")) continue;
|
| 299 |
+
const payload = s.slice(5).trim();
|
| 300 |
+
if (!payload || payload === "[DONE]") continue;
|
| 301 |
+
let chunk;
|
| 302 |
+
try { chunk = JSON.parse(payload); } catch { continue; }
|
| 303 |
+
if (chunk.error) throw new Error(chunk.error.message || JSON.stringify(chunk.error));
|
| 304 |
+
// `reasoning_content` from thinking models is deliberately ignored.
|
| 305 |
+
const delta = chunk.choices?.[0]?.delta?.content;
|
| 306 |
+
if (!delta) continue;
|
| 307 |
+
|
| 308 |
+
raw += delta;
|
| 309 |
+
el.code.value = raw;
|
| 310 |
+
el.code.scrollTop = el.code.scrollHeight;
|
| 311 |
+
el.bytes.textContent = fmtBytes(raw.length);
|
| 312 |
+
|
| 313 |
+
// Throttled progressive render — partial documents render fine.
|
| 314 |
+
const now = performance.now();
|
| 315 |
+
if (now - lastPreview > 1500 && /<body[\s>]/i.test(raw)) {
|
| 316 |
+
lastPreview = now;
|
| 317 |
+
renderPreview(extractHtml(raw));
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
const html = extractHtml(raw);
|
| 323 |
+
if (!html) throw new Error("The model returned no HTML — try another model or rephrase the prompt.");
|
| 324 |
+
|
| 325 |
+
el.code.value = html;
|
| 326 |
+
el.bytes.textContent = fmtBytes(html.length);
|
| 327 |
+
renderPreview(html);
|
| 328 |
+
showTab("preview");
|
| 329 |
+
status(`${fmtBytes(html.length)} · ${fmtSecs(performance.now() - started)}`, "ok");
|
| 330 |
+
el.deploy.disabled = false;
|
| 331 |
+
} catch (e) {
|
| 332 |
+
if (e.name === "AbortError") {
|
| 333 |
+
// Keep whatever streamed in; a partial document is often still usable.
|
| 334 |
+
const html = extractHtml(raw);
|
| 335 |
+
if (html) { el.code.value = html; renderPreview(html); el.deploy.disabled = false; }
|
| 336 |
+
status("stopped", "");
|
| 337 |
+
} else {
|
| 338 |
+
console.error("[generate]", e);
|
| 339 |
+
status("failed", "err");
|
| 340 |
+
toast(String(e.message || e), "err");
|
| 341 |
+
}
|
| 342 |
+
} finally {
|
| 343 |
+
controller = null;
|
| 344 |
+
el.generate.hidden = false;
|
| 345 |
+
el.stop.hidden = true;
|
| 346 |
+
syncGenerateEnabled();
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
el.generate.addEventListener("click", generate);
|
| 351 |
+
el.stop.addEventListener("click", () => controller?.abort());
|
| 352 |
+
el.prompt.addEventListener("keydown", (e) => {
|
| 353 |
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); if (!el.generate.disabled) generate(); }
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
/* ── preview ─────────────────────────────────────────────────────── */
|
| 357 |
+
function renderPreview(html) {
|
| 358 |
+
if (!html) return;
|
| 359 |
+
el.empty.hidden = true;
|
| 360 |
+
el.preview.hidden = false;
|
| 361 |
+
el.preview.srcdoc = html;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
el.reload.addEventListener("click", () => {
|
| 365 |
+
const html = el.code.value.trim();
|
| 366 |
+
if (!html) { toast("Nothing to render yet.", "err"); return; }
|
| 367 |
+
renderPreview(html);
|
| 368 |
+
showTab("preview");
|
| 369 |
+
});
|
| 370 |
+
|
| 371 |
+
el.code.addEventListener("input", () => {
|
| 372 |
+
el.bytes.textContent = fmtBytes(el.code.value.length);
|
| 373 |
+
el.deploy.disabled = !el.code.value.trim();
|
| 374 |
+
clearTimeout(editTimer);
|
| 375 |
+
editTimer = setTimeout(() => { if (el.code.value.trim()) renderPreview(el.code.value); }, 700);
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
/* ── tabs ────────────────────────────────────────────────────────── */
|
| 379 |
+
function showTab(which) {
|
| 380 |
+
const isPreview = which === "preview";
|
| 381 |
+
el.tabPreview.classList.toggle("is-active", isPreview);
|
| 382 |
+
el.tabCode.classList.toggle("is-active", !isPreview);
|
| 383 |
+
el.tabPreview.setAttribute("aria-selected", String(isPreview));
|
| 384 |
+
el.tabCode.setAttribute("aria-selected", String(!isPreview));
|
| 385 |
+
el.panePreview.hidden = !isPreview;
|
| 386 |
+
el.paneCode.hidden = isPreview;
|
| 387 |
+
}
|
| 388 |
+
el.tabPreview.addEventListener("click", () => showTab("preview"));
|
| 389 |
+
el.tabCode.addEventListener("click", () => showTab("code"));
|
| 390 |
+
|
| 391 |
+
/* ── download ────────────────────────────────────────────────────── */
|
| 392 |
+
el.download.addEventListener("click", () => {
|
| 393 |
+
const html = el.code.value.trim();
|
| 394 |
+
if (!html) { toast("Nothing to download yet.", "err"); return; }
|
| 395 |
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
| 396 |
+
const a = Object.assign(document.createElement("a"), { href: url, download: "index.html" });
|
| 397 |
+
a.click();
|
| 398 |
+
URL.revokeObjectURL(url);
|
| 399 |
+
});
|
| 400 |
+
|
| 401 |
+
/* ── deploy ──────────────────────────────────────────────────────── */
|
| 402 |
+
function siteName() {
|
| 403 |
+
const p = el.prompt.value.trim().split("\n")[0].slice(0, 48).trim();
|
| 404 |
+
return p || "html-studio";
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
function renderDeployResult(r, { updated = false, restored = false, ms = 0, bytes = 0 } = {}) {
|
| 408 |
+
const lead = restored ? "last deploy" : updated ? "updated" : "live";
|
| 409 |
+
const meta = restored ? "" : ` · ${fmtBytes(bytes)}${ms ? ` · ${fmtSecs(ms)}` : ""}`;
|
| 410 |
+
el.deployresult.className = "deployresult";
|
| 411 |
+
el.deployresult.innerHTML = `
|
| 412 |
+
<span class="live"><span class="dot is-live"></span>${lead}${meta}</span>
|
| 413 |
+
<a href="${r.url}" target="_blank" rel="noopener">${r.url}</a>
|
| 414 |
+
<button class="copy" type="button" data-copy="${r.url}">copy</button>
|
| 415 |
+
<span class="claim-note">
|
| 416 |
+
Private claim link — open it and sign in to keep this site. It cannot be recovered if lost,
|
| 417 |
+
and an unclaimed site expires 24 hours after its last deploy.<br>
|
| 418 |
+
<a href="${r.claimUrl}" target="_blank" rel="noopener">${r.claimUrl}</a>
|
| 419 |
+
<button class="copy" type="button" data-copy="${r.claimUrl}">copy</button>
|
| 420 |
+
</span>`;
|
| 421 |
+
el.newsite.hidden = false;
|
| 422 |
+
el.deploylabel.textContent = "Update site";
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
el.deployresult.addEventListener("click", (e) => {
|
| 426 |
+
const btn = e.target.closest("[data-copy]");
|
| 427 |
+
if (!btn) return;
|
| 428 |
+
navigator.clipboard.writeText(btn.dataset.copy).then(() => toast("Copied."), () => toast("Copy failed.", "err"));
|
| 429 |
+
});
|
| 430 |
+
|
| 431 |
+
el.deploy.addEventListener("click", async () => {
|
| 432 |
+
const html = el.code.value.trim();
|
| 433 |
+
if (!html) { toast("Generate something first.", "err"); return; }
|
| 434 |
+
|
| 435 |
+
el.deploy.disabled = true;
|
| 436 |
+
status("deploying", "busy");
|
| 437 |
+
const started = performance.now();
|
| 438 |
+
|
| 439 |
+
const body = { name: siteName(), files: [{ path: "index.html", content: html, encoding: "text" }] };
|
| 440 |
+
if (site?.subdomain && site?.deployToken) {
|
| 441 |
+
body.subdomain = site.subdomain;
|
| 442 |
+
body.deployToken = site.deployToken;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
try {
|
| 446 |
+
const res = await fetch(HARVIS, {
|
| 447 |
+
method: "POST",
|
| 448 |
+
headers: { "Content-Type": "application/json" },
|
| 449 |
+
body: JSON.stringify(body),
|
| 450 |
+
});
|
| 451 |
+
const text = await res.text();
|
| 452 |
+
let r;
|
| 453 |
+
try { r = JSON.parse(text); } catch { throw new Error(`harvis.dev returned ${res.status} — ${text.slice(0, 160)}`); }
|
| 454 |
+
if (!res.ok) throw new Error(r.error || r.message || `harvis.dev returned ${res.status}`);
|
| 455 |
+
|
| 456 |
+
const ms = performance.now() - started;
|
| 457 |
+
site = { url: r.url, claimUrl: r.claimUrl, subdomain: r.subdomain, deployToken: r.deployToken };
|
| 458 |
+
store.set(LS.site, site);
|
| 459 |
+
renderDeployResult(r, { updated: !!r.updated, ms, bytes: html.length });
|
| 460 |
+
status(`${fmtBytes(html.length)} · ${fmtSecs(ms)}`, "ok");
|
| 461 |
+
// No success toast: it is fixed bottom-right and would cover the claim link
|
| 462 |
+
// at the exact moment it appears. The deploy bar already reports the result.
|
| 463 |
+
} catch (e) {
|
| 464 |
+
console.error("[deploy]", e);
|
| 465 |
+
el.deployresult.className = "deployresult is-err";
|
| 466 |
+
el.deployresult.textContent = String(e.message || e);
|
| 467 |
+
status("failed", "err");
|
| 468 |
+
toast("Deploy failed.", "err");
|
| 469 |
+
} finally {
|
| 470 |
+
el.deploy.disabled = false;
|
| 471 |
+
}
|
| 472 |
+
});
|
| 473 |
+
|
| 474 |
+
el.newsite.addEventListener("click", () => {
|
| 475 |
+
site = null;
|
| 476 |
+
store.del(LS.site);
|
| 477 |
+
el.deployresult.className = "deployresult";
|
| 478 |
+
el.deployresult.textContent = "";
|
| 479 |
+
el.newsite.hidden = true;
|
| 480 |
+
el.deploylabel.textContent = "Deploy to harvis.dev";
|
| 481 |
+
toast("Next deploy creates a new site.", "idle");
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
/* ── examples ──────────────────────────��─────────────────────────── */
|
| 485 |
+
for (const tag of document.querySelectorAll(".tag")) {
|
| 486 |
+
tag.addEventListener("click", () => {
|
| 487 |
+
el.prompt.value = tag.dataset.example;
|
| 488 |
+
el.prompt.focus();
|
| 489 |
+
store.set(LS.prompt, el.prompt.value);
|
| 490 |
+
});
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/* ── misc wiring ─────────────────────────────────────────────────── */
|
| 494 |
+
el.temp.addEventListener("input", () => { el.tempval.textContent = parseFloat(el.temp.value).toFixed(2); });
|
| 495 |
+
el.prompt.addEventListener("change", () => store.set(LS.prompt, el.prompt.value));
|
| 496 |
+
|
| 497 |
+
/* ── boot ────────────────────────────────────────────────────────── */
|
| 498 |
+
applyTheme(store.get(LS.theme) || "ink");
|
| 499 |
+
el.system.value = DEFAULT_SYSTEM;
|
| 500 |
+
el.tempval.textContent = parseFloat(el.temp.value).toFixed(2);
|
| 501 |
+
el.prompt.value = store.get(LS.prompt) || "";
|
| 502 |
+
|
| 503 |
+
site = store.get(LS.site);
|
| 504 |
+
if (site?.url) renderDeployResult(site, { restored: true });
|
| 505 |
+
|
| 506 |
+
initAuth();
|
| 507 |
+
loadModels();
|
assets/fonts/NordwandMono-Bold.woff2
ADDED
|
Binary file (15.6 kB). View file
|
|
|
assets/fonts/NordwandMono-Medium.woff2
ADDED
|
Binary file (15.5 kB). View file
|
|
|
assets/fonts/NordwandMono-Regular.woff2
ADDED
|
Binary file (16.2 kB). View file
|
|
|
assets/icons/remixicon.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/icons/remixicon.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e92dc38bfb1d15dbb9d9ca9e645754f68e7c5b224e33c45a17048bd0cf2407a6
|
| 3 |
+
size 189216
|
index.html
CHANGED
|
@@ -1,19 +1,159 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>html studio — harvis</title>
|
| 7 |
+
<meta name="description" content="Describe a site, let any open-weights model write it, and deploy it to harvis.dev in about two seconds.">
|
| 8 |
+
<link rel="stylesheet" href="styles.css">
|
| 9 |
+
<link rel="stylesheet" href="app.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
|
| 13 |
+
<header class="nav">
|
| 14 |
+
<a class="lockup" href="https://harvis.dev" target="_blank" rel="noopener">
|
| 15 |
+
<span class="mark">[#]</span>
|
| 16 |
+
<span class="wordmark">harvis</span>
|
| 17 |
+
</a>
|
| 18 |
+
<span class="nav-div" aria-hidden="true"></span>
|
| 19 |
+
<span class="nav-title">html studio</span>
|
| 20 |
+
|
| 21 |
+
<div class="nav-spacer"></div>
|
| 22 |
+
|
| 23 |
+
<div class="auth">
|
| 24 |
+
<button id="signin" class="btn btn-outline btn-sm" type="button">
|
| 25 |
+
<i class="ri-user-line" aria-hidden="true"></i>Sign in with Hugging Face
|
| 26 |
+
</button>
|
| 27 |
+
<button id="tokentoggle" class="btn btn-ghost btn-sm" type="button">
|
| 28 |
+
<i class="ri-key-2-line" aria-hidden="true"></i>Use a token
|
| 29 |
+
</button>
|
| 30 |
+
<div id="whoami" class="whoami" hidden>
|
| 31 |
+
<img id="avatar" alt="" width="22" height="22">
|
| 32 |
+
<span id="username" class="mono"></span>
|
| 33 |
+
<button id="signout" class="btn btn-ghost btn-sm" type="button">Sign out</button>
|
| 34 |
+
</div>
|
| 35 |
+
<button id="theme" class="btn btn-ghost btn-sm btn-icon" type="button" title="Toggle ink / paper theme" aria-label="Toggle theme">
|
| 36 |
+
<i class="ri-contrast-2-line" aria-hidden="true"></i>
|
| 37 |
+
</button>
|
| 38 |
+
</div>
|
| 39 |
+
</header>
|
| 40 |
+
|
| 41 |
+
<div id="tokenrow" class="tokenrow" hidden>
|
| 42 |
+
<label for="token">access token</label>
|
| 43 |
+
<span class="inputwrap">
|
| 44 |
+
<i class="ri-key-2-line" aria-hidden="true"></i>
|
| 45 |
+
<input id="token" type="password" autocomplete="off" spellcheck="false"
|
| 46 |
+
placeholder="hf_… — huggingface.co/settings/tokens">
|
| 47 |
+
</span>
|
| 48 |
+
<button id="tokensave" class="btn btn-secondary btn-sm" type="button">Save</button>
|
| 49 |
+
<span class="hint">Stored in this browser. Sent only to the Hugging Face router.</span>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<main class="layout">
|
| 53 |
+
|
| 54 |
+
<!-- ── controls ───────────────────────────────────────────────── -->
|
| 55 |
+
<aside class="rail">
|
| 56 |
+
<div class="field">
|
| 57 |
+
<label for="model">model</label>
|
| 58 |
+
<span class="selectwrap">
|
| 59 |
+
<select id="model"><option value="">loading…</option></select>
|
| 60 |
+
<span class="caret" aria-hidden="true">▾</span>
|
| 61 |
+
</span>
|
| 62 |
+
<p class="hint" id="modelhint">Open-weights chat models on the Hugging Face router.</p>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div class="field field-grow">
|
| 66 |
+
<label for="prompt">describe the site</label>
|
| 67 |
+
<textarea id="prompt" spellcheck="false"
|
| 68 |
+
placeholder="A landing page for a coffee subscription called Ember. Hero with a tagline, three plan cards with prices, an FAQ, and a footer. Warm, generous whitespace, restrained motion."></textarea>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<details class="advanced">
|
| 72 |
+
<summary>advanced</summary>
|
| 73 |
+
<div class="field">
|
| 74 |
+
<label for="system">system prompt</label>
|
| 75 |
+
<textarea id="system" rows="5" spellcheck="false"></textarea>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="field-row">
|
| 78 |
+
<div class="field">
|
| 79 |
+
<label for="temp">temperature <span id="tempval" class="mono-val">0.70</span></label>
|
| 80 |
+
<input id="temp" type="range" min="0" max="1.5" step="0.05" value="0.7">
|
| 81 |
+
</div>
|
| 82 |
+
<div class="field">
|
| 83 |
+
<label for="maxtok">max tokens</label>
|
| 84 |
+
<input id="maxtok" type="number" min="1000" max="128000" step="1000" value="16000">
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</details>
|
| 88 |
+
|
| 89 |
+
<div class="actions">
|
| 90 |
+
<button id="generate" class="btn btn-primary btn-lg btn-full" type="button">
|
| 91 |
+
<i class="ri-terminal-line" aria-hidden="true"></i>Generate
|
| 92 |
+
</button>
|
| 93 |
+
<button id="stop" class="btn btn-secondary btn-lg" type="button" hidden>Stop</button>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div class="examples">
|
| 97 |
+
<span class="examples-label">try</span>
|
| 98 |
+
<button class="tag" type="button" data-example="A one-page portfolio for a landscape photographer named Ilse Moreau. Full-bleed hero image from picsum.photos, a masonry gallery of 9 photos with a lightbox, a short about section, and a contact form. Minimal, thin serif headings.">portfolio</button>
|
| 99 |
+
<button class="tag" type="button" data-example="A playable Snake game. Canvas-based, arrow keys and swipe support, score and high score persisted, a start overlay, and a monochrome terminal aesthetic.">snake</button>
|
| 100 |
+
<button class="tag" type="button" data-example="A pricing page for a developer tool called Relay. Three tiers with a monthly/annual toggle that recalculates prices, a feature comparison table, and an FAQ. Square corners, one accent colour, mono for every number.">pricing</button>
|
| 101 |
+
<button class="tag" type="button" data-example="A deployment dashboard: four KPI tiles, a line chart of requests over 12 months, and a table of recent deploys with status dots and mono timestamps. Draw charts with inline SVG, no libraries.">dashboard</button>
|
| 102 |
+
</div>
|
| 103 |
+
</aside>
|
| 104 |
+
|
| 105 |
+
<!-- ── output ─────────────────────────────────────────────────── -->
|
| 106 |
+
<section class="output">
|
| 107 |
+
<div class="tabs" role="tablist">
|
| 108 |
+
<button class="tab is-active" id="tab-preview" role="tab" aria-selected="true" type="button">preview</button>
|
| 109 |
+
<button class="tab" id="tab-code" role="tab" aria-selected="false" type="button">
|
| 110 |
+
code <span id="bytes" class="meta"></span>
|
| 111 |
+
</button>
|
| 112 |
+
<div class="tabs-spacer"></div>
|
| 113 |
+
<span id="status" class="status"></span>
|
| 114 |
+
<button id="reload" class="btn btn-ghost btn-sm" type="button">
|
| 115 |
+
<i class="ri-refresh-line" aria-hidden="true"></i>Re-render
|
| 116 |
+
</button>
|
| 117 |
+
<button id="download" class="btn btn-ghost btn-sm" type="button">
|
| 118 |
+
<i class="ri-download-2-line" aria-hidden="true"></i>Download
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<div class="pane pane-preview" id="pane-preview">
|
| 123 |
+
<div class="empty" id="empty">
|
| 124 |
+
<span class="corners" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
|
| 125 |
+
<i class="ri-code-s-slash-line empty-mark" aria-hidden="true"></i>
|
| 126 |
+
<p class="empty-title">Nothing rendered yet.</p>
|
| 127 |
+
<p class="hint">Pick a model, describe the site, then generate.</p>
|
| 128 |
+
</div>
|
| 129 |
+
<iframe id="preview" title="Live preview of the generated site"
|
| 130 |
+
sandbox="allow-scripts allow-modals allow-popups allow-forms allow-pointer-lock allow-downloads"
|
| 131 |
+
hidden></iframe>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div class="pane pane-code" id="pane-code" hidden>
|
| 135 |
+
<textarea id="code" spellcheck="false" autocomplete="off"
|
| 136 |
+
placeholder="The generated document lands here, and it is editable. Edits re-render and are what gets deployed."></textarea>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<!-- ── deploy ───────────────────────────────────────────────── -->
|
| 140 |
+
<div class="deploybar">
|
| 141 |
+
<button id="deploy" class="btn btn-primary" type="button" disabled>
|
| 142 |
+
<i class="ri-rocket-line" aria-hidden="true"></i><span id="deploylabel">Deploy to harvis.dev</span>
|
| 143 |
+
</button>
|
| 144 |
+
<button id="newsite" class="btn btn-ghost btn-sm" type="button" hidden
|
| 145 |
+
title="Create a new site instead of updating this one">New site</button>
|
| 146 |
+
<div class="deployresult" id="deployresult"></div>
|
| 147 |
+
</div>
|
| 148 |
+
</section>
|
| 149 |
+
</main>
|
| 150 |
+
|
| 151 |
+
<div id="toast" class="toast" role="status" aria-live="polite" hidden>
|
| 152 |
+
<span class="corners" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
|
| 153 |
+
<span id="toastdot" class="dot"></span>
|
| 154 |
+
<span id="toastmsg"></span>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<script type="module" src="app.js"></script>
|
| 158 |
+
</body>
|
| 159 |
</html>
|
styles.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tokens/fonts.css";
|
| 2 |
+
@import "assets/icons/remixicon.css";
|
| 3 |
+
@import "tokens/colors.css";
|
| 4 |
+
@import "tokens/typography.css";
|
| 5 |
+
@import "tokens/type-nordwand.css";
|
| 6 |
+
@import "tokens/spacing.css";
|
| 7 |
+
@import "tokens/radius-shadow.css";
|
| 8 |
+
@import "tokens/motion.css";
|
| 9 |
+
@import "tokens/base.css";
|
| 10 |
+
@import "tokens/theme-light.css";
|
tokens/base.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*,*::before,*::after{box-sizing:border-box}
|
| 2 |
+
html{-webkit-text-size-adjust:100%}
|
| 3 |
+
body{margin:0;background:var(--bg-canvas);color:var(--text-primary);font:var(--type-body);letter-spacing:var(--tracking-body);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
| 4 |
+
h1,h2,h3,h4{margin:0;letter-spacing:var(--tracking-heading);text-wrap:balance}
|
| 5 |
+
h1{font:var(--type-h1);letter-spacing:var(--tracking-display)}h2{font:var(--type-h2)}h3{font:var(--type-h3)}
|
| 6 |
+
p{margin:0;text-wrap:pretty}
|
| 7 |
+
a{color:var(--text-link);text-decoration:none;transition:var(--t-color)}
|
| 8 |
+
a:hover{color:var(--text-link-hover)}
|
| 9 |
+
code,kbd,pre,samp{font-family:var(--font-mono);font-size:.92em}
|
| 10 |
+
::selection{background:var(--selection-bg)}
|
| 11 |
+
:focus-visible{outline:none;box-shadow:var(--ring-focus)}
|
| 12 |
+
hr{border:0;border-top:1px solid var(--border-hairline);margin:var(--space-8) 0}
|
| 13 |
+
button{font-family:inherit}
|
tokens/colors.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
/* --- Base ramp: ink (warm near-black) --- */
|
| 3 |
+
--ink-1000:#0C0E10;--ink-900:#13161A;--ink-850:#191D21;--ink-800:#20242A;--ink-750:#262B31;--ink-700:#2F343B;--ink-600:#3A4047;--ink-500:#484F57;
|
| 4 |
+
/* --- Base ramp: neutral --- */
|
| 5 |
+
--gray-500:#767E86;--gray-400:#949BA3;--gray-300:#B4BAC1;--gray-200:#D2D6DA;--gray-100:#E9EBED;
|
| 6 |
+
/* --- Base ramp: paper (warm off-white, carried from harvis.dev) --- */
|
| 7 |
+
--paper-50:#FDFDF8;--paper-100:#F6F7EE;--paper-200:#ECEEE1;--paper-300:#DEE0D2;--white:#FFFFFF;
|
| 8 |
+
/* --- Accent: flare (brand orange-red) --- */
|
| 9 |
+
--flare-300:#FF9575;--flare-400:#FF6E42;--flare-500:#FF4D17;--flare-600:#E03A08;--flare-700:#B32D05;
|
| 10 |
+
--flare-a12:rgba(255,77,23,.12);--flare-a24:rgba(255,77,23,.24);--flare-a40:rgba(255,77,23,.40);
|
| 11 |
+
/* --- Semantic hues --- */
|
| 12 |
+
--signal-400:#5CE49F;--signal-500:#2DD37F;--signal-600:#17A863;--signal-a12:rgba(45,211,127,.12);
|
| 13 |
+
--azure-400:#7AACFF;--azure-500:#4C8DFF;--azure-600:#2E6FE0;--azure-a12:rgba(76,141,255,.12);
|
| 14 |
+
--amber-400:#FFC94D;--amber-500:#F5B324;--amber-600:#D2930C;--amber-a12:rgba(245,179,36,.12);
|
| 15 |
+
--rose-400:#FF6E88;--rose-500:#FF3B5C;--rose-600:#DC1F3F;--rose-a12:rgba(255,59,92,.12);
|
| 16 |
+
/* --- Alpha utilities (dark surfaces) --- */
|
| 17 |
+
--w-a04:rgba(255,255,255,.04);--w-a06:rgba(255,255,255,.06);--w-a08:rgba(255,255,255,.08);--w-a10:rgba(255,255,255,.10);--w-a12:rgba(255,255,255,.12);--w-a14:rgba(255,255,255,.14);--w-a18:rgba(255,255,255,.18);--w-a24:rgba(255,255,255,.24);--w-a28:rgba(255,255,255,.28);
|
| 18 |
+
--k-a06:rgba(6,7,8,.06);--k-a10:rgba(6,7,8,.10);--k-a16:rgba(6,7,8,.16);--k-a40:rgba(6,7,8,.40);
|
| 19 |
+
|
| 20 |
+
/* === Semantic aliases — dark is the default theme === */
|
| 21 |
+
--bg-canvas:var(--ink-900);--bg-surface:var(--ink-850);--bg-raised:var(--ink-800);--bg-inset:var(--ink-1000);
|
| 22 |
+
--bg-hover:var(--w-a06);--bg-active:var(--w-a10);--bg-selected:var(--flare-a12);
|
| 23 |
+
--surface-card:var(--ink-850);--surface-overlay:rgba(12,14,16,.74);--surface-code:var(--ink-1000);
|
| 24 |
+
--surface-nav:rgba(10,11,12,.72);
|
| 25 |
+
--text-primary:#F7F8F6;--text-secondary:var(--gray-200);--text-muted:var(--gray-300);--text-faint:var(--gray-400);
|
| 26 |
+
--text-inverse:var(--ink-900);--text-accent:var(--flare-300);--text-link:var(--flare-300);--text-link-hover:#FFB59B;
|
| 27 |
+
--border-hairline:var(--w-a10);--border-default:var(--w-a14);--border-strong:var(--w-a28);--border-focus:var(--flare-400);
|
| 28 |
+
--accent:var(--flare-500);--accent-hover:var(--flare-400);--accent-press:var(--flare-600);--accent-fg:#FFFFFF;--accent-quiet:var(--flare-a12);
|
| 29 |
+
--status-live:var(--signal-400);--status-building:var(--amber-400);--status-error:var(--rose-400);--status-idle:var(--gray-400);--status-info:var(--azure-400);
|
| 30 |
+
--selection-bg:var(--flare-a24);
|
| 31 |
+
--control-track:var(--ink-600);--control-knob:var(--gray-300);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* The light theme lives in tokens/theme-light.css (imported last). */
|
tokens/fonts.css
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Harvis typefaces.
|
| 2 |
+
Display/UI + code are the closest Google Fonts matches to the reference developer-tool
|
| 3 |
+
aesthetic (no binaries were supplied): Instrument Sans + JetBrains Mono.
|
| 4 |
+
The logomark and the all-mono variant both use Nordwand Mono — see tokens/type-nordwand.css. */
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..600&family=JetBrains+Mono:ital,wght@0,400..700;1,400..600&display=swap');
|
tokens/motion.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
--dur-instant:80ms; /* @kind other */
|
| 3 |
+
--dur-fast:140ms; /* @kind other */
|
| 4 |
+
--dur-base:220ms; /* @kind other */
|
| 5 |
+
--dur-slow:400ms; /* @kind other */
|
| 6 |
+
--dur-slower:700ms; /* @kind other */
|
| 7 |
+
--ease-out:cubic-bezier(.2,.8,.2,1); /* @kind other */
|
| 8 |
+
--ease-in-out:cubic-bezier(.4,0,.2,1); /* @kind other */
|
| 9 |
+
--ease-in:cubic-bezier(.5,0,1,1); /* @kind other */
|
| 10 |
+
--ease-snap:cubic-bezier(.16,1,.3,1); /* @kind other */
|
| 11 |
+
--t-color:color var(--dur-fast) var(--ease-out),background-color var(--dur-fast) var(--ease-out),border-color var(--dur-fast) var(--ease-out);
|
| 12 |
+
--t-control:var(--t-color),box-shadow var(--dur-fast) var(--ease-out),transform var(--dur-instant) var(--ease-out);
|
| 13 |
+
--t-panel:opacity var(--dur-base) var(--ease-out),transform var(--dur-base) var(--ease-snap);
|
| 14 |
+
}
|
| 15 |
+
@media (prefers-reduced-motion:reduce){:root{
|
| 16 |
+
--dur-instant:0ms; /* @kind other */
|
| 17 |
+
--dur-fast:0ms; /* @kind other */
|
| 18 |
+
--dur-base:0ms; /* @kind other */
|
| 19 |
+
--dur-slow:0ms; /* @kind other */
|
| 20 |
+
--dur-slower:0ms; /* @kind other */
|
| 21 |
+
}}
|
tokens/radius-shadow.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
/* Blocky by design: square corners everywhere, 2px only where a card meets the canvas */
|
| 3 |
+
--radius-xs:0px;--radius-sm:0px;--radius-md:2px;--radius-lg:2px;--radius-xl:4px;--radius-pill:0px;
|
| 4 |
+
--radius-control:var(--radius-sm);--radius-card:var(--radius-md);--radius-panel:var(--radius-lg);
|
| 5 |
+
/* Signature corner brackets — crop marks echoing the [#] logomark */
|
| 6 |
+
--corner-arm:9px;--corner-weight:1px;--corner-inset:-1px;--corner-color:var(--border-strong);--corner-color-accent:var(--flare-500);
|
| 7 |
+
/* Elevation: hard offset blocks, no blur — the shadow is a shifted copy of the shape */
|
| 8 |
+
--shadow-none:none;
|
| 9 |
+
--shadow-1:2px 2px 0 rgba(4,5,6,.65);
|
| 10 |
+
--shadow-2:3px 3px 0 rgba(4,5,6,.70);
|
| 11 |
+
--shadow-3:5px 5px 0 rgba(4,5,6,.75);
|
| 12 |
+
--shadow-4:8px 8px 0 rgba(4,5,6,.80);
|
| 13 |
+
--shadow-inset-top:inset 0 1px 0 rgba(255,255,255,.10);
|
| 14 |
+
--shadow-inset-well:inset 0 0 0 1px rgba(4,5,6,.70);
|
| 15 |
+
--ring-focus:0 0 0 2px var(--bg-canvas),0 0 0 4px var(--flare-500);
|
| 16 |
+
--glow-accent:0 0 0 1px var(--flare-500),4px 4px 0 var(--flare-a24);
|
| 17 |
+
--glow-live:0 0 0 1px rgba(45,211,127,.45);
|
| 18 |
+
/* Signature background wash — a soft accent bloom, no grid or pattern */
|
| 19 |
+
--wash-tint:rgba(255,77,23,.09);
|
| 20 |
+
--bg-grid:radial-gradient(120% 92% at 50% -12%,var(--wash-tint),transparent 62%);
|
| 21 |
+
--bg-grid-size:cover;
|
| 22 |
+
--bg-wash:var(--bg-grid);
|
| 23 |
+
--bg-wash-neutral:radial-gradient(110% 90% at 50% -10%,var(--w-a04),transparent 60%);
|
| 24 |
+
--fade-bottom:linear-gradient(to bottom,transparent,var(--bg-canvas));
|
| 25 |
+
--fade-top:linear-gradient(to top,transparent,var(--bg-canvas));
|
| 26 |
+
}
|
tokens/spacing.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
--space-0:0px;--space-px:1px;--space-0-5:2px;--space-1:4px;--space-1-5:6px;--space-2:8px;--space-2-5:10px;
|
| 3 |
+
--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-7:28px;--space-8:32px;--space-10:40px;
|
| 4 |
+
--space-12:48px;--space-16:64px;--space-20:80px;--space-24:96px;--space-32:128px;--space-40:160px;
|
| 5 |
+
/* Layout */
|
| 6 |
+
--container-max:1200px;--container-prose:660px;--container-wide:1440px;
|
| 7 |
+
--gutter-sm:20px;--gutter-md:32px;--gutter-lg:48px;
|
| 8 |
+
--section-y:96px;--section-y-lg:144px;
|
| 9 |
+
--nav-h:56px;--sidebar-w:240px;--sidebar-w-collapsed:56px;--control-h-sm:28px;--control-h:34px;--control-h-lg:42px;
|
| 10 |
+
}
|
tokens/theme-light.css
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* === Light theme ("paper") — a complete inversion, not a tint pass ===
|
| 2 |
+
Applied with data-theme="light" (or the legacy alias data-theme="paper") on
|
| 3 |
+
<html>, <body> or any subtree. Every semantic alias is redefined here, plus
|
| 4 |
+
the few raw ramp steps and white-alpha utilities that components reach for
|
| 5 |
+
directly, so no component needs a light-specific branch.
|
| 6 |
+
Imported last so these overrides win over the :root defaults. */
|
| 7 |
+
|
| 8 |
+
[data-theme="light"],[data-theme="paper"]{
|
| 9 |
+
/* --- Raw ramp remaps: keep component internals honest --- */
|
| 10 |
+
--ink-500:#AEB4BB;--ink-600:#C7CCD2;--ink-700:#E3E5E8;--ink-750:#EDEFF1;--ink-800:#F3F4F0;--ink-850:#F9FAF4;--ink-900:#F6F7EE;--ink-1000:#ECEEE1;
|
| 11 |
+
/* Alpha utilities flip from white-on-dark to ink-on-paper */
|
| 12 |
+
--w-a04:rgba(6,7,8,.035);--w-a06:rgba(6,7,8,.05);--w-a08:rgba(6,7,8,.065);--w-a10:rgba(6,7,8,.08);--w-a12:rgba(6,7,8,.10);--w-a14:rgba(6,7,8,.13);--w-a18:rgba(6,7,8,.16);--w-a24:rgba(6,7,8,.21);--w-a28:rgba(6,7,8,.26);
|
| 13 |
+
/* Status hues darken so they carry on paper (400 is the on-surface rank) */
|
| 14 |
+
--flare-400:#D63A06;--signal-400:#0E8B54;--azure-400:#255FCC;--amber-400:#8C6006;--rose-400:#C81438;
|
| 15 |
+
|
| 16 |
+
/* --- Surfaces --- */
|
| 17 |
+
--bg-canvas:var(--paper-100);--bg-surface:var(--paper-50);--bg-raised:var(--white);--bg-inset:var(--paper-200);
|
| 18 |
+
--bg-hover:var(--k-a06);--bg-active:var(--k-a10);--bg-selected:var(--flare-a12);
|
| 19 |
+
--surface-card:var(--paper-50);--surface-overlay:rgba(236,238,225,.78);--surface-code:var(--paper-200);
|
| 20 |
+
--surface-nav:rgba(246,247,238,.76);
|
| 21 |
+
|
| 22 |
+
/* --- Text: four ranks, same hierarchy, inverted --- */
|
| 23 |
+
--text-primary:#16191D;--text-secondary:#454C53;--text-muted:#636A72;--text-faint:#7C848C;
|
| 24 |
+
--text-inverse:var(--paper-50);--text-accent:var(--flare-600);--text-link:var(--flare-600);--text-link-hover:var(--flare-700);
|
| 25 |
+
|
| 26 |
+
/* --- Structure --- */
|
| 27 |
+
--border-hairline:rgba(6,7,8,.11);--border-default:rgba(6,7,8,.17);--border-strong:rgba(6,7,8,.30);--border-focus:var(--flare-600);
|
| 28 |
+
--corner-color:rgba(6,7,8,.30);--corner-color-accent:var(--flare-600);
|
| 29 |
+
|
| 30 |
+
/* --- Accent: steps down a rank so white text still clears contrast --- */
|
| 31 |
+
--accent:var(--flare-600);--accent-hover:var(--flare-500);--accent-press:var(--flare-700);--accent-fg:#FFFFFF;--accent-quiet:var(--flare-a12);
|
| 32 |
+
--status-live:var(--signal-600);--status-building:var(--amber-600);--status-error:var(--rose-600);--status-idle:var(--gray-500);--status-info:var(--azure-600);
|
| 33 |
+
--selection-bg:var(--flare-a24);
|
| 34 |
+
--control-track:#B9BFC6;--control-knob:var(--white);
|
| 35 |
+
|
| 36 |
+
/* --- Elevation: the hard offset block stays, at paper weight --- */
|
| 37 |
+
--shadow-1:2px 2px 0 rgba(18,21,25,.13);
|
| 38 |
+
--shadow-2:3px 3px 0 rgba(18,21,25,.15);
|
| 39 |
+
--shadow-3:5px 5px 0 rgba(18,21,25,.17);
|
| 40 |
+
--shadow-4:8px 8px 0 rgba(18,21,25,.19);
|
| 41 |
+
--shadow-inset-top:inset 0 1px 0 rgba(255,255,255,.45);
|
| 42 |
+
--shadow-inset-well:inset 0 0 0 1px rgba(18,21,25,.12);
|
| 43 |
+
--ring-focus:0 0 0 2px var(--bg-canvas),0 0 0 4px var(--flare-600);
|
| 44 |
+
--glow-accent:0 0 0 1px var(--flare-600),4px 4px 0 var(--flare-a24);
|
| 45 |
+
--glow-live:0 0 0 1px rgba(23,168,99,.42);
|
| 46 |
+
/* --- Wash: the same bloom, dialled back so paper stays paper --- */
|
| 47 |
+
--wash-tint:rgba(255,77,23,.07);
|
| 48 |
+
--bg-wash-neutral:radial-gradient(110% 90% at 50% -10%,rgba(6,7,8,.045),transparent 60%);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* A themed subtree paints its own canvas rather than inheriting the page's */
|
| 52 |
+
[data-theme="light"],[data-theme="paper"]{background:var(--bg-canvas);color:var(--text-primary)}
|
tokens/type-nordwand.css
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
Nordwand Mono — the logomark face (see --font-logomark) and the
|
| 3 |
+
all-mono cut of the whole system.
|
| 4 |
+
Scope the variant with data-typeface="nordwand" on <html> or any subtree.
|
| 5 |
+
============================================================ */
|
| 6 |
+
|
| 7 |
+
@font-face{font-family:'Nordwand Mono';src:url('../assets/fonts/NordwandMono-Regular.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
|
| 8 |
+
@font-face{font-family:'Nordwand Mono';src:url('../assets/fonts/NordwandMono-Medium.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}
|
| 9 |
+
@font-face{font-family:'Nordwand Mono';src:url('../assets/fonts/NordwandMono-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}
|
| 10 |
+
|
| 11 |
+
:root{--font-nordwand:'Nordwand Mono',ui-monospace,monospace}
|
| 12 |
+
|
| 13 |
+
[data-typeface="nordwand"]{
|
| 14 |
+
--font-sans:var(--font-nordwand);
|
| 15 |
+
--font-mono:var(--font-nordwand);
|
| 16 |
+
--font-display:var(--font-nordwand);
|
| 17 |
+
/* A mono face carries its own rhythm: relax the negative tracking and open the leading */
|
| 18 |
+
--tracking-display:-0.01em;--tracking-heading:-0.005em;--tracking-body:0em;--tracking-label:0.06em;
|
| 19 |
+
--leading-tight:1.14;--leading-snug:1.3;--leading-normal:1.55;--leading-relaxed:1.7;
|
| 20 |
+
/* Mono runs wide — step the display ranks down so headlines still fit their measure */
|
| 21 |
+
--text-4xl:36px;--text-5xl:46px;--text-6xl:58px;
|
| 22 |
+
--weight-semibold:500;--weight-bold:700;
|
| 23 |
+
}
|
tokens/typography.css
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
--font-sans:'Instrument Sans',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
|
| 3 |
+
--font-mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;
|
| 4 |
+
--font-display:var(--font-sans);
|
| 5 |
+
--font-logomark:'Nordwand Mono',ui-monospace,monospace;
|
| 6 |
+
--font-pixel:var(--font-logomark);
|
| 7 |
+
/* Scale — 13px is the UI baseline, mono runs 1px smaller than sans at the same rank */
|
| 8 |
+
--text-2xs:11px;--text-xs:13px;--text-sm:14px;--text-md:15px;--text-base:16px;--text-lg:19px;
|
| 9 |
+
--text-xl:21px;--text-2xl:26px;--text-3xl:32px;--text-4xl:42px;--text-5xl:56px;--text-6xl:72px;
|
| 10 |
+
--leading-tight:1.1;--leading-snug:1.28;--leading-normal:1.55;--leading-relaxed:1.7;--leading-code:1.65;
|
| 11 |
+
--tracking-display:-0.03em;--tracking-heading:-0.018em;--tracking-body:-0.005em;--tracking-mono:0em;--tracking-label:0.08em;
|
| 12 |
+
--weight-regular:400;--weight-book:450;--weight-medium:500;--weight-semibold:600;--weight-bold:700;
|
| 13 |
+
/* Named roles */
|
| 14 |
+
--type-display:var(--weight-semibold) var(--text-5xl)/var(--leading-tight) var(--font-display);
|
| 15 |
+
--type-h1:var(--weight-semibold) var(--text-4xl)/var(--leading-tight) var(--font-display);
|
| 16 |
+
--type-h2:var(--weight-semibold) var(--text-2xl)/var(--leading-snug) var(--font-display);
|
| 17 |
+
--type-h3:var(--weight-semibold) var(--text-lg)/var(--leading-snug) var(--font-sans);
|
| 18 |
+
--type-body:var(--weight-regular) var(--text-base)/var(--leading-relaxed) var(--font-sans);
|
| 19 |
+
--type-ui:var(--weight-medium) var(--text-sm)/1.4 var(--font-sans);
|
| 20 |
+
--type-code:var(--weight-regular) var(--text-xs)/var(--leading-code) var(--font-mono);
|
| 21 |
+
--type-label:var(--weight-medium) var(--text-2xs)/1.2 var(--font-mono);
|
| 22 |
+
}
|