1import type { Paint } from "@alchemy.run/sigil/color";
2
3/**
4 * Alchemy's palette is anchored in terracotta and green. The supporting
5 * amber, olive, sage, and coral tones stay within that warm botanical range.
6 */
7const color = {
8 brand: "#e28a5b",
9 accent: "#9acb69",
10 accentBright: "#c5df8c",
11 accentMuted: "#60764b",
12 success: "#9acb69",
13 warning: "#efb85a",
14 danger: "#d96f52",
15 magenta: "#c084d6",
16 info: "#b6c77a",
17 sage: "#b8cf83",
18 olive: "#9ea85e",
19 coral: "#ef9a6a",
20 muted: "#8f887c",
21 diffAddBackground: "#263b2a",
22 diffRemoveBackground: "#422a26",
23 onAccent: "#14110d",
24 emphasis: "#f5f0e6",
25} as const;
26
27export const theme = {
28 color,
29 space: {
30 inline: 1,
31 indent: 2,
32 section: 1,
33 },
34 paint: {
35 focus: color.brand,
36 interactive: color.brand,
37 info: color.info,
38 success: color.success,
39 warning: color.warning,
40 error: color.danger,
41 },
42 glyph: {
43 section: "●",
44 /**
45 * Marks the step currently being answered — the same "active one" glyph
46 * the profile tabs use. Deliberately not a chevron so the list cursor
47 * (`pointer`) is the only chevron on screen.
48 */
49 active: "◆",
50 /** List cursor: the one focus glyph shared by every prompt and dashboard row. */
51 pointer: "❯",
52 success: "✓",
53 warning: "!",
54 error: "×",
55 info: "•",
56 selected: "◆",
57 unselected: "·",
58 checked: "✓",
59 unchecked: "·",
60 add: "+",
61 adopt: "(+)",
62 edit: "~",
63 refresh: "↻",
64 delete: "-",
65 orphan: "(-)",
66 replace: "↔",
67 run: "▶",
68 bar: "┊",
69 mask: "•",
70 bullet: "·",
71 overflowUp: "↑",
72 overflowDown: "↓",
73 overflowLeft: "‹",
74 overflowRight: "›",
75 },
76 /**
77 * Key-hint labels for KeyBar footers. Resolve through `useKeyGlyphs()`
78 * (components/Environment.tsx) so ASCII terminals get readable fallbacks
79 * instead of mojibake.
80 */
81 keyHint: {
82 enter: "↩",
83 upDown: "↑/↓",
84 leftRight: "←/→",
85 escape: "esc",
86 space: "space",
87 tab: "tab",
88 yesNo: "y/n",
89 },
90} as const;
91
92export type KeyHint = { readonly [Key in keyof typeof theme.keyHint]: string };
93export type GlyphName = keyof typeof theme.glyph;
94
95export const asciiGlyphs: { readonly [Key in GlyphName]: string } = {
96 section: "@",
97 active: "*",
98 pointer: ">",
99 success: "+",
100 warning: "!",
101 error: "x",
102 info: "i",
103 selected: "*",
104 unselected: ".",
105 checked: "x",
106 unchecked: ".",
107 add: "+",
108 adopt: "(+)",
109 edit: "~",
110 refresh: "r",
111 delete: "-",
112 orphan: "(-)",
113 replace: "~",
114 run: ">",
115 bar: ":",
116 mask: "*",
117 bullet: ".",
118 overflowUp: "^",
119 overflowDown: "v",
120 overflowLeft: "<",
121 overflowRight: ">",
122};
123
124export const glyphsFor = (unicode: boolean) =>
125 unicode ? theme.glyph : asciiGlyphs;
126
127const spinnerFrames = [
128 "⠋",
129 "⠙",
130 "⠹",
131 "⠸",
132 "⠼",
133 "⠴",
134 "⠦",
135 "⠧",
136 "⠇",
137 "⠏",
138] as const;
139const asciiSpinnerFrames = ["-", "\\", "|", "/"] as const;
140
141export const spinnerFramesFor = (unicode: boolean): readonly string[] =>
142 unicode ? spinnerFrames : asciiSpinnerFrames;
143
144export type StatusVariant = "info" | "success" | "warning" | "error";
145
146export const statusColor = (variant: StatusVariant): string =>
147 variant === "error" ? theme.color.danger : theme.color[variant];
148
149export const statusPaint = (variant: StatusVariant): Paint =>
150 theme.paint[variant];