tokengolf 0.4.2 → 0.5.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 +9 -0
- package/CLAUDE.md +10 -5
- package/LICENSE +21 -0
- package/README.md +228 -109
- package/dist/cli.js +97 -33
- package/docs/assets/banner.svg +45 -0
- package/docs/index.html +376 -197
- package/hooks/session-end.js +12 -27
- package/install.sh +69 -0
- package/package.json +3 -2
- package/scripts/update-homebrew.sh +39 -0
- package/src/components/ActiveRun.js +7 -3
- package/src/components/ScoreCard.js +7 -9
- package/src/components/StartRun.js +27 -8
- package/src/components/StatsView.js +31 -11
- package/src/lib/score.js +4 -4
- package/src/lib/ui.js +16 -0
- package/assets/demo-hud.png +0 -0
- package/assets/scorecard.png +0 -0
- package/docs/assets/demo-hud.png +0 -0
- package/docs/assets/scorecard.png +0 -0
package/dist/cli.js
CHANGED
|
@@ -575,28 +575,28 @@ var init_score = __esm({
|
|
|
575
575
|
name: "Haiku",
|
|
576
576
|
label: "Rogue",
|
|
577
577
|
emoji: "\u{1F3F9}",
|
|
578
|
-
difficulty: "
|
|
578
|
+
difficulty: "Nightmare",
|
|
579
579
|
color: "red"
|
|
580
580
|
},
|
|
581
581
|
sonnet: {
|
|
582
582
|
name: "Sonnet",
|
|
583
583
|
label: "Fighter",
|
|
584
584
|
emoji: "\u2694\uFE0F",
|
|
585
|
-
difficulty: "
|
|
585
|
+
difficulty: "Standard",
|
|
586
586
|
color: "cyan"
|
|
587
587
|
},
|
|
588
588
|
opusplan: {
|
|
589
589
|
name: "Paladin",
|
|
590
590
|
label: "Paladin",
|
|
591
591
|
emoji: "\u269C\uFE0F",
|
|
592
|
-
difficulty: "
|
|
592
|
+
difficulty: "Tactical",
|
|
593
593
|
color: "yellow"
|
|
594
594
|
},
|
|
595
595
|
opus: {
|
|
596
596
|
name: "Opus",
|
|
597
597
|
label: "Warlock",
|
|
598
598
|
emoji: "\u{1F9D9}",
|
|
599
|
-
difficulty: "
|
|
599
|
+
difficulty: "Casual",
|
|
600
600
|
color: "magenta"
|
|
601
601
|
}
|
|
602
602
|
};
|
|
@@ -610,6 +610,24 @@ var init_score = __esm({
|
|
|
610
610
|
}
|
|
611
611
|
});
|
|
612
612
|
|
|
613
|
+
// src/lib/ui.js
|
|
614
|
+
var ACCENT_BORDER, ACCENT_PADDING;
|
|
615
|
+
var init_ui = __esm({
|
|
616
|
+
"src/lib/ui.js"() {
|
|
617
|
+
ACCENT_BORDER = {
|
|
618
|
+
topLeft: " ",
|
|
619
|
+
top: " ",
|
|
620
|
+
topRight: " ",
|
|
621
|
+
left: "\u2588\u2588",
|
|
622
|
+
right: " ",
|
|
623
|
+
bottomLeft: " ",
|
|
624
|
+
bottom: " ",
|
|
625
|
+
bottomRight: " "
|
|
626
|
+
};
|
|
627
|
+
ACCENT_PADDING = 3;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
613
631
|
// src/components/ActiveRun.js
|
|
614
632
|
import React2, { useState as useState2, useEffect } from "react";
|
|
615
633
|
import { Box as Box2, Text as Text2, useApp as useApp2, useInput } from "ink";
|
|
@@ -637,9 +655,12 @@ function ActiveRun({ run: initialRun }) {
|
|
|
637
655
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1, paddingX: 1, paddingY: 1 }, /* @__PURE__ */ React2.createElement(Box2, { gap: 2 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Active Run"), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, formatElapsed(run.startedAt))), /* @__PURE__ */ React2.createElement(
|
|
638
656
|
Box2,
|
|
639
657
|
{
|
|
640
|
-
borderStyle:
|
|
658
|
+
borderStyle: ACCENT_BORDER,
|
|
641
659
|
borderColor: "yellow",
|
|
642
|
-
|
|
660
|
+
borderRight: false,
|
|
661
|
+
borderTop: false,
|
|
662
|
+
borderBottom: false,
|
|
663
|
+
paddingLeft: ACCENT_PADDING,
|
|
643
664
|
paddingY: 1,
|
|
644
665
|
flexDirection: "column",
|
|
645
666
|
gap: 1
|
|
@@ -664,13 +685,14 @@ function ActiveRun({ run: initialRun }) {
|
|
|
664
685
|
));
|
|
665
686
|
})),
|
|
666
687
|
/* @__PURE__ */ React2.createElement(Box2, { gap: 3, marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Prompts ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.promptCount || 0)), /* @__PURE__ */ React2.createElement(Text2, { color: "gray" }, "Tools ", /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, run.totalToolCalls || 0))),
|
|
667
|
-
pct >= 80 && pct < 100 && /* @__PURE__ */ React2.createElement(Box2, {
|
|
688
|
+
pct >= 80 && pct < 100 && /* @__PURE__ */ React2.createElement(Box2, { paddingLeft: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "\u26A0\uFE0F BUDGET WARNING \u2014 ", formatCost(run.budget - run.spent), " left"))
|
|
668
689
|
), /* @__PURE__ */ React2.createElement(Text2, { color: "gray", dimColor: true }, "tokengolf win [--spent 0.18] \xB7 tokengolf bust \xB7 q to close"));
|
|
669
690
|
}
|
|
670
691
|
var init_ActiveRun = __esm({
|
|
671
692
|
"src/components/ActiveRun.js"() {
|
|
672
693
|
init_state();
|
|
673
694
|
init_score();
|
|
695
|
+
init_ui();
|
|
674
696
|
}
|
|
675
697
|
});
|
|
676
698
|
|
|
@@ -697,9 +719,12 @@ function ScoreCard({ run }) {
|
|
|
697
719
|
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
698
720
|
Box3,
|
|
699
721
|
{
|
|
700
|
-
borderStyle:
|
|
722
|
+
borderStyle: ACCENT_BORDER,
|
|
701
723
|
borderColor: won ? "yellow" : "red",
|
|
702
|
-
|
|
724
|
+
borderRight: false,
|
|
725
|
+
borderTop: false,
|
|
726
|
+
borderBottom: false,
|
|
727
|
+
paddingLeft: ACCENT_PADDING,
|
|
703
728
|
paddingY: 1,
|
|
704
729
|
flexDirection: "column",
|
|
705
730
|
gap: 1
|
|
@@ -720,23 +745,13 @@ function ScoreCard({ run }) {
|
|
|
720
745
|
return /* @__PURE__ */ React3.createElement(Text3, { key: model, color: "gray" }, short, " ", /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, pctOfTotal, "%"), " ", /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, formatCost(cost)));
|
|
721
746
|
}))),
|
|
722
747
|
run.toolCalls && Object.keys(run.toolCalls).length > 0 && /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 0, marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tool calls:"), /* @__PURE__ */ React3.createElement(Box3, { gap: 2, flexWrap: "wrap" }, Object.entries(run.toolCalls).map(([tool, count]) => /* @__PURE__ */ React3.createElement(Text3, { key: tool, color: "gray" }, /* @__PURE__ */ React3.createElement(Text3, { color: "white" }, tool), " \xD7", count)))),
|
|
723
|
-
!won && run.budget && /* @__PURE__ */ React3.createElement(
|
|
724
|
-
Box3,
|
|
725
|
-
{
|
|
726
|
-
borderStyle: "single",
|
|
727
|
-
borderColor: "red",
|
|
728
|
-
paddingX: 1,
|
|
729
|
-
marginTop: 1,
|
|
730
|
-
flexDirection: "column"
|
|
731
|
-
},
|
|
732
|
-
/* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "Cause of death: Budget exceeded by ", formatCost(run.spent - run.budget)),
|
|
733
|
-
/* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tip: Use Read with line ranges instead of full file reads.")
|
|
734
|
-
)
|
|
748
|
+
!won && run.budget && /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1, flexDirection: "column", paddingLeft: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "Cause of death: Budget exceeded by ", formatCost(run.spent - run.budget)), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "Tip: Use Read with line ranges instead of full file reads."))
|
|
735
749
|
), /* @__PURE__ */ React3.createElement(Box3, { gap: 2 }, /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf start \u2014 run again"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "tokengolf stats \u2014 career stats"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "\xB7"), /* @__PURE__ */ React3.createElement(Text3, { color: "gray", dimColor: true }, "q to exit")));
|
|
736
750
|
}
|
|
737
751
|
var init_ScoreCard = __esm({
|
|
738
752
|
"src/components/ScoreCard.js"() {
|
|
739
753
|
init_score();
|
|
754
|
+
init_ui();
|
|
740
755
|
}
|
|
741
756
|
});
|
|
742
757
|
|
|
@@ -751,15 +766,44 @@ function StatsView({ stats }) {
|
|
|
751
766
|
if (stats.total === 0) {
|
|
752
767
|
return /* @__PURE__ */ React4.createElement(Box4, { paddingX: 1, paddingY: 1, flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u26F3 TokenGolf Stats"), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "No completed runs yet."), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "Start one: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, "tokengolf start")));
|
|
753
768
|
}
|
|
754
|
-
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React4.createElement(Box4, { gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "Career Stats")), /* @__PURE__ */ React4.createElement(
|
|
769
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1 }, /* @__PURE__ */ React4.createElement(Box4, { gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "yellow" }, "\u26F3 TokenGolf"), /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "Career Stats")), /* @__PURE__ */ React4.createElement(
|
|
770
|
+
Box4,
|
|
771
|
+
{
|
|
772
|
+
borderStyle: ACCENT_BORDER,
|
|
773
|
+
borderColor: "gray",
|
|
774
|
+
borderRight: false,
|
|
775
|
+
borderTop: false,
|
|
776
|
+
borderBottom: false,
|
|
777
|
+
paddingLeft: ACCENT_PADDING,
|
|
778
|
+
paddingY: 1,
|
|
779
|
+
gap: 4
|
|
780
|
+
},
|
|
781
|
+
/* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "RUNS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "white" }, stats.total)),
|
|
782
|
+
/* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "WINS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "green" }, stats.wins)),
|
|
783
|
+
/* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "DEATHS"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "red" }, stats.deaths)),
|
|
784
|
+
/* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "WIN RATE"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: stats.winRate >= 70 ? "green" : stats.winRate >= 40 ? "yellow" : "red" }, stats.winRate, "%")),
|
|
785
|
+
/* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "AVG SPEND"), /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, formatCost(stats.avgSpend)))
|
|
786
|
+
), stats.bestRun && (() => {
|
|
755
787
|
const bestTier = getTier(stats.bestRun.spent);
|
|
756
788
|
const bestMc = getModelClass(stats.bestRun.model);
|
|
757
789
|
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "\u{1F3C6} Personal Best"), /* @__PURE__ */ React4.createElement(
|
|
758
790
|
Box4,
|
|
759
791
|
{
|
|
760
|
-
borderStyle:
|
|
792
|
+
borderStyle: {
|
|
793
|
+
topLeft: " ",
|
|
794
|
+
top: " ",
|
|
795
|
+
topRight: " ",
|
|
796
|
+
left: "\u2588\u2588",
|
|
797
|
+
right: " ",
|
|
798
|
+
bottomLeft: " ",
|
|
799
|
+
bottom: " ",
|
|
800
|
+
bottomRight: " "
|
|
801
|
+
},
|
|
761
802
|
borderColor: "yellow",
|
|
762
|
-
|
|
803
|
+
borderRight: false,
|
|
804
|
+
borderTop: false,
|
|
805
|
+
borderBottom: false,
|
|
806
|
+
paddingLeft: ACCENT_PADDING,
|
|
763
807
|
paddingY: 1,
|
|
764
808
|
flexDirection: "column"
|
|
765
809
|
},
|
|
@@ -772,11 +816,12 @@ function StatsView({ stats }) {
|
|
|
772
816
|
const mc = getModelClass(run.model);
|
|
773
817
|
const pct = run.budget ? getBudgetPct(run.spent, run.budget) : null;
|
|
774
818
|
return /* @__PURE__ */ React4.createElement(Box4, { key: i, gap: 2 }, /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, won ? "\u2713" : "\u2717"), /* @__PURE__ */ React4.createElement(Text4, { color: "white" }, (run.quest || "Flow").slice(0, 34).padEnd(34)), /* @__PURE__ */ React4.createElement(Text4, { color: won ? "green" : "red" }, formatCost(run.spent)), run.budget ? /* @__PURE__ */ React4.createElement(Text4, { color: "gray" }, "/", formatCost(run.budget)) : null, /* @__PURE__ */ React4.createElement(Text4, null, mc.emoji), /* @__PURE__ */ React4.createElement(Text4, { color: tier.color }, tier.emoji), pct !== null && /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, pct, "%"));
|
|
775
|
-
})), stats.achievements.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "Recent achievements:"), /* @__PURE__ */ React4.createElement(Box4, { flexWrap: "wrap",
|
|
819
|
+
})), stats.achievements.length > 0 && /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "Recent achievements:"), /* @__PURE__ */ React4.createElement(Box4, { flexWrap: "wrap", columnGap: 2 }, stats.achievements.slice(0, 12).map((a, i) => /* @__PURE__ */ React4.createElement(Text4, { key: i }, a.emoji, " ", /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, a.label))))), /* @__PURE__ */ React4.createElement(Text4, { color: "gray", dimColor: true }, "q to exit"));
|
|
776
820
|
}
|
|
777
821
|
var init_StatsView = __esm({
|
|
778
822
|
"src/components/StatsView.js"() {
|
|
779
823
|
init_score();
|
|
824
|
+
init_ui();
|
|
780
825
|
}
|
|
781
826
|
});
|
|
782
827
|
|
|
@@ -2020,14 +2065,18 @@ function autoDetectCost(run) {
|
|
|
2020
2065
|
// src/components/StartRun.js
|
|
2021
2066
|
init_state();
|
|
2022
2067
|
init_score();
|
|
2068
|
+
init_ui();
|
|
2023
2069
|
import React, { useState } from "react";
|
|
2024
2070
|
import { Box, Text, useApp } from "ink";
|
|
2025
2071
|
import { TextInput, Select, ConfirmInput } from "@inkjs/ui";
|
|
2026
2072
|
var MODEL_OPTIONS = [
|
|
2027
|
-
{ label: "\u2694\uFE0F Sonnet \u2014 Balanced. The default run. [
|
|
2028
|
-
{
|
|
2029
|
-
|
|
2030
|
-
|
|
2073
|
+
{ label: "\u2694\uFE0F Sonnet \u2014 Balanced. The default run. [Standard]", value: "claude-sonnet-4-6" },
|
|
2074
|
+
{
|
|
2075
|
+
label: "\u{1F3F9} Haiku \u2014 Glass cannon. Hard mode. [Nightmare]",
|
|
2076
|
+
value: "claude-haiku-4-5-20251001"
|
|
2077
|
+
},
|
|
2078
|
+
{ label: "\u269C\uFE0F Paladin \u2014 Opus plans, Sonnet executes. [Tactical]", value: "opusplan" },
|
|
2079
|
+
{ label: "\u{1F9D9} Opus \u2014 Powerful but expensive. [Casual]", value: "claude-opus-4-6" }
|
|
2031
2080
|
];
|
|
2032
2081
|
var EFFORT_OPTIONS_BASE = [
|
|
2033
2082
|
{ label: "\u2696\uFE0F Medium \u2014 Balanced (Anthropic recommended for Sonnet)", value: "medium" },
|
|
@@ -2066,9 +2115,12 @@ function StartRun() {
|
|
|
2066
2115
|
{
|
|
2067
2116
|
flexDirection: "column",
|
|
2068
2117
|
gap: 1,
|
|
2069
|
-
borderStyle:
|
|
2118
|
+
borderStyle: ACCENT_BORDER,
|
|
2070
2119
|
borderColor: "gray",
|
|
2071
|
-
|
|
2120
|
+
borderRight: false,
|
|
2121
|
+
borderTop: false,
|
|
2122
|
+
borderBottom: false,
|
|
2123
|
+
paddingLeft: ACCENT_PADDING,
|
|
2072
2124
|
paddingY: 1
|
|
2073
2125
|
},
|
|
2074
2126
|
/* @__PURE__ */ React.createElement(Box, { gap: 2, alignItems: "flex-start" }, /* @__PURE__ */ React.createElement(Text, { color: step === "quest" ? "cyan" : "gray" }, "\u{1F4CB} Quest "), step === "quest" ? /* @__PURE__ */ React.createElement(
|
|
@@ -2135,9 +2187,21 @@ function StartRun() {
|
|
|
2135
2187
|
step === "confirm" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1, marginTop: 1 }, /* @__PURE__ */ React.createElement(
|
|
2136
2188
|
Box,
|
|
2137
2189
|
{
|
|
2138
|
-
borderStyle:
|
|
2190
|
+
borderStyle: {
|
|
2191
|
+
topLeft: " ",
|
|
2192
|
+
top: " ",
|
|
2193
|
+
topRight: " ",
|
|
2194
|
+
left: "\u2588\u2588",
|
|
2195
|
+
right: " ",
|
|
2196
|
+
bottomLeft: " ",
|
|
2197
|
+
bottom: " ",
|
|
2198
|
+
bottomRight: " "
|
|
2199
|
+
},
|
|
2139
2200
|
borderColor: "yellow",
|
|
2140
|
-
|
|
2201
|
+
borderRight: false,
|
|
2202
|
+
borderTop: false,
|
|
2203
|
+
borderBottom: false,
|
|
2204
|
+
paddingLeft: ACCENT_PADDING,
|
|
2141
2205
|
paddingY: 1,
|
|
2142
2206
|
flexDirection: "column"
|
|
2143
2207
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#1a1812"/>
|
|
5
|
+
<stop offset="100%" stop-color="#23211a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="gold" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0%" stop-color="#d4a840"/>
|
|
9
|
+
<stop offset="100%" stop-color="#e8c060"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="rule" x1="0" y1="0" x2="1" y2="0">
|
|
12
|
+
<stop offset="0%" stop-color="#3d3828" stop-opacity="0"/>
|
|
13
|
+
<stop offset="15%" stop-color="#3d3828"/>
|
|
14
|
+
<stop offset="85%" stop-color="#3d3828"/>
|
|
15
|
+
<stop offset="100%" stop-color="#3d3828" stop-opacity="0"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
</defs>
|
|
18
|
+
|
|
19
|
+
<!-- Background -->
|
|
20
|
+
<rect width="1200" height="300" fill="url(#bg)"/>
|
|
21
|
+
|
|
22
|
+
<!-- Top decorative rule -->
|
|
23
|
+
<rect x="100" y="40" width="1000" height="1" fill="url(#rule)"/>
|
|
24
|
+
|
|
25
|
+
<!-- Flag icon -->
|
|
26
|
+
<text x="600" y="115" text-anchor="middle" font-size="52" fill="#d4a840" opacity="0.9">⛳</text>
|
|
27
|
+
|
|
28
|
+
<!-- Title -->
|
|
29
|
+
<text x="600" y="175" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="64" font-weight="bold" letter-spacing="6">
|
|
30
|
+
<tspan fill="url(#gold)">Token</tspan><tspan fill="#e8dcc4">Golf</tspan>
|
|
31
|
+
</text>
|
|
32
|
+
|
|
33
|
+
<!-- Tagline -->
|
|
34
|
+
<text x="600" y="215" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="20" font-style="italic" fill="#9a9180" letter-spacing="1">
|
|
35
|
+
Every token matters.
|
|
36
|
+
</text>
|
|
37
|
+
|
|
38
|
+
<!-- Bottom decorative rule -->
|
|
39
|
+
<rect x="100" y="248" width="1000" height="1" fill="url(#rule)"/>
|
|
40
|
+
|
|
41
|
+
<!-- Subtle tagline detail -->
|
|
42
|
+
<text x="600" y="278" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="13" fill="#3d3828" letter-spacing="3">
|
|
43
|
+
4 classes · 60+ achievements · 9 hooks
|
|
44
|
+
</text>
|
|
45
|
+
</svg>
|