foreman/ is a Claude Code mod (job runner + hooks) that imports the claude-code runtime types, so tsc over the app failed on it. Excluding it from tsconfig keeps the Next build green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
7.7 KiB
TypeScript
142 lines
7.7 KiB
TypeScript
export type Mood = 'idle' | 'running' | 'clear' | 'attention' | 'failed'
|
|
|
|
export type Theme = {
|
|
name: string
|
|
bg: string // pane background
|
|
panel: string // card background
|
|
chip: string // chip / column background
|
|
line: string // borders
|
|
text: string // main text
|
|
sub: string // secondary text
|
|
muted: string // hints
|
|
accent: string // headings, primary
|
|
good: string // clear / done
|
|
warn: string // needs attention
|
|
bad: string // failed
|
|
run: string // running
|
|
/** Dark themes only: the light chip buttons and fields sit on, since the app draws their text dark. */
|
|
control?: string
|
|
}
|
|
|
|
// Bright by default; Dracula and Monokai for the evenings.
|
|
export const THEMES = {
|
|
hivis: { name: 'Hi-Vis', bg: '#FFFCF2', panel: '#FFFFFF', chip: '#FFF3C4', line: '#EED68A', text: '#23201A', sub: '#4D473A', muted: '#8A8270', accent: '#D99100', good: '#2E9E5B', warn: '#E8590C', bad: '#D6336C', run: '#1C7ED6' },
|
|
sky: { name: 'Sky', bg: '#F3F8FF', panel: '#FFFFFF', chip: '#E3EEFF', line: '#BCD3F5', text: '#142338', sub: '#3A4E6B', muted: '#7A8BA3', accent: '#2F80ED', good: '#12A37A', warn: '#F76707', bad: '#E03131', run: '#7048E8' },
|
|
mint: { name: 'Mint', bg: '#F1FBF6', panel: '#FFFFFF', chip: '#DDF5E8', line: '#A8DFC1', text: '#12261C', sub: '#345244', muted: '#6F8F80', accent: '#0CA678', good: '#2B8A3E', warn: '#F08C00', bad: '#E8590C', run: '#1C7ED6' },
|
|
sunrise: { name: 'Sunrise', bg: '#FFF6F2', panel: '#FFFFFF', chip: '#FFE6DC', line: '#F7C2AE', text: '#2B1A14', sub: '#5A3A2E', muted: '#9A7A6E', accent: '#FF6B4A', good: '#2F9E44', warn: '#F59F00', bad: '#D6336C', run: '#4263EB' },
|
|
grape: { name: 'Grape', bg: '#F8F5FF', panel: '#FFFFFF', chip: '#EDE6FF', line: '#CFC0F5', text: '#1F1636', sub: '#463A66', muted: '#8A80A6', accent: '#7950F2', good: '#12A37A', warn: '#FD7E14', bad: '#FA5252', run: '#339AF0' },
|
|
dracula: { name: 'Dracula', bg: '#282A36', panel: '#2F3242', chip: '#383A4C', line: '#6272A4', text: '#F8F8F2', sub: '#E9E9F0', muted: '#A4ACD6', accent: '#BD93F9', good: '#50FA7B', warn: '#FFB86C', bad: '#FF5555', run: '#8BE9FD', control: '#ECE4FF' },
|
|
monokai: { name: 'Monokai', bg: '#272822', panel: '#2F302A', chip: '#3E3D32', line: '#75715E', text: '#F8F8F2', sub: '#E6E6DC', muted: '#B5AF96', accent: '#E6DB74', good: '#A6E22E', warn: '#FD971F', bad: '#F92672', run: '#66D9EF', control: '#F5F1D3' },
|
|
} satisfies Record<string, Theme>
|
|
|
|
export type ThemeId = keyof typeof THEMES
|
|
export const THEME_ORDER = Object.keys(THEMES) as ThemeId[]
|
|
export const isThemeId = (v: unknown): v is ThemeId => typeof v === 'string' && v in THEMES
|
|
|
|
export const MOOD_LABEL: Record<Mood, string> = {
|
|
idle: 'On site, waiting for the next job',
|
|
running: 'Out checking paperwork',
|
|
clear: 'All clear on the last run',
|
|
attention: 'Found things that need a look',
|
|
failed: 'A job hit a problem',
|
|
}
|
|
|
|
// Terminal fallback: the block-glyph critter in a hard hat.
|
|
export const ASCII: Record<Mood, string> = {
|
|
idle: ' ▄██▄\n▐▛█M█▜▌ z',
|
|
running: ' ▄██▄\n▐▛█M█▜▌ ~',
|
|
clear: ' ▄██▄\n▐▛█M█▜▌ ✓',
|
|
attention: ' ▄██▄\n▐▛█M█▜▌ !',
|
|
failed: ' ▄██▄\n▐▛█M█▜▌ ✗',
|
|
}
|
|
|
|
const CRAB = { body: '#3F7FB8', light: '#7DB1DE', dark: '#2A5A86', eye: '#F7F3EA', ink: '#1E1A16' }
|
|
const HAT = { main: '#FFC21A', shade: '#E0A100', shine: '#FFE27A' }
|
|
const VEST = { main: '#FF7A1A', strip: '#E8EEF2' }
|
|
|
|
const px = (x: number, y: number, w: number, h: number, fill: string, extra = '') =>
|
|
`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}"${extra}/>`
|
|
|
|
const bob = (dy: number, dur: string, begin = '0s') =>
|
|
`<animateTransform attributeName="transform" type="translate" values="0 0;0 ${dy};0 0" dur="${dur}" begin="${begin}" repeatCount="indefinite" calcMode="discrete"/>`
|
|
|
|
const M_ROWS = ['X...X', 'XX.XX', 'X.X.X', 'X...X']
|
|
const monogram = (x0: number, y0: number, fill: string) =>
|
|
M_ROWS.flatMap((row, r) => [...row].map((c, col) => (c === 'X' ? px(x0 + col * 0.5, y0 + r * 0.5, 0.5, 0.5, fill) : ''))).join('')
|
|
|
|
/** Foreman: the blue pixel critter in a yellow hard hat and hi-vis vest. */
|
|
export function foremanSvg(mood: Mood, t: Theme): string {
|
|
const running = mood === 'running'
|
|
const idle = mood === 'idle'
|
|
const happy = mood === 'clear'
|
|
const alert = mood === 'attention'
|
|
const sad = mood === 'failed'
|
|
const body = CRAB.body
|
|
const eye = CRAB.eye
|
|
|
|
const eyes = idle
|
|
? px(4, 3.5, 1, 0.5, CRAB.ink) + px(11, 3.5, 1, 0.5, CRAB.ink)
|
|
: happy
|
|
? px(4, 2.5, 1, 0.5, eye) + px(3.5, 3, 0.5, 0.5, eye) + px(5, 3, 0.5, 0.5, eye) +
|
|
px(11, 2.5, 1, 0.5, eye) + px(10.5, 3, 0.5, 0.5, eye) + px(12, 3, 0.5, 0.5, eye)
|
|
: sad
|
|
? px(4, 2.5, 1, 1.5, eye) + px(11, 2.5, 1, 1.5, eye) + px(3.5, 2, 1.5, 0.5, CRAB.dark) + px(11, 2, 1.5, 0.5, CRAB.dark)
|
|
: [4, 11]
|
|
.map(x =>
|
|
running
|
|
? `<rect x="${x}" y="2" width="1" height="2" fill="${eye}">
|
|
<animate attributeName="height" values="2;2;0.3;2" keyTimes="0;0.9;0.95;1" dur="3s" repeatCount="indefinite"/>
|
|
<animate attributeName="y" values="2;2;3.7;2" keyTimes="0;0.9;0.95;1" dur="3s" repeatCount="indefinite"/></rect>`
|
|
: px(x, alert ? 1.5 : 2, 1, alert ? 2.5 : 2, eye),
|
|
)
|
|
.join('')
|
|
|
|
const legsA = px(3, 8, 1, 2, body) + px(10, 8, 1, 2, body)
|
|
const legsB = px(5, 8, 1, 2, body) + px(12, 8, 1, 2, body)
|
|
|
|
// Hard hat: dome, ridge, shine and a wide brim over the top of the body.
|
|
const hat =
|
|
px(4, -3, 8, 0.5, HAT.main) + px(3, -2.5, 10, 2, HAT.main) + px(7.5, -3.5, 1, 3, HAT.shade) +
|
|
px(4, -2.5, 1.5, 0.5, HAT.shine) + px(1, -0.5, 14, 0.75, HAT.main) + px(1, 0.25, 14, 0.25, HAT.shade)
|
|
|
|
// Hi-vis vest straps with reflective strips.
|
|
const vest =
|
|
px(2, 4.5, 2.5, 3.5, VEST.main) + px(11.5, 4.5, 2.5, 3.5, VEST.main) +
|
|
px(2, 6, 2.5, 0.5, VEST.strip) + px(11.5, 6, 2.5, 0.5, VEST.strip)
|
|
|
|
const extras = idle
|
|
? `<g>${px(16, -2, 1, 0.25, t.muted)}${px(16.75, -1.75, 0.25, 0.5, t.muted)}${px(16, -1.25, 1, 0.25, t.muted)}
|
|
<animate attributeName="opacity" values="0;1;0" dur="2.6s" repeatCount="indefinite"/></g>`
|
|
: running
|
|
? [0, 0.8, 1.6]
|
|
.map(
|
|
(d, i) => `<rect x="${16 + (i % 2) * 0.6}" y="2" width="0.5" height="0.5" fill="${CRAB.light}">
|
|
<animate attributeName="y" values="2;-2.5" dur="2.4s" begin="${d}s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values="1;0" dur="2.4s" begin="${d}s" repeatCount="indefinite"/></rect>`,
|
|
)
|
|
.join('')
|
|
: happy
|
|
? `<g>${px(16.5, -3, 0.5, 1.5, t.good)}${px(16, -2.5, 1.5, 0.5, t.good)}
|
|
<animate attributeName="opacity" values="1;0.2;1" dur="1.4s" repeatCount="indefinite"/></g>
|
|
<g>${px(-0.5, -2.5, 0.5, 1, HAT.main)}${px(-0.75, -2.25, 1, 0.5, HAT.main)}
|
|
<animate attributeName="opacity" values="0.2;1;0.2" dur="1.4s" repeatCount="indefinite"/></g>`
|
|
: alert
|
|
? `<g>${px(16.25, -3.5, 1, 2.5, t.warn)}${px(16.25, -0.5, 1, 1, t.warn)}
|
|
<animate attributeName="opacity" values="1;0.35;1" dur="1s" repeatCount="indefinite"/></g>`
|
|
: px(5.25, 4.5, 0.5, 0.75, CRAB.light) + px(12.25, 4.5, 0.5, 0.75, CRAB.light)
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -5 21 16" width="132" height="100" shape-rendering="crispEdges">
|
|
${px(1, 10.4, 14, 0.4, t.muted, ' opacity="0.25"')}
|
|
<g>${running ? bob(-0.25, '0.8s') : ''}
|
|
<g>${running ? bob(-0.5, '0.4s') : ''}${legsA}</g>
|
|
<g>${running ? bob(-0.5, '0.4s', '0.2s') : ''}${legsB}</g>
|
|
<g>${running ? bob(-0.5, '0.6s') : ''}${px(0, 4, 2, 2, body)}${px(14, 4, 2, 2, body)}</g>
|
|
${px(2, 0, 12, 8, body)}
|
|
${vest}
|
|
${eyes}
|
|
${monogram(6.75, 5, HAT.main)}
|
|
${hat}
|
|
</g>
|
|
${extras}
|
|
</svg>`
|
|
}
|