@cfbender/cesium 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +26 -9
- package/assets/styleguide.html +149 -0
- package/package.json +1 -1
- package/src/cli/commands/theme.ts +2 -0
- package/src/render/critique.ts +1 -1
- package/src/render/favicon.ts +56 -0
- package/src/render/theme.ts +81 -0
- package/src/render/wrap.ts +20 -1
- package/src/server/favicon.ts +28 -0
- package/src/server/lifecycle.ts +4 -0
- package/src/storage/favicon-write.ts +23 -0
- package/src/storage/index-gen.ts +22 -4
- package/src/tools/ask.ts +3 -1
- package/src/tools/publish.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.6 — 2026-05-11
|
|
4
|
+
|
|
5
|
+
Adds a periodic-table-themed favicon for the cesium HTTP server.
|
|
6
|
+
|
|
7
|
+
- **feat:** Element-55 ("Cs") periodic-table tile favicon in the claret-dark
|
|
8
|
+
palette. Source SVG at `assets/favicon.svg`.
|
|
9
|
+
- **feat:** `writeFaviconSvg` drops `<stateDir>/favicon.svg` next to
|
|
10
|
+
`theme.css` on every publish/ask (and on `cesium theme apply`). The static
|
|
11
|
+
HTTP server then serves it at `/favicon.svg`.
|
|
12
|
+
- **feat:** `<link rel="icon" type="image/svg+xml">` is now emitted in
|
|
13
|
+
artifact pages, project index pages, and the global index page (paths
|
|
14
|
+
derived from the existing theme.css href so suppression behavior matches).
|
|
15
|
+
- **feat:** Inline favicon emblem rendered next to the "cesium" / "cesium ·
|
|
16
|
+
project" eyebrow on both index pages.
|
|
17
|
+
- **feat:** `/favicon.ico` shim — server pre-handler serves the SVG bytes
|
|
18
|
+
with `image/svg+xml` content type so browsers that auto-request the legacy
|
|
19
|
+
`.ico` path don't see a 404.
|
|
20
|
+
|
|
3
21
|
## v0.3.5 — 2026-05-11
|
|
4
22
|
|
|
5
23
|
Fixes the `Publish to npm` GitHub Action.
|
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
<h1>
|
|
2
|
+
<img src="assets/favicon.svg" alt="" width="48" height="48" align="left" style="margin-right: 12px; vertical-align: middle;">
|
|
3
|
+
Cesium
|
|
4
|
+
</h1>
|
|
2
5
|
|
|
3
6
|
Cesium publishes substantive opencode agent responses — plans, code reviews,
|
|
4
7
|
comparisons, explainers, audits, RFCs — as self-contained beautiful HTML artifacts
|
|
@@ -6,10 +9,6 @@ on disk, instead of dumping markdown into the terminal. The browser becomes the
|
|
|
6
9
|
reading surface; the terminal stays the control surface. Each artifact is a single
|
|
7
10
|
`.html` file: portable, archivable, viewable offline, shareable as a URL over SSH.
|
|
8
11
|
|
|
9
|
-
v0.3.0 adds **interactive Q&A artifacts** — the agent can now publish a question
|
|
10
|
-
form, wait for the user to answer in their browser, and receive the structured
|
|
11
|
-
responses before continuing work.
|
|
12
|
-
|
|
13
12
|
<video src="assets/cesium.mp4" autoplay loop muted playsinline width="720">
|
|
14
13
|
Demo video — see <a href="assets/cesium.mp4">assets/cesium.mp4</a> if it
|
|
15
14
|
doesn't play inline (some markdown viewers strip <code><video></code>).
|
|
@@ -51,19 +50,37 @@ unreleased changes).
|
|
|
51
50
|
|
|
52
51
|
### CLI
|
|
53
52
|
|
|
53
|
+
The CLI puts a `cesium` binary on your `PATH` for browsing, opening, and
|
|
54
|
+
managing artifacts (`cesium ls`, `cesium open`, `cesium serve`, `cesium prune`,
|
|
55
|
+
`cesium theme`).
|
|
56
|
+
|
|
57
|
+
**Recommended: install with [mise](https://mise.jdx.dev/)** so cesium is pinned
|
|
58
|
+
in your config and tracks with the rest of your toolchain. Add to your
|
|
59
|
+
`~/.config/mise/config.toml` (or a project-local `mise.toml`):
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[tools]
|
|
63
|
+
"npm:@cfbender/cesium" = "latest"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then run `mise install` (or `mise use -g npm:@cfbender/cesium@latest` for the
|
|
67
|
+
one-liner equivalent). Pin to a specific release with `"0.3.6"` instead of
|
|
68
|
+
`"latest"`. Upgrade with `mise upgrade npm:@cfbender/cesium`.
|
|
69
|
+
|
|
70
|
+
**Alternative: install with bun directly:**
|
|
71
|
+
|
|
54
72
|
```bash
|
|
55
73
|
bun install -g @cfbender/cesium
|
|
56
74
|
```
|
|
57
75
|
|
|
58
|
-
This puts
|
|
59
|
-
|
|
76
|
+
This puts the binary at `~/.bun/bin/cesium`. If `which cesium` returns nothing,
|
|
77
|
+
add `~/.bun/bin` to your shell rc:
|
|
60
78
|
|
|
61
79
|
```bash
|
|
62
80
|
export PATH="$HOME/.bun/bin:$PATH"
|
|
63
81
|
```
|
|
64
82
|
|
|
65
|
-
Upgrade
|
|
66
|
-
manager — e.g. `mise use -g npm:@cfbender/cesium@latest`). To uninstall:
|
|
83
|
+
Upgrade with `bun update -g @cfbender/cesium`. Uninstall with
|
|
67
84
|
`bun remove -g @cfbender/cesium`.
|
|
68
85
|
|
|
69
86
|
### Developing on cesium itself
|
package/assets/styleguide.html
CHANGED
|
@@ -329,6 +329,89 @@
|
|
|
329
329
|
color: var(--muted);
|
|
330
330
|
text-transform: lowercase;
|
|
331
331
|
}
|
|
332
|
+
.pill.accent {
|
|
333
|
+
background: color-mix(in srgb, var(--accent) 18%, var(--surface));
|
|
334
|
+
color: var(--accent);
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ranked list */
|
|
339
|
+
.ranked-list {
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
gap: 1em;
|
|
343
|
+
margin: 0 0 1.5em;
|
|
344
|
+
padding: 0;
|
|
345
|
+
list-style: none;
|
|
346
|
+
}
|
|
347
|
+
.ranked-item {
|
|
348
|
+
background: var(--surface);
|
|
349
|
+
border: 1.5px solid var(--rule);
|
|
350
|
+
border-radius: 12px;
|
|
351
|
+
padding: 22px 26px;
|
|
352
|
+
display: grid;
|
|
353
|
+
grid-template-columns: 64px 1fr;
|
|
354
|
+
gap: 6px 24px;
|
|
355
|
+
align-items: start;
|
|
356
|
+
}
|
|
357
|
+
.ranked-item .rank-num {
|
|
358
|
+
font-family: var(--serif);
|
|
359
|
+
font-size: 2.4rem;
|
|
360
|
+
font-weight: 500;
|
|
361
|
+
color: var(--oat);
|
|
362
|
+
line-height: 1;
|
|
363
|
+
letter-spacing: -0.02em;
|
|
364
|
+
padding-top: 2px;
|
|
365
|
+
}
|
|
366
|
+
.ranked-item .rank-title {
|
|
367
|
+
font-family: var(--serif);
|
|
368
|
+
font-size: 1.2rem;
|
|
369
|
+
font-weight: 600;
|
|
370
|
+
color: var(--ink);
|
|
371
|
+
margin: 0 0 6px;
|
|
372
|
+
line-height: 1.3;
|
|
373
|
+
}
|
|
374
|
+
.ranked-item .rank-meta {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 8px;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
margin-bottom: 14px;
|
|
380
|
+
}
|
|
381
|
+
.ranked-item .rank-body > p {
|
|
382
|
+
color: var(--ink-soft);
|
|
383
|
+
font-size: 0.95rem;
|
|
384
|
+
line-height: 1.65;
|
|
385
|
+
margin: 0 0 0.85em;
|
|
386
|
+
}
|
|
387
|
+
.ranked-item .rank-body > p:last-child {
|
|
388
|
+
margin-bottom: 0;
|
|
389
|
+
}
|
|
390
|
+
.ranked-item .rank-body > ul,
|
|
391
|
+
.ranked-item .rank-body > ol {
|
|
392
|
+
color: var(--ink-soft);
|
|
393
|
+
font-size: 0.95rem;
|
|
394
|
+
line-height: 1.65;
|
|
395
|
+
margin: 0 0 0.85em;
|
|
396
|
+
padding-left: 1.2em;
|
|
397
|
+
}
|
|
398
|
+
.ranked-item .rank-aside {
|
|
399
|
+
color: var(--muted);
|
|
400
|
+
font-size: 0.9rem;
|
|
401
|
+
line-height: 1.6;
|
|
402
|
+
padding-left: 14px;
|
|
403
|
+
border-left: 2px solid var(--rule);
|
|
404
|
+
margin: 0;
|
|
405
|
+
}
|
|
406
|
+
.ranked-item .rank-aside-label {
|
|
407
|
+
font-family: var(--mono);
|
|
408
|
+
font-size: 0.7rem;
|
|
409
|
+
font-weight: 600;
|
|
410
|
+
letter-spacing: 0.1em;
|
|
411
|
+
text-transform: uppercase;
|
|
412
|
+
color: var(--accent);
|
|
413
|
+
margin-right: 6px;
|
|
414
|
+
}
|
|
332
415
|
|
|
333
416
|
/* byline */
|
|
334
417
|
.byline {
|
|
@@ -845,6 +928,72 @@
|
|
|
845
928
|
</div>
|
|
846
929
|
</section>
|
|
847
930
|
|
|
931
|
+
<!-- ============================================================
|
|
932
|
+
Section 13 · Ranked list
|
|
933
|
+
============================================================ -->
|
|
934
|
+
<section>
|
|
935
|
+
<p class="eyebrow">13 · ranked-list</p>
|
|
936
|
+
<h2 class="h-section"><span class="section-num">13</span>Ranked list</h2>
|
|
937
|
+
<p>
|
|
938
|
+
Use <code>.ranked-list</code> + <code>.ranked-item</code> for ordered recommendations,
|
|
939
|
+
findings, or rankings — anything that would otherwise be a heavy numbered
|
|
940
|
+
<code><h3></code> sequence. Each item gets a soft serif numeral on the left and a
|
|
941
|
+
calm title + meta + body on the right. Pair <code>.pill.accent</code> +
|
|
942
|
+
<code>.tag</code> in the meta row for impact and savings; use <code>.rank-aside</code> for
|
|
943
|
+
a quieter "bonus" addendum.
|
|
944
|
+
</p>
|
|
945
|
+
|
|
946
|
+
<ol class="ranked-list">
|
|
947
|
+
<li class="ranked-item">
|
|
948
|
+
<div class="rank-num">01</div>
|
|
949
|
+
<div class="rank-body">
|
|
950
|
+
<h3 class="rank-title">Replace the styleguide payload with a compact catalog</h3>
|
|
951
|
+
<div class="rank-meta">
|
|
952
|
+
<span class="pill accent">High impact</span>
|
|
953
|
+
<span class="tag">~6–8k tokens / call</span>
|
|
954
|
+
</div>
|
|
955
|
+
<p>
|
|
956
|
+
Return a structured Markdown reference of <em>class → purpose → minimal example</em>
|
|
957
|
+
instead of the full HTML framework. Realistic target: 3–5 KB / ~1k tokens, down from
|
|
958
|
+
~7–9k.
|
|
959
|
+
</p>
|
|
960
|
+
<p class="rank-aside">
|
|
961
|
+
<span class="rank-aside-label">Bonus</span>
|
|
962
|
+
Derive the catalog from a single source so it can't drift from the framework.
|
|
963
|
+
</p>
|
|
964
|
+
</div>
|
|
965
|
+
</li>
|
|
966
|
+
<li class="ranked-item">
|
|
967
|
+
<div class="rank-num">02</div>
|
|
968
|
+
<div class="rank-body">
|
|
969
|
+
<h3 class="rank-title">Trim the system prompt fragment</h3>
|
|
970
|
+
<div class="rank-meta">
|
|
971
|
+
<span class="pill">Low impact</span>
|
|
972
|
+
<span class="tag">~400 tokens / session</span>
|
|
973
|
+
</div>
|
|
974
|
+
<p>
|
|
975
|
+
Remove redundancy with in-tool <code>description</code> strings. Nice but not
|
|
976
|
+
transformative.
|
|
977
|
+
</p>
|
|
978
|
+
</div>
|
|
979
|
+
</li>
|
|
980
|
+
<li class="ranked-item">
|
|
981
|
+
<div class="rank-num">03</div>
|
|
982
|
+
<div class="rank-body">
|
|
983
|
+
<h3 class="rank-title">Stop double-emitting CSS in <code>wrapDocument()</code></h3>
|
|
984
|
+
<div class="rank-meta">
|
|
985
|
+
<span class="pill">Disk only</span>
|
|
986
|
+
<span class="tag">~17 KB / artifact</span>
|
|
987
|
+
</div>
|
|
988
|
+
<p>
|
|
989
|
+
Inline the framework only when the linked stylesheet is unavailable; emit tokens
|
|
990
|
+
inline always.
|
|
991
|
+
</p>
|
|
992
|
+
</div>
|
|
993
|
+
</li>
|
|
994
|
+
</ol>
|
|
995
|
+
</section>
|
|
996
|
+
|
|
848
997
|
<!-- ============================================================
|
|
849
998
|
Footer / byline
|
|
850
999
|
============================================================ -->
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ThemePalette,
|
|
11
11
|
} from "../../render/theme.ts";
|
|
12
12
|
import { writeThemeCss, themeCssPath } from "../../storage/theme-write.ts";
|
|
13
|
+
import { writeFaviconSvg } from "../../storage/favicon-write.ts";
|
|
13
14
|
import { themeTokensCss } from "../../render/theme.ts";
|
|
14
15
|
import { atomicWrite } from "../../storage/write.ts";
|
|
15
16
|
import { readdir } from "node:fs/promises";
|
|
@@ -307,6 +308,7 @@ async function themeApplyCommand(argv: string[], ctx: ThemeContext): Promise<num
|
|
|
307
308
|
const cfg = (ctx.loadConfig ?? loadConfig)();
|
|
308
309
|
const { theme, presetLabel } = resolveTheme(cfg);
|
|
309
310
|
const cssPath = await writeThemeCss(cfg.stateDir, theme);
|
|
311
|
+
await writeFaviconSvg(cfg.stateDir);
|
|
310
312
|
|
|
311
313
|
if (values["rewrite-artifacts"]) {
|
|
312
314
|
const { artifacts, indexes } = await retrofitAll(cfg.stateDir, ctx.stdout);
|
package/src/render/critique.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface CritiqueResult {
|
|
|
39
39
|
const HTTP_RE = /^https?:\/\//i;
|
|
40
40
|
|
|
41
41
|
/** The only cesium-* class the framework ships with. All others are unknown. */
|
|
42
|
-
const KNOWN_CESIUM_CLASSES = new Set(["cesium-back"]);
|
|
42
|
+
const KNOWN_CESIUM_CLASSES = new Set(["cesium-back", "cesium-eyebrow"]);
|
|
43
43
|
|
|
44
44
|
/** Callout severity modifiers — a callout needs at least one of these. */
|
|
45
45
|
const CALLOUT_MODIFIERS = new Set(["note", "warn", "risk"]);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Cesium favicon — periodic-table tile, element 55 ("Cs"), claret-dark palette.
|
|
2
|
+
//
|
|
3
|
+
// The SVG is theme-independent (always claret-dark wine + rose) so it stays
|
|
4
|
+
// recognizable as the cesium emblem regardless of the user's chosen theme.
|
|
5
|
+
//
|
|
6
|
+
// Two consumers:
|
|
7
|
+
// 1. `writeFaviconSvg` writes this string to <stateDir>/favicon.svg, which
|
|
8
|
+
// is then referenced by <link rel="icon"> in artifact + index pages and
|
|
9
|
+
// auto-served by the cesium HTTP server.
|
|
10
|
+
// 2. `faviconEmblemSvg` returns inline SVG markup suitable for placing next
|
|
11
|
+
// to the "cesium" eyebrow on index pages (no <?xml?> declaration, no
|
|
12
|
+
// role/aria — those are decorative chrome).
|
|
13
|
+
|
|
14
|
+
/** The full standalone favicon SVG written to <stateDir>/favicon.svg. */
|
|
15
|
+
export const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Cesium — element 55">
|
|
16
|
+
<!-- Periodic table tile, claret-dark palette -->
|
|
17
|
+
<rect x="2" y="2" width="60" height="60" rx="10" ry="10" fill="#180810"/>
|
|
18
|
+
<rect x="2.75" y="2.75" width="58.5" height="58.5" rx="9.25" ry="9.25"
|
|
19
|
+
fill="none" stroke="#C75B7A" stroke-width="1.5"/>
|
|
20
|
+
|
|
21
|
+
<!-- Atomic number, top-left -->
|
|
22
|
+
<text x="8" y="18" fill="#C75B7A"
|
|
23
|
+
font-family="ui-monospace, 'SF Mono', Menlo, Monaco, monospace"
|
|
24
|
+
font-size="13" font-weight="700" letter-spacing="0.5">55</text>
|
|
25
|
+
|
|
26
|
+
<!-- Element symbol, centered -->
|
|
27
|
+
<text x="32" y="48" fill="#DDD3C7" text-anchor="middle"
|
|
28
|
+
font-family="Georgia, 'Times New Roman', serif"
|
|
29
|
+
font-size="36" font-weight="700" letter-spacing="-1">Cs</text>
|
|
30
|
+
</svg>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/** Inline SVG emblem for placing next to the "cesium" eyebrow text.
|
|
34
|
+
* Decorative — uses aria-hidden so screen readers skip it (the text label
|
|
35
|
+
* next to it already says "cesium").
|
|
36
|
+
*
|
|
37
|
+
* @param size — pixel size for both width and height. Defaults to 18.
|
|
38
|
+
*/
|
|
39
|
+
export function faviconEmblemSvg(size = 18): string {
|
|
40
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="${size}" height="${size}" aria-hidden="true" focusable="false" style="display:inline-block;vertical-align:-3px;flex-shrink:0;">
|
|
41
|
+
<rect x="2" y="2" width="60" height="60" rx="10" ry="10" fill="#180810"/>
|
|
42
|
+
<rect x="2.75" y="2.75" width="58.5" height="58.5" rx="9.25" ry="9.25" fill="none" stroke="#C75B7A" stroke-width="1.5"/>
|
|
43
|
+
<text x="8" y="18" fill="#C75B7A" font-family="ui-monospace, 'SF Mono', Menlo, Monaco, monospace" font-size="13" font-weight="700" letter-spacing="0.5">55</text>
|
|
44
|
+
<text x="32" y="48" fill="#DDD3C7" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="36" font-weight="700" letter-spacing="-1">Cs</text>
|
|
45
|
+
</svg>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Returns the <link rel="icon"> tag for an HTML head, given a relative href.
|
|
49
|
+
*
|
|
50
|
+
* - Artifact pages (3 levels deep): href = "../../../favicon.svg"
|
|
51
|
+
* - Project index (2 levels deep): href = "../../favicon.svg"
|
|
52
|
+
* - Global index (root): href = "favicon.svg"
|
|
53
|
+
*/
|
|
54
|
+
export function faviconLinkTag(href: string): string {
|
|
55
|
+
return `<link rel="icon" type="image/svg+xml" href="${href}">`;
|
|
56
|
+
}
|
package/src/render/theme.ts
CHANGED
|
@@ -456,6 +456,87 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
456
456
|
color: var(--muted);
|
|
457
457
|
text-transform: lowercase;
|
|
458
458
|
}
|
|
459
|
+
.pill.accent {
|
|
460
|
+
background: color-mix(in srgb, var(--accent) 18%, var(--surface));
|
|
461
|
+
color: var(--accent);
|
|
462
|
+
font-weight: 600;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* ranked list — numbered cards for ordered recommendations / findings */
|
|
466
|
+
.ranked-list {
|
|
467
|
+
display: flex;
|
|
468
|
+
flex-direction: column;
|
|
469
|
+
gap: 1em;
|
|
470
|
+
margin: 0 0 1.5em;
|
|
471
|
+
padding: 0;
|
|
472
|
+
list-style: none;
|
|
473
|
+
}
|
|
474
|
+
.ranked-item {
|
|
475
|
+
background: var(--surface);
|
|
476
|
+
border: 1.5px solid var(--rule);
|
|
477
|
+
border-radius: 12px;
|
|
478
|
+
padding: 22px 26px;
|
|
479
|
+
display: grid;
|
|
480
|
+
grid-template-columns: 64px 1fr;
|
|
481
|
+
gap: 6px 24px;
|
|
482
|
+
align-items: start;
|
|
483
|
+
}
|
|
484
|
+
.ranked-item .rank-num {
|
|
485
|
+
font-family: var(--serif);
|
|
486
|
+
font-size: 2.4rem;
|
|
487
|
+
font-weight: 500;
|
|
488
|
+
color: var(--oat);
|
|
489
|
+
line-height: 1;
|
|
490
|
+
letter-spacing: -0.02em;
|
|
491
|
+
padding-top: 2px;
|
|
492
|
+
}
|
|
493
|
+
.ranked-item .rank-title {
|
|
494
|
+
font-family: var(--serif);
|
|
495
|
+
font-size: 1.2rem;
|
|
496
|
+
font-weight: 600;
|
|
497
|
+
color: var(--ink);
|
|
498
|
+
margin: 0 0 6px;
|
|
499
|
+
line-height: 1.3;
|
|
500
|
+
}
|
|
501
|
+
.ranked-item .rank-meta {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
gap: 8px;
|
|
505
|
+
flex-wrap: wrap;
|
|
506
|
+
margin-bottom: 14px;
|
|
507
|
+
}
|
|
508
|
+
.ranked-item .rank-body > p {
|
|
509
|
+
color: var(--ink-soft);
|
|
510
|
+
font-size: 0.95rem;
|
|
511
|
+
line-height: 1.65;
|
|
512
|
+
margin: 0 0 0.85em;
|
|
513
|
+
}
|
|
514
|
+
.ranked-item .rank-body > p:last-child { margin-bottom: 0; }
|
|
515
|
+
.ranked-item .rank-body > ul,
|
|
516
|
+
.ranked-item .rank-body > ol {
|
|
517
|
+
color: var(--ink-soft);
|
|
518
|
+
font-size: 0.95rem;
|
|
519
|
+
line-height: 1.65;
|
|
520
|
+
margin: 0 0 0.85em;
|
|
521
|
+
padding-left: 1.2em;
|
|
522
|
+
}
|
|
523
|
+
.ranked-item .rank-aside {
|
|
524
|
+
color: var(--muted);
|
|
525
|
+
font-size: 0.9rem;
|
|
526
|
+
line-height: 1.6;
|
|
527
|
+
padding-left: 14px;
|
|
528
|
+
border-left: 2px solid var(--rule);
|
|
529
|
+
margin: 0;
|
|
530
|
+
}
|
|
531
|
+
.ranked-item .rank-aside-label {
|
|
532
|
+
font-family: var(--mono);
|
|
533
|
+
font-size: 0.7rem;
|
|
534
|
+
font-weight: 600;
|
|
535
|
+
letter-spacing: 0.1em;
|
|
536
|
+
text-transform: uppercase;
|
|
537
|
+
color: var(--accent);
|
|
538
|
+
margin-right: 6px;
|
|
539
|
+
}
|
|
459
540
|
|
|
460
541
|
/* byline */
|
|
461
542
|
.byline {
|
package/src/render/wrap.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { frameworkRulesCss, themeTokensCss, type ThemeTokens } from "./theme.ts";
|
|
4
4
|
import { renderControl, renderAnswered } from "./controls.ts";
|
|
5
5
|
import { getClientJs } from "./client-js.ts";
|
|
6
|
+
import { faviconLinkTag } from "./favicon.ts";
|
|
6
7
|
import type { InteractiveData, Question } from "./validate.ts";
|
|
7
8
|
|
|
8
9
|
export interface ArtifactMeta {
|
|
@@ -47,6 +48,20 @@ function escapeHtml(str: string): string {
|
|
|
47
48
|
.replace(/"/g, """);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** Derives the favicon href from the theme.css href.
|
|
52
|
+
*
|
|
53
|
+
* Both files live in the stateDir root, so the relative depth is identical:
|
|
54
|
+
* swap the trailing "theme.css" segment for "favicon.svg". For atypical
|
|
55
|
+
* themeCssHref values (absolute URLs, paths without "theme.css" suffix), fall
|
|
56
|
+
* back to the artifact-context default.
|
|
57
|
+
*/
|
|
58
|
+
function deriveFaviconHref(themeCssHref: string): string {
|
|
59
|
+
if (themeCssHref.endsWith("theme.css")) {
|
|
60
|
+
return themeCssHref.slice(0, -"theme.css".length) + "favicon.svg";
|
|
61
|
+
}
|
|
62
|
+
return "../../../favicon.svg";
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
function safeJsonForScript(obj: unknown): string {
|
|
51
66
|
return JSON.stringify(obj, null, 2).replace(/<\/script>/gi, "<\\/script>");
|
|
52
67
|
}
|
|
@@ -145,6 +160,10 @@ export function wrapDocument(opts: WrapOptions): string {
|
|
|
145
160
|
: "";
|
|
146
161
|
|
|
147
162
|
const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
|
|
163
|
+
// Favicon path mirrors the theme.css path: artifacts live three levels deep
|
|
164
|
+
// in <stateDir>/projects/<slug>/artifacts/, so favicon.svg is "../../../".
|
|
165
|
+
// When suppressed (standalone/test mode), we suppress favicon too.
|
|
166
|
+
const faviconTag = suppressLink ? "" : `\n ${faviconLinkTag(deriveFaviconHref(href ?? ""))}`;
|
|
148
167
|
|
|
149
168
|
return `<!doctype html>
|
|
150
169
|
<html lang="en">
|
|
@@ -154,7 +173,7 @@ export function wrapDocument(opts: WrapOptions): string {
|
|
|
154
173
|
<title>${titleEsc} · cesium</title>
|
|
155
174
|
<style>${rules}
|
|
156
175
|
/* fallback theme tokens — used when theme.css is missing or unreachable */
|
|
157
|
-
${tokens}</style>${linkTag}
|
|
176
|
+
${tokens}</style>${linkTag}${faviconTag}
|
|
158
177
|
<script type="application/json" id="cesium-meta">${metaJson}</script>
|
|
159
178
|
</head>
|
|
160
179
|
<body>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Favicon shim handler — serves the cesium favicon at /favicon.ico (the legacy
|
|
2
|
+
// path browsers request automatically) by returning the in-memory SVG bytes
|
|
3
|
+
// with image/svg+xml content type. All evergreen browsers accept SVG favicons
|
|
4
|
+
// regardless of the URL extension.
|
|
5
|
+
//
|
|
6
|
+
// The static server already serves /favicon.svg from <stateDir>/favicon.svg
|
|
7
|
+
// (written by writeFaviconSvg on every publish). This shim covers the .ico
|
|
8
|
+
// fallback so users don't see a 404 in DevTools.
|
|
9
|
+
|
|
10
|
+
import { FAVICON_SVG } from "../render/favicon.ts";
|
|
11
|
+
|
|
12
|
+
const SVG_RESPONSE_HEADERS: Record<string, string> = {
|
|
13
|
+
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
14
|
+
"Cache-Control": "public, max-age=86400",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createFaviconHandler(): (req: Request) => Promise<Response | undefined> {
|
|
18
|
+
return async (req: Request): Promise<Response | undefined> => {
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
if (url.pathname !== "/favicon.ico") {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return new Response(FAVICON_SVG, { status: 200, headers: SVG_RESPONSE_HEADERS });
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { unlink, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { startServer, type ServerHandle } from "./http.ts";
|
|
7
7
|
import { acquireLock } from "../storage/lock.ts";
|
|
8
8
|
import { createApiHandler } from "./api.ts";
|
|
9
|
+
import { createFaviconHandler } from "./favicon.ts";
|
|
9
10
|
|
|
10
11
|
export interface LifecycleConfig {
|
|
11
12
|
stateDir: string;
|
|
@@ -260,6 +261,9 @@ export async function ensureRunning(cfg: LifecycleConfig): Promise<RunningInfo>
|
|
|
260
261
|
|
|
261
262
|
// Wire API handler before static file fallback
|
|
262
263
|
handle.addHandler(createApiHandler({ stateDir }));
|
|
264
|
+
// /favicon.ico shim — browsers auto-request this even when the page
|
|
265
|
+
// declares an SVG favicon. Serve the SVG bytes inline so we don't 404.
|
|
266
|
+
handle.addHandler(createFaviconHandler());
|
|
263
267
|
|
|
264
268
|
const startedAt = new Date().toISOString();
|
|
265
269
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Writes favicon.svg to the state directory.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors theme-write.ts: produces a single static asset alongside theme.css
|
|
4
|
+
// that artifact pages and index pages reference via relative <link rel="icon">.
|
|
5
|
+
// The cesium HTTP server serves it automatically because the state dir is the
|
|
6
|
+
// server's static root.
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { FAVICON_SVG } from "../render/favicon.ts";
|
|
10
|
+
import { atomicWrite } from "./write.ts";
|
|
11
|
+
|
|
12
|
+
/** Returns the absolute path to favicon.svg in the given stateDir. */
|
|
13
|
+
export function faviconSvgPath(stateDir: string): string {
|
|
14
|
+
return join(stateDir, "favicon.svg");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Writes <stateDir>/favicon.svg. Atomic. Returns the absolute path.
|
|
18
|
+
* Idempotent — content is theme-independent so writing twice is a no-op. */
|
|
19
|
+
export async function writeFaviconSvg(stateDir: string): Promise<string> {
|
|
20
|
+
const path = faviconSvgPath(stateDir);
|
|
21
|
+
await atomicWrite(path, FAVICON_SVG);
|
|
22
|
+
return path;
|
|
23
|
+
}
|
package/src/storage/index-gen.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { IndexEntry } from "./index-cache.ts";
|
|
4
4
|
import type { ThemeTokens } from "../render/theme.ts";
|
|
5
5
|
import { frameworkRulesCss, themeTokensCss } from "../render/theme.ts";
|
|
6
|
+
import { faviconLinkTag, faviconEmblemSvg } from "../render/favicon.ts";
|
|
6
7
|
|
|
7
8
|
export interface RenderProjectIndexArgs {
|
|
8
9
|
projectSlug: string;
|
|
@@ -88,6 +89,11 @@ function formatDate(iso: string): string {
|
|
|
88
89
|
function indexCss(): string {
|
|
89
90
|
return `
|
|
90
91
|
/* index-page chrome */
|
|
92
|
+
.cesium-eyebrow {
|
|
93
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
94
|
+
/* the eyebrow text is uppercased + tracked; the emblem sits flush left */
|
|
95
|
+
}
|
|
96
|
+
.cesium-eyebrow svg { display: block; }
|
|
91
97
|
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; align-items: center; }
|
|
92
98
|
.filter-chip {
|
|
93
99
|
display: inline-block; font-family: var(--sans); font-size: 0.8em; font-weight: 500;
|
|
@@ -274,6 +280,12 @@ export function renderProjectIndex(args: RenderProjectIndexArgs): string {
|
|
|
274
280
|
const iJs = indexJs();
|
|
275
281
|
|
|
276
282
|
const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
|
|
283
|
+
// Favicon sits next to theme.css in the stateDir root, so swap the suffix.
|
|
284
|
+
const faviconHref =
|
|
285
|
+
href !== null && href.endsWith("theme.css")
|
|
286
|
+
? href.slice(0, -"theme.css".length) + "favicon.svg"
|
|
287
|
+
: "../../favicon.svg";
|
|
288
|
+
const faviconTag = suppressLink ? "" : `\n ${faviconLinkTag(faviconHref)}`;
|
|
277
289
|
|
|
278
290
|
// Sort entries newest-first
|
|
279
291
|
const sorted = [...entries].toSorted(
|
|
@@ -348,11 +360,11 @@ ${cardsHtml}
|
|
|
348
360
|
<title>${esc(projectName)} · cesium</title>
|
|
349
361
|
<style>${rules}
|
|
350
362
|
/* fallback theme tokens — used when theme.css is missing or unreachable */
|
|
351
|
-
${tokens}${iCss}</style>${linkTag}
|
|
363
|
+
${tokens}${iCss}</style>${linkTag}${faviconTag}
|
|
352
364
|
</head>
|
|
353
365
|
<body>
|
|
354
366
|
<div class="page">
|
|
355
|
-
<p class="eyebrow">cesium · project</p>
|
|
367
|
+
<p class="eyebrow cesium-eyebrow">${faviconEmblemSvg(18)}<span>cesium · project</span></p>
|
|
356
368
|
<h1 class="h-display">${esc(projectName)}</h1>
|
|
357
369
|
${subhead}
|
|
358
370
|
${filterRow}
|
|
@@ -383,6 +395,12 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
|
|
|
383
395
|
const iCss = indexCss();
|
|
384
396
|
|
|
385
397
|
const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
|
|
398
|
+
// Favicon sits next to theme.css; default href is "theme.css" → "favicon.svg".
|
|
399
|
+
const faviconHref =
|
|
400
|
+
href !== null && href.endsWith("theme.css")
|
|
401
|
+
? href.slice(0, -"theme.css".length) + "favicon.svg"
|
|
402
|
+
: "favicon.svg";
|
|
403
|
+
const faviconTag = suppressLink ? "" : `\n ${faviconLinkTag(faviconHref)}`;
|
|
386
404
|
|
|
387
405
|
const sorted = [...projects].toSorted(
|
|
388
406
|
(a, b) => new Date(b.latestCreatedAt).getTime() - new Date(a.latestCreatedAt).getTime(),
|
|
@@ -432,11 +450,11 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
|
|
|
432
450
|
<title>All projects · cesium</title>
|
|
433
451
|
<style>${rules}
|
|
434
452
|
/* fallback theme tokens — used when theme.css is missing or unreachable */
|
|
435
|
-
${tokens}${iCss}</style>${linkTag}
|
|
453
|
+
${tokens}${iCss}</style>${linkTag}${faviconTag}
|
|
436
454
|
</head>
|
|
437
455
|
<body>
|
|
438
456
|
<div class="page">
|
|
439
|
-
<p class="eyebrow">cesium</p>
|
|
457
|
+
<p class="eyebrow cesium-eyebrow">${faviconEmblemSvg(18)}<span>cesium</span></p>
|
|
440
458
|
<h1 class="h-display">All projects</h1>
|
|
441
459
|
${subhead}
|
|
442
460
|
${bodyContent}
|
package/src/tools/ask.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { InteractiveData } from "../render/validate.ts";
|
|
|
14
14
|
import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
|
|
15
15
|
import { atomicWrite } from "../storage/write.ts";
|
|
16
16
|
import { writeThemeCss } from "../storage/theme-write.ts";
|
|
17
|
+
import { writeFaviconSvg } from "../storage/favicon-write.ts";
|
|
17
18
|
import { loadIndex, writeIndex, appendEntry, type IndexEntry } from "../storage/index-cache.ts";
|
|
18
19
|
import { withLock } from "../storage/lock.ts";
|
|
19
20
|
import { renderProjectIndex, renderGlobalIndex } from "../storage/index-gen.ts";
|
|
@@ -227,8 +228,9 @@ export function createAskTool(
|
|
|
227
228
|
// 14. Build theme + wrap document
|
|
228
229
|
const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
|
|
229
230
|
|
|
230
|
-
// 14a. Write theme.css (idempotent, outside index lock — separate
|
|
231
|
+
// 14a. Write theme.css + favicon.svg (idempotent, outside index lock — separate files)
|
|
231
232
|
await writeThemeCss(config.stateDir, theme);
|
|
233
|
+
await writeFaviconSvg(config.stateDir);
|
|
232
234
|
|
|
233
235
|
const fullHtml = wrapDocument({
|
|
234
236
|
body: scrubbed.html,
|
package/src/tools/publish.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
|
|
|
14
14
|
import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
|
|
15
15
|
import { atomicWrite, patchEmbeddedMetadata } from "../storage/write.ts";
|
|
16
16
|
import { writeThemeCss } from "../storage/theme-write.ts";
|
|
17
|
+
import { writeFaviconSvg } from "../storage/favicon-write.ts";
|
|
17
18
|
import {
|
|
18
19
|
loadIndex,
|
|
19
20
|
writeIndex,
|
|
@@ -224,8 +225,9 @@ export function createPublishTool(
|
|
|
224
225
|
// 12. Build theme + wrap document
|
|
225
226
|
const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
|
|
226
227
|
|
|
227
|
-
// 12a. Write theme.css (idempotent, outside index lock — separate
|
|
228
|
+
// 12a. Write theme.css + favicon.svg (idempotent, outside index lock — separate files)
|
|
228
229
|
await writeThemeCss(config.stateDir, theme);
|
|
230
|
+
await writeFaviconSvg(config.stateDir);
|
|
229
231
|
|
|
230
232
|
const fullHtml = wrapDocument({
|
|
231
233
|
body: scrubbed.html,
|