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); +}