@wasm-gaming/engine-specs 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demo/README.md +75 -22
- package/demo/components/drop-area.html +4 -3
- package/demo/components/esc-menu.html +540 -0
- package/demo/components/launcher.html +25 -25
- package/demo/components/sdk-info.html +14 -2
- package/demo/demo.css +5 -0
- package/demo/demo.js +578 -121
- package/demo/demo.sdk.js +3 -0
- package/demo/index.html +58 -1
- package/package.json +1 -1
- package/demo/components/file-info.html +0 -66
package/demo/README.md
CHANGED
|
@@ -6,19 +6,75 @@ This directory contains a standalone, themable web interface template for hostin
|
|
|
6
6
|
|
|
7
7
|
1. **Copy the `demo/` folder** into your engine repository.
|
|
8
8
|
|
|
9
|
-
2. **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
2. **Initialize via ES module script**:
|
|
10
|
+
```html
|
|
11
|
+
<script type="module">
|
|
12
|
+
import demo from './demo.js'
|
|
13
|
+
import sdk from './demo.sdk.js' // Or your engine SDK import
|
|
14
|
+
|
|
15
|
+
demo
|
|
16
|
+
.init({
|
|
17
|
+
messages: {
|
|
18
|
+
title: 'My Custom Engine Demo',
|
|
19
|
+
},
|
|
20
|
+
bios: false, // 'required' | 'optional' | false
|
|
21
|
+
escMenu: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
title: 'Game Pause Menu',
|
|
24
|
+
items: [
|
|
25
|
+
{ label: 'Toggle Fullscreen', action: (instance) => { document.documentElement.requestFullscreen?.(); } }
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
.on('file', ({ type, action, file }) => {
|
|
30
|
+
if (file) {
|
|
31
|
+
console.log(`[Demo] Selected ${type} file: ${file.name}`)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.on('launch', async (e) => {
|
|
35
|
+
const assets = {}
|
|
36
|
+
|
|
37
|
+
// if rom loaded
|
|
38
|
+
if (demo.hasFile('rom')) {
|
|
39
|
+
assets.rom = await demo.loadToMEMFS('rom')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// if bios required and loaded
|
|
43
|
+
if (demo.hasFile('bios')) {
|
|
44
|
+
assets.bios = await demo.loadToMEMFS('bios')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const instance = await sdk.load({
|
|
48
|
+
attachTo: e.attachTo,
|
|
49
|
+
assets,
|
|
50
|
+
options: { fileName: e.fileName, byteLength: e.byteLength },
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
instance.start()
|
|
54
|
+
return instance
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
17
57
|
```
|
|
18
|
-
*Alternatively*, if embedding in HTML without modifying `sdk.js`, set `window.SDK = myEngineSdk;` before `demo.js` runs.
|
|
19
58
|
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🎮 Component79 ESC Menu
|
|
62
|
+
|
|
63
|
+
The demo includes an interactive, customizable in-game ESC pause menu component built with **Component79**.
|
|
64
|
+
|
|
65
|
+
### ESC Menu Options (`escMenu`)
|
|
66
|
+
|
|
67
|
+
| Option | Type | Default | Description |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `enabled` | `boolean` | `true` | Enable/disable in-game ESC menu overlay. |
|
|
70
|
+
| `title` | `string` | `"Game Menu"` | Title displayed at top of ESC menu modal. |
|
|
71
|
+
| `shortcutKey` | `string` | `"Escape"` | Keyboard key that toggles the menu overlay. |
|
|
72
|
+
| `showTriggerButton` | `boolean` | `true` | Display floating top-right "ESC Menu" trigger badge for mouse/touch access. |
|
|
73
|
+
| `items` | `Array<{label: string, action: function}>` | `[]` | Custom action buttons added to ESC menu. |
|
|
74
|
+
| `onReset` | `function` | `instance.reset()` | Callback when Reset Game button is pressed. |
|
|
75
|
+
| `onExit` | `function` | `instance.destroy()` | Callback when Exit to Launcher button is pressed. |
|
|
76
|
+
| `onSaveState` | `function` | `instance.saveState()` | Callback when Save State button is pressed. |
|
|
77
|
+
| `onLoadState` | `function` | `instance.loadState()` | Callback when Load State button is pressed. |
|
|
22
78
|
|
|
23
79
|
---
|
|
24
80
|
|
|
@@ -50,15 +106,10 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
|
|
|
50
106
|
--demo-subpanel-bg: rgba(30, 41, 59, 0.6);
|
|
51
107
|
--demo-subpanel-border: rgba(255, 255, 255, 0.1);
|
|
52
108
|
|
|
53
|
-
/*
|
|
54
|
-
--demo-
|
|
55
|
-
--demo-
|
|
56
|
-
--demo-
|
|
57
|
-
--demo-picker-text: #ffffff;
|
|
58
|
-
|
|
59
|
-
/* Launch button */
|
|
60
|
-
--demo-launch-bg: var(--demo-accent);
|
|
61
|
-
--demo-launch-text: #ffffff;
|
|
109
|
+
/* ESC Menu styling */
|
|
110
|
+
--demo-esc-bg: rgba(6, 9, 16, 0.85);
|
|
111
|
+
--demo-esc-card-bg: rgba(15, 23, 42, 0.9);
|
|
112
|
+
--demo-esc-text: #f8fafc;
|
|
62
113
|
}
|
|
63
114
|
```
|
|
64
115
|
|
|
@@ -84,8 +135,9 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
|
|
|
84
135
|
| `--demo-picker-bg` | Pick File button background | `rgba(255, 255, 255, 0.5)` |
|
|
85
136
|
| `--demo-pill-bg` | Metadata badge background | `rgba(var(--demo-accent-rgb), 0.1)` |
|
|
86
137
|
| `--demo-file-info-bg` | Canvas overlay panel background | `rgba(255, 255, 255, 0.7)` |
|
|
87
|
-
| `--demo-
|
|
88
|
-
| `--demo-
|
|
138
|
+
| `--demo-esc-bg` | ESC menu overlay background | `rgba(6, 9, 16, 0.82)` |
|
|
139
|
+
| `--demo-esc-card-bg` | ESC menu card background | `rgba(15, 23, 42, 0.88)` |
|
|
140
|
+
| `--demo-esc-text` | ESC menu text color | `#f8fafc` |
|
|
89
141
|
| `--demo-font-sans` | Primary sans-serif font family | `"Outfit", system-ui, sans-serif` |
|
|
90
142
|
| `--demo-font-mono` | Monospace code font family | `"Space Mono", monospace` |
|
|
91
143
|
|
|
@@ -93,6 +145,7 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
|
|
|
93
145
|
|
|
94
146
|
## 🛠 Features
|
|
95
147
|
|
|
148
|
+
- **Component79 ESC Menu**: Customizable pause menu overlay with keydown listeners and touch/mouse badges.
|
|
96
149
|
- **OPFS Asset Persistence**: Automatically stores selected ROM and BIOS files in the Origin Private File System (`navigator.storage.getDirectory()`), restoring them automatically across page reloads.
|
|
97
150
|
- **Split Asset Drop Zones**: Independent drop/picker areas for optional **BIOS** firmware and game **ROM** assets.
|
|
98
151
|
- **Dedicated Launch Trigger**: Prominent launch button that activates once required game assets are selected.
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
@dragleave="dragging = false"
|
|
4
4
|
@drop.prevent="onDrop">
|
|
5
5
|
<strong>{{ prompt }}</strong>
|
|
6
|
-
<p :class.file-selected="selectedName">{{ selectedName ? 'Selected: ' + selectedName : assetHint }}</p>
|
|
6
|
+
<p :class.file-selected="selectedName">{{ selectedName ? ((messages.selectedPrefix || 'Selected: ') + selectedName) : assetHint }}</p>
|
|
7
7
|
<div class="picker-actions">
|
|
8
8
|
<label class="picker">
|
|
9
|
-
{{ selectedName ? 'Change File' : buttonText }}
|
|
9
|
+
{{ selectedName ? (messages.changeFileButton || 'Change File') : buttonText }}
|
|
10
10
|
<input type="file" :accept="accept" @change="onPick" />
|
|
11
11
|
</label>
|
|
12
12
|
<button type="button" :if="selectedName" class="remove-btn" @click.stop="onRemove">
|
|
13
|
-
Remove
|
|
13
|
+
{{ messages.removeFileButton || 'Remove' }}
|
|
14
14
|
</button>
|
|
15
15
|
</div>
|
|
16
16
|
</section>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
accept,
|
|
22
22
|
buttonText = 'Pick file',
|
|
23
23
|
fileName = '',
|
|
24
|
+
messages = {},
|
|
24
25
|
}">
|
|
25
26
|
let dragging = false;
|
|
26
27
|
let selectedName = fileName || "";
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
<div class="esc-menu-wrapper">
|
|
2
|
+
<!-- Fullscreen Modal Overlay -->
|
|
3
|
+
<div :if="open" class="esc-overlay" @click.self="$emit('close')">
|
|
4
|
+
<div class="esc-panel" tabindex="-1">
|
|
5
|
+
<!-- Header -->
|
|
6
|
+
<header class="esc-header">
|
|
7
|
+
<span class="esc-badge">{{ messages.escMenuBadge || 'PAUSED' }}</span>
|
|
8
|
+
<h2>{{ title }}</h2>
|
|
9
|
+
<button type="button" class="esc-header-exit-btn" @click="$emit('exit')">
|
|
10
|
+
<i class="esc-icon exit"></i>
|
|
11
|
+
{{ messages.escMenuExit || 'Exit to Launcher' }}
|
|
12
|
+
</button>
|
|
13
|
+
<button type="button" class="esc-close-btn" @click="$emit('close')" aria-label="Close menu">×</button>
|
|
14
|
+
</header>
|
|
15
|
+
|
|
16
|
+
<!-- Scrollable Options Body -->
|
|
17
|
+
<div class="esc-body">
|
|
18
|
+
<div :each="group in optionGroups" class="esc-group">
|
|
19
|
+
<h3 class="esc-group-title">{{ group.label }}</h3>
|
|
20
|
+
|
|
21
|
+
<div :each="option in group.options" class="esc-row" :class.active="activeKey === option.key"
|
|
22
|
+
@mouseenter="onHoverOption(option)" @click="onSelectOption(option)">
|
|
23
|
+
|
|
24
|
+
<div class="esc-row-label">
|
|
25
|
+
{{ option.label }}
|
|
26
|
+
<span :if="option.requiresReset" class="esc-tag">{{ messages.escMenuNeedsResetTag || 'needs reset' }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="esc-row-control">
|
|
30
|
+
<!-- Boolean Chips: Off / On -->
|
|
31
|
+
<div :if="option.type === 'boolean'" class="esc-chip-group">
|
|
32
|
+
<button type="button" class="esc-chip" :class.active="!option.value"
|
|
33
|
+
@click.stop="onChangeOption(option, false)">
|
|
34
|
+
Off
|
|
35
|
+
</button>
|
|
36
|
+
<button type="button" class="esc-chip" :class.active="option.value"
|
|
37
|
+
@click.stop="onChangeOption(option, true)">
|
|
38
|
+
On
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Enum Choice Chips -->
|
|
43
|
+
<div :if="option.type === 'enum' || option.type === 'select'" class="esc-chip-group">
|
|
44
|
+
<button :each="choice in (option.values || option.choices || [])" type="button" class="esc-chip"
|
|
45
|
+
:class.active="option.value === choice.value" @click.stop="onChangeOption(option, choice.value)">
|
|
46
|
+
{{ choice.label }}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- Footer with Description, Actions & Shortcut Guide -->
|
|
55
|
+
<footer class="esc-footer">
|
|
56
|
+
<p class="esc-desc">{{ activeDescription || 'Select an option to view details.' }}</p>
|
|
57
|
+
|
|
58
|
+
<div class="esc-actions">
|
|
59
|
+
<button type="button" class="esc-action-btn primary" @click="$emit('close')">
|
|
60
|
+
<i class="esc-icon resume"></i>
|
|
61
|
+
{{ messages.escMenuResume || 'Resume' }}
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
<button type="button" class="esc-action-btn" @click="$emit('restore-defaults')">
|
|
65
|
+
<i class="esc-icon defaults"></i>
|
|
66
|
+
{{ messages.escMenuRestoreDefaults || 'Restore defaults' }}
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<button type="button" class="esc-action-btn" @click="$emit('reset')">
|
|
70
|
+
<i class="esc-icon reset"></i>
|
|
71
|
+
{{ messages.escMenuReset || 'Reset game' }}
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
<button :if="hasSaveStates" type="button" class="esc-action-btn" @click="$emit('save-state')">
|
|
75
|
+
<i class="esc-icon save"></i>
|
|
76
|
+
{{ messages.escMenuSaveState || 'Save State' }}
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<button :if="hasSaveStates" type="button" class="esc-action-btn" @click="$emit('load-state')">
|
|
80
|
+
<i class="esc-icon load"></i>
|
|
81
|
+
{{ messages.escMenuLoadState || 'Load State' }}
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<button :each="item in customItems" type="button" class="esc-action-btn custom" @click="$emit('custom-action', item)">
|
|
85
|
+
{{ item.label }}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<p class="esc-keys-hint">{{ messages.escMenuKeysHint || '↑ ↓ select · ← → change · Enter apply · Esc close' }}</p>
|
|
90
|
+
</footer>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<script :setup="{
|
|
96
|
+
open = false,
|
|
97
|
+
title = 'Settings',
|
|
98
|
+
subtitle = '',
|
|
99
|
+
hasSaveStates = false,
|
|
100
|
+
optionGroups = [],
|
|
101
|
+
customItems = [],
|
|
102
|
+
messages = {},
|
|
103
|
+
}">
|
|
104
|
+
let activeKey = optionGroups?.[0]?.options?.[0]?.key || "";
|
|
105
|
+
let activeDescription = optionGroups?.[0]?.options?.[0]?.description || "";
|
|
106
|
+
|
|
107
|
+
const getFlatOptions = () => {
|
|
108
|
+
const list = [];
|
|
109
|
+
for (const group of optionGroups || []) {
|
|
110
|
+
for (const opt of group.options || []) {
|
|
111
|
+
list.push(opt);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return list;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onHoverOption = (option) => {
|
|
118
|
+
if (!option) return;
|
|
119
|
+
activeKey = option.key;
|
|
120
|
+
activeDescription = option.description || "";
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const onSelectOption = (option) => {
|
|
124
|
+
onHoverOption(option);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const onChangeOption = (option, newValue) => {
|
|
128
|
+
option.value = newValue;
|
|
129
|
+
onHoverOption(option);
|
|
130
|
+
onOptionChange?.(option.key, newValue, option);
|
|
131
|
+
$emit("option-change", { key: option.key, value: newValue, option });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleKeyDown = (e) => {
|
|
135
|
+
if (!open) return;
|
|
136
|
+
const key = e.key;
|
|
137
|
+
|
|
138
|
+
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"].includes(key)) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
|
|
142
|
+
const options = getFlatOptions();
|
|
143
|
+
if (options.length === 0) return;
|
|
144
|
+
|
|
145
|
+
let idx = options.findIndex((o) => o.key === activeKey);
|
|
146
|
+
if (idx < 0) idx = 0;
|
|
147
|
+
|
|
148
|
+
if (key === "ArrowDown") {
|
|
149
|
+
const nextIdx = (idx + 1) % options.length;
|
|
150
|
+
onHoverOption(options[nextIdx]);
|
|
151
|
+
} else if (key === "ArrowUp") {
|
|
152
|
+
const prevIdx = (idx - 1 + options.length) % options.length;
|
|
153
|
+
onHoverOption(options[prevIdx]);
|
|
154
|
+
} else if (key === "ArrowRight" || key === "ArrowLeft") {
|
|
155
|
+
const opt = options[idx];
|
|
156
|
+
if (!opt) return;
|
|
157
|
+
|
|
158
|
+
if (opt.type === "boolean") {
|
|
159
|
+
onChangeOption(opt, !opt.value);
|
|
160
|
+
} else if (opt.type === "enum" || opt.type === "select") {
|
|
161
|
+
const choices = opt.values || opt.choices || [];
|
|
162
|
+
if (choices.length > 0) {
|
|
163
|
+
const valIdx = choices.findIndex((c) => c.value === opt.value);
|
|
164
|
+
let nextValIdx;
|
|
165
|
+
if (key === "ArrowRight") {
|
|
166
|
+
nextValIdx = (valIdx + 1) % choices.length;
|
|
167
|
+
} else {
|
|
168
|
+
nextValIdx = (valIdx - 1 + choices.length) % choices.length;
|
|
169
|
+
}
|
|
170
|
+
onChangeOption(opt, choices[nextValIdx].value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else if (key === "Enter") {
|
|
174
|
+
onClose?.();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (typeof window !== "undefined") {
|
|
180
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
181
|
+
}
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<style>
|
|
185
|
+
:host {
|
|
186
|
+
display: block;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.esc-menu-wrapper {
|
|
191
|
+
pointer-events: auto;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Modal Scrim / Overlay - Translucent lighter backdrop with blur */
|
|
195
|
+
.esc-overlay {
|
|
196
|
+
position: fixed;
|
|
197
|
+
inset: 0;
|
|
198
|
+
z-index: 9999;
|
|
199
|
+
background: var(--demo-esc-bg, rgba(0, 0, 0, 0.45));
|
|
200
|
+
backdrop-filter: blur(8px);
|
|
201
|
+
-webkit-backdrop-filter: blur(8px);
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
padding: clamp(0.5rem, 3vh, 2rem);
|
|
206
|
+
animation: escFadeIn 0.2s ease-out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@keyframes escFadeIn {
|
|
210
|
+
from {
|
|
211
|
+
opacity: 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
to {
|
|
215
|
+
opacity: 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Panel */
|
|
220
|
+
.esc-panel {
|
|
221
|
+
width: min(540px, 100%);
|
|
222
|
+
max-height: 90vh;
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
background: var(--demo-esc-card-bg, rgba(15, 23, 42, 0.95));
|
|
226
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
227
|
+
border-top: 2px solid var(--demo-accent, #ff007f);
|
|
228
|
+
border-radius: var(--demo-panel-radius, 12px);
|
|
229
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
animation: escSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@keyframes escSlideUp {
|
|
235
|
+
from {
|
|
236
|
+
opacity: 0;
|
|
237
|
+
transform: translateY(12px) scale(0.98);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
to {
|
|
241
|
+
opacity: 1;
|
|
242
|
+
transform: translateY(0) scale(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Header */
|
|
247
|
+
header.esc-header {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 0.75rem;
|
|
251
|
+
padding: 0.85rem 1.25rem;
|
|
252
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
253
|
+
background: rgba(0, 0, 0, 0.2);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.esc-badge {
|
|
257
|
+
padding: 0.15rem 0.5rem;
|
|
258
|
+
background: rgba(255, 0, 127, 0.15);
|
|
259
|
+
border: 1px solid var(--demo-accent, #ff007f);
|
|
260
|
+
border-radius: 4px;
|
|
261
|
+
color: var(--demo-accent, #ff007f);
|
|
262
|
+
font-family: var(--demo-font-mono, monospace);
|
|
263
|
+
font-size: 0.7rem;
|
|
264
|
+
font-weight: 800;
|
|
265
|
+
letter-spacing: 0.1em;
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
header.esc-header h2 {
|
|
270
|
+
margin: 0;
|
|
271
|
+
font-size: 1.1rem;
|
|
272
|
+
font-weight: 800;
|
|
273
|
+
letter-spacing: 0.1em;
|
|
274
|
+
text-transform: uppercase;
|
|
275
|
+
color: var(--demo-esc-text, #f8fafc);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.esc-header-exit-btn {
|
|
279
|
+
margin-left: auto;
|
|
280
|
+
padding: 0.4rem 0.8rem;
|
|
281
|
+
border-radius: 6px;
|
|
282
|
+
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
283
|
+
background: rgba(239, 68, 68, 0.15);
|
|
284
|
+
color: #fca5a5;
|
|
285
|
+
font-family: var(--demo-font-sans, sans-serif);
|
|
286
|
+
font-size: 0.8rem;
|
|
287
|
+
font-weight: 600;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 0.4rem;
|
|
292
|
+
transition: all 0.15s ease;
|
|
293
|
+
text-transform: uppercase;
|
|
294
|
+
letter-spacing: 0.03em;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.esc-header-exit-btn:hover {
|
|
298
|
+
background: rgba(239, 68, 68, 0.3);
|
|
299
|
+
border-color: rgba(239, 68, 68, 0.7);
|
|
300
|
+
color: #ffffff;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.esc-close-btn {
|
|
304
|
+
background: rgba(255, 255, 255, 0.08);
|
|
305
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
306
|
+
color: var(--demo-muted, #94a3b8);
|
|
307
|
+
border-radius: 6px;
|
|
308
|
+
width: 28px;
|
|
309
|
+
height: 28px;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
font-size: 1.2rem;
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
transition: all 0.2s;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.esc-close-btn:hover {
|
|
319
|
+
background: rgba(255, 255, 255, 0.2);
|
|
320
|
+
color: #ffffff;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Scrollable Body */
|
|
324
|
+
.esc-body {
|
|
325
|
+
padding: 1rem 1.25rem;
|
|
326
|
+
overflow-y: auto;
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
gap: 1.25rem;
|
|
330
|
+
max-height: 48vh;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.esc-group-title {
|
|
334
|
+
margin: 0 0 0.5rem;
|
|
335
|
+
font-size: 0.75rem;
|
|
336
|
+
font-weight: 700;
|
|
337
|
+
letter-spacing: 0.15em;
|
|
338
|
+
text-transform: uppercase;
|
|
339
|
+
color: var(--demo-accent, #ff007f);
|
|
340
|
+
opacity: 0.9;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.esc-row {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
justify-content: space-between;
|
|
347
|
+
padding: 0.5rem 0.75rem;
|
|
348
|
+
border-radius: 6px;
|
|
349
|
+
border: 1px solid transparent;
|
|
350
|
+
transition: all 0.15s ease;
|
|
351
|
+
gap: 1rem;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.esc-row:hover,
|
|
356
|
+
.esc-row.active {
|
|
357
|
+
background: rgba(255, 255, 255, 0.06);
|
|
358
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.esc-row-label {
|
|
362
|
+
font-size: 0.88rem;
|
|
363
|
+
font-weight: 600;
|
|
364
|
+
color: var(--demo-esc-text, #f8fafc);
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 0.5rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.esc-tag {
|
|
371
|
+
font-size: 0.65rem;
|
|
372
|
+
font-family: var(--demo-font-mono, monospace);
|
|
373
|
+
padding: 0.1rem 0.35rem;
|
|
374
|
+
border-radius: 3px;
|
|
375
|
+
background: rgba(234, 179, 8, 0.15);
|
|
376
|
+
border: 1px solid rgba(234, 179, 8, 0.4);
|
|
377
|
+
color: #fde047;
|
|
378
|
+
text-transform: lowercase;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.esc-row-control {
|
|
382
|
+
display: flex;
|
|
383
|
+
align-items: center;
|
|
384
|
+
gap: 0.4rem;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.esc-chip-group {
|
|
388
|
+
display: inline-flex;
|
|
389
|
+
background: rgba(0, 0, 0, 0.3);
|
|
390
|
+
padding: 2px;
|
|
391
|
+
border-radius: 6px;
|
|
392
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.esc-chip {
|
|
396
|
+
padding: 0.25rem 0.6rem;
|
|
397
|
+
border: none;
|
|
398
|
+
background: transparent;
|
|
399
|
+
color: var(--demo-muted, #94a3b8);
|
|
400
|
+
font-family: var(--demo-font-sans, sans-serif);
|
|
401
|
+
font-size: 0.78rem;
|
|
402
|
+
font-weight: 600;
|
|
403
|
+
border-radius: 4px;
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
transition: all 0.15s ease;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.esc-chip:hover {
|
|
409
|
+
color: #ffffff;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.esc-chip.active {
|
|
413
|
+
background: var(--demo-accent, #ff007f);
|
|
414
|
+
color: #ffffff;
|
|
415
|
+
box-shadow: 0 2px 8px rgba(255, 0, 127, 0.3);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* Footer */
|
|
419
|
+
footer.esc-footer {
|
|
420
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
421
|
+
padding: 0.9rem 1.25rem;
|
|
422
|
+
background: rgba(0, 0, 0, 0.25);
|
|
423
|
+
display: flex;
|
|
424
|
+
flex-direction: column;
|
|
425
|
+
gap: 0.85rem;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.esc-desc {
|
|
429
|
+
margin: 0;
|
|
430
|
+
font-size: 0.82rem;
|
|
431
|
+
color: var(--demo-muted, #94a3b8);
|
|
432
|
+
font-family: var(--demo-font-mono, monospace);
|
|
433
|
+
min-height: 2.8rem;
|
|
434
|
+
line-height: 1.4;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.esc-actions {
|
|
438
|
+
display: flex;
|
|
439
|
+
flex-wrap: wrap;
|
|
440
|
+
gap: 0.6rem;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Action buttons */
|
|
444
|
+
.esc-action-btn {
|
|
445
|
+
padding: 0.6rem 1.1rem;
|
|
446
|
+
border-radius: 6px;
|
|
447
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
448
|
+
background: rgba(255, 255, 255, 0.08);
|
|
449
|
+
color: var(--demo-esc-text, #f8fafc);
|
|
450
|
+
font-family: var(--demo-font-sans, sans-serif);
|
|
451
|
+
font-size: 0.85rem;
|
|
452
|
+
font-weight: 600;
|
|
453
|
+
cursor: pointer;
|
|
454
|
+
display: inline-flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 0.5rem;
|
|
457
|
+
transition: all 0.15s ease;
|
|
458
|
+
text-transform: uppercase;
|
|
459
|
+
letter-spacing: 0.04em;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.esc-action-btn:hover {
|
|
463
|
+
background: rgba(255, 255, 255, 0.2);
|
|
464
|
+
border-color: rgba(255, 255, 255, 0.35);
|
|
465
|
+
transform: translateY(-1px);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.esc-action-btn:active {
|
|
469
|
+
transform: translateY(0);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.esc-action-btn.primary {
|
|
473
|
+
background: var(--demo-accent, #ff007f);
|
|
474
|
+
border-color: var(--demo-accent, #ff007f);
|
|
475
|
+
color: #ffffff;
|
|
476
|
+
box-shadow: 0 4px 15px rgba(255, 0, 127, 0.3);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.esc-action-btn.primary:hover {
|
|
480
|
+
filter: brightness(1.15);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.esc-action-btn.custom {
|
|
484
|
+
background: rgba(56, 189, 248, 0.12);
|
|
485
|
+
border-color: rgba(56, 189, 248, 0.3);
|
|
486
|
+
color: #7dd3fc;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.esc-action-btn.custom:hover {
|
|
490
|
+
background: rgba(56, 189, 248, 0.25);
|
|
491
|
+
color: #ffffff;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Reliable CSS Mask Icon Implementation */
|
|
495
|
+
.esc-icon {
|
|
496
|
+
width: 16px;
|
|
497
|
+
height: 16px;
|
|
498
|
+
min-width: 16px;
|
|
499
|
+
min-height: 16px;
|
|
500
|
+
display: inline-block;
|
|
501
|
+
vertical-align: middle;
|
|
502
|
+
flex-shrink: 0;
|
|
503
|
+
background-color: currentColor;
|
|
504
|
+
-webkit-mask: var(--icon-url) no-repeat center / contain;
|
|
505
|
+
mask: var(--icon-url) no-repeat center / contain;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.esc-icon.resume {
|
|
509
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="5,3 19,12 5,21"/></svg>');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.esc-icon.defaults {
|
|
513
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.esc-icon.reset {
|
|
517
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.esc-icon.exit {
|
|
521
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.esc-icon.save {
|
|
525
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.esc-icon.load {
|
|
529
|
+
--icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.esc-keys-hint {
|
|
533
|
+
margin: 0;
|
|
534
|
+
text-align: center;
|
|
535
|
+
font-size: 0.75rem;
|
|
536
|
+
color: var(--demo-muted, #94a3b8);
|
|
537
|
+
font-family: var(--demo-font-mono, monospace);
|
|
538
|
+
opacity: 0.8;
|
|
539
|
+
}
|
|
540
|
+
</style>
|