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.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<School | null> {
|
||||
const modal = new Modal<School | null>(null);
|
||||
modal.element.classList.add('dialog--wide');
|
||||
const defaults = toDateAndTimeInputs(options.defaultStartDate);
|
||||
|
||||
const extraModIds = new Set<string>();
|
||||
let catalog: CatalogResponse | null = null;
|
||||
let currentMap: MapLayout | null = null;
|
||||
let editor: ReturnType<typeof mapEditor> | 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<School
|
||||
modsField.append(modsLabel, modsList);
|
||||
|
||||
const mapLabel = el('span', { class: 'field__label' });
|
||||
const resetButton = el('button', { class: 'button', type: 'button' });
|
||||
const editorHost = el('div', { class: 'map-editor-host' });
|
||||
const mapField = el('div', { class: 'field' }, mapLabel, el('div', { class: 'field__row' }, resetButton), editorHost);
|
||||
const editMapButton = el('button', { class: 'button', type: 'button' });
|
||||
const mapHint = el('p', { class: 'hint' });
|
||||
const mapField = el('div', { class: 'field' }, mapLabel, editMapButton, mapHint);
|
||||
|
||||
const nameInput = el('input', { class: 'input', type: 'text' });
|
||||
nameInput.maxLength = 40;
|
||||
@@ -63,24 +62,16 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
const submitButton = el('button', { class: 'button button--primary', type: 'submit' });
|
||||
const cancelButton = el('button', { class: 'button', type: 'button', onClick: () => 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<School
|
||||
|
||||
const setBusy = (value: boolean): void => {
|
||||
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<School
|
||||
error.hidden = false;
|
||||
};
|
||||
|
||||
const paintMapHint = (): void => {
|
||||
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<School
|
||||
await reloadCatalog();
|
||||
};
|
||||
|
||||
resetButton.addEventListener('click', () => {
|
||||
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<School
|
||||
return modal.open(nameInput);
|
||||
}
|
||||
|
||||
function mapsEqual(left: MapLayout, right: MapLayout): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function describe(reason: unknown): string {
|
||||
if (!(reason instanceof ApiError)) {
|
||||
return t('serverUnavailable');
|
||||
|
||||
@@ -15,6 +15,7 @@ interface MapEditorOptions {
|
||||
export function mapEditor(options: MapEditorOptions): {
|
||||
readonly element: HTMLElement;
|
||||
setCatalog: (catalog: CatalogResponse, map: MapLayout) => 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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<MapLayout> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user