Initial commit
This commit is contained in:
54
src/components/layout/AppShell.module.css
Normal file
54
src/components/layout/AppShell.module.css
Normal 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;
|
||||
}
|
||||
83
src/components/layout/AppShell.tsx
Normal file
83
src/components/layout/AppShell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
src/components/layout/CenterPanel.module.css
Normal file
23
src/components/layout/CenterPanel.module.css
Normal 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;
|
||||
}
|
||||
35
src/components/layout/CenterPanel.tsx
Normal file
35
src/components/layout/CenterPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
src/components/layout/LeftPanel.module.css
Normal file
64
src/components/layout/LeftPanel.module.css
Normal 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;
|
||||
}
|
||||
53
src/components/layout/LeftPanel.tsx
Normal file
53
src/components/layout/LeftPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
src/components/layout/RightPanel.module.css
Normal file
58
src/components/layout/RightPanel.module.css
Normal 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;
|
||||
}
|
||||
72
src/components/layout/RightPanel.tsx
Normal file
72
src/components/layout/RightPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user