| 1 | import 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 | */ |
| 7 | const 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 | |
| 27 | export 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 | |
| 92 | export type KeyHint = { readonly [Key in keyof typeof theme.keyHint]: string }; |
| 93 | export type GlyphName = keyof typeof theme.glyph; |
| 94 | |
| 95 | export 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 | |
| 124 | export const glyphsFor = (unicode: boolean) => |
| 125 | unicode ? theme.glyph : asciiGlyphs; |
| 126 | |
| 127 | const spinnerFrames = [ |
| 128 | "⠋", |
| 129 | "⠙", |
| 130 | "⠹", |
| 131 | "⠸", |
| 132 | "⠼", |
| 133 | "⠴", |
| 134 | "⠦", |
| 135 | "⠧", |
| 136 | "⠇", |
| 137 | "⠏", |
| 138 | ] as const; |
| 139 | const asciiSpinnerFrames = ["-", "\\", "|", "/"] as const; |
| 140 | |
| 141 | export const spinnerFramesFor = (unicode: boolean): readonly string[] => |
| 142 | unicode ? spinnerFrames : asciiSpinnerFrames; |
| 143 | |
| 144 | export type StatusVariant = "info" | "success" | "warning" | "error"; |
| 145 | |
| 146 | export const statusColor = (variant: StatusVariant): string => |
| 147 | variant === "error" ? theme.color.danger : theme.color[variant]; |
| 148 | |
| 149 | export const statusPaint = (variant: StatusVariant): Paint => |
| 150 | theme.paint[variant]; |