fordnox commited on
Commit
8da8a3a
·
verified ·
1 Parent(s): 0798d16

Upload folder using huggingface_hub

Browse files
.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: Html Studio
3
- emoji: 📊
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: static
 
 
 
 
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }