stuff
BIN
public/boat.webp
Normal file
|
After Width: | Height: | Size: 518 B |
BIN
public/gas.webp
Normal file
|
After Width: | Height: | Size: 534 B |
BIN
public/laura-selected.webp
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
public/laura.webp
Normal file
|
After Width: | Height: | Size: 1018 B |
BIN
public/mama-selected.webp
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
public/mama.webp
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
public/med.webp
Normal file
|
After Width: | Height: | Size: 536 B |
BIN
public/nick-selected.webp
Normal file
|
After Width: | Height: | Size: 876 B |
BIN
public/nick.webp
Normal file
|
After Width: | Height: | Size: 986 B |
BIN
public/papa-selected.webp
Normal file
|
After Width: | Height: | Size: 948 B |
BIN
public/papa.webp
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
public/pepper-selected.webp
Normal file
|
After Width: | Height: | Size: 844 B |
BIN
public/pepper.webp
Normal file
|
After Width: | Height: | Size: 946 B |
BIN
public/pizza.webp
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
public/race.webp
Normal file
|
After Width: | Height: | Size: 520 B |
BIN
public/save.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
@ -412,7 +412,8 @@ body {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#configure-page .page-inner-content.config-layout {
|
#configure-page .page-inner-content.config-layout,
|
||||||
|
#save-editor .page-inner-content.config-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--color-bg-input);
|
background-color: var(--color-bg-input);
|
||||||
border: 1px solid #303030;
|
border: 1px solid #303030;
|
||||||
@ -1403,7 +1404,8 @@ select:focus {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#configure-page .page-inner-content.config-layout {
|
#configure-page .page-inner-content.config-layout,
|
||||||
|
#save-editor .page-inner-content.config-layout {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@ -1,17 +1,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import BackButton from './BackButton.svelte';
|
import BackButton from './BackButton.svelte';
|
||||||
import SaveSlotList from './save-editor/SaveSlotList.svelte';
|
|
||||||
import PlayerInfoEditor from './save-editor/PlayerInfoEditor.svelte';
|
|
||||||
import MissionScoresEditor from './save-editor/MissionScoresEditor.svelte';
|
import MissionScoresEditor from './save-editor/MissionScoresEditor.svelte';
|
||||||
import { saveEditorState, configToastVisible } from '../stores.js';
|
import { saveEditorState, currentPage } from '../stores.js';
|
||||||
import { listSaveSlots, updateSaveSlot } from '../core/savegame/index.js';
|
import { listSaveSlots, updateSaveSlot, updatePlayerName } from '../core/savegame/index.js';
|
||||||
|
import { Actor, ActorNames } from '../core/savegame/constants.js';
|
||||||
|
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error = null;
|
let error = null;
|
||||||
let slots = [];
|
let slots = [];
|
||||||
let selectedSlot = null;
|
let selectedSlot = null;
|
||||||
|
|
||||||
|
// Tab state
|
||||||
|
let activeTab = 'player';
|
||||||
|
let openSection = 'name';
|
||||||
|
|
||||||
|
const saveTabs = [
|
||||||
|
{ id: 'player', label: 'Player', firstSection: 'name' },
|
||||||
|
{ id: 'scores', label: 'Scores', firstSection: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reset to default tab/section when navigating to this page
|
||||||
|
$: if ($currentPage === 'save-editor') {
|
||||||
|
activeTab = 'player';
|
||||||
|
openSection = 'name';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name editing state (7 characters)
|
||||||
|
let nameSlots = ['', '', '', '', '', '', ''];
|
||||||
|
let slotRefs = [];
|
||||||
|
|
||||||
|
// Character/Act state
|
||||||
|
let currentAct = 0;
|
||||||
|
let actorId = 1;
|
||||||
|
|
||||||
|
// Character icons mapping
|
||||||
|
const characterIcons = {
|
||||||
|
[Actor.PEPPER]: { normal: 'pepper.webp', selected: 'pepper-selected.webp' },
|
||||||
|
[Actor.MAMA]: { normal: 'mama.webp', selected: 'mama-selected.webp' },
|
||||||
|
[Actor.PAPA]: { normal: 'papa.webp', selected: 'papa-selected.webp' },
|
||||||
|
[Actor.NICK]: { normal: 'nick.webp', selected: 'nick-selected.webp' },
|
||||||
|
[Actor.LAURA]: { normal: 'laura.webp', selected: 'laura-selected.webp' }
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadSlots();
|
await loadSlots();
|
||||||
});
|
});
|
||||||
@ -42,7 +73,6 @@
|
|||||||
try {
|
try {
|
||||||
const updated = await updateSaveSlot(selectedSlot, { header: updates });
|
const updated = await updateSaveSlot(selectedSlot, { header: updates });
|
||||||
if (updated) {
|
if (updated) {
|
||||||
// Update the slot in our list
|
|
||||||
slots = slots.map(s =>
|
slots = slots.map(s =>
|
||||||
s.slotNumber === selectedSlot
|
s.slotNumber === selectedSlot
|
||||||
? { ...s, header: updated.header }
|
? { ...s, header: updated.header }
|
||||||
@ -60,7 +90,6 @@
|
|||||||
try {
|
try {
|
||||||
const updated = await updateSaveSlot(selectedSlot, { missionScore: missionUpdate });
|
const updated = await updateSaveSlot(selectedSlot, { missionScore: missionUpdate });
|
||||||
if (updated) {
|
if (updated) {
|
||||||
// Update the slot in our list
|
|
||||||
slots = slots.map(s =>
|
slots = slots.map(s =>
|
||||||
s.slotNumber === selectedSlot
|
s.slotNumber === selectedSlot
|
||||||
? { ...s, missions: updated.missions }
|
? { ...s, missions: updated.missions }
|
||||||
@ -72,97 +101,400 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNameUpdate(newName) {
|
// Tab/section functions
|
||||||
// Update the slot's playerName in our list
|
function switchTab(tab) {
|
||||||
|
activeTab = tab.id;
|
||||||
|
openSection = tab.firstSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(sectionId) {
|
||||||
|
openSection = openSection === sectionId ? null : sectionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name editing functions
|
||||||
|
async function saveNameFromSlots() {
|
||||||
|
const newName = nameSlots.join('').replace(/[^A-Z]/g, '');
|
||||||
|
if (newName.length === 0) return;
|
||||||
|
|
||||||
|
const success = await updatePlayerName(currentSlot.slotNumber, newName);
|
||||||
|
if (success) {
|
||||||
slots = slots.map(s =>
|
slots = slots.map(s =>
|
||||||
s.slotNumber === selectedSlot
|
s.slotNumber === selectedSlot
|
||||||
? { ...s, playerName: newName }
|
? { ...s, playerName: newName }
|
||||||
: s
|
: s
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlotBeforeInput(index, e) {
|
||||||
|
if (!e.data) return;
|
||||||
|
|
||||||
|
const char = e.data.toUpperCase();
|
||||||
|
if (!/^[A-Z]$/.test(char)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
nameSlots[index] = char;
|
||||||
|
nameSlots = [...nameSlots];
|
||||||
|
saveNameFromSlots();
|
||||||
|
|
||||||
|
if (index < 6) {
|
||||||
|
slotRefs[index + 1]?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilledCount() {
|
||||||
|
return nameSlots.filter(c => c !== '').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlotKeydown(index, e) {
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
if (nameSlots[index] === '' && index > 0) {
|
||||||
|
if (getFilledCount() > 1 || nameSlots[index - 1] === '') {
|
||||||
|
slotRefs[index - 1]?.focus();
|
||||||
|
if (nameSlots[index - 1] !== '' && getFilledCount() > 1) {
|
||||||
|
nameSlots[index - 1] = '';
|
||||||
|
nameSlots = [...nameSlots];
|
||||||
|
saveNameFromSlots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (nameSlots[index] !== '') {
|
||||||
|
if (getFilledCount() > 1) {
|
||||||
|
nameSlots[index] = '';
|
||||||
|
nameSlots = [...nameSlots];
|
||||||
|
saveNameFromSlots();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowLeft' && index > 0) {
|
||||||
|
slotRefs[index - 1]?.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'ArrowRight' && index < 6) {
|
||||||
|
slotRefs[index + 1]?.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Delete') {
|
||||||
|
if (getFilledCount() > 1 && nameSlots[index] !== '') {
|
||||||
|
nameSlots[index] = '';
|
||||||
|
nameSlots = [...nameSlots];
|
||||||
|
saveNameFromSlots();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlotFocus(e) {
|
||||||
|
e.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character/Act handlers
|
||||||
|
function handleActChange(e) {
|
||||||
|
currentAct = parseInt(e.target.value);
|
||||||
|
handleHeaderUpdate({ currentAct });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleActorSelect(id) {
|
||||||
|
actorId = id;
|
||||||
|
handleHeaderUpdate({ actorId: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor options (ordered: Mama, Papa, Pepper, Nick, Laura)
|
||||||
|
const actorOptions = [
|
||||||
|
{ id: Actor.MAMA, name: ActorNames[Actor.MAMA] },
|
||||||
|
{ id: Actor.PAPA, name: ActorNames[Actor.PAPA] },
|
||||||
|
{ id: Actor.PEPPER, name: ActorNames[Actor.PEPPER] },
|
||||||
|
{ id: Actor.NICK, name: ActorNames[Actor.NICK] },
|
||||||
|
{ id: Actor.LAURA, name: ActorNames[Actor.LAURA] }
|
||||||
|
];
|
||||||
|
|
||||||
$: existingSlots = slots.filter(s => s.exists);
|
$: existingSlots = slots.filter(s => s.exists);
|
||||||
$: currentSlot = slots.find(s => s.slotNumber === selectedSlot);
|
$: currentSlot = slots.find(s => s.slotNumber === selectedSlot);
|
||||||
|
|
||||||
|
// Update local state when currentSlot changes
|
||||||
|
$: if (currentSlot?.header) {
|
||||||
|
currentAct = currentSlot.header.currentAct;
|
||||||
|
actorId = currentSlot.header.actorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update name slots when player name changes
|
||||||
|
$: if (currentSlot?.playerName) {
|
||||||
|
const name = currentSlot.playerName.toUpperCase();
|
||||||
|
nameSlots = Array.from({ length: 7 }, (_, i) => name[i] || '');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="save-editor-page" class="page-content">
|
<div id="save-editor" class="page-content">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<div class="page-inner-content save-editor-layout">
|
<div class="page-inner-content config-layout">
|
||||||
<h1>Save Editor</h1>
|
<div class="config-art-panel">
|
||||||
|
<img src="save.webp" alt="LEGO Island Save Editor">
|
||||||
|
</div>
|
||||||
|
<div class="config-main">
|
||||||
|
<div class="config-presets">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="save-editor-status">
|
<span class="save-status-text">Loading save files...</span>
|
||||||
<p>Loading save files...</p>
|
|
||||||
</div>
|
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="save-editor-status error">
|
<span class="save-status-text error">{error}</span>
|
||||||
<p>Error: {error}</p>
|
|
||||||
</div>
|
|
||||||
{:else if existingSlots.length === 0}
|
{:else if existingSlots.length === 0}
|
||||||
<div class="save-editor-status">
|
<span class="save-status-text">No save files found</span>
|
||||||
<p>No save files found. Start a new game to create a save file.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="save-editor-content">
|
{#each existingSlots as slot}
|
||||||
<SaveSlotList
|
<button
|
||||||
{slots}
|
type="button"
|
||||||
{selectedSlot}
|
class="save-slot-card"
|
||||||
onSelect={handleSlotSelect}
|
class:selected={selectedSlot === slot.slotNumber}
|
||||||
|
onclick={() => handleSlotSelect(slot.slotNumber)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={characterIcons[slot.header?.actorId]?.selected || 'pepper-selected.webp'}
|
||||||
|
alt={ActorNames[slot.header?.actorId] || 'Character'}
|
||||||
|
class="slot-character-icon"
|
||||||
/>
|
/>
|
||||||
|
<span class="slot-name">{slot.playerName}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if currentSlot && currentSlot.exists}
|
{#if currentSlot && currentSlot.exists}
|
||||||
<div class="save-editor-details">
|
<div class="config-tabs">
|
||||||
<PlayerInfoEditor
|
<div class="config-tab-buttons">
|
||||||
slot={currentSlot}
|
{#each saveTabs as tab}
|
||||||
onUpdate={handleHeaderUpdate}
|
<button
|
||||||
onNameUpdate={handleNameUpdate}
|
class="config-tab-btn"
|
||||||
|
class:active={activeTab === tab.id}
|
||||||
|
onclick={() => switchTab(tab)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player Tab -->
|
||||||
|
<div class:hidden={activeTab !== 'player'}>
|
||||||
|
<div class="config-section-card">
|
||||||
|
<button type="button" class="config-card-header" onclick={() => toggleSection('name')}>
|
||||||
|
Name
|
||||||
|
</button>
|
||||||
|
<div class="config-card-content" class:open={openSection === 'name'}>
|
||||||
|
<div class="section-inner">
|
||||||
|
<div class="name-slots">
|
||||||
|
{#each nameSlots as char, i}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="name-slot"
|
||||||
|
value={char}
|
||||||
|
maxlength="1"
|
||||||
|
onbeforeinput={(e) => handleSlotBeforeInput(i, e)}
|
||||||
|
onkeydown={(e) => handleSlotKeydown(i, e)}
|
||||||
|
onfocus={handleSlotFocus}
|
||||||
|
bind:this={slotRefs[i]}
|
||||||
/>
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section-card">
|
||||||
|
<button type="button" class="config-card-header" onclick={() => toggleSection('character')}>
|
||||||
|
Character / Act
|
||||||
|
</button>
|
||||||
|
<div class="config-card-content" class:open={openSection === 'character'}>
|
||||||
|
<div class="section-inner">
|
||||||
|
<div class="character-act-row">
|
||||||
|
<div class="character-selection">
|
||||||
|
<label class="form-group-label">Character</label>
|
||||||
|
<div class="character-icons">
|
||||||
|
{#each actorOptions as actor}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="character-icon-btn"
|
||||||
|
class:selected={actorId === actor.id}
|
||||||
|
onclick={() => handleActorSelect(actor.id)}
|
||||||
|
title={actor.name}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={actorId === actor.id
|
||||||
|
? characterIcons[actor.id].selected
|
||||||
|
: characterIcons[actor.id].normal}
|
||||||
|
alt={actor.name}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="act-selection">
|
||||||
|
<label class="form-group-label" for="act-select">Act</label>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select id="act-select" value={currentAct} onchange={handleActChange}>
|
||||||
|
<option value={0}>Act 1</option>
|
||||||
|
<option value={1}>Act 2</option>
|
||||||
|
<option value={2}>Act 3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scores Tab -->
|
||||||
|
<div class:hidden={activeTab !== 'scores'}>
|
||||||
<MissionScoresEditor
|
<MissionScoresEditor
|
||||||
slot={currentSlot}
|
slot={currentSlot}
|
||||||
onUpdate={handleMissionUpdate}
|
onUpdate={handleMissionUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.save-editor-layout {
|
.save-status-text {
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-editor-layout h1 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 2em;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-editor-status {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px;
|
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
background: var(--gradient-panel);
|
font-size: 0.9em;
|
||||||
border: 1px solid var(--color-border-dark);
|
padding: 8px 0;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-editor-status.error {
|
.save-status-text.error {
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
border-color: #ff6b6b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-editor-content {
|
.save-slot-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--gradient-panel);
|
||||||
|
border: 2px solid var(--color-border-medium);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-editor-details {
|
.save-slot-card:hover {
|
||||||
|
border-color: var(--color-border-light);
|
||||||
|
background: var(--gradient-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-slot-card.selected {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 8px var(--color-primary-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-character-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 46px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-name {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-inner {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-slots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-slot {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: var(--color-bg-input);
|
||||||
|
border: 1px solid var(--color-border-medium);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-slot:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 204, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-slot:hover {
|
||||||
|
border-color: var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-act-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-selection,
|
||||||
|
.act-selection {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icon-btn {
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--color-bg-input);
|
||||||
|
border: 2px solid var(--color-border-medium);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icon-btn:hover {
|
||||||
|
border-color: var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icon-btn.selected {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 6px var(--color-primary-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icon-btn img {
|
||||||
|
width: 40px;
|
||||||
|
height: 46px;
|
||||||
|
display: block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.name-slot {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-icon-btn img {
|
||||||
|
width: 32px;
|
||||||
|
height: 37px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-character-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 37px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -6,11 +6,11 @@
|
|||||||
export let onUpdate = () => {};
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
const missions = [
|
const missions = [
|
||||||
{ key: 'pizza', name: MissionNames.pizza },
|
{ key: 'pizza', name: MissionNames.pizza, icon: 'pizza.webp' },
|
||||||
{ key: 'carRace', name: MissionNames.carRace },
|
{ key: 'carRace', name: MissionNames.carRace, icon: 'race.webp' },
|
||||||
{ key: 'jetskiRace', name: MissionNames.jetskiRace },
|
{ key: 'jetskiRace', name: MissionNames.jetskiRace, icon: 'boat.webp' },
|
||||||
{ key: 'towTrack', name: MissionNames.towTrack },
|
{ key: 'towTrack', name: MissionNames.towTrack, icon: 'gas.webp' },
|
||||||
{ key: 'ambulance', name: MissionNames.ambulance }
|
{ key: 'ambulance', name: MissionNames.ambulance, icon: 'med.webp' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const actors = [
|
const actors = [
|
||||||
@ -45,9 +45,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mission-scores-editor">
|
|
||||||
<h3 class="section-title">Mission Scores</h3>
|
|
||||||
|
|
||||||
<div class="scores-table-wrapper">
|
<div class="scores-table-wrapper">
|
||||||
{#key missionData}
|
{#key missionData}
|
||||||
<div class="scores-table">
|
<div class="scores-table">
|
||||||
@ -60,7 +57,9 @@
|
|||||||
|
|
||||||
{#each missions as mission}
|
{#each missions as mission}
|
||||||
<div class="scores-row">
|
<div class="scores-row">
|
||||||
<div class="mission-col">{mission.name}</div>
|
<div class="mission-col">
|
||||||
|
<img src={mission.icon} alt={mission.name} class="mission-icon" title={mission.name} />
|
||||||
|
</div>
|
||||||
{#each actors as actor}
|
{#each actors as actor}
|
||||||
<div class="actor-col">
|
<div class="actor-col">
|
||||||
<ScoreColorButton
|
<ScoreColorButton
|
||||||
@ -98,22 +97,8 @@
|
|||||||
<span class="legend-divider">|</span>
|
<span class="legend-divider">|</span>
|
||||||
<span class="legend-note">H = High Score</span>
|
<span class="legend-note">H = High Score</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.mission-scores-editor {
|
|
||||||
background: var(--gradient-panel);
|
|
||||||
border: 1px solid var(--color-border-dark);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scores-table-wrapper {
|
.scores-table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@ -128,7 +113,7 @@
|
|||||||
.scores-header,
|
.scores-header,
|
||||||
.scores-row {
|
.scores-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 120px repeat(5, 1fr);
|
grid-template-columns: 50px repeat(5, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -142,9 +127,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mission-col {
|
.mission-col {
|
||||||
color: var(--color-text-medium);
|
display: flex;
|
||||||
font-size: 0.85em;
|
align-items: center;
|
||||||
white-space: nowrap;
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actor-col {
|
.actor-col {
|
||||||
@ -201,15 +192,16 @@
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.scores-header,
|
.scores-header,
|
||||||
.scores-row {
|
.scores-row {
|
||||||
grid-template-columns: 90px repeat(5, 1fr);
|
grid-template-columns: 40px repeat(5, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scores-header .actor-col {
|
.scores-header .actor-col {
|
||||||
font-size: 0.65em;
|
font-size: 0.65em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mission-col {
|
.mission-icon {
|
||||||
font-size: 0.75em;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,298 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { Actor, ActorNames } from '../../core/savegame/constants.js';
|
|
||||||
import { updatePlayerName } from '../../core/savegame/index.js';
|
|
||||||
|
|
||||||
export let slot;
|
|
||||||
export let onUpdate = () => {};
|
|
||||||
export let onNameUpdate = () => {};
|
|
||||||
|
|
||||||
// Local state for form values
|
|
||||||
let currentAct = slot?.header?.currentAct ?? 0;
|
|
||||||
let actorId = slot?.header?.actorId ?? 1;
|
|
||||||
|
|
||||||
// Name slots (7 characters)
|
|
||||||
let nameSlots = ['', '', '', '', '', '', ''];
|
|
||||||
let slotRefs = [];
|
|
||||||
|
|
||||||
// Update local state when slot changes
|
|
||||||
$: if (slot?.header) {
|
|
||||||
currentAct = slot.header.currentAct;
|
|
||||||
actorId = slot.header.actorId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update name slots when player name changes
|
|
||||||
$: if (slot?.playerName) {
|
|
||||||
const name = slot.playerName.toUpperCase();
|
|
||||||
nameSlots = Array.from({ length: 7 }, (_, i) => name[i] || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleActChange(e) {
|
|
||||||
currentAct = parseInt(e.target.value);
|
|
||||||
onUpdate({ currentAct });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleActorChange(e) {
|
|
||||||
actorId = parseInt(e.target.value);
|
|
||||||
onUpdate({ actorId });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveNameFromSlots() {
|
|
||||||
const newName = nameSlots.join('').replace(/[^A-Z]/g, '');
|
|
||||||
if (newName.length === 0) return;
|
|
||||||
|
|
||||||
const success = await updatePlayerName(slot.slotNumber, newName);
|
|
||||||
if (success) {
|
|
||||||
onNameUpdate(newName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSlotBeforeInput(index, e) {
|
|
||||||
// Allow deletions
|
|
||||||
if (!e.data) return;
|
|
||||||
|
|
||||||
// Only allow A-Z (case insensitive)
|
|
||||||
const char = e.data.toUpperCase();
|
|
||||||
if (!/^[A-Z]$/.test(char)) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent default and handle manually for better control
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
nameSlots[index] = char;
|
|
||||||
nameSlots = [...nameSlots];
|
|
||||||
|
|
||||||
// Auto-save
|
|
||||||
saveNameFromSlots();
|
|
||||||
|
|
||||||
// Move to next slot
|
|
||||||
if (index < 6) {
|
|
||||||
slotRefs[index + 1]?.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilledCount() {
|
|
||||||
return nameSlots.filter(c => c !== '').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSlotKeydown(index, e) {
|
|
||||||
if (e.key === 'Backspace') {
|
|
||||||
if (nameSlots[index] === '' && index > 0) {
|
|
||||||
// Move to previous slot and clear it (if not the last character)
|
|
||||||
if (getFilledCount() > 1 || nameSlots[index - 1] === '') {
|
|
||||||
slotRefs[index - 1]?.focus();
|
|
||||||
if (nameSlots[index - 1] !== '' && getFilledCount() > 1) {
|
|
||||||
nameSlots[index - 1] = '';
|
|
||||||
nameSlots = [...nameSlots];
|
|
||||||
saveNameFromSlots();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (nameSlots[index] !== '') {
|
|
||||||
// Clear current slot only if not the last character
|
|
||||||
if (getFilledCount() > 1) {
|
|
||||||
nameSlots[index] = '';
|
|
||||||
nameSlots = [...nameSlots];
|
|
||||||
saveNameFromSlots();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (e.key === 'ArrowLeft' && index > 0) {
|
|
||||||
slotRefs[index - 1]?.focus();
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === 'ArrowRight' && index < 6) {
|
|
||||||
slotRefs[index + 1]?.focus();
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === 'Delete') {
|
|
||||||
// Only delete if not the last character
|
|
||||||
if (getFilledCount() > 1 && nameSlots[index] !== '') {
|
|
||||||
nameSlots[index] = '';
|
|
||||||
nameSlots = [...nameSlots];
|
|
||||||
saveNameFromSlots();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSlotFocus(e) {
|
|
||||||
e.target.select();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get actor options (excluding NONE)
|
|
||||||
const actorOptions = [
|
|
||||||
{ id: Actor.PEPPER, name: ActorNames[Actor.PEPPER] },
|
|
||||||
{ id: Actor.MAMA, name: ActorNames[Actor.MAMA] },
|
|
||||||
{ id: Actor.PAPA, name: ActorNames[Actor.PAPA] },
|
|
||||||
{ id: Actor.NICK, name: ActorNames[Actor.NICK] },
|
|
||||||
{ id: Actor.LAURA, name: ActorNames[Actor.LAURA] }
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="player-info-editor">
|
|
||||||
<h3 class="section-title">Player Info</h3>
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="row-label">Name</span>
|
|
||||||
<div class="name-slots">
|
|
||||||
{#each nameSlots as char, i}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="name-slot"
|
|
||||||
value={char}
|
|
||||||
maxlength="1"
|
|
||||||
onbeforeinput={(e) => handleSlotBeforeInput(i, e)}
|
|
||||||
onkeydown={(e) => handleSlotKeydown(i, e)}
|
|
||||||
onfocus={handleSlotFocus}
|
|
||||||
bind:this={slotRefs[i]}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<label class="row-label" for="act-select">Act</label>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select id="act-select" value={currentAct} onchange={handleActChange}>
|
|
||||||
<option value={0}>Act 1</option>
|
|
||||||
<option value={1}>Act 2</option>
|
|
||||||
<option value={2}>Act 3</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<label class="row-label" for="actor-select">Character</label>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select id="actor-select" value={actorId} onchange={handleActorChange}>
|
|
||||||
{#each actorOptions as actor}
|
|
||||||
<option value={actor.id}>{actor.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.player-info-editor {
|
|
||||||
background: var(--gradient-panel);
|
|
||||||
border: 1px solid var(--color-border-dark);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-label {
|
|
||||||
color: var(--color-text-medium);
|
|
||||||
font-size: 0.9em;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-slots {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-slot {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: inherit;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background: var(--color-bg-input);
|
|
||||||
border: 1px solid var(--color-border-medium);
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--color-text-light);
|
|
||||||
padding: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-slot:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 0 0 2px rgba(255, 204, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-slot:hover {
|
|
||||||
border-color: var(--color-border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper::after {
|
|
||||||
content: '\25BC';
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
color: var(--color-text-medium);
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-bg-input);
|
|
||||||
color: var(--color-text-light);
|
|
||||||
border: 1px solid var(--color-border-medium);
|
|
||||||
padding: 8px 30px 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:hover {
|
|
||||||
border-color: var(--color-border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
.info-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-label {
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
max-width: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-slot {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { ActorNames } from '../../core/savegame/constants.js';
|
|
||||||
|
|
||||||
export let slot;
|
|
||||||
export let selected = false;
|
|
||||||
export let onClick = () => {};
|
|
||||||
|
|
||||||
$: actorName = slot.header?.actorId ? ActorNames[slot.header.actorId] : 'None';
|
|
||||||
$: actNumber = (slot.header?.currentAct ?? 0) + 1;
|
|
||||||
$: playerName = slot.playerName || 'Unknown';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="save-slot-card"
|
|
||||||
class:selected
|
|
||||||
onclick={onClick}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div class="slot-header">
|
|
||||||
<span class="slot-number">Slot {slot.slotNumber}</span>
|
|
||||||
<span class="player-name">{playerName}</span>
|
|
||||||
</div>
|
|
||||||
<div class="slot-info">
|
|
||||||
<span class="actor">{actorName}</span>
|
|
||||||
<span class="act">Act {actNumber}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.save-slot-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: var(--color-bg-panel);
|
|
||||||
border: 2px solid var(--color-border-dark);
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
text-align: left;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-slot-card:hover {
|
|
||||||
border-color: var(--color-border-light);
|
|
||||||
background: var(--gradient-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-slot-card.selected {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 0 10px var(--color-primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slot-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slot-number {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-name {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slot-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: var(--color-text-medium);
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
<script>
|
|
||||||
import SaveSlotCard from './SaveSlotCard.svelte';
|
|
||||||
|
|
||||||
export let slots = [];
|
|
||||||
export let selectedSlot = null;
|
|
||||||
export let onSelect = () => {};
|
|
||||||
|
|
||||||
$: existingSlots = slots.filter(s => s.exists);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="save-slot-list">
|
|
||||||
<h2 class="section-title">Save Slots</h2>
|
|
||||||
{#if existingSlots.length === 0}
|
|
||||||
<p class="no-slots">No save files found</p>
|
|
||||||
{:else}
|
|
||||||
<div class="slot-grid">
|
|
||||||
{#each existingSlots as slot}
|
|
||||||
<SaveSlotCard
|
|
||||||
{slot}
|
|
||||||
selected={selectedSlot === slot.slotNumber}
|
|
||||||
onClick={() => onSelect(slot.slotNumber)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.save-slot-list {
|
|
||||||
background: var(--gradient-panel);
|
|
||||||
border: 1px solid var(--color-border-dark);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slot-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-slots {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||