From 3068d1040931bbd46d84224364fce189557dc5a2 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 18 Aug 2026 16:16:05 +0300 Subject: [PATCH] Refactor map editing and school creation UI components. Update styles for the dialog and map editor, enhancing layout and responsiveness. Introduce new localization strings for map editing features. Replace the inline map editor with a separate dialog for improved usability. Update event handling for map editing actions and hints. --- src/HSchool.Client/src/i18n/strings.ts | 8 ++ src/HSchool.Client/src/style.css | 56 ++++++++---- .../src/ui/createSchoolDialog.ts | 89 ++++++++++--------- src/HSchool.Client/src/ui/mapEditor.ts | 2 + src/HSchool.Client/src/ui/mapEditorDialog.ts | 55 ++++++++++++ 5 files changed, 151 insertions(+), 59 deletions(-) create mode 100644 src/HSchool.Client/src/ui/mapEditorDialog.ts diff --git a/src/HSchool.Client/src/i18n/strings.ts b/src/HSchool.Client/src/i18n/strings.ts index c4d76be..13d80ce 100644 --- a/src/HSchool.Client/src/i18n/strings.ts +++ b/src/HSchool.Client/src/i18n/strings.ts @@ -46,7 +46,11 @@ const ru = { modsTitle: 'Моды', coreModLocked: '{id} (всегда включён)', mapEditorTitle: 'Карта', + editMap: 'Редактировать карту', + mapDefaultHint: 'Будет использована карта по умолчанию.', + mapEditedHint: 'Карта изменена.', resetMap: 'Сбросить к умолчанию', + done: 'Готово', editorSlots: 'Слоты', editorLinks: 'Проходы', addLink: 'Связать', @@ -121,7 +125,11 @@ const en: Messages = { modsTitle: 'Mods', coreModLocked: '{id} (always on)', mapEditorTitle: 'Map', + editMap: 'Edit map', + mapDefaultHint: 'The default map will be used.', + mapEditedHint: 'The map has been edited.', resetMap: 'Reset to default', + done: 'Done', editorSlots: 'Slots', editorLinks: 'Passages', addLink: 'Link', diff --git a/src/HSchool.Client/src/style.css b/src/HSchool.Client/src/style.css index 483911f..a7e4d5f 100644 --- a/src/HSchool.Client/src/style.css +++ b/src/HSchool.Client/src/style.css @@ -369,10 +369,44 @@ body { color: var(--text); } -.dialog--wide { - min-width: 640px; - max-width: 860px; - width: min(860px, calc(100vw - 32px)); +.dialog--screen[open] { + display: flex; + flex-direction: column; + min-width: unset; + max-width: none; + width: min(1080px, calc(100vw - 24px)); + height: min(800px, calc(100vh - 24px)); + padding: 16px 20px 20px; +} + +.dialog--screen .screen__header { + flex: 0 0 auto; + margin-bottom: 12px; +} + +.dialog--screen .screen__title { + font-size: 19px; +} + +.dialog--screen .map-editor { + flex: 1; + min-height: 0; + grid-template-columns: minmax(220px, 0.9fr) minmax(280px, 1.3fr); + gap: 16px; +} + +.dialog--screen .map-editor__pane, +.dialog--screen .map-editor__details { + min-height: 0; + overflow: auto; + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px; +} + +.dialog--screen .dialog__actions { + flex: 0 0 auto; + margin-top: 12px; } .dialog::backdrop { @@ -419,9 +453,9 @@ body { color: var(--text-muted); } -.field__row { - display: flex; - gap: 8px; +.field .hint { + margin: 0; + font-size: 13px; } .mod-list { @@ -437,14 +471,6 @@ body { font-size: 14px; } -.map-editor-host { - max-height: 360px; - overflow: auto; - border: 1px solid var(--border); - border-radius: 10px; - padding: 8px; -} - .map-editor { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(240px, 1.4fr); diff --git a/src/HSchool.Client/src/ui/createSchoolDialog.ts b/src/HSchool.Client/src/ui/createSchoolDialog.ts index a6f95d2..0eb6464 100644 --- a/src/HSchool.Client/src/ui/createSchoolDialog.ts +++ b/src/HSchool.Client/src/ui/createSchoolDialog.ts @@ -11,7 +11,7 @@ import { fromDateAndTimeInputs, toDateAndTimeInputs } from '../format/gameTime.t import { getLocale } from '../i18n/locale.ts'; import { t } from '../i18n/strings.ts'; import { el } from './dom.ts'; -import { mapEditor } from './mapEditor.ts'; +import { mapEditorDialog } from './mapEditorDialog.ts'; import { Modal } from './modal.ts'; interface CreateSchoolOptions { @@ -22,17 +22,16 @@ interface CreateSchoolOptions { } /** - * Creation form: mods, a map (edit or reset to the pack default), then name and start date. + * Creation form: mods, name and start date. The map editor is a separate dialog — this one + * stays short so the list behind it remains usable. */ export function createSchoolDialog(options: CreateSchoolOptions): Promise { const modal = new Modal(null); - modal.element.classList.add('dialog--wide'); const defaults = toDateAndTimeInputs(options.defaultStartDate); const extraModIds = new Set(); let catalog: CatalogResponse | null = null; let currentMap: MapLayout | null = null; - let editor: ReturnType | null = null; const modsField = el('div', { class: 'field' }); const modsLabel = el('span', { class: 'field__label' }); @@ -40,9 +39,9 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise modal.close(null) }); const title = el('h2', { class: 'dialog__title' }); + const nameLabel = el('span', { class: 'field__label' }); + const startLabel = el('span', { class: 'field__label' }); const form = el( 'form', { class: 'form' }, modsField, + el('label', { class: 'field' }, nameLabel, el('div', { class: 'field__row' }, nameInput, randomButton)), + el('div', { class: 'field' }, startLabel, el('div', { class: 'field__row' }, dateInput, timeInput)), mapField, - el( - 'label', - { class: 'field' }, - el('span', { class: 'field__label', text: t('schoolName') }), - el('div', { class: 'field__row' }, nameInput, randomButton), - ), - el( - 'div', - { class: 'field' }, - el('span', { class: 'field__label', text: t('gameStart') }), - el('div', { class: 'field__row' }, dateInput, timeInput), - ), error, el('div', { class: 'dialog__actions' }, cancelButton, submitButton), ); @@ -89,9 +80,10 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise { busy = value; - submitButton.toggleAttribute('disabled', value || catalog === null); + const waiting = value || catalog === null; + submitButton.toggleAttribute('disabled', waiting); randomButton.toggleAttribute('disabled', value); - resetButton.toggleAttribute('disabled', value || catalog === null); + editMapButton.toggleAttribute('disabled', waiting); }; const showError = (message: string): void => { @@ -99,22 +91,21 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise { + if (catalog === null || currentMap === null) { + mapHint.textContent = ''; + return; + } + + mapHint.textContent = mapsEqual(currentMap, catalog.defaultMap) + ? t('mapDefaultHint') + : t('mapEditedHint'); + }; + const applyCatalog = (next: CatalogResponse): void => { catalog = next; currentMap = structuredClone(next.defaultMap); - if (editor === null) { - editor = mapEditor({ - catalog: next, - map: currentMap, - onChange: (map) => { - currentMap = map; - }, - }); - editorHost.append(editor.element); - } else { - editor.setCatalog(next, currentMap); - } - + paintMapHint(); setBusy(busy); }; @@ -167,29 +158,35 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise { - if (catalog === null) { - return; - } - - applyCatalog(catalog); - }); - const localize = (): void => { title.textContent = t('newSchool'); modsLabel.textContent = t('modsTitle'); + nameLabel.textContent = t('schoolName'); + startLabel.textContent = t('gameStart'); mapLabel.textContent = t('mapEditorTitle'); - resetButton.textContent = t('resetMap'); + editMapButton.textContent = t('editMap'); nameInput.placeholder = t('schoolNamePlaceholder'); randomButton.textContent = t('randomName'); randomButton.title = t('randomNameTitle'); submitButton.textContent = t('create'); cancelButton.textContent = t('cancel'); - editor?.localize(); + paintMapHint(); }; localize(); + editMapButton.addEventListener('click', () => { + if (busy || catalog === null || currentMap === null) { + return; + } + + void mapEditorDialog({ catalog, map: currentMap }).then((map) => { + currentMap = map; + paintMapHint(); + editMapButton.focus(); + }); + }); + randomButton.addEventListener('click', () => { if (busy) { return; @@ -237,6 +234,10 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise void; + getMap: () => MapLayout; localize: () => void; } { let catalog = options.catalog; @@ -227,6 +228,7 @@ export function mapEditor(options: MapEditorOptions): { selectedId = map.territory.id; render(); }, + getMap: () => cloneMap(map), localize: () => render(), }; } diff --git a/src/HSchool.Client/src/ui/mapEditorDialog.ts b/src/HSchool.Client/src/ui/mapEditorDialog.ts new file mode 100644 index 0000000..897d40a --- /dev/null +++ b/src/HSchool.Client/src/ui/mapEditorDialog.ts @@ -0,0 +1,55 @@ +import type { CatalogResponse, MapLayout } from '../net/api.ts'; +import { t } from '../i18n/strings.ts'; +import { el } from './dom.ts'; +import { mapEditor } from './mapEditor.ts'; +import { Modal } from './modal.ts'; + +/** + * Full-size create-only map editor. Lives in its own dialog so the school form stays short. + * Escape and "Done" both keep the current layout — reset is the explicit undo. + */ +export function mapEditorDialog(options: { + catalog: CatalogResponse; + map: MapLayout; +}): Promise { + const session = { map: structuredClone(options.map) }; + const modal = new Modal(session); + + const editor = mapEditor({ + catalog: options.catalog, + map: session.map, + onChange: (map) => { + session.map = map; + }, + }); + + const title = el('h2', { class: 'screen__title', text: t('mapEditorTitle') }); + const resetButton = el('button', { + class: 'button', + type: 'button', + text: t('resetMap'), + onClick: () => { + const next = structuredClone(options.catalog.defaultMap); + session.map = next; + editor.setCatalog(options.catalog, next); + }, + }); + const doneButton = el('button', { + class: 'button button--primary', + type: 'button', + text: t('done'), + onClick: () => { + session.map = editor.getMap(); + modal.close(session); + }, + }); + + modal.element.classList.add('dialog--screen'); + modal.element.append( + el('header', { class: 'screen__header' }, title, el('div', { class: 'screen__actions' }, resetButton)), + editor.element, + el('div', { class: 'dialog__actions' }, doneButton), + ); + + return modal.open(doneButton).then((value) => value.map); +}