Initial commit

This commit is contained in:
2026-06-05 20:53:53 -05:00
commit f9a59e9a66
99 changed files with 15897 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
.shell {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-base);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 42px;
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.title {
font-size: 15px;
color: var(--accent-gold);
letter-spacing: 0.05em;
}
.headerActions {
display: flex;
gap: 8px;
}
.headerBtn {
background: none;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 4px 10px;
font-size: 13px;
font-family: 'Cinzel', serif;
transition: border-color 0.15s, color 0.15s;
}
.headerBtn:hover {
border-color: var(--border-accent);
color: var(--accent-gold);
}
.headerBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.panels {
display: flex;
flex: 1;
overflow: hidden;
}

View File

@@ -0,0 +1,83 @@
import { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useAppStore } from '../../store/appStore';
import LeftPanel from './LeftPanel';
import CenterPanel from './CenterPanel';
import RightPanel from './RightPanel';
import ConfigScreen from '../config/ConfigScreen';
import styles from './AppShell.module.css';
export default function AppShell() {
const { centerMode, setCenterMode, uoRoot, servuoScripts } = useAppStore();
const [indexingAssets, setIndexingAssets] = useState(false);
const [indexingScripts, setIndexingScripts] = useState(false);
async function handleIndexAssets() {
if (!uoRoot) return;
setIndexingAssets(true);
try {
const count = await invoke<number>('index_assets', { uoRoot });
alert(`Indexed ${count.toLocaleString()} tiles.`);
} catch (e) {
alert(`Asset index failed: ${e}`);
} finally {
setIndexingAssets(false);
}
}
async function handleIndexScripts() {
if (!servuoScripts) {
alert('Set the ServUO Scripts path in Config first.');
return;
}
setIndexingScripts(true);
try {
const count = await invoke<number>('index_scripts', { scriptsPath: servuoScripts });
alert(`Indexed ${count.toLocaleString()} classes.`);
} catch (e) {
alert(`Script index failed: ${e}`);
} finally {
setIndexingScripts(false);
}
}
return (
<div className={styles.shell}>
<header className={styles.header}>
<span className={`${styles.title} font-cinzel`}>Artificer's Scrollwork</span>
<div className={styles.headerActions}>
<button
className={styles.headerBtn}
onClick={handleIndexAssets}
disabled={indexingAssets}
>
{indexingAssets ? 'Indexing' : '[Assets]'}
</button>
<button
className={styles.headerBtn}
onClick={handleIndexScripts}
disabled={indexingScripts}
>
{indexingScripts ? 'Indexing' : '[Scripts]'}
</button>
<button
className={styles.headerBtn}
onClick={() => setCenterMode('config')}
>
[Config]
</button>
</div>
</header>
{centerMode === 'config' ? (
<ConfigScreen onDone={() => setCenterMode('empty')} />
) : (
<div className={styles.panels}>
<LeftPanel />
<CenterPanel />
<RightPanel />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,23 @@
.panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-base);
overflow: hidden;
}
.empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 14px;
letter-spacing: 0.05em;
}
.placeholder {
padding: 40px;
color: var(--text-muted);
font-style: italic;
}

View File

@@ -0,0 +1,35 @@
import { useAppStore } from '../../store/appStore';
import ItemPreview from '../asset/ItemPreview';
import ClassDetail from '../script/ClassDetail';
import FlowViewer from '../flow/FlowViewer';
import GumpPreview from '../gump/GumpPreview';
import ScriptGumpDetail from '../gump/ScriptGumpDetail';
import styles from './CenterPanel.module.css';
export default function CenterPanel() {
const { centerMode, selectedTile, selectedGump, selectedScriptGump } = useAppStore();
return (
<div className={styles.panel}>
{centerMode === 'empty' && (
<div className={styles.empty}>
<span className="font-cinzel">Select an item from the left panel</span>
</div>
)}
{centerMode === 'asset_static' && selectedTile && (
<ItemPreview tile={selectedTile} />
)}
{centerMode === 'script_class' && <ClassDetail />}
{centerMode === 'flow_method' && <FlowViewer />}
{centerMode === 'gump_render' && selectedScriptGump && (
<ScriptGumpDetail />
)}
{centerMode === 'gump_render' && !selectedScriptGump && (
<div className={styles.placeholder}>Gump Renderer Phase 4</div>
)}
{centerMode === 'gump_image' && selectedGump && (
<GumpPreview gump={selectedGump} />
)}
</div>
);
}

View File

@@ -0,0 +1,64 @@
.panel {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--bg-panel);
border-right: 1px solid var(--border);
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
}
.tab {
flex: 1;
padding: 8px 4px;
background: none;
border: none;
border-right: 1px solid var(--border);
color: var(--text-secondary);
font-size: 11px;
font-family: 'Cinzel', serif;
letter-spacing: 0.04em;
transition: background 0.15s, color 0.15s;
}
.tab:last-child {
border-right: none;
}
.tab:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tab.active {
background: var(--bg-elevated);
color: var(--accent-gold);
border-bottom: 2px solid var(--accent-gold);
}
.search {
padding: 8px;
border-bottom: 1px solid var(--border);
}
.searchInput {
width: 100%;
font-size: 13px;
}
.content {
flex: 1;
overflow-y: auto;
}
.placeholder {
padding: 20px;
color: var(--text-muted);
font-size: 13px;
font-style: italic;
}

View File

@@ -0,0 +1,53 @@
import { useState } from 'react';
import { useAppStore, type LeftPanelTab } from '../../store/appStore';
import StaticBrowser from '../asset/StaticBrowser';
import ScriptTree from '../script/ScriptTree';
import GumpBrowser from '../gump/GumpBrowser';
import styles from './LeftPanel.module.css';
const TABS: { id: LeftPanelTab; label: string }[] = [
{ id: 'scripts', label: 'Scripts' },
{ id: 'statics', label: 'Statics' },
{ id: 'mobiles', label: 'Mobiles' },
{ id: 'gumps', label: 'Gumps' },
];
export default function LeftPanel() {
const { activeTab, setActiveTab } = useAppStore();
const [search, setSearch] = useState('');
return (
<div className={styles.panel}>
<div className={styles.tabs}>
{TABS.map((t) => (
<button
key={t.id}
className={`${styles.tab} ${activeTab === t.id ? styles.active : ''}`}
onClick={() => { setActiveTab(t.id); setSearch(''); }}
>
{t.label}
</button>
))}
</div>
<div className={styles.search}>
<input
type="text"
placeholder="Search…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className={styles.searchInput}
/>
</div>
<div className={styles.content}>
{activeTab === 'statics' && <StaticBrowser search={search} />}
{activeTab === 'scripts' && <ScriptTree search={search} />}
{activeTab === 'mobiles' && (
<div className={styles.placeholder}>Mobile browser Phase 1 (coming)</div>
)}
{activeTab === 'gumps' && <GumpBrowser search={search} />}
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
.panel {
width: 320px;
flex-shrink: 0;
background: var(--bg-panel);
border-left: 1px solid var(--border);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.section {
padding: 12px;
border-bottom: 1px solid var(--border);
}
.sectionTitle {
font-size: 11px;
color: var(--accent-gold);
letter-spacing: 0.08em;
margin-bottom: 10px;
text-transform: uppercase;
}
.propList {
display: flex;
flex-direction: column;
gap: 4px;
}
.propRow {
display: flex;
justify-content: space-between;
font-size: 13px;
}
.propLabel {
color: var(--text-secondary);
}
.propValue {
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
.subTitle {
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 10px 0 6px;
}
.empty {
color: var(--text-muted);
font-size: 13px;
font-style: italic;
}

View File

@@ -0,0 +1,72 @@
import { useAppStore } from '../../store/appStore';
import FakeDataInputs from '../flow/FakeDataInputs';
import styles from './RightPanel.module.css';
export default function RightPanel() {
const { selectedTile, selectedMethod, centerMode } = useAppStore();
return (
<div className={styles.panel}>
{/* Asset properties */}
{centerMode === 'asset_static' && selectedTile && (
<div className={styles.section}>
<div className={`${styles.sectionTitle} font-cinzel`}>Properties</div>
<div className={styles.propList}>
<PropRow label="ID" value={selectedTile.id.toString()} />
<PropRow label="Name" value={selectedTile.name || '(unnamed)'} />
<PropRow label="Flags" value={`0x${selectedTile.flags.toString(16).toUpperCase()}`} />
<PropRow label="Weight" value={selectedTile.weight.toString()} />
<PropRow label="Height" value={selectedTile.height.toString()} />
<PropRow label="Quality" value={selectedTile.quality.toString()} />
<PropRow label="Hue" value={selectedTile.hue.toString()} />
<PropRow label="Anim ID" value={selectedTile.animId.toString()} />
</div>
</div>
)}
{/* Method detail */}
{centerMode === 'flow_method' && selectedMethod && (
<div className={styles.section}>
<div className={`${styles.sectionTitle} font-cinzel`}>Method</div>
<div className={styles.propList}>
<PropRow label="Name" value={selectedMethod.name} />
<PropRow label="Returns" value={selectedMethod.returnType} />
<PropRow label="Override" value={selectedMethod.isOverride ? 'yes' : 'no'} />
<PropRow label="Virtual" value={selectedMethod.isVirtual ? 'yes' : 'no'} />
{selectedMethod.callsGump && (
<PropRow label="Opens Gump" value={selectedMethod.gumpClass ?? '(dynamic)'} />
)}
</div>
{selectedMethod.parameters.length > 0 && (
<>
<div className={styles.subTitle}>Parameters</div>
{selectedMethod.parameters.map((p, i) => (
<PropRow key={i} label={p.name} value={p.type} />
))}
</>
)}
</div>
)}
{/* Fake data inputs for flow conditions */}
{centerMode === 'flow_method' && <FakeDataInputs />}
{/* Default empty state */}
{centerMode === 'empty' && (
<div className={styles.section}>
<div className={styles.empty}>Nothing selected</div>
</div>
)}
</div>
);
}
function PropRow({ label, value }: { label: string; value: string }) {
return (
<div className={styles.propRow}>
<span className={styles.propLabel}>{label}</span>
<span className={styles.propValue}>{value}</span>
</div>
);
}