fix(toolbar): enforce single-row flex layout and remove server host control

This commit is contained in:
troytt 2026-10-01 09:47:19 +00:00
parent 3c1ed16bf9
commit c6bf2464e8
4 changed files with 326 additions and 93 deletions

137
play.html
View File

@ -46,9 +46,146 @@
#d2-toolbar-container { #d2-toolbar-container {
flex: 0 0 auto; flex: 0 0 auto;
width: 100%;
z-index: 50; z-index: 50;
} }
#d2-toolbar,
.d2-toolbar {
position: relative;
z-index: 1000;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
height: 32px;
min-height: 32px;
max-height: 32px;
padding: 0 12px;
box-sizing: border-box;
background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%);
border-bottom: 1px solid #4a3c24;
color: #d6c496;
font-family: var(--font-display);
font-size: 12px;
line-height: 1;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
user-select: none;
}
.d2-toolbar__group {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
flex-shrink: 0;
gap: 6px;
white-space: nowrap;
}
.d2-toolbar__status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 8px;
border-radius: 3px;
background: #14110d;
border: 1px solid #352b1b;
}
.d2-toolbar__status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
background: #6b6b6b;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}
.d2-toolbar__status-dot--disconnected { background: #767676; }
.d2-toolbar__status-dot--connecting { background: #e6a822; box-shadow: 0 0 6px #e6a822; }
.d2-toolbar__status-dot--bncs { background: #4495e8; box-shadow: 0 0 6px #4495e8; }
.d2-toolbar__status-dot--mcp { background: #36c5d6; box-shadow: 0 0 6px #36c5d6; }
.d2-toolbar__status-dot--d2gs { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }
.d2-toolbar__status-dot--error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }
.d2-toolbar__rtt {
font-family: "JetBrains Mono", "Consolas", monospace;
font-size: 11px;
color: #a89974;
}
.d2-toolbar__label {
color: #b59e68;
font-size: 11px;
letter-spacing: 0.03em;
}
.d2-toolbar__select {
height: 22px;
padding: 0 6px;
background: #120f0b;
border: 1px solid #4a3c24;
border-radius: 2px;
color: #e2d1a0;
font-size: 11px;
cursor: pointer;
outline: none;
}
.d2-toolbar__select:hover,
.d2-toolbar__btn:hover {
border-color: #c7b377;
color: #fff2c6;
}
.d2-toolbar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 22px;
padding: 0 8px;
background: linear-gradient(180deg, #2b2316 0%, #17120b 100%);
border: 1px solid #574629;
border-radius: 2px;
color: #dfc991;
font-size: 11px;
cursor: pointer;
}
.d2-toolbar__btn--active {
background: linear-gradient(180deg, #524020 0%, #2e2310 100%);
border-color: #d4b46a;
color: #fff5d6;
}
.d2-toolbar__diag {
position: absolute;
top: 32px;
left: 12px;
right: 12px;
z-index: 1001;
display: none;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: rgba(38, 12, 10, 0.96);
border: 1px solid #a93226;
border-top: none;
border-radius: 0 0 4px 4px;
color: #ffd6d1;
font-size: 12px;
}
.d2-toolbar__diag--visible {
display: flex;
}
#play-workspace { #play-workspace {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;

View File

@ -169,7 +169,39 @@ async function bootPlayApp(): Promise<PlayAppController> {
const initialSettings = settingsStore.get() const initialSettings = settingsStore.get()
setLang(initialSettings.lang) setLang(initialSettings.lang)
let sessionRef: OnlineSession | null = null
const toolbar = new Toolbar({
store: settingsStore,
container: toolbarContainer,
callbacks: {
onLangChange: (lang: GameLang) => {
setLang(lang)
sessionRef?.hudManager.setLang(lang)
},
onViewportChange: (profile: ViewportProfile) => {
applyViewportProfileToDom(profile, {
stageEl,
glCanvas: viewCanvas,
hudCanvas,
})
sessionRef?.hudManager.resize(profile.width, profile.height)
sessionRef?.camera.resize(profile)
},
onAutomapRevealChange: (mode) => {
sessionRef?.automapView.setMode(mode)
},
onInspectorToggle: (open) => {
sessionRef?.packetInspector.setOpen(open)
},
onServerHostReconnect: (newHost) => {
void sessionRef?.disconnect(`Reconnecting to ${newHost}`)
},
},
})
const initialCacheStatus = await registerAssetCache() const initialCacheStatus = await registerAssetCache()
toolbar.setAssetCacheUsageBytes(initialCacheStatus.usageBytes)
const minimap = new Minimap() const minimap = new Minimap()
const renderer = new SpriteRenderer(viewCanvas) const renderer = new SpriteRenderer(viewCanvas)
@ -183,8 +215,6 @@ async function bootPlayApp(): Promise<PlayAppController> {
let itemsAtlasHandle: AtlasHandle | undefined let itemsAtlasHandle: AtlasHandle | undefined
let lastRenderSnapshot: SceneFrameSnapshot | null = null let lastRenderSnapshot: SceneFrameSnapshot | null = null
let sessionRef: OnlineSession | null = null
const hudManager = new HudManager(hudCanvas, { const hudManager = new HudManager(hudCanvas, {
onRenderAutomapOverlay: (ctx, font, logicalWidth, logicalHeight) => { onRenderAutomapOverlay: (ctx, font, logicalWidth, logicalHeight) => {
minimap.drawHudOverlay(ctx, font, logicalWidth, logicalHeight) minimap.drawHudOverlay(ctx, font, logicalWidth, logicalHeight)
@ -339,35 +369,6 @@ async function bootPlayApp(): Promise<PlayAppController> {
} }
} }
const toolbar = new Toolbar({
store: settingsStore,
container: toolbarContainer,
callbacks: {
onLangChange: (lang: GameLang) => {
setLang(lang)
session.hudManager.setLang(lang)
},
onViewportChange: (profile: ViewportProfile) => {
applyViewportProfileToDom(profile, {
stageEl,
glCanvas: viewCanvas,
hudCanvas,
})
session.hudManager.resize(profile.width, profile.height)
session.camera.resize(profile)
},
onAutomapRevealChange: (mode) => {
session.automapView.setMode(mode)
},
onInspectorToggle: (open) => {
inspector.setOpen(open)
},
onServerHostReconnect: (newHost) => {
void session.disconnect(`Reconnecting to ${newHost}`)
},
},
})
toolbar.setAssetCacheUsageBytes(initialCacheStatus.usageBytes)
void assetsReadyPromise.then(async () => { void assetsReadyPromise.then(async () => {
const updatedStatus = await getAssetCacheStatus() const updatedStatus = await getAssetCacheStatus()
toolbar.setAssetCacheUsageBytes(updatedStatus.usageBytes) toolbar.setAssetCacheUsageBytes(updatedStatus.usageBytes)

View File

@ -5,9 +5,11 @@
position: relative; position: relative;
z-index: 1000; z-index: 1000;
display: flex; display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 12px;
width: 100%; width: 100%;
height: 32px; height: 32px;
min-height: 32px; min-height: 32px;
@ -17,15 +19,21 @@
background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%); background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%);
border-bottom: 1px solid #4a3c24; border-bottom: 1px solid #4a3c24;
color: #d6c496; color: #d6c496;
font-family: "Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif; font-family: var(--font-display, "D2Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif);
font-size: 12px; font-size: 12px;
line-height: 1; line-height: 1;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
user-select: none; user-select: none;
} }
.d2-toolbar__group { .d2-toolbar__group {
display: inline-flex; display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center; align-items: center;
flex-shrink: 0;
gap: 6px; gap: 6px;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -82,13 +82,12 @@ export interface ToolbarSnapshot {
} }
/** /**
* Persistent 32px Top DOM Toolbar (`#d2-toolbar`) with 6 primary controls: * Persistent 32px Top DOM Toolbar (`#d2-toolbar`) with 5 primary controls:
* 1. Connection Status + RTT (`ms`) + Diagnostics (5-per-IP quota / WSS TLS error) + Inspector toggle * 1. Connection Status + RTT (`ms`) + Diagnostics (5-per-IP quota / WSS TLS error) + Inspector toggle
* 2. Server Host Input (`www.laiseek.xyz` default, bare-IP over HTTPS guard, mid-game reconnect prompt) * 2. Live Language Toggle (`中` / `EN`)
* 3. Live Language Toggle (`中` / `EN`) * 3. Lighting Preset Selector (`auto | noon | dusk | night | torch | cold | fullbright`)
* 4. Lighting Preset Selector (`auto | noon | dusk | night | torch | cold | fullbright`) * 4. Automap Reveal Selector (`off | level | act`)
* 5. Automap Reveal Selector (`off | level | act`) * 5. Viewport Size Toggle (`800x600 | 1024x768`) + Asset Cache Clear button
* 6. Viewport Size Toggle (`800x600 | 1024x768`) + Asset Cache Clear button
*/ */
export class Toolbar { export class Toolbar {
private readonly store: SettingsStore private readonly store: SettingsStore
@ -112,9 +111,6 @@ export class Toolbar {
private statusDotEl: HTMLElement | null = null private statusDotEl: HTMLElement | null = null
private statusTextEl: HTMLElement | null = null private statusTextEl: HTMLElement | null = null
private rttTextEl: HTMLElement | null = null private rttTextEl: HTMLElement | null = null
private serverLabelEl: HTMLElement | null = null
private serverInputEl: HTMLInputElement | null = null
private serverApplyBtnEl: HTMLButtonElement | null = null
private langBtnZhEl: HTMLButtonElement | null = null private langBtnZhEl: HTMLButtonElement | null = null
private langBtnEnEl: HTMLButtonElement | null = null private langBtnEnEl: HTMLButtonElement | null = null
private lightingLabelEl: HTMLElement | null = null private lightingLabelEl: HTMLElement | null = null
@ -395,8 +391,146 @@ export class Toolbar {
this.rootEl = null this.rootEl = null
} }
private ensureStyles(doc: Document): void {
if (!doc.head || doc.getElementById('d2-toolbar-styles')) return
const styleEl = doc.createElement('style')
styleEl.id = 'd2-toolbar-styles'
styleEl.textContent = `
#d2-toolbar,
.d2-toolbar {
position: relative;
z-index: 1000;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
height: 32px;
min-height: 32px;
max-height: 32px;
padding: 0 12px;
box-sizing: border-box;
background: linear-gradient(180deg, #1b1712 0%, #0d0b08 100%);
border-bottom: 1px solid #4a3c24;
color: #d6c496;
font-family: var(--font-display, "D2Exocet", "Cinzel", "Noto Serif SC", "PingFang SC", Georgia, serif);
font-size: 12px;
line-height: 1;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
user-select: none;
}
.d2-toolbar__group {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
flex-shrink: 0;
gap: 6px;
white-space: nowrap;
}
.d2-toolbar__divider {
width: 1px;
height: 18px;
background: #3a2f1d;
}
.d2-toolbar__status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 8px;
border-radius: 3px;
background: #14110d;
border: 1px solid #352b1b;
}
.d2-toolbar__status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
background: #6b6b6b;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}
.d2-toolbar__status-dot--disconnected { background: #767676; }
.d2-toolbar__status-dot--connecting { background: #e6a822; box-shadow: 0 0 6px #e6a822; }
.d2-toolbar__status-dot--bncs { background: #4495e8; box-shadow: 0 0 6px #4495e8; }
.d2-toolbar__status-dot--mcp { background: #36c5d6; box-shadow: 0 0 6px #36c5d6; }
.d2-toolbar__status-dot--d2gs { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }
.d2-toolbar__status-dot--error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }
.d2-toolbar__rtt {
font-family: "JetBrains Mono", "Consolas", monospace;
font-size: 11px;
color: #a89974;
}
.d2-toolbar__label {
color: #b59e68;
font-size: 11px;
letter-spacing: 0.03em;
}
.d2-toolbar__select {
height: 22px;
padding: 0 6px;
background: #120f0b;
border: 1px solid #4a3c24;
border-radius: 2px;
color: #e2d1a0;
font-size: 11px;
cursor: pointer;
outline: none;
}
.d2-toolbar__select:hover,
.d2-toolbar__btn:hover {
border-color: #c7b377;
color: #fff2c6;
}
.d2-toolbar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 22px;
padding: 0 8px;
background: linear-gradient(180deg, #2b2316 0%, #17120b 100%);
border: 1px solid #574629;
border-radius: 2px;
color: #dfc991;
font-size: 11px;
cursor: pointer;
}
.d2-toolbar__btn--active {
background: linear-gradient(180deg, #524020 0%, #2e2310 100%);
border-color: #d4b46a;
color: #fff5d6;
}
.d2-toolbar__diag {
position: absolute;
top: 32px;
left: 12px;
right: 12px;
z-index: 1001;
display: none;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: rgba(38, 12, 10, 0.96);
border: 1px solid #a93226;
border-top: none;
border-radius: 0 0 4px 4px;
color: #ffd6d1;
font-size: 12px;
}
.d2-toolbar__diag--visible {
display: flex;
}
`
doc.head.appendChild(styleEl)
}
private mountDom(container: HTMLElement | null): void { private mountDom(container: HTMLElement | null): void {
const doc = document const doc = document
this.ensureStyles(doc)
const root = doc.createElement('header') const root = doc.createElement('header')
root.id = 'd2-toolbar' root.id = 'd2-toolbar'
root.className = 'd2-toolbar' root.className = 'd2-toolbar'
@ -430,35 +564,7 @@ export class Toolbar {
statusGroup.appendChild(statusBadge) statusGroup.appendChild(statusBadge)
statusGroup.appendChild(inspectorBtn) statusGroup.appendChild(inspectorBtn)
// Group 2: Server Host Input // Group 2: Language Toggle (中 / EN)
const serverGroup = doc.createElement('div')
serverGroup.className = 'd2-toolbar__group'
const serverLabel = doc.createElement('label')
serverLabel.className = 'd2-toolbar__label'
serverLabel.htmlFor = 'd2-toolbar-server-host'
const serverInput = doc.createElement('input')
serverInput.type = 'text'
serverInput.id = 'd2-toolbar-server-host'
serverInput.className = 'd2-toolbar__input'
serverInput.value = this.serverInputValue
serverInput.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault()
this.submitServerHost(serverInput.value)
}
})
const serverApplyBtn = doc.createElement('button')
serverApplyBtn.type = 'button'
serverApplyBtn.id = 'd2-toolbar-server-apply'
serverApplyBtn.className = 'd2-toolbar__btn'
serverApplyBtn.addEventListener('click', () => {
this.submitServerHost(serverInput.value)
})
serverGroup.appendChild(serverLabel)
serverGroup.appendChild(serverInput)
serverGroup.appendChild(serverApplyBtn)
// Group 3: Language Toggle (中 / EN)
const langGroup = doc.createElement('div') const langGroup = doc.createElement('div')
langGroup.className = 'd2-toolbar__group' langGroup.className = 'd2-toolbar__group'
const langBtnZh = doc.createElement('button') const langBtnZh = doc.createElement('button')
@ -478,7 +584,7 @@ export class Toolbar {
langGroup.appendChild(langBtnZh) langGroup.appendChild(langBtnZh)
langGroup.appendChild(langBtnEn) langGroup.appendChild(langBtnEn)
// Group 4: Lighting Selector // Group 3: Lighting Selector
const lightingGroup = doc.createElement('div') const lightingGroup = doc.createElement('div')
lightingGroup.className = 'd2-toolbar__group' lightingGroup.className = 'd2-toolbar__group'
const lightingLabel = doc.createElement('label') const lightingLabel = doc.createElement('label')
@ -493,7 +599,7 @@ export class Toolbar {
lightingGroup.appendChild(lightingLabel) lightingGroup.appendChild(lightingLabel)
lightingGroup.appendChild(lightingSelect) lightingGroup.appendChild(lightingSelect)
// Group 5: Automap Reveal Selector (Off / Level / Act) // Group 4: Automap Reveal Selector (Off / Level / Act)
const automapGroup = doc.createElement('div') const automapGroup = doc.createElement('div')
automapGroup.className = 'd2-toolbar__group' automapGroup.className = 'd2-toolbar__group'
const automapLabel = doc.createElement('label') const automapLabel = doc.createElement('label')
@ -508,7 +614,7 @@ export class Toolbar {
automapGroup.appendChild(automapLabel) automapGroup.appendChild(automapLabel)
automapGroup.appendChild(automapSelect) automapGroup.appendChild(automapSelect)
// Group 6: Viewport Size Selector (800x600 / 1024x768) + Asset Cache Clear button // Group 5: Viewport Size Selector (800x600 / 1024x768) + Asset Cache Clear button
const viewportGroup = doc.createElement('div') const viewportGroup = doc.createElement('div')
viewportGroup.className = 'd2-toolbar__group' viewportGroup.className = 'd2-toolbar__group'
const viewportLabel = doc.createElement('label') const viewportLabel = doc.createElement('label')
@ -545,7 +651,6 @@ export class Toolbar {
diagBanner.className = 'd2-toolbar__diag' diagBanner.className = 'd2-toolbar__diag'
root.appendChild(statusGroup) root.appendChild(statusGroup)
root.appendChild(serverGroup)
root.appendChild(langGroup) root.appendChild(langGroup)
root.appendChild(lightingGroup) root.appendChild(lightingGroup)
root.appendChild(automapGroup) root.appendChild(automapGroup)
@ -560,9 +665,6 @@ export class Toolbar {
this.statusDotEl = dot this.statusDotEl = dot
this.statusTextEl = statusText this.statusTextEl = statusText
this.rttTextEl = rttText this.rttTextEl = rttText
this.serverLabelEl = serverLabel
this.serverInputEl = serverInput
this.serverApplyBtnEl = serverApplyBtn
this.langBtnZhEl = langBtnZh this.langBtnZhEl = langBtnZh
this.langBtnEnEl = langBtnEn this.langBtnEnEl = langBtnEn
this.lightingLabelEl = lightingLabel this.lightingLabelEl = lightingLabel
@ -598,21 +700,6 @@ export class Toolbar {
? 'd2-toolbar__btn d2-toolbar__btn--active' ? 'd2-toolbar__btn d2-toolbar__btn--active'
: 'd2-toolbar__btn' : 'd2-toolbar__btn'
} }
if (this.serverLabelEl) {
this.serverLabelEl.textContent = tToolbar('toolbar.server.label', lang)
}
if (this.serverInputEl) {
this.serverInputEl.placeholder = tToolbar('toolbar.server.placeholder', lang)
if (this.serverInputEl.value !== snap.serverInputValue) {
this.serverInputEl.value = snap.serverInputValue
}
this.serverInputEl.className = snap.serverInputValid
? 'd2-toolbar__input'
: 'd2-toolbar__input d2-toolbar__input--invalid'
}
if (this.serverApplyBtnEl) {
this.serverApplyBtnEl.textContent = tToolbar('toolbar.server.apply', lang)
}
if (this.langBtnZhEl) { if (this.langBtnZhEl) {
this.langBtnZhEl.className = this.langBtnZhEl.className =
lang === 'zh' ? 'd2-toolbar__btn d2-toolbar__btn--active' : 'd2-toolbar__btn' lang === 'zh' ? 'd2-toolbar__btn d2-toolbar__btn--active' : 'd2-toolbar__btn'