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: 'Моды',
|
modsTitle: 'Моды',
|
||||||
coreModLocked: '{id} (всегда включён)',
|
coreModLocked: '{id} (всегда включён)',
|
||||||
mapEditorTitle: 'Карта',
|
mapEditorTitle: 'Карта',
|
||||||
|
editMap: 'Редактировать карту',
|
||||||
|
mapDefaultHint: 'Будет использована карта по умолчанию.',
|
||||||
|
mapEditedHint: 'Карта изменена.',
|
||||||
resetMap: 'Сбросить к умолчанию',
|
resetMap: 'Сбросить к умолчанию',
|
||||||
|
done: 'Готово',
|
||||||
editorSlots: 'Слоты',
|
editorSlots: 'Слоты',
|
||||||
editorLinks: 'Проходы',
|
editorLinks: 'Проходы',
|
||||||
addLink: 'Связать',
|
addLink: 'Связать',
|
||||||
@@ -121,7 +125,11 @@ const en: Messages = {
|
|||||||
modsTitle: 'Mods',
|
modsTitle: 'Mods',
|
||||||
coreModLocked: '{id} (always on)',
|
coreModLocked: '{id} (always on)',
|
||||||
mapEditorTitle: 'Map',
|
mapEditorTitle: 'Map',
|
||||||
|
editMap: 'Edit map',
|
||||||
|
mapDefaultHint: 'The default map will be used.',
|
||||||
|
mapEditedHint: 'The map has been edited.',
|
||||||
resetMap: 'Reset to default',
|
resetMap: 'Reset to default',
|
||||||
|
done: 'Done',
|
||||||
editorSlots: 'Slots',
|
editorSlots: 'Slots',
|
||||||
editorLinks: 'Passages',
|
editorLinks: 'Passages',
|
||||||
addLink: 'Link',
|
addLink: 'Link',
|
||||||
|
|||||||
@@ -369,10 +369,44 @@ body {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog--wide {
|
.dialog--screen[open] {
|
||||||
min-width: 640px;
|
display: flex;
|
||||||
max-width: 860px;
|
flex-direction: column;
|
||||||
width: min(860px, calc(100vw - 32px));
|
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 {
|
.dialog::backdrop {
|
||||||
@@ -419,9 +453,9 @@ body {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field__row {
|
.field .hint {
|
||||||
display: flex;
|
margin: 0;
|
||||||
gap: 8px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-list {
|
.mod-list {
|
||||||
@@ -437,14 +471,6 @@ body {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-editor-host {
|
|
||||||
max-height: 360px;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-editor {
|
.map-editor {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 1fr) minmax(240px, 1.4fr);
|
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 { getLocale } from '../i18n/locale.ts';
|
||||||
import { t } from '../i18n/strings.ts';
|
import { t } from '../i18n/strings.ts';
|
||||||
import { el } from './dom.ts';
|
import { el } from './dom.ts';
|
||||||
import { mapEditor } from './mapEditor.ts';
|
import { mapEditorDialog } from './mapEditorDialog.ts';
|
||||||
import { Modal } from './modal.ts';
|
import { Modal } from './modal.ts';
|
||||||
|
|
||||||
interface CreateSchoolOptions {
|
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> {
|
export function createSchoolDialog(options: CreateSchoolOptions): Promise<School | null> {
|
||||||
const modal = new Modal<School | null>(null);
|
const modal = new Modal<School | null>(null);
|
||||||
modal.element.classList.add('dialog--wide');
|
|
||||||
const defaults = toDateAndTimeInputs(options.defaultStartDate);
|
const defaults = toDateAndTimeInputs(options.defaultStartDate);
|
||||||
|
|
||||||
const extraModIds = new Set<string>();
|
const extraModIds = new Set<string>();
|
||||||
let catalog: CatalogResponse | null = null;
|
let catalog: CatalogResponse | null = null;
|
||||||
let currentMap: MapLayout | null = null;
|
let currentMap: MapLayout | null = null;
|
||||||
let editor: ReturnType<typeof mapEditor> | null = null;
|
|
||||||
|
|
||||||
const modsField = el('div', { class: 'field' });
|
const modsField = el('div', { class: 'field' });
|
||||||
const modsLabel = el('span', { class: 'field__label' });
|
const modsLabel = el('span', { class: 'field__label' });
|
||||||
@@ -40,9 +39,9 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
modsField.append(modsLabel, modsList);
|
modsField.append(modsLabel, modsList);
|
||||||
|
|
||||||
const mapLabel = el('span', { class: 'field__label' });
|
const mapLabel = el('span', { class: 'field__label' });
|
||||||
const resetButton = el('button', { class: 'button', type: 'button' });
|
const editMapButton = el('button', { class: 'button', type: 'button' });
|
||||||
const editorHost = el('div', { class: 'map-editor-host' });
|
const mapHint = el('p', { class: 'hint' });
|
||||||
const mapField = el('div', { class: 'field' }, mapLabel, el('div', { class: 'field__row' }, resetButton), editorHost);
|
const mapField = el('div', { class: 'field' }, mapLabel, editMapButton, mapHint);
|
||||||
|
|
||||||
const nameInput = el('input', { class: 'input', type: 'text' });
|
const nameInput = el('input', { class: 'input', type: 'text' });
|
||||||
nameInput.maxLength = 40;
|
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 submitButton = el('button', { class: 'button button--primary', type: 'submit' });
|
||||||
const cancelButton = el('button', { class: 'button', type: 'button', onClick: () => modal.close(null) });
|
const cancelButton = el('button', { class: 'button', type: 'button', onClick: () => modal.close(null) });
|
||||||
const title = el('h2', { class: 'dialog__title' });
|
const title = el('h2', { class: 'dialog__title' });
|
||||||
|
const nameLabel = el('span', { class: 'field__label' });
|
||||||
|
const startLabel = el('span', { class: 'field__label' });
|
||||||
|
|
||||||
const form = el(
|
const form = el(
|
||||||
'form',
|
'form',
|
||||||
{ class: 'form' },
|
{ class: 'form' },
|
||||||
modsField,
|
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,
|
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,
|
error,
|
||||||
el('div', { class: 'dialog__actions' }, cancelButton, submitButton),
|
el('div', { class: 'dialog__actions' }, cancelButton, submitButton),
|
||||||
);
|
);
|
||||||
@@ -89,9 +80,10 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
|
|
||||||
const setBusy = (value: boolean): void => {
|
const setBusy = (value: boolean): void => {
|
||||||
busy = value;
|
busy = value;
|
||||||
submitButton.toggleAttribute('disabled', value || catalog === null);
|
const waiting = value || catalog === null;
|
||||||
|
submitButton.toggleAttribute('disabled', waiting);
|
||||||
randomButton.toggleAttribute('disabled', value);
|
randomButton.toggleAttribute('disabled', value);
|
||||||
resetButton.toggleAttribute('disabled', value || catalog === null);
|
editMapButton.toggleAttribute('disabled', waiting);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showError = (message: string): void => {
|
const showError = (message: string): void => {
|
||||||
@@ -99,22 +91,21 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
error.hidden = false;
|
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 => {
|
const applyCatalog = (next: CatalogResponse): void => {
|
||||||
catalog = next;
|
catalog = next;
|
||||||
currentMap = structuredClone(next.defaultMap);
|
currentMap = structuredClone(next.defaultMap);
|
||||||
if (editor === null) {
|
paintMapHint();
|
||||||
editor = mapEditor({
|
|
||||||
catalog: next,
|
|
||||||
map: currentMap,
|
|
||||||
onChange: (map) => {
|
|
||||||
currentMap = map;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
editorHost.append(editor.element);
|
|
||||||
} else {
|
|
||||||
editor.setCatalog(next, currentMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBusy(busy);
|
setBusy(busy);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,29 +158,35 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
await reloadCatalog();
|
await reloadCatalog();
|
||||||
};
|
};
|
||||||
|
|
||||||
resetButton.addEventListener('click', () => {
|
|
||||||
if (catalog === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyCatalog(catalog);
|
|
||||||
});
|
|
||||||
|
|
||||||
const localize = (): void => {
|
const localize = (): void => {
|
||||||
title.textContent = t('newSchool');
|
title.textContent = t('newSchool');
|
||||||
modsLabel.textContent = t('modsTitle');
|
modsLabel.textContent = t('modsTitle');
|
||||||
|
nameLabel.textContent = t('schoolName');
|
||||||
|
startLabel.textContent = t('gameStart');
|
||||||
mapLabel.textContent = t('mapEditorTitle');
|
mapLabel.textContent = t('mapEditorTitle');
|
||||||
resetButton.textContent = t('resetMap');
|
editMapButton.textContent = t('editMap');
|
||||||
nameInput.placeholder = t('schoolNamePlaceholder');
|
nameInput.placeholder = t('schoolNamePlaceholder');
|
||||||
randomButton.textContent = t('randomName');
|
randomButton.textContent = t('randomName');
|
||||||
randomButton.title = t('randomNameTitle');
|
randomButton.title = t('randomNameTitle');
|
||||||
submitButton.textContent = t('create');
|
submitButton.textContent = t('create');
|
||||||
cancelButton.textContent = t('cancel');
|
cancelButton.textContent = t('cancel');
|
||||||
editor?.localize();
|
paintMapHint();
|
||||||
};
|
};
|
||||||
|
|
||||||
localize();
|
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', () => {
|
randomButton.addEventListener('click', () => {
|
||||||
if (busy) {
|
if (busy) {
|
||||||
return;
|
return;
|
||||||
@@ -237,6 +234,10 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
|||||||
return modal.open(nameInput);
|
return modal.open(nameInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapsEqual(left: MapLayout, right: MapLayout): boolean {
|
||||||
|
return JSON.stringify(left) === JSON.stringify(right);
|
||||||
|
}
|
||||||
|
|
||||||
function describe(reason: unknown): string {
|
function describe(reason: unknown): string {
|
||||||
if (!(reason instanceof ApiError)) {
|
if (!(reason instanceof ApiError)) {
|
||||||
return t('serverUnavailable');
|
return t('serverUnavailable');
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface MapEditorOptions {
|
|||||||
export function mapEditor(options: MapEditorOptions): {
|
export function mapEditor(options: MapEditorOptions): {
|
||||||
readonly element: HTMLElement;
|
readonly element: HTMLElement;
|
||||||
setCatalog: (catalog: CatalogResponse, map: MapLayout) => void;
|
setCatalog: (catalog: CatalogResponse, map: MapLayout) => void;
|
||||||
|
getMap: () => MapLayout;
|
||||||
localize: () => void;
|
localize: () => void;
|
||||||
} {
|
} {
|
||||||
let catalog = options.catalog;
|
let catalog = options.catalog;
|
||||||
@@ -227,6 +228,7 @@ export function mapEditor(options: MapEditorOptions): {
|
|||||||
selectedId = map.territory.id;
|
selectedId = map.territory.id;
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
|
getMap: () => cloneMap(map),
|
||||||
localize: () => render(),
|
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