@wasm-gaming/engine-specs 0.2.6 → 0.3.0

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.
Files changed (48) hide show
  1. package/README.md +63 -55
  2. package/demo/app.html +227 -0
  3. package/demo/canvas-renderer.js +153 -0
  4. package/demo/components/asset-picker.html +137 -0
  5. package/demo/components/call-log.html +33 -0
  6. package/demo/components/engine-header.html +78 -0
  7. package/demo/components/lifecycle-bar.html +69 -0
  8. package/demo/components/property-table.html +119 -0
  9. package/demo/demo.css +26 -130
  10. package/demo/demo.sdk.js +405 -203
  11. package/demo/index.html +7 -56
  12. package/demo/tsconfig.json +17 -0
  13. package/dist/engine-specs.d.ts +99 -0
  14. package/dist/engine-specs.d.ts.map +1 -0
  15. package/dist/engine-specs.js +3 -0
  16. package/dist/engine-specs.js.map +1 -0
  17. package/package.json +8 -12
  18. package/src/engine-specs.ts +110 -0
  19. package/demo/README.md +0 -155
  20. package/demo/checksums.js +0 -101
  21. package/demo/components/drop-area.html +0 -171
  22. package/demo/components/esc-menu.html +0 -540
  23. package/demo/components/launcher.html +0 -220
  24. package/demo/components/sdk-info.html +0 -54
  25. package/demo/demo.js +0 -622
  26. package/demo/sdk.js +0 -19
  27. package/demo/storage.js +0 -88
  28. package/dist/engine.d.ts +0 -78
  29. package/dist/engine.d.ts.map +0 -1
  30. package/dist/engine.js +0 -8
  31. package/dist/engine.js.map +0 -1
  32. package/dist/index.d.ts +0 -7
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js +0 -9
  35. package/dist/index.js.map +0 -1
  36. package/dist/manifest.d.ts +0 -69
  37. package/dist/manifest.d.ts.map +0 -1
  38. package/dist/manifest.js +0 -8
  39. package/dist/manifest.js.map +0 -1
  40. package/dist/validate.d.ts +0 -10
  41. package/dist/validate.d.ts.map +0 -1
  42. package/dist/validate.js +0 -79
  43. package/dist/validate.js.map +0 -1
  44. package/schema/engine-manifest.schema.json +0 -74
  45. package/src/engine.ts +0 -88
  46. package/src/index.ts +0 -32
  47. package/src/manifest.ts +0 -82
  48. package/src/validate.ts +0 -92
@@ -1,171 +0,0 @@
1
- <section class="drop" :class.active="dragging" :class.selected="selectedName"
2
- @dragover.prevent="dragging = true"
3
- @dragleave="dragging = false"
4
- @drop.prevent="onDrop">
5
- <strong>{{ prompt }}</strong>
6
- <p :class.file-selected="selectedName">{{ selectedName ? ((messages.selectedPrefix || 'Selected: ') + selectedName) : assetHint }}</p>
7
- <div class="picker-actions">
8
- <label class="picker">
9
- {{ selectedName ? (messages.changeFileButton || 'Change File') : buttonText }}
10
- <input type="file" :accept="accept" @change="onPick" />
11
- </label>
12
- <button type="button" :if="selectedName" class="remove-btn" @click.stop="onRemove">
13
- {{ messages.removeFileButton || 'Remove' }}
14
- </button>
15
- </div>
16
- </section>
17
-
18
- <script :setup="{
19
- prompt,
20
- assetHint = 'or choose one from your computer',
21
- accept,
22
- buttonText = 'Pick file',
23
- fileName = '',
24
- messages = {},
25
- }">
26
- let dragging = false;
27
- let selectedName = fileName || "";
28
-
29
- const extractFile = (arg) => {
30
- if (!arg) return null;
31
- if (arg instanceof File) return arg;
32
- if (arg.detail instanceof File) return arg.detail;
33
- if (arg.target?.files?.item(0)) return arg.target.files.item(0);
34
- if (arg.dataTransfer?.files?.item(0)) return arg.dataTransfer.files.item(0);
35
- return null;
36
- };
37
-
38
- const emitFile = (file) => {
39
- if (!file) {
40
- selectedName = "";
41
- $emit("remove");
42
- $emit("select", null);
43
- return;
44
- }
45
- selectedName = file.name;
46
- $emit("select", file);
47
- };
48
-
49
- const onDrop = (event) => {
50
- dragging = false;
51
- const file = extractFile(event);
52
- if (file) emitFile(file);
53
- };
54
-
55
- const onPick = (event) => {
56
- const file = extractFile(event);
57
- if (file) emitFile(file);
58
- };
59
-
60
- const onRemove = () => {
61
- selectedName = "";
62
- $emit("remove");
63
- $emit("select", null);
64
- };
65
- </script>
66
-
67
- <style>
68
- .drop {
69
- border: 2px dashed var(--demo-drop-border, rgba(255, 255, 255, 0.6));
70
- border-radius: var(--demo-subpanel-radius, 8px);
71
- padding: 2rem 1.25rem;
72
- text-align: center;
73
- transition: all 250ms ease;
74
- background: var(--demo-drop-bg, rgba(255, 255, 255, 0.15));
75
- backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
76
- -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
77
- }
78
-
79
- .drop.active {
80
- border-color: var(--demo-drop-active-border, var(--demo-accent2, #0284c7));
81
- background: var(--demo-drop-active-bg, rgba(255, 255, 255, 0.4));
82
- box-shadow: var(--demo-drop-active-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.8));
83
- }
84
-
85
- .drop.selected {
86
- border-style: solid;
87
- border-color: var(--demo-accent2, #0284c7);
88
- background: var(--demo-subpanel-bg, rgba(255, 255, 255, 0.35));
89
- }
90
-
91
- .drop strong {
92
- display: block;
93
- margin-bottom: 0.5rem;
94
- font-size: 1.1rem;
95
- font-weight: 600;
96
- text-transform: uppercase;
97
- letter-spacing: 0.05em;
98
- color: var(--demo-text, #0f172a);
99
- }
100
-
101
- .drop p {
102
- margin: 0.2rem 0;
103
- color: var(--demo-muted, #475569);
104
- font-family: var(--demo-font-mono, monospace);
105
- font-size: 0.85rem;
106
- word-break: break-all;
107
- }
108
-
109
- .drop p.file-selected {
110
- color: var(--demo-text, #0f172a);
111
- font-weight: 600;
112
- }
113
-
114
- .picker-actions {
115
- display: inline-flex;
116
- align-items: center;
117
- gap: 0.5rem;
118
- margin-top: 1rem;
119
- }
120
-
121
- .picker {
122
- display: inline-flex;
123
- background: var(--demo-picker-bg, rgba(255, 255, 255, 0.5));
124
- color: var(--demo-picker-text, var(--demo-text, #0f172a));
125
- border: 1px solid var(--demo-picker-border, rgba(255, 255, 255, 0.6));
126
- border-radius: var(--demo-subpanel-radius, 8px);
127
- padding: 0.5rem 1rem;
128
- cursor: pointer;
129
- text-transform: uppercase;
130
- font-weight: 600;
131
- font-size: 0.85rem;
132
- letter-spacing: 0.05em;
133
- transition: all 0.2s;
134
- backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
135
- -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
136
- }
137
-
138
- .picker:hover {
139
- background: var(--demo-picker-hover-bg, rgba(255, 255, 255, 0.8));
140
- border-color: var(--demo-picker-hover-border, rgba(255, 255, 255, 0.9));
141
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
142
- }
143
-
144
- .picker input {
145
- display: none;
146
- }
147
-
148
- .remove-btn {
149
- display: inline-flex;
150
- align-items: center;
151
- background: rgba(239, 68, 68, 0.15);
152
- color: #ef4444;
153
- border: 1px solid rgba(239, 68, 68, 0.4);
154
- border-radius: var(--demo-subpanel-radius, 8px);
155
- padding: 0.5rem 0.8rem;
156
- cursor: pointer;
157
- text-transform: uppercase;
158
- font-weight: 600;
159
- font-size: 0.85rem;
160
- letter-spacing: 0.05em;
161
- transition: all 0.2s;
162
- backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
163
- -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
164
- }
165
-
166
- .remove-btn:hover {
167
- background: rgba(239, 68, 68, 0.3);
168
- border-color: rgba(239, 68, 68, 0.8);
169
- color: #dc2626;
170
- }
171
- </style>
@@ -1,540 +0,0 @@
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">&times;</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>