decklight 0.1.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/LICENSE +21 -0
- package/README.md +257 -0
- package/SPEC.md +372 -0
- package/cli/bundle.mjs +388 -0
- package/cli/decklight.mjs +95 -0
- package/cli/edit.mjs +142 -0
- package/cli/init.mjs +215 -0
- package/cli/rec.mjs +538 -0
- package/dist/decklight.css +1068 -0
- package/dist/decklight.js +239 -0
- package/dist/decklight.js.map +7 -0
- package/docs/architecture.svg +94 -0
- package/docs/demo.svg +124 -0
- package/package.json +54 -0
- package/themes/README.md +61 -0
- package/themes/aliens.css +84 -0
- package/themes/apple2.css +126 -0
- package/themes/aurora.css +84 -0
- package/themes/berry.css +80 -0
- package/themes/blade-runner.css +83 -0
- package/themes/c64.css +85 -0
- package/themes/carbon.css +82 -0
- package/themes/citrus.css +80 -0
- package/themes/coastal.css +80 -0
- package/themes/cosmos.css +86 -0
- package/themes/dune.css +80 -0
- package/themes/eclipse.css +82 -0
- package/themes/ember.css +80 -0
- package/themes/fjord.css +80 -0
- package/themes/friends.css +82 -0
- package/themes/gallery.html +203 -0
- package/themes/gameboy.css +118 -0
- package/themes/genesis.css +84 -0
- package/themes/glacier.css +82 -0
- package/themes/godfather.css +82 -0
- package/themes/graphite.css +80 -0
- package/themes/harvest.css +80 -0
- package/themes/ibm-modern.css +83 -0
- package/themes/ibm-oldschool.css +84 -0
- package/themes/ink.css +82 -0
- package/themes/latte.css +80 -0
- package/themes/linen.css +80 -0
- package/themes/meadow.css +80 -0
- package/themes/metropolis.css +85 -0
- package/themes/miami-vice.css +84 -0
- package/themes/midnight.css +80 -0
- package/themes/mint.css +82 -0
- package/themes/moss.css +82 -0
- package/themes/obsidian.css +82 -0
- package/themes/orchid.css +82 -0
- package/themes/packs.json +89 -0
- package/themes/paper.css +83 -0
- package/themes/peony.css +84 -0
- package/themes/porcelain.css +82 -0
- package/themes/pulp-fiction.css +84 -0
- package/themes/reveal-beige.css +87 -0
- package/themes/reveal-black.css +83 -0
- package/themes/reveal-blood.css +84 -0
- package/themes/reveal-dracula.css +83 -0
- package/themes/reveal-league.css +88 -0
- package/themes/reveal-moon.css +83 -0
- package/themes/reveal-night.css +86 -0
- package/themes/reveal-serif.css +82 -0
- package/themes/reveal-simple.css +84 -0
- package/themes/reveal-sky.css +85 -0
- package/themes/reveal-solarized.css +83 -0
- package/themes/reveal-white.css +82 -0
- package/themes/sepia.css +81 -0
- package/themes/seriph.css +82 -0
- package/themes/severance.css +82 -0
- package/themes/slate.css +80 -0
- package/themes/snes.css +82 -0
- package/themes/star-wars.css +85 -0
- package/themes/storm.css +80 -0
- package/themes/stranger-things.css +84 -0
- package/themes/synthwave.css +90 -0
- package/themes/terminator.css +84 -0
- package/themes/velvet.css +80 -0
- package/tools/gemini-tts.mjs +140 -0
- package/tools/publish-site-voices.mjs +71 -0
- package/tools/voiceover-server.mjs +0 -0
- package/tools/voiceover.mjs +155 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Decklight theme · star-wars
|
|
3
|
+
dark · deep space black, crawl yellow, saber blue
|
|
4
|
+
Inspired by Star Wars — the opening crawl — palette kept canonical where possible,
|
|
5
|
+
every gated pair fitted to the WCAG contract.
|
|
6
|
+
rule-exception: R3 the canonical crawl yellow (#ffe81f) is the identity
|
|
7
|
+
Token contract: SPEC.md §5 — validated by test/contrast.mjs
|
|
8
|
+
═══════════════════════════════════════════════════════════════ */
|
|
9
|
+
|
|
10
|
+
.decklight {
|
|
11
|
+
/* ── canvas ─────────────────────────────── */
|
|
12
|
+
--bg: #0a0a10;
|
|
13
|
+
--bg-accent: none;
|
|
14
|
+
--fg: #e6e2d2;
|
|
15
|
+
--muted: #98948a;
|
|
16
|
+
|
|
17
|
+
/* ── type ───────────────────────────────── */
|
|
18
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
19
|
+
--font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
20
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
21
|
+
--heading-color: #ffe81f;
|
|
22
|
+
--heading-weight: 800;
|
|
23
|
+
--link: #f0d860;
|
|
24
|
+
|
|
25
|
+
/* ── accent ─────────────────────────────── */
|
|
26
|
+
--accent: #e8c825;
|
|
27
|
+
--accent-contrast: #131313;
|
|
28
|
+
|
|
29
|
+
/* ── blocks ─────────────────────────────── */
|
|
30
|
+
--block-bg: #11111d;
|
|
31
|
+
--block-border: 1px solid #262631;
|
|
32
|
+
--block-radius: 4px;
|
|
33
|
+
--shadow: 0 8px 26px rgba(0, 0, 0, 0.5);
|
|
34
|
+
|
|
35
|
+
/* ── code ───────────────────────────────── */
|
|
36
|
+
--code-bg: #10101a;
|
|
37
|
+
--code-fg: #e8e4d8;
|
|
38
|
+
--hl-keyword: #f0d860;
|
|
39
|
+
--hl-string: #7fc8f0;
|
|
40
|
+
--hl-number: #f0a860;
|
|
41
|
+
--hl-comment: #70706a;
|
|
42
|
+
--hl-function: #8fd08f;
|
|
43
|
+
--hl-type: #e88a80;
|
|
44
|
+
--hl-punct: #b8b4a8;
|
|
45
|
+
|
|
46
|
+
/* ── diagram ────────────────────────────── */
|
|
47
|
+
--d-stroke: #98948a;
|
|
48
|
+
--d-text: #e6e2d2;
|
|
49
|
+
--d-muted: #98948a;
|
|
50
|
+
--d-accent: #4fb8f0;
|
|
51
|
+
--d-fill-1: #211f18;
|
|
52
|
+
--d-fill-2: #1a2129;
|
|
53
|
+
--d-fill-3: #311c1c;
|
|
54
|
+
--d-fill-4: #182118;
|
|
55
|
+
--d-fill-5: #1a1d29;
|
|
56
|
+
--d-fill-6: #31281c;
|
|
57
|
+
|
|
58
|
+
/* ── terminal ───────────────────────────── */
|
|
59
|
+
--term-bg: #08080e;
|
|
60
|
+
--term-fg: #e6e6e2;
|
|
61
|
+
--term-prompt: #e8c825;
|
|
62
|
+
--term-cursor: #e8c825;
|
|
63
|
+
--term-selection: rgba(232, 200, 37, 0.28);
|
|
64
|
+
--ansi-black: #9f9b89;
|
|
65
|
+
--ansi-red: #d97963;
|
|
66
|
+
--ansi-green: #63d973;
|
|
67
|
+
--ansi-yellow: #d9c763;
|
|
68
|
+
--ansi-blue: #638ed9;
|
|
69
|
+
--ansi-magenta: #cb63d9;
|
|
70
|
+
--ansi-cyan: #63d9d5;
|
|
71
|
+
--ansi-white: #e3e2de;
|
|
72
|
+
--ansi-bright-black: #b4b2a7;
|
|
73
|
+
--ansi-bright-red: #eaa08f;
|
|
74
|
+
--ansi-bright-green: #8fea9b;
|
|
75
|
+
--ansi-bright-yellow: #eadd8f;
|
|
76
|
+
--ansi-bright-blue: #8fb0ea;
|
|
77
|
+
--ansi-bright-magenta: #e08fea;
|
|
78
|
+
--ansi-bright-cyan: #8feae7;
|
|
79
|
+
--ansi-bright-white: #f3f3f2;
|
|
80
|
+
|
|
81
|
+
/* ── builds ─────────────────────────────── */
|
|
82
|
+
--dim-opacity: 0.3;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.decklight h1, .decklight h2 { text-transform: uppercase; letter-spacing: 0.04em; }
|
package/themes/storm.css
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Decklight theme · storm
|
|
3
|
+
dark · Squall-line slate with one bolt of electric yellow
|
|
4
|
+
Token contract: SPEC.md §5 — all values validated by test/contrast.mjs
|
|
5
|
+
═══════════════════════════════════════════════════════════════ */
|
|
6
|
+
|
|
7
|
+
.decklight {
|
|
8
|
+
/* ── canvas ─────────────────────────────── */
|
|
9
|
+
--bg: linear-gradient(165deg, #1a2030 0%, #29313f 55%, #343c49 100%);
|
|
10
|
+
--bg-accent: linear-gradient(165deg, #253349 0%, #34455f 100%);
|
|
11
|
+
--fg: #e6ebf4;
|
|
12
|
+
--muted: #a6b2c8;
|
|
13
|
+
|
|
14
|
+
/* ── type ───────────────────────────────── */
|
|
15
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
16
|
+
--font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
17
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
18
|
+
--heading-color: #fafafa;
|
|
19
|
+
--heading-weight: 800;
|
|
20
|
+
--link: #8ec1f0;
|
|
21
|
+
|
|
22
|
+
/* ── accent ─────────────────────────────── */
|
|
23
|
+
--accent: #e4c459;
|
|
24
|
+
--accent-contrast: #241c00;
|
|
25
|
+
|
|
26
|
+
/* ── blocks ─────────────────────────────── */
|
|
27
|
+
--block-bg: rgba(30, 40, 60, 0.6);
|
|
28
|
+
--block-border: 1px solid rgba(142, 193, 240, 0.22);
|
|
29
|
+
--block-radius: 10px;
|
|
30
|
+
--shadow: 0 12px 36px rgba(6, 10, 18, 0.55);
|
|
31
|
+
|
|
32
|
+
/* ── code ───────────────────────────────── */
|
|
33
|
+
--code-bg: #161d2b;
|
|
34
|
+
--code-fg: #dfe6f2;
|
|
35
|
+
--hl-keyword: #8ec1f0;
|
|
36
|
+
--hl-string: #9cdea4;
|
|
37
|
+
--hl-number: #f7ce46;
|
|
38
|
+
--hl-comment: #8593ab;
|
|
39
|
+
--hl-function: #a8b8f0;
|
|
40
|
+
--hl-type: #8fd8dd;
|
|
41
|
+
--hl-punct: #a9b5ca;
|
|
42
|
+
|
|
43
|
+
/* ── diagram ────────────────────────────── */
|
|
44
|
+
--d-stroke: #6b7f9e;
|
|
45
|
+
--d-text: #e6ebf4;
|
|
46
|
+
--d-muted: #93a2ba;
|
|
47
|
+
--d-accent: #ffd23e;
|
|
48
|
+
--d-fill-1: #22304a;
|
|
49
|
+
--d-fill-2: #283a54;
|
|
50
|
+
--d-fill-3: #2c3350;
|
|
51
|
+
--d-fill-4: #1f3a55;
|
|
52
|
+
--d-fill-5: #2e3d58;
|
|
53
|
+
--d-fill-6: #26364e;
|
|
54
|
+
|
|
55
|
+
/* ── terminal ───────────────────────────── */
|
|
56
|
+
--term-bg: #121826;
|
|
57
|
+
--term-fg: #dae2ef;
|
|
58
|
+
--term-prompt: #ffd23e;
|
|
59
|
+
--term-cursor: #ffd23e;
|
|
60
|
+
--term-selection: rgba(255, 210, 62, 0.24);
|
|
61
|
+
--ansi-black: #8593a8;
|
|
62
|
+
--ansi-red: #f28293;
|
|
63
|
+
--ansi-green: #8fd39a;
|
|
64
|
+
--ansi-yellow: #ffd23e;
|
|
65
|
+
--ansi-blue: #82aede;
|
|
66
|
+
--ansi-magenta: #bfa2dd;
|
|
67
|
+
--ansi-cyan: #7fd2d8;
|
|
68
|
+
--ansi-white: #e2e8f2;
|
|
69
|
+
--ansi-bright-black: #9dabc0;
|
|
70
|
+
--ansi-bright-red: #f7a7b4;
|
|
71
|
+
--ansi-bright-green: #aee5b8;
|
|
72
|
+
--ansi-bright-yellow: #ffe07a;
|
|
73
|
+
--ansi-bright-blue: #a6c6ea;
|
|
74
|
+
--ansi-bright-magenta: #d5bfec;
|
|
75
|
+
--ansi-bright-cyan: #a5e3e8;
|
|
76
|
+
--ansi-bright-white: #fafafa;
|
|
77
|
+
|
|
78
|
+
/* ── builds ─────────────────────────────── */
|
|
79
|
+
--dim-opacity: 0.3;
|
|
80
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Decklight theme · stranger-things
|
|
3
|
+
dark · upside-down black, title red, a serif that means business
|
|
4
|
+
Inspired by Stranger Things — the red title in the dark — palette kept canonical where possible,
|
|
5
|
+
every gated pair fitted to the WCAG contract.
|
|
6
|
+
Token contract: SPEC.md §5 — validated by test/contrast.mjs
|
|
7
|
+
═══════════════════════════════════════════════════════════════ */
|
|
8
|
+
|
|
9
|
+
.decklight {
|
|
10
|
+
/* ── canvas ─────────────────────────────── */
|
|
11
|
+
--bg: #0d0a0e;
|
|
12
|
+
--bg-accent: none;
|
|
13
|
+
--fg: #e8dfd8;
|
|
14
|
+
--muted: #9a8d88;
|
|
15
|
+
|
|
16
|
+
/* ── type ───────────────────────────────── */
|
|
17
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
18
|
+
--font-heading: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
|
19
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
20
|
+
--heading-color: #e2242e;
|
|
21
|
+
--heading-weight: 700;
|
|
22
|
+
--link: #ef6a5e;
|
|
23
|
+
|
|
24
|
+
/* ── accent ─────────────────────────────── */
|
|
25
|
+
--accent: #c42832;
|
|
26
|
+
--accent-contrast: #fbfbfa;
|
|
27
|
+
|
|
28
|
+
/* ── blocks ─────────────────────────────── */
|
|
29
|
+
--block-bg: #18121b;
|
|
30
|
+
--block-border: 1px solid #2d2530;
|
|
31
|
+
--block-radius: 4px;
|
|
32
|
+
--shadow: 0 8px 26px rgba(0, 0, 0, 0.5);
|
|
33
|
+
|
|
34
|
+
/* ── code ───────────────────────────────── */
|
|
35
|
+
--code-bg: #141014;
|
|
36
|
+
--code-fg: #e8e0da;
|
|
37
|
+
--hl-keyword: #ee7c72;
|
|
38
|
+
--hl-string: #cab988;
|
|
39
|
+
--hl-number: #e8a860;
|
|
40
|
+
--hl-comment: #7a6f6c;
|
|
41
|
+
--hl-function: #a8b8e0;
|
|
42
|
+
--hl-type: #d8c0a0;
|
|
43
|
+
--hl-punct: #b0a8a4;
|
|
44
|
+
|
|
45
|
+
/* ── diagram ────────────────────────────── */
|
|
46
|
+
--d-stroke: #9a8d88;
|
|
47
|
+
--d-text: #e8dfd8;
|
|
48
|
+
--d-muted: #9a8d88;
|
|
49
|
+
--d-accent: #c42832;
|
|
50
|
+
--d-fill-1: #201718;
|
|
51
|
+
--d-fill-2: #281e19;
|
|
52
|
+
--d-fill-3: #1b1e30;
|
|
53
|
+
--d-fill-4: #201d17;
|
|
54
|
+
--d-fill-5: #231928;
|
|
55
|
+
--d-fill-6: #1b2930;
|
|
56
|
+
|
|
57
|
+
/* ── terminal ───────────────────────────── */
|
|
58
|
+
--term-bg: #0a080b;
|
|
59
|
+
--term-fg: #e6e6e2;
|
|
60
|
+
--term-prompt: #c42832;
|
|
61
|
+
--term-cursor: #c42832;
|
|
62
|
+
--term-selection: rgba(196, 40, 50, 0.28);
|
|
63
|
+
--ansi-black: #9f898b;
|
|
64
|
+
--ansi-red: #ce866e;
|
|
65
|
+
--ansi-green: #6ece81;
|
|
66
|
+
--ansi-yellow: #cec66e;
|
|
67
|
+
--ansi-blue: #6e8bce;
|
|
68
|
+
--ansi-magenta: #c96ece;
|
|
69
|
+
--ansi-cyan: #6ecbce;
|
|
70
|
+
--ansi-white: #e3dede;
|
|
71
|
+
--ansi-bright-black: #b4a7a8;
|
|
72
|
+
--ansi-bright-red: #e3a997;
|
|
73
|
+
--ansi-bright-green: #97e3a5;
|
|
74
|
+
--ansi-bright-yellow: #e3dc97;
|
|
75
|
+
--ansi-bright-blue: #97aee3;
|
|
76
|
+
--ansi-bright-magenta: #df97e3;
|
|
77
|
+
--ansi-bright-cyan: #97e1e3;
|
|
78
|
+
--ansi-bright-white: #f3f2f2;
|
|
79
|
+
|
|
80
|
+
/* ── builds ─────────────────────────────── */
|
|
81
|
+
--dim-opacity: 0.25;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.decklight h1, .decklight h2 { text-transform: uppercase; letter-spacing: 0.08em; }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@600;800&display=swap');
|
|
2
|
+
|
|
3
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4
|
+
Decklight theme · synthwave
|
|
5
|
+
dark · Retro-future horizon — neon pink and cyan over violet dusk
|
|
6
|
+
Token contract: SPEC.md §5 — all values validated by test/contrast.mjs
|
|
7
|
+
═══════════════════════════════════════════════════════════════ */
|
|
8
|
+
|
|
9
|
+
/* palette-rule exceptions (test/palette-rules.mjs):
|
|
10
|
+
rule-exception: R3 neon is the identity — the accent stays hot
|
|
11
|
+
rule-exception: R2 the neon-sunset duotone canvas is the identity
|
|
12
|
+
═══════════════════════════════════════════════════════════════ */
|
|
13
|
+
|
|
14
|
+
.decklight {
|
|
15
|
+
/* ── canvas ─────────────────────────────── */
|
|
16
|
+
--bg: linear-gradient(180deg, #160a2e 0%, #2b1055 62%, #45156b 100%);
|
|
17
|
+
--bg-accent: linear-gradient(180deg, #45156b 0%, #7a1f5c 100%);
|
|
18
|
+
--fg: #f2e7ff;
|
|
19
|
+
--muted: #c0a8e0;
|
|
20
|
+
|
|
21
|
+
/* ── type ───────────────────────────────── */
|
|
22
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
23
|
+
--font-heading: Orbitron, "Avenir Next", Avenir, Futura, "Century Gothic", "Segoe UI", sans-serif;
|
|
24
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
25
|
+
--heading-color: #29f3ff;
|
|
26
|
+
--heading-weight: 800;
|
|
27
|
+
--link: #63c9ff;
|
|
28
|
+
|
|
29
|
+
/* ── accent ─────────────────────────────── */
|
|
30
|
+
--accent: #ff2ea6;
|
|
31
|
+
--accent-contrast: #330014;
|
|
32
|
+
|
|
33
|
+
/* ── blocks ─────────────────────────────── */
|
|
34
|
+
--block-bg: rgba(38, 12, 72, 0.6);
|
|
35
|
+
--block-border: 1px solid rgba(41, 243, 255, 0.3);
|
|
36
|
+
--block-radius: 10px;
|
|
37
|
+
--shadow: 0 0 32px rgba(255, 46, 166, 0.25);
|
|
38
|
+
|
|
39
|
+
/* ── code ───────────────────────────────── */
|
|
40
|
+
--code-bg: #170933;
|
|
41
|
+
--code-fg: #eee2ff;
|
|
42
|
+
--hl-keyword: #ff7ec8;
|
|
43
|
+
--hl-string: #7df5d8;
|
|
44
|
+
--hl-number: #ffd166;
|
|
45
|
+
--hl-comment: #8f7cb8;
|
|
46
|
+
--hl-function: #63c9ff;
|
|
47
|
+
--hl-type: #c79bff;
|
|
48
|
+
--hl-punct: #bda8dd;
|
|
49
|
+
|
|
50
|
+
/* ── diagram ────────────────────────────── */
|
|
51
|
+
--d-stroke: #b03fd0;
|
|
52
|
+
--d-text: #f2e7ff;
|
|
53
|
+
--d-muted: #ab93d0;
|
|
54
|
+
--d-accent: #29f3ff;
|
|
55
|
+
--d-fill-1: #2a0f52;
|
|
56
|
+
--d-fill-2: #3c1160;
|
|
57
|
+
--d-fill-3: #221650;
|
|
58
|
+
--d-fill-4: #4a1258;
|
|
59
|
+
--d-fill-5: #321a68;
|
|
60
|
+
--d-fill-6: #3d0f4e;
|
|
61
|
+
|
|
62
|
+
/* ── terminal ───────────────────────────── */
|
|
63
|
+
--term-bg: #120726;
|
|
64
|
+
--term-fg: #e8dcfc;
|
|
65
|
+
--term-prompt: #29f3ff;
|
|
66
|
+
--term-cursor: #ff2ea6;
|
|
67
|
+
--term-selection: rgba(41, 243, 255, 0.25);
|
|
68
|
+
--ansi-black: #8d7cb0;
|
|
69
|
+
--ansi-red: #ff6e9c;
|
|
70
|
+
--ansi-green: #6ef2c2;
|
|
71
|
+
--ansi-yellow: #ffd166;
|
|
72
|
+
--ansi-blue: #7ab5ff;
|
|
73
|
+
--ansi-magenta: #e08aff;
|
|
74
|
+
--ansi-cyan: #54e6f5;
|
|
75
|
+
--ansi-white: #ecdff8;
|
|
76
|
+
--ansi-bright-black: #a894cc;
|
|
77
|
+
--ansi-bright-red: #ff9ebd;
|
|
78
|
+
--ansi-bright-green: #9af8d6;
|
|
79
|
+
--ansi-bright-yellow: #ffe08c;
|
|
80
|
+
--ansi-bright-blue: #a3ccff;
|
|
81
|
+
--ansi-bright-magenta: #ecacff;
|
|
82
|
+
--ansi-bright-cyan: #8ff0fa;
|
|
83
|
+
--ansi-bright-white: #fafafa;
|
|
84
|
+
|
|
85
|
+
/* ── builds ─────────────────────────────── */
|
|
86
|
+
--dim-opacity: 0.3;
|
|
87
|
+
--build-duration: 420ms;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.decklight h1, .decklight h2 { letter-spacing: 0.06em; text-transform: uppercase; }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Decklight theme · terminator
|
|
3
|
+
dark · gunmetal chrome, target-lock red, cold machine grays
|
|
4
|
+
Inspired by The Terminator — chrome endoskeleton, HUD red — palette kept canonical where possible,
|
|
5
|
+
every gated pair fitted to the WCAG contract.
|
|
6
|
+
Token contract: SPEC.md §5 — validated by test/contrast.mjs
|
|
7
|
+
═══════════════════════════════════════════════════════════════ */
|
|
8
|
+
|
|
9
|
+
.decklight {
|
|
10
|
+
/* ── canvas ─────────────────────────────── */
|
|
11
|
+
--bg: linear-gradient(180deg, #16181d 0%, #0d0e12 100%);
|
|
12
|
+
--bg-accent: none;
|
|
13
|
+
--fg: #dde1e8;
|
|
14
|
+
--muted: #8f95a0;
|
|
15
|
+
|
|
16
|
+
/* ── type ───────────────────────────────── */
|
|
17
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
18
|
+
--font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
19
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
20
|
+
--heading-color: #e04248;
|
|
21
|
+
--heading-weight: 800;
|
|
22
|
+
--link: #ef7a74;
|
|
23
|
+
|
|
24
|
+
/* ── accent ─────────────────────────────── */
|
|
25
|
+
--accent: #c42f38;
|
|
26
|
+
--accent-contrast: #fbfbfa;
|
|
27
|
+
|
|
28
|
+
/* ── blocks ─────────────────────────────── */
|
|
29
|
+
--block-bg: #1e212a;
|
|
30
|
+
--block-border: 1px solid #31353f;
|
|
31
|
+
--block-radius: 3px;
|
|
32
|
+
--shadow: 0 8px 26px rgba(0, 0, 0, 0.5);
|
|
33
|
+
|
|
34
|
+
/* ── code ───────────────────────────────── */
|
|
35
|
+
--code-bg: #101318;
|
|
36
|
+
--code-fg: #dfe3ea;
|
|
37
|
+
--hl-keyword: #e78186;
|
|
38
|
+
--hl-string: #a4b8cc;
|
|
39
|
+
--hl-number: #e2a866;
|
|
40
|
+
--hl-comment: #6a707a;
|
|
41
|
+
--hl-function: #8fb8e0;
|
|
42
|
+
--hl-type: #c3c9dd;
|
|
43
|
+
--hl-punct: #a0a6b0;
|
|
44
|
+
|
|
45
|
+
/* ── diagram ────────────────────────────── */
|
|
46
|
+
--d-stroke: #8f95a0;
|
|
47
|
+
--d-text: #dde1e8;
|
|
48
|
+
--d-muted: #8f95a0;
|
|
49
|
+
--d-accent: #d55159;
|
|
50
|
+
--d-fill-1: #22272f;
|
|
51
|
+
--d-fill-2: #382425;
|
|
52
|
+
--d-fill-3: #253841;
|
|
53
|
+
--d-fill-4: #22222f;
|
|
54
|
+
--d-fill-5: #382e24;
|
|
55
|
+
--d-fill-6: #254141;
|
|
56
|
+
|
|
57
|
+
/* ── terminal ───────────────────────────── */
|
|
58
|
+
--term-bg: #0b0d11;
|
|
59
|
+
--term-fg: #e6e6e2;
|
|
60
|
+
--term-prompt: #c42f38;
|
|
61
|
+
--term-cursor: #c42f38;
|
|
62
|
+
--term-selection: rgba(196, 47, 56, 0.28);
|
|
63
|
+
--ansi-black: #9f898a;
|
|
64
|
+
--ansi-red: #cb8872;
|
|
65
|
+
--ansi-green: #72cb83;
|
|
66
|
+
--ansi-yellow: #cbc372;
|
|
67
|
+
--ansi-blue: #728dcb;
|
|
68
|
+
--ansi-magenta: #c672cb;
|
|
69
|
+
--ansi-cyan: #72c8cb;
|
|
70
|
+
--ansi-white: #e3dede;
|
|
71
|
+
--ansi-bright-black: #b4a7a8;
|
|
72
|
+
--ansi-bright-red: #e0ab99;
|
|
73
|
+
--ansi-bright-green: #99e0a7;
|
|
74
|
+
--ansi-bright-yellow: #e0da99;
|
|
75
|
+
--ansi-bright-blue: #99afe0;
|
|
76
|
+
--ansi-bright-magenta: #dd99e0;
|
|
77
|
+
--ansi-bright-cyan: #99dee0;
|
|
78
|
+
--ansi-bright-white: #f3f2f2;
|
|
79
|
+
|
|
80
|
+
/* ── builds ─────────────────────────────── */
|
|
81
|
+
--dim-opacity: 0.25;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.decklight h1, .decklight h2 { text-transform: uppercase; letter-spacing: 0.03em; }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Decklight theme · velvet
|
|
3
|
+
dark · Plum velvet and rose gold — an opera box after dark
|
|
4
|
+
Token contract: SPEC.md §5 — all values validated by test/contrast.mjs
|
|
5
|
+
═══════════════════════════════════════════════════════════════ */
|
|
6
|
+
|
|
7
|
+
.decklight {
|
|
8
|
+
/* ── canvas ─────────────────────────────── */
|
|
9
|
+
--bg: #221126;
|
|
10
|
+
--bg-accent: linear-gradient(150deg, #2f1533 0%, #221126 70%);
|
|
11
|
+
--fg: #f2e6ee;
|
|
12
|
+
--muted: #c6aac0;
|
|
13
|
+
|
|
14
|
+
/* ── type ───────────────────────────────── */
|
|
15
|
+
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
16
|
+
--font-heading: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
|
17
|
+
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
|
|
18
|
+
--heading-color: #f3cdb8;
|
|
19
|
+
--heading-weight: 700;
|
|
20
|
+
--link: #e8a4c8;
|
|
21
|
+
|
|
22
|
+
/* ── accent ─────────────────────────────── */
|
|
23
|
+
--accent: #e8b4bc;
|
|
24
|
+
--accent-contrast: #3a0e1c;
|
|
25
|
+
|
|
26
|
+
/* ── blocks ─────────────────────────────── */
|
|
27
|
+
--block-bg: #2c1731;
|
|
28
|
+
--block-border: 1px solid #46264c;
|
|
29
|
+
--block-radius: 14px;
|
|
30
|
+
--shadow: 0 12px 36px rgba(12, 3, 14, 0.6);
|
|
31
|
+
|
|
32
|
+
/* ── code ───────────────────────────────── */
|
|
33
|
+
--code-bg: #1a0d1e;
|
|
34
|
+
--code-fg: #ecdfe8;
|
|
35
|
+
--hl-keyword: #e8a4c8;
|
|
36
|
+
--hl-string: #c9c98a;
|
|
37
|
+
--hl-number: #f0b890;
|
|
38
|
+
--hl-comment: #9d86a0;
|
|
39
|
+
--hl-function: #a8b8f0;
|
|
40
|
+
--hl-type: #d0a8e8;
|
|
41
|
+
--hl-punct: #c0a8ba;
|
|
42
|
+
|
|
43
|
+
/* ── diagram ────────────────────────────── */
|
|
44
|
+
--d-stroke: #96608f;
|
|
45
|
+
--d-text: #f2e6ee;
|
|
46
|
+
--d-muted: #b299ad;
|
|
47
|
+
--d-accent: #e8b4bc;
|
|
48
|
+
--d-fill-1: #331a3a;
|
|
49
|
+
--d-fill-2: #3e1c3a;
|
|
50
|
+
--d-fill-3: #2c1f42;
|
|
51
|
+
--d-fill-4: #421f30;
|
|
52
|
+
--d-fill-5: #38224a;
|
|
53
|
+
--d-fill-6: #2e1836;
|
|
54
|
+
|
|
55
|
+
/* ── terminal ───────────────────────────── */
|
|
56
|
+
--term-bg: #160a19;
|
|
57
|
+
--term-fg: #e8dbe4;
|
|
58
|
+
--term-prompt: #e8b4bc;
|
|
59
|
+
--term-cursor: #e8b4bc;
|
|
60
|
+
--term-selection: rgba(232, 180, 188, 0.26);
|
|
61
|
+
--ansi-black: #9c86a0;
|
|
62
|
+
--ansi-red: #f28299;
|
|
63
|
+
--ansi-green: #a2d68f;
|
|
64
|
+
--ansi-yellow: #ecc687;
|
|
65
|
+
--ansi-blue: #9badf0;
|
|
66
|
+
--ansi-magenta: #d79be8;
|
|
67
|
+
--ansi-cyan: #93d5d0;
|
|
68
|
+
--ansi-white: #eee2ea;
|
|
69
|
+
--ansi-bright-black: #b29cb8;
|
|
70
|
+
--ansi-bright-red: #f8a8ba;
|
|
71
|
+
--ansi-bright-green: #bfe5ae;
|
|
72
|
+
--ansi-bright-yellow: #f5d8a8;
|
|
73
|
+
--ansi-bright-blue: #b8c5f5;
|
|
74
|
+
--ansi-bright-magenta: #e5baf2;
|
|
75
|
+
--ansi-bright-cyan: #b2e5e0;
|
|
76
|
+
--ansi-bright-white: #fdf8fc;
|
|
77
|
+
|
|
78
|
+
/* ── builds ─────────────────────────────── */
|
|
79
|
+
--dim-opacity: 0.3;
|
|
80
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Shared Gemini TTS synthesis (Vertex AI) — used by tools/voiceover.mjs
|
|
2
|
+
// (batch pre-render) and tools/voiceover-server.mjs (live bridge for the
|
|
3
|
+
// player). generateContent with responseModalities AUDIO returns base64 raw
|
|
4
|
+
// PCM (audio/L16, 24 kHz mono); we wrap a WAV header so both afconvert and
|
|
5
|
+
// the browser's <audio> can read it. Auth is ADC via gcloud; the model id /
|
|
6
|
+
// location pair is probed once (GA vs -preview, global vs us-central1) and
|
|
7
|
+
// cached for the synth's lifetime.
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
// The prebuilt voice roster (docs' flavor words) — the player shows this
|
|
12
|
+
// same list, keep the two in sync (engine.js GEMINI_VOICES).
|
|
13
|
+
export const GEMINI_VOICES = [
|
|
14
|
+
['Zephyr', 'bright'], ['Puck', 'upbeat'], ['Charon', 'informative'],
|
|
15
|
+
['Kore', 'firm'], ['Fenrir', 'excitable'], ['Leda', 'youthful'],
|
|
16
|
+
['Orus', 'firm'], ['Aoede', 'breezy'], ['Callirrhoe', 'easy-going'],
|
|
17
|
+
['Autonoe', 'bright'], ['Enceladus', 'breathy'], ['Iapetus', 'clear'],
|
|
18
|
+
['Umbriel', 'easy-going'], ['Algieba', 'smooth'], ['Despina', 'smooth'],
|
|
19
|
+
['Erinome', 'clear'], ['Algenib', 'gravelly'], ['Rasalgethi', 'informative'],
|
|
20
|
+
['Laomedeia', 'upbeat'], ['Achernar', 'soft'], ['Alnilam', 'firm'],
|
|
21
|
+
['Schedar', 'even'], ['Gacrux', 'mature'], ['Pulcherrima', 'forward'],
|
|
22
|
+
['Achird', 'friendly'], ['Zubenelgenubi', 'casual'], ['Vindemiatrix', 'gentle'],
|
|
23
|
+
['Sadachbia', 'lively'], ['Sadaltager', 'knowledgeable'], ['Sulafat', 'warm'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function gcloudToken() {
|
|
27
|
+
return execFileSync('gcloud', ['auth', 'application-default', 'print-access-token'],
|
|
28
|
+
{ encoding: 'utf8' }).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Published Vertex AI list prices (USD per 1M tokens) — the API returns
|
|
32
|
+
// token counts in usageMetadata, never dollars, so cost is an ESTIMATE.
|
|
33
|
+
const PRICES = [
|
|
34
|
+
[/flash/i, { input: 0.50, output: 10.00 }],
|
|
35
|
+
[/./, { input: 1.00, output: 20.00 }], // gemini-2.5-pro-tts
|
|
36
|
+
];
|
|
37
|
+
function estimateCost(model, usage) {
|
|
38
|
+
const p = PRICES.find(([re]) => re.test(model))[1];
|
|
39
|
+
return ((usage.promptTokenCount ?? 0) * p.input + (usage.candidatesTokenCount ?? 0) * p.output) / 1e6;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wavFromPcm(pcm, rate) {
|
|
43
|
+
const h = Buffer.alloc(44);
|
|
44
|
+
h.write('RIFF', 0); h.writeUInt32LE(36 + pcm.length, 4); h.write('WAVE', 8);
|
|
45
|
+
h.write('fmt ', 12); h.writeUInt32LE(16, 16); h.writeUInt16LE(1, 20); h.writeUInt16LE(1, 22);
|
|
46
|
+
h.writeUInt32LE(rate, 24); h.writeUInt32LE(rate * 2, 28); h.writeUInt16LE(2, 32); h.writeUInt16LE(16, 34);
|
|
47
|
+
h.write('data', 36); h.writeUInt32LE(pcm.length, 40);
|
|
48
|
+
return Buffer.concat([h, pcm]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compose the steering prompt in the DOCUMENTED shape: one directive
|
|
53
|
+
* clause ending in a colon, fused to the content ("Say cheerfully: …").
|
|
54
|
+
* Free-form styles are normalized — multi-sentence personas collapse into
|
|
55
|
+
* a single clause (periods → semicolons) and anything not already phrased
|
|
56
|
+
* as a speech directive gets wrapped in one — because instruction-looking
|
|
57
|
+
* text is steering, but content-looking text ("You're a friendly senior
|
|
58
|
+
* engineer…") can stochastically be read aloud, especially when the
|
|
59
|
+
* instruction dwarfs a short sentence.
|
|
60
|
+
*/
|
|
61
|
+
export function styledPrompt(style, text) {
|
|
62
|
+
const s = (style ?? '').trim();
|
|
63
|
+
if (!s) return text;
|
|
64
|
+
const clause = s
|
|
65
|
+
.replace(/\s+/g, ' ')
|
|
66
|
+
.replace(/[.!?;:\s]+$/, '') // drop trailing punctuation
|
|
67
|
+
.replace(/\.\s+/g, '; '); // fuse sentences into one clause
|
|
68
|
+
const directive = /^(read|say|speak|narrate|deliver|announce|whisper|shout|recite|tell)\b/i.test(clause)
|
|
69
|
+
? clause
|
|
70
|
+
: `Say this in the following style — ${clause}`;
|
|
71
|
+
return `${directive}: ${text}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns async (text, { voice, style }) → { wav: Buffer, usage } where
|
|
76
|
+
* usage = { model, promptTokens, audioTokens, cost } (cost is an estimate
|
|
77
|
+
* from published list prices — the API only reports token counts).
|
|
78
|
+
* `style` steers delivery via styledPrompt() — never spoken content.
|
|
79
|
+
*/
|
|
80
|
+
export function createSynth({ project, ttsModel, location } = {}) {
|
|
81
|
+
if (!project) throw new Error('Gemini TTS needs a GCP project — pass { project } (CLI: --project <id> or set GOOGLE_CLOUD_PROJECT)');
|
|
82
|
+
let token = null;
|
|
83
|
+
let route = null;
|
|
84
|
+
|
|
85
|
+
async function call(text, voice, model, loc) {
|
|
86
|
+
const host = loc === 'global' ? 'aiplatform.googleapis.com' : `${loc}-aiplatform.googleapis.com`;
|
|
87
|
+
const url = `https://${host}/v1/projects/${project}/locations/${loc}/publishers/google/models/${model}:generateContent`;
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
contents: [{ role: 'user', parts: [{ text }] }],
|
|
93
|
+
generationConfig: {
|
|
94
|
+
responseModalities: ['AUDIO'],
|
|
95
|
+
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) { const e = new Error(`${res.status} ${(await res.text()).slice(0, 200)}`); e.status = res.status; throw e; }
|
|
100
|
+
const json = await res.json();
|
|
101
|
+
const data = json.candidates?.[0]?.content?.parts?.find((p) => p.inlineData)?.inlineData;
|
|
102
|
+
if (!data) throw new Error('no audio in response');
|
|
103
|
+
const rate = Number(/rate=(\d+)/.exec(data.mimeType)?.[1] ?? 24000);
|
|
104
|
+
const um = json.usageMetadata ?? {};
|
|
105
|
+
return {
|
|
106
|
+
wav: wavFromPcm(Buffer.from(data.data, 'base64'), rate),
|
|
107
|
+
usage: {
|
|
108
|
+
model,
|
|
109
|
+
promptTokens: um.promptTokenCount ?? 0,
|
|
110
|
+
audioTokens: um.candidatesTokenCount ?? 0,
|
|
111
|
+
cost: estimateCost(model, um),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return async function synth(text, { voice = 'Alnilam', style = '' } = {}) {
|
|
117
|
+
token ??= gcloudToken();
|
|
118
|
+
const prompt = styledPrompt(style, text);
|
|
119
|
+
const routes = route ? [route]
|
|
120
|
+
: [ttsModel ?? 'gemini-2.5-pro-tts', 'gemini-2.5-pro-preview-tts']
|
|
121
|
+
.flatMap((m) => [location ?? 'global', 'us-central1'].map((l) => ({ model: m, location: l })))
|
|
122
|
+
.filter((r, i, a) => a.findIndex((x) => x.model === r.model && x.location === r.location) === i);
|
|
123
|
+
let lastErr;
|
|
124
|
+
for (const r of routes) {
|
|
125
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
const out = await call(prompt, voice, r.model, r.location);
|
|
128
|
+
route ??= r;
|
|
129
|
+
return out;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
lastErr = e;
|
|
132
|
+
if (e.status === 429) { await new Promise((ok) => setTimeout(ok, 15000 * (attempt + 1))); continue; }
|
|
133
|
+
if (e.status === 401) { token = gcloudToken(); continue; }
|
|
134
|
+
break; // 404/400 → next route
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw lastErr;
|
|
139
|
+
};
|
|
140
|
+
}
|