agentcraft 0.0.1
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/.claude-plugin/plugin.json +7 -0
- package/README.md +95 -0
- package/bin/agentcraft.js +12 -0
- package/commands/agentcraft.md +46 -0
- package/hooks/hooks.json +13 -0
- package/hooks/play-sound.sh +69 -0
- package/opencode.js +87 -0
- package/package.json +19 -0
- package/screenshot.jpg +0 -0
- package/social-share-og.jpg +0 -0
- package/social-share-tw.jpg +0 -0
- package/social-share.png +0 -0
- package/web/README.md +36 -0
- package/web/app/api/agents/[name]/route.ts +44 -0
- package/web/app/api/agents/route.ts +62 -0
- package/web/app/api/assignments/route.ts +53 -0
- package/web/app/api/audio/[...path]/route.ts +35 -0
- package/web/app/api/health/route.ts +5 -0
- package/web/app/api/preview/route.ts +22 -0
- package/web/app/api/skills/route.ts +83 -0
- package/web/app/api/sounds/route.ts +106 -0
- package/web/app/api/ui-sounds/route.ts +44 -0
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +99 -0
- package/web/app/icon.svg +13 -0
- package/web/app/layout.tsx +19 -0
- package/web/app/opengraph-image.tsx +96 -0
- package/web/app/page.tsx +268 -0
- package/web/bun.lock +292 -0
- package/web/components/agent-form.tsx +190 -0
- package/web/components/agent-roster-panel.tsx +502 -0
- package/web/components/assignment-log-panel.tsx +109 -0
- package/web/components/hook-slot.tsx +149 -0
- package/web/components/hud-header.tsx +135 -0
- package/web/components/sound-browser-panel.tsx +206 -0
- package/web/components/sound-unit.tsx +203 -0
- package/web/components/ui-sounds-modal.tsx +308 -0
- package/web/lib/types.ts +87 -0
- package/web/lib/ui-audio.ts +126 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.ts +8 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +1 -0
- package/web/public/file.svg +1 -0
- package/web/public/fonts/starcraft-normal.ttf +0 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/tsconfig.json +34 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readdir, readFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import type { SkillInfo } from '@/lib/types';
|
|
6
|
+
|
|
7
|
+
const HOME = homedir();
|
|
8
|
+
const USER_SKILLS_DIR = join(HOME, '.claude', 'skills');
|
|
9
|
+
const PLUGINS_JSON = join(HOME, '.claude', 'plugins', 'installed_plugins.json');
|
|
10
|
+
|
|
11
|
+
async function readSkillFromDir(dir: string, namespace?: string): Promise<SkillInfo | null> {
|
|
12
|
+
try {
|
|
13
|
+
const skillMd = await readFile(join(dir, 'SKILL.md'), 'utf-8');
|
|
14
|
+
const descMatch = skillMd.match(/^description:\s*(.+)$/m);
|
|
15
|
+
const dirName = dir.split('/').pop() ?? '';
|
|
16
|
+
const qualifiedName = namespace ? `${namespace}:${dirName}` : dirName;
|
|
17
|
+
return {
|
|
18
|
+
name: dirName,
|
|
19
|
+
qualifiedName,
|
|
20
|
+
description: descMatch ? descMatch[1].trim() : dirName,
|
|
21
|
+
namespace,
|
|
22
|
+
};
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function GET() {
|
|
29
|
+
const skills: SkillInfo[] = [];
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
|
|
32
|
+
// User skills from ~/.claude/skills/
|
|
33
|
+
try {
|
|
34
|
+
const entries = await readdir(USER_SKILLS_DIR, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
const skill = await readSkillFromDir(join(USER_SKILLS_DIR, entry.name));
|
|
38
|
+
if (skill && !seen.has(skill.qualifiedName)) {
|
|
39
|
+
skills.push(skill);
|
|
40
|
+
seen.add(skill.qualifiedName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// user skills dir may not exist
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Plugin skills from installed_plugins.json
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(PLUGINS_JSON, 'utf-8');
|
|
50
|
+
const data = JSON.parse(raw) as {
|
|
51
|
+
version: number;
|
|
52
|
+
plugins: Record<string, Array<{ scope: string; installPath: string }>>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const [pluginKey, installs] of Object.entries(data.plugins)) {
|
|
56
|
+
const pluginName = pluginKey.split('@')[0];
|
|
57
|
+
// Deduplicate: use first install (user-scope preferred over project)
|
|
58
|
+
const install = installs.find(i => i.scope === 'user') ?? installs[0];
|
|
59
|
+
if (!install?.installPath) continue;
|
|
60
|
+
|
|
61
|
+
const skillsDir = join(install.installPath, 'skills');
|
|
62
|
+
try {
|
|
63
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!entry.isDirectory()) continue;
|
|
66
|
+
const qualifiedName = `${pluginName}:${entry.name}`;
|
|
67
|
+
if (seen.has(qualifiedName)) continue;
|
|
68
|
+
const skill = await readSkillFromDir(join(skillsDir, entry.name), pluginName);
|
|
69
|
+
if (skill) {
|
|
70
|
+
skills.push(skill);
|
|
71
|
+
seen.add(qualifiedName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// plugin may not have a skills dir
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// installed_plugins.json may not exist
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return NextResponse.json(skills.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName)));
|
|
83
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import type { SoundAsset } from '@/lib/types';
|
|
7
|
+
|
|
8
|
+
const SOUND_LIBRARY = join(homedir(), '.agentcraft', 'sounds');
|
|
9
|
+
const WAVEFORM_CACHE = join(homedir(), '.agentcraft', 'waveforms.json');
|
|
10
|
+
const BARS = 16;
|
|
11
|
+
const FALLBACK = [5, 7, 4, 9, 6, 8, 3, 7, 5, 8, 4, 6, 7, 5, 8, 4];
|
|
12
|
+
|
|
13
|
+
function computeWaveform(filePath: string): number[] {
|
|
14
|
+
// Decode to mono f32le PCM at 1000 Hz (massively downsampled — fast, still accurate)
|
|
15
|
+
const result = spawnSync('ffmpeg', [
|
|
16
|
+
'-i', filePath,
|
|
17
|
+
'-ac', '1', // mono
|
|
18
|
+
'-ar', '1000', // 1000 Hz sample rate
|
|
19
|
+
'-f', 'f32le', // raw 32-bit float output
|
|
20
|
+
'-', // pipe to stdout
|
|
21
|
+
], { maxBuffer: 1024 * 1024 * 4 });
|
|
22
|
+
|
|
23
|
+
if (result.status !== 0 || !result.stdout?.length) return FALLBACK;
|
|
24
|
+
|
|
25
|
+
const buf = result.stdout as Buffer;
|
|
26
|
+
const samples = buf.length / 4; // 4 bytes per f32
|
|
27
|
+
const blockSize = Math.floor(samples / BARS);
|
|
28
|
+
const values: number[] = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < BARS; i++) {
|
|
31
|
+
let sum = 0;
|
|
32
|
+
for (let j = 0; j < blockSize; j++) {
|
|
33
|
+
sum += Math.abs(buf.readFloatLE((i * blockSize + j) * 4));
|
|
34
|
+
}
|
|
35
|
+
values.push(sum / blockSize);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const max = Math.max(...values, 0.0001);
|
|
39
|
+
return values.map((v) => Math.max(1, Math.round((v / max) * 10)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadCache(): Promise<Record<string, number[]>> {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(await readFile(WAVEFORM_CACHE, 'utf-8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function saveCache(cache: Record<string, number[]>) {
|
|
51
|
+
await mkdir(dirname(WAVEFORM_CACHE), { recursive: true });
|
|
52
|
+
await writeFile(WAVEFORM_CACHE, JSON.stringify(cache), 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function walkDir(dir: string, base: string): Promise<SoundAsset[]> {
|
|
56
|
+
const assets: SoundAsset[] = [];
|
|
57
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const fullPath = join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
assets.push(...await walkDir(fullPath, base));
|
|
63
|
+
} else if (entry.name.match(/\.(mp3|wav|ogg|m4a)$/i)) {
|
|
64
|
+
const relativePath = fullPath.replace(base + '/', '');
|
|
65
|
+
const parts = relativePath.split('/');
|
|
66
|
+
// For flat dirs (depth 1): icq/uh-oh.mp3 → category=icq, subcategory=''
|
|
67
|
+
// For nested dirs (depth 2+): sc2/terran/session-start/scv.mp3 → category=sc2/terran, subcategory=session-start
|
|
68
|
+
const category = parts.length > 2 ? parts.slice(0, -2).join('/') : parts[0];
|
|
69
|
+
const subcategory = parts.length > 2 ? parts[parts.length - 2] : '';
|
|
70
|
+
assets.push({
|
|
71
|
+
id: relativePath.replace(/\.[^/.]+$/, ''),
|
|
72
|
+
filename: parts[parts.length - 1],
|
|
73
|
+
category,
|
|
74
|
+
subcategory,
|
|
75
|
+
path: relativePath,
|
|
76
|
+
waveform: FALLBACK,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return assets;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function GET() {
|
|
85
|
+
try {
|
|
86
|
+
const assets = await walkDir(SOUND_LIBRARY, SOUND_LIBRARY);
|
|
87
|
+
const cache = await loadCache();
|
|
88
|
+
|
|
89
|
+
let dirty = false;
|
|
90
|
+
const results = assets.map((asset) => {
|
|
91
|
+
if (cache[asset.path]) {
|
|
92
|
+
return { ...asset, waveform: cache[asset.path] };
|
|
93
|
+
}
|
|
94
|
+
const waveform = computeWaveform(join(SOUND_LIBRARY, asset.path));
|
|
95
|
+
cache[asset.path] = waveform;
|
|
96
|
+
dirty = true;
|
|
97
|
+
return { ...asset, waveform };
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (dirty) await saveCache(cache);
|
|
101
|
+
|
|
102
|
+
return NextResponse.json(results);
|
|
103
|
+
} catch {
|
|
104
|
+
return NextResponse.json({ error: 'Failed to read sound library' }, { status: 500 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readdir, stat } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
const UI_DIR = join(homedir(), '.agentcraft', 'sounds', 'ui');
|
|
7
|
+
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.m4a']);
|
|
8
|
+
|
|
9
|
+
interface UISound {
|
|
10
|
+
path: string; // relative to ~/.agentcraft/sounds/, e.g. "ui/sc2/click.mp3"
|
|
11
|
+
filename: string; // e.g. "click.mp3"
|
|
12
|
+
group: string; // e.g. "sc2"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function scanDir(dir: string, base: string): Promise<UISound[]> {
|
|
16
|
+
const results: UISound[] = [];
|
|
17
|
+
let entries: string[];
|
|
18
|
+
try {
|
|
19
|
+
entries = await readdir(dir);
|
|
20
|
+
} catch {
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries.sort()) {
|
|
24
|
+
const full = join(dir, entry);
|
|
25
|
+
const s = await stat(full).catch(() => null);
|
|
26
|
+
if (!s) continue;
|
|
27
|
+
if (s.isDirectory()) {
|
|
28
|
+
results.push(...await scanDir(full, `${base}/${entry}`));
|
|
29
|
+
} else {
|
|
30
|
+
const ext = entry.toLowerCase().slice(entry.lastIndexOf('.'));
|
|
31
|
+
if (AUDIO_EXTS.has(ext)) {
|
|
32
|
+
const rel = `${base}/${entry}`;
|
|
33
|
+
const group = base.replace('ui/', '');
|
|
34
|
+
results.push({ path: rel, filename: entry, group });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function GET() {
|
|
42
|
+
const sounds = await scanDir(UI_DIR, 'ui');
|
|
43
|
+
return NextResponse.json(sounds);
|
|
44
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Share+Tech+Mono&display=swap');
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
@font-face {
|
|
5
|
+
font-family: 'Starcraft';
|
|
6
|
+
src: url('/fonts/starcraft-normal.ttf') format('truetype');
|
|
7
|
+
font-weight: normal;
|
|
8
|
+
font-style: normal;
|
|
9
|
+
font-display: swap;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--sf-bg: #07090F;
|
|
14
|
+
--sf-panel: #0A0E18;
|
|
15
|
+
--sf-panel2: #0D1520;
|
|
16
|
+
--sf-cyan: #00E5FF;
|
|
17
|
+
--sf-blue: #00A8FF;
|
|
18
|
+
--sf-alert: #FF3366;
|
|
19
|
+
--sf-gold: #FFC000;
|
|
20
|
+
--sf-green: #00FF88;
|
|
21
|
+
--sf-border: rgba(0, 229, 255, 0.2);
|
|
22
|
+
--sf-border-gold: rgba(255, 192, 0, 0.4);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
background-color: var(--sf-bg);
|
|
32
|
+
color: #e0e8ff;
|
|
33
|
+
font-family: 'Share Tech Mono', monospace;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sf-heading {
|
|
38
|
+
font-family: 'Chakra Petch', sans-serif;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sf-logo {
|
|
42
|
+
font-family: 'Starcraft', 'Chakra Petch', sans-serif;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Scanlines overlay */
|
|
46
|
+
body::before {
|
|
47
|
+
content: '';
|
|
48
|
+
position: fixed;
|
|
49
|
+
inset: 0;
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
z-index: 9999;
|
|
52
|
+
background: repeating-linear-gradient(
|
|
53
|
+
0deg,
|
|
54
|
+
transparent,
|
|
55
|
+
transparent 2px,
|
|
56
|
+
rgba(0, 0, 0, 0.08) 2px,
|
|
57
|
+
rgba(0, 0, 0, 0.08) 4px
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Angled clip-path card */
|
|
62
|
+
.sf-card {
|
|
63
|
+
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Cyan glow */
|
|
67
|
+
.sf-glow {
|
|
68
|
+
box-shadow: 0 0 8px rgba(0, 229, 255, 0.3), inset 0 0 8px rgba(0, 229, 255, 0.05);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Gold glow */
|
|
72
|
+
.sf-glow-gold {
|
|
73
|
+
box-shadow: 0 0 8px rgba(255, 192, 0, 0.3), inset 0 0 8px rgba(255, 192, 0, 0.05);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Pulse animation for dirty state */
|
|
77
|
+
@keyframes pulse-gold {
|
|
78
|
+
0%, 100% { box-shadow: 0 0 4px rgba(255, 192, 0, 0.4); }
|
|
79
|
+
50% { box-shadow: 0 0 16px rgba(255, 192, 0, 0.8), 0 0 32px rgba(255, 192, 0, 0.3); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sf-pulse-gold {
|
|
83
|
+
animation: pulse-gold 1.5s ease-in-out infinite;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Scrollbar styling */
|
|
87
|
+
::-webkit-scrollbar {
|
|
88
|
+
width: 4px;
|
|
89
|
+
}
|
|
90
|
+
::-webkit-scrollbar-track {
|
|
91
|
+
background: var(--sf-bg);
|
|
92
|
+
}
|
|
93
|
+
::-webkit-scrollbar-thumb {
|
|
94
|
+
background: var(--sf-border);
|
|
95
|
+
border-radius: 2px;
|
|
96
|
+
}
|
|
97
|
+
::-webkit-scrollbar-thumb:hover {
|
|
98
|
+
background: var(--sf-cyan);
|
|
99
|
+
}
|
package/web/app/icon.svg
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
|
2
|
+
<rect width="32" height="32" fill="#07090F"/>
|
|
3
|
+
<!-- Waveform bars: 8 bars of varying height, centered -->
|
|
4
|
+
<rect x="2" y="18" width="3" height="8" fill="#00E5FF" opacity="0.5"/>
|
|
5
|
+
<rect x="6" y="12" width="3" height="14" fill="#00E5FF" opacity="0.7"/>
|
|
6
|
+
<rect x="10" y="7" width="3" height="19" fill="#00E5FF"/>
|
|
7
|
+
<rect x="14" y="10" width="3" height="16" fill="#00E5FF"/>
|
|
8
|
+
<rect x="18" y="4" width="3" height="22" fill="#00E5FF"/>
|
|
9
|
+
<rect x="22" y="9" width="3" height="17" fill="#00E5FF"/>
|
|
10
|
+
<rect x="26" y="14" width="3" height="12" fill="#00E5FF" opacity="0.7"/>
|
|
11
|
+
<!-- Top cyan border accent -->
|
|
12
|
+
<rect x="0" y="0" width="32" height="2" fill="#00E5FF" opacity="0.6"/>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'AgentCraft',
|
|
6
|
+
description: 'Assign sounds to AI coding agent lifecycle events',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ImageResponse } from 'next/og';
|
|
2
|
+
|
|
3
|
+
export const runtime = 'edge';
|
|
4
|
+
export const alt = 'AgentCraft — Assign sounds to AI coding agent lifecycle events';
|
|
5
|
+
export const size = { width: 1200, height: 630 };
|
|
6
|
+
export const contentType = 'image/png';
|
|
7
|
+
|
|
8
|
+
export default function OGImage() {
|
|
9
|
+
const bars = [4, 7, 5, 9, 6, 10, 7, 8, 5, 9, 6, 8, 4, 7, 5, 8];
|
|
10
|
+
const maxH = 120;
|
|
11
|
+
|
|
12
|
+
return new ImageResponse(
|
|
13
|
+
(
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
width: 1200,
|
|
17
|
+
height: 630,
|
|
18
|
+
background: '#07090F',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
fontFamily: 'monospace',
|
|
24
|
+
position: 'relative',
|
|
25
|
+
overflow: 'hidden',
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{/* Top border accent */}
|
|
29
|
+
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, background: '#00E5FF', opacity: 0.8, display: 'flex' }} />
|
|
30
|
+
|
|
31
|
+
{/* Grid overlay */}
|
|
32
|
+
<div style={{
|
|
33
|
+
position: 'absolute', inset: 0,
|
|
34
|
+
backgroundImage: 'linear-gradient(rgba(0,229,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px)',
|
|
35
|
+
backgroundSize: '40px 40px',
|
|
36
|
+
display: 'flex',
|
|
37
|
+
}} />
|
|
38
|
+
|
|
39
|
+
{/* Waveform */}
|
|
40
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, marginBottom: 48, height: maxH + 20 }}>
|
|
41
|
+
{bars.map((v, i) => (
|
|
42
|
+
<div
|
|
43
|
+
key={i}
|
|
44
|
+
style={{
|
|
45
|
+
width: 28,
|
|
46
|
+
height: Math.round((v / 10) * maxH),
|
|
47
|
+
background: i === 8 ? '#00E5FF' : `rgba(0,229,255,${0.3 + (v / 10) * 0.7})`,
|
|
48
|
+
borderRadius: 2,
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Title */}
|
|
55
|
+
<div style={{
|
|
56
|
+
fontSize: 72,
|
|
57
|
+
fontWeight: 700,
|
|
58
|
+
color: '#00E5FF',
|
|
59
|
+
letterSpacing: '0.2em',
|
|
60
|
+
textTransform: 'uppercase',
|
|
61
|
+
display: 'flex',
|
|
62
|
+
}}>
|
|
63
|
+
AGENTCRAFT
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Subtitle */}
|
|
67
|
+
<div style={{
|
|
68
|
+
fontSize: 22,
|
|
69
|
+
color: 'rgba(255,255,255,0.5)',
|
|
70
|
+
letterSpacing: '0.3em',
|
|
71
|
+
textTransform: 'uppercase',
|
|
72
|
+
marginTop: 16,
|
|
73
|
+
display: 'flex',
|
|
74
|
+
}}>
|
|
75
|
+
AUDIO ASSIGNMENT TERMINAL
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Tag line */}
|
|
79
|
+
<div style={{
|
|
80
|
+
fontSize: 16,
|
|
81
|
+
color: 'rgba(0,229,255,0.5)',
|
|
82
|
+
letterSpacing: '0.15em',
|
|
83
|
+
textTransform: 'uppercase',
|
|
84
|
+
marginTop: 24,
|
|
85
|
+
display: 'flex',
|
|
86
|
+
}}>
|
|
87
|
+
CLAUDE CODE · OPENCODE · AI LIFECYCLE HOOKS
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Bottom border */}
|
|
91
|
+
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: '#00E5FF', opacity: 0.3, display: 'flex' }} />
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
{ ...size }
|
|
95
|
+
);
|
|
96
|
+
}
|