@rohal12/spindle 0.59.7 → 0.59.9
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +332 -87
- package/dist/pkg/story-variables.js +13 -2
- package/package.json +1 -1
- package/src/class-registry.ts +22 -1
- package/src/components/App.tsx +4 -4
- package/src/components/PassageDialog.tsx +4 -3
- package/src/components/macros/Button.tsx +1 -0
- package/src/components/macros/Cycle.tsx +1 -0
- package/src/components/macros/Dialog.tsx +1 -0
- package/src/components/macros/Do.tsx +4 -12
- package/src/components/macros/MacroError.tsx +1 -1
- package/src/components/macros/MenubarButtons.tsx +9 -5
- package/src/components/macros/PassageDisplay.tsx +19 -5
- package/src/components/macros/SaveManager.tsx +42 -3
- package/src/components/macros/Type.tsx +57 -8
- package/src/components/macros/detached-body.tsx +16 -3
- package/src/expression.ts +9 -21
- package/src/hooks/use-modal-focus.ts +15 -1
- package/src/quick-actions.ts +23 -0
- package/src/saves/format.ts +10 -1
- package/src/saves/save-manager.ts +66 -19
- package/src/saves/storage.ts +10 -8
- package/src/saves/types.ts +3 -0
- package/src/settings.ts +5 -1
- package/src/store.ts +41 -6
- package/src/story-init.ts +13 -1
- package/src/structural.ts +6 -2
- package/src/styles.css +14 -0
- package/src/triggers.ts +30 -0
- package/src/utils/stable-key.ts +1 -1
- package/src/widgets/widget-def.ts +15 -3
package/dist/pkg/headless.js
CHANGED
|
@@ -10096,18 +10096,18 @@ function historyQueries(visitCounts, renderCounts) {
|
|
|
10096
10096
|
};
|
|
10097
10097
|
}
|
|
10098
10098
|
var cachedFns = null;
|
|
10099
|
-
|
|
10100
|
-
|
|
10099
|
+
/**
|
|
10100
|
+
* The functions compiled code calls. The history queries read the counters
|
|
10101
|
+
* when called, not when the functions were built: mutation code that
|
|
10102
|
+
* navigates (`Story.goto()`) goes on with the counters of the new state,
|
|
10103
|
+
* as the Story API does.
|
|
10104
|
+
*/
|
|
10101
10105
|
function buildExpressionFns() {
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
cachedFns = {
|
|
10105
|
-
...historyQueries(() => visitCounts, () => renderCounts),
|
|
10106
|
+
cachedFns ??= {
|
|
10107
|
+
...historyQueries(() => useStoryStore.getState().visitCounts, () => useStoryStore.getState().renderCounts),
|
|
10106
10108
|
random,
|
|
10107
10109
|
randomInt
|
|
10108
10110
|
};
|
|
10109
|
-
cachedVisitCounts = visitCounts;
|
|
10110
|
-
cachedRenderCounts = renderCounts;
|
|
10111
10111
|
return cachedFns;
|
|
10112
10112
|
}
|
|
10113
10113
|
/**
|
|
@@ -11143,7 +11143,7 @@ function useRunOnce(macro, rawArgs, effect) {
|
|
|
11143
11143
|
effect();
|
|
11144
11144
|
} catch (error) {
|
|
11145
11145
|
failure.current = { error };
|
|
11146
|
-
logMacroError(`${macro} ${rawArgs}
|
|
11146
|
+
logMacroError(`${macro} ${rawArgs}`.trim(), error, location);
|
|
11147
11147
|
}
|
|
11148
11148
|
}
|
|
11149
11149
|
if (!failure.current) return null;
|
|
@@ -12548,6 +12548,7 @@ var CLASS_PREFIX = "c:";
|
|
|
12548
12548
|
var ERROR_PREFIX = "E:";
|
|
12549
12549
|
var SYMBOL_TAG = "S";
|
|
12550
12550
|
var KEYS_TAG = "K";
|
|
12551
|
+
var REGEXP_TAG = "R";
|
|
12551
12552
|
var BUILTIN_ERRORS = new Map([
|
|
12552
12553
|
Error,
|
|
12553
12554
|
EvalError,
|
|
@@ -12609,6 +12610,11 @@ function reducers() {
|
|
|
12609
12610
|
const key = Symbol.keyFor(v);
|
|
12610
12611
|
return key !== void 0 && [key];
|
|
12611
12612
|
};
|
|
12613
|
+
out[REGEXP_TAG] = (v) => v instanceof RegExp && v.lastIndex !== 0 && [
|
|
12614
|
+
v.source,
|
|
12615
|
+
v.flags,
|
|
12616
|
+
v.lastIndex
|
|
12617
|
+
];
|
|
12612
12618
|
out[KEYS_TAG] = (v) => {
|
|
12613
12619
|
if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
|
|
12614
12620
|
const keys = Object.keys(v);
|
|
@@ -12682,6 +12688,12 @@ function reviverFor(key) {
|
|
|
12682
12688
|
return Symbol.for(data[0]);
|
|
12683
12689
|
};
|
|
12684
12690
|
if (key === KEYS_TAG) return reviveKeyed;
|
|
12691
|
+
if (key === REGEXP_TAG) return (data) => {
|
|
12692
|
+
if (!Array.isArray(data) || typeof data[0] !== "string" || typeof data[1] !== "string" || !Number.isSafeInteger(data[2])) throw new TypeError("spindle: Malformed regular expression");
|
|
12693
|
+
const re = new RegExp(data[0], data[1]);
|
|
12694
|
+
re.lastIndex = data[2];
|
|
12695
|
+
return re;
|
|
12696
|
+
};
|
|
12685
12697
|
if (key.startsWith(CLASS_PREFIX)) return reviveClass(key.slice(2));
|
|
12686
12698
|
if (key.startsWith(ERROR_PREFIX)) return reviveError(key.slice(2));
|
|
12687
12699
|
}
|
|
@@ -12793,6 +12805,7 @@ function isOptionalPRNGSnapshot(value) {
|
|
|
12793
12805
|
return isRecord$1(value) && typeof value.seed === "string" && typeof value.pull === "number" && Number.isInteger(value.pull) && value.pull >= 0;
|
|
12794
12806
|
}
|
|
12795
12807
|
var isMoment = (value) => isRecord$1(value) && typeof value.passage === "string" && isRecord$1(value.variables) && typeof value.timestamp === "number" && isOptionalPRNGSnapshot(value.prng);
|
|
12808
|
+
var isWatchers = (value) => value === void 0 || Array.isArray(value) && Array.from(value).every((w) => isRecord$1(w) && typeof w.condition === "string" && isRecord$1(w.options));
|
|
12796
12809
|
var isCountsMap = (value) => value === void 0 || value instanceof Map && [...value.keys()].every((k) => typeof k === "string");
|
|
12797
12810
|
/**
|
|
12798
12811
|
* Whether a deserialized payload is well formed: every history moment is,
|
|
@@ -12806,7 +12819,7 @@ function isPayload(value) {
|
|
|
12806
12819
|
if (!Array.isArray(history) || history.length === 0) return false;
|
|
12807
12820
|
if (!Array.from(history).every(isMoment)) return false;
|
|
12808
12821
|
if (typeof historyIndex !== "number" || !Number.isInteger(historyIndex) || historyIndex < 0 || historyIndex >= history.length || history[historyIndex].passage !== value.passage) return false;
|
|
12809
|
-
return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng);
|
|
12822
|
+
return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng) && isWatchers(value.watchers);
|
|
12810
12823
|
}
|
|
12811
12824
|
var mapToCounts = (counts) => counts instanceof Map ? createCounts(Object.fromEntries(counts)) : void 0;
|
|
12812
12825
|
/**
|
|
@@ -13176,24 +13189,28 @@ function createIDBBackend() {
|
|
|
13176
13189
|
}
|
|
13177
13190
|
var _backend = null;
|
|
13178
13191
|
async function detectBackend() {
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
req.
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13192
|
+
try {
|
|
13193
|
+
if (typeof indexedDB !== "undefined") {
|
|
13194
|
+
await new Promise((resolve, reject) => {
|
|
13195
|
+
const req = indexedDB.open("__spindle_probe__", 1);
|
|
13196
|
+
req.onsuccess = () => {
|
|
13197
|
+
req.result.close();
|
|
13198
|
+
indexedDB.deleteDatabase("__spindle_probe__");
|
|
13199
|
+
resolve();
|
|
13200
|
+
};
|
|
13201
|
+
req.onerror = () => reject(req.error);
|
|
13202
|
+
});
|
|
13203
|
+
return createIDBBackend();
|
|
13204
|
+
}
|
|
13190
13205
|
} catch {}
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13206
|
+
try {
|
|
13207
|
+
if (typeof localStorage !== "undefined") {
|
|
13208
|
+
const probeKey = "__spindle_probe__";
|
|
13209
|
+
localStorage.setItem(probeKey, "1");
|
|
13210
|
+
const val = localStorage.getItem(probeKey);
|
|
13211
|
+
localStorage.removeItem(probeKey);
|
|
13212
|
+
if (val === "1") return createLocalStorageBackend();
|
|
13213
|
+
}
|
|
13197
13214
|
} catch {}
|
|
13198
13215
|
return createMemoryBackend();
|
|
13199
13216
|
}
|
|
@@ -13402,7 +13419,11 @@ function deepClone(value, options = {}) {
|
|
|
13402
13419
|
}
|
|
13403
13420
|
function cloneBuiltin(val) {
|
|
13404
13421
|
if (val instanceof Date) return keep(val, new Date(val.getTime()));
|
|
13405
|
-
if (val instanceof RegExp)
|
|
13422
|
+
if (val instanceof RegExp) {
|
|
13423
|
+
const copy = new RegExp(val.source, val.flags);
|
|
13424
|
+
copy.lastIndex = val.lastIndex;
|
|
13425
|
+
return keep(val, copy);
|
|
13426
|
+
}
|
|
13406
13427
|
if (val instanceof ArrayBuffer) return keep(val, val.slice(0));
|
|
13407
13428
|
if (ArrayBuffer.isView(val)) {
|
|
13408
13429
|
const buffer = clone(val.buffer);
|
|
@@ -13585,7 +13606,7 @@ function equalBytes(a, b) {
|
|
|
13585
13606
|
/** Equality of the content of atomic built-ins (see isAtomic) but Map/Set. */
|
|
13586
13607
|
function equalBuiltin(a, b, assumed) {
|
|
13587
13608
|
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
13588
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
13609
|
+
if (a instanceof RegExp) return String(a) === String(b) && a.lastIndex === b.lastIndex;
|
|
13589
13610
|
if (ArrayBuffer.isView(a)) {
|
|
13590
13611
|
const v = b;
|
|
13591
13612
|
return a.byteOffset === v.byteOffset && a.byteLength === v.byteLength && equal(a.buffer, v.buffer, assumed);
|
|
@@ -14144,15 +14165,17 @@ var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(i
|
|
|
14144
14165
|
var startNewPlaythrough = queued(startNewPlaythroughNow);
|
|
14145
14166
|
async function startNewPlaythroughNow(ifid, id = randomUUID()) {
|
|
14146
14167
|
const backend = await getBackend();
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14168
|
+
await withTabLock(`spindle-playthrough-count:${ifid}`, async () => {
|
|
14169
|
+
const num = await nextPlaythroughNumber(ifid);
|
|
14170
|
+
const record = {
|
|
14171
|
+
id,
|
|
14172
|
+
ifid,
|
|
14173
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14174
|
+
label: `Playthrough ${num}`
|
|
14175
|
+
};
|
|
14176
|
+
await backend.putPlaythrough(record);
|
|
14177
|
+
await backend.setMeta(playthroughCountKey(ifid), num);
|
|
14178
|
+
});
|
|
14156
14179
|
await backend.setMeta(currentPlaythroughKey(ifid), id);
|
|
14157
14180
|
return id;
|
|
14158
14181
|
}
|
|
@@ -14405,12 +14428,41 @@ function slotMetaKey(ifid, slot) {
|
|
|
14405
14428
|
return isNamedSlot(slot) ? `${SLOT_KEY_PREFIX}${slot}.${ifid}` : `${AUTOSAVE_KEY_PREFIX}${ifid}`;
|
|
14406
14429
|
}
|
|
14407
14430
|
var slotIndexKey = (ifid) => `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
14408
|
-
/**
|
|
14431
|
+
/**
|
|
14432
|
+
* Run `op` holding the browser's lock `name`, so other tabs of the story
|
|
14433
|
+
* (the operation queue only orders this document's operations) wait for it.
|
|
14434
|
+
* Runs `op` without a lock where the browser offers none (or refuses it, as
|
|
14435
|
+
* in a sandboxed frame, which has no other tabs sharing its storage).
|
|
14436
|
+
*/
|
|
14437
|
+
async function withTabLock(name, op) {
|
|
14438
|
+
let locks;
|
|
14439
|
+
try {
|
|
14440
|
+
locks = navigator.locks;
|
|
14441
|
+
} catch {}
|
|
14442
|
+
if (!locks) return op();
|
|
14443
|
+
let started = false;
|
|
14444
|
+
try {
|
|
14445
|
+
return await locks.request(name, () => {
|
|
14446
|
+
started = true;
|
|
14447
|
+
return op();
|
|
14448
|
+
});
|
|
14449
|
+
} catch (err) {
|
|
14450
|
+
if (started) throw err;
|
|
14451
|
+
return op();
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
/**
|
|
14455
|
+
* Read-modify-write the per-story slot index. Under a lock shared by the
|
|
14456
|
+
* story's tabs: two tabs adding a slot at once would both read the old index
|
|
14457
|
+
* and the later write would drop the other's slot.
|
|
14458
|
+
*/
|
|
14409
14459
|
async function updateSlotIndex(ifid, update) {
|
|
14410
14460
|
const backend = await getBackend();
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14461
|
+
await withTabLock(`spindle-slot-index:${ifid}`, async () => {
|
|
14462
|
+
const existing = await backend.getMeta(slotIndexKey(ifid)) ?? [];
|
|
14463
|
+
const updated = update(existing);
|
|
14464
|
+
if (updated !== existing) await backend.setMeta(slotIndexKey(ifid), updated);
|
|
14465
|
+
});
|
|
14414
14466
|
}
|
|
14415
14467
|
/** Every slot: the default one (undefined), then those in the slot index. */
|
|
14416
14468
|
async function allSlots(ifid) {
|
|
@@ -14463,7 +14515,7 @@ function toSaveInfo(record, slot) {
|
|
|
14463
14515
|
* (settled by operations issued before this call), so the save takes its
|
|
14464
14516
|
* place in the order of operations when it is called.
|
|
14465
14517
|
*/
|
|
14466
|
-
function quickSave(ifid, playthroughId, payload, slot, custom) {
|
|
14518
|
+
function quickSave$1(ifid, playthroughId, payload, slot, custom) {
|
|
14467
14519
|
return inOrder(async () => {
|
|
14468
14520
|
const ptId = await playthroughId;
|
|
14469
14521
|
const existingId = await slotSaveId(ifid, slot);
|
|
@@ -14549,12 +14601,18 @@ var SESSION_KEY_PREFIX = "spindle.session.";
|
|
|
14549
14601
|
* encodePayload). Throws, like a save, when the state holds a value a save
|
|
14550
14602
|
* cannot (a function, an instance of an unregistered class, a unique
|
|
14551
14603
|
* symbol); the session then keeps its previous copy.
|
|
14604
|
+
*
|
|
14605
|
+
* A storage that is unavailable or full is returned, not thrown: the session
|
|
14606
|
+
* keeps its previous copy then too, but the story can go on, so the caller
|
|
14607
|
+
* decides how to tell the player.
|
|
14552
14608
|
*/
|
|
14553
14609
|
function saveSession(ifid, payload) {
|
|
14554
14610
|
const text = JSON.stringify(encodePayload(payload));
|
|
14555
14611
|
try {
|
|
14556
14612
|
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, text);
|
|
14557
|
-
} catch {
|
|
14613
|
+
} catch (error) {
|
|
14614
|
+
return { error };
|
|
14615
|
+
}
|
|
14558
14616
|
}
|
|
14559
14617
|
/**
|
|
14560
14618
|
* The live payload of the stored session, if there is one that loads. A
|
|
@@ -14671,14 +14729,14 @@ var clearAllData = queued(clearAllDataNow);
|
|
|
14671
14729
|
async function clearAllDataNow() {
|
|
14672
14730
|
await (await getBackend()).destroy();
|
|
14673
14731
|
resetBackend();
|
|
14674
|
-
|
|
14732
|
+
try {
|
|
14675
14733
|
const toRemove = [];
|
|
14676
14734
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
14677
14735
|
const key = sessionStorage.key(i);
|
|
14678
14736
|
if (key?.startsWith("spindle.session.")) toRemove.push(key);
|
|
14679
14737
|
}
|
|
14680
14738
|
for (const key of toRemove) sessionStorage.removeItem(key);
|
|
14681
|
-
}
|
|
14739
|
+
} catch {}
|
|
14682
14740
|
}
|
|
14683
14741
|
/**
|
|
14684
14742
|
* Delete a playthrough with its saves, emptying the slots that held them.
|
|
@@ -14751,6 +14809,11 @@ function executeStoryInit() {
|
|
|
14751
14809
|
*/
|
|
14752
14810
|
function initializeStory(session) {
|
|
14753
14811
|
executeStoryInit();
|
|
14812
|
+
const missing = session ? missingPassage(useStoryStore.getState().storyData, session) : void 0;
|
|
14813
|
+
if (missing !== void 0) {
|
|
14814
|
+
console.warn(`spindle: discarding the saved session: the story no longer has the passage "${missing}"`);
|
|
14815
|
+
session = void 0;
|
|
14816
|
+
}
|
|
14754
14817
|
if (session) useStoryStore.getState().loadFromPayload(session);
|
|
14755
14818
|
emit("storyinit");
|
|
14756
14819
|
if (!session) recordStoryInitState();
|
|
@@ -15244,6 +15307,27 @@ function addMacroTrigger(condition, options) {
|
|
|
15244
15307
|
if (triggers.some((t) => t.macroKey === macroKey)) return;
|
|
15245
15308
|
registerTrigger(condition, options, macroKey);
|
|
15246
15309
|
}
|
|
15310
|
+
/**
|
|
15311
|
+
* The {watch} macro watchers now registered, for a save or the session:
|
|
15312
|
+
* watchers outlive the passage that declared them, so a game restored
|
|
15313
|
+
* where it stopped needs the ones its earlier passages registered (and
|
|
15314
|
+
* not the ones {unwatch} or `once` removed).
|
|
15315
|
+
*/
|
|
15316
|
+
function savedMacroWatchers() {
|
|
15317
|
+
return triggers.filter((t) => t.macroKey !== void 0 && t.options).map((t) => ({
|
|
15318
|
+
condition: t.condition,
|
|
15319
|
+
options: { ...t.options }
|
|
15320
|
+
}));
|
|
15321
|
+
}
|
|
15322
|
+
/**
|
|
15323
|
+
* Make the {watch} macro watchers those of a loaded game: the ones of
|
|
15324
|
+
* `watchers` replace the registered ones (watchers added by Story.watch
|
|
15325
|
+
* code stay). The passage shown mounts its own {watch} macros again.
|
|
15326
|
+
*/
|
|
15327
|
+
function restoreMacroWatchers(watchers) {
|
|
15328
|
+
triggers = triggers.filter((t) => t.macroKey === void 0);
|
|
15329
|
+
for (const { condition, options } of watchers) addMacroTrigger(condition, options);
|
|
15330
|
+
}
|
|
15247
15331
|
function removeTrigger(name) {
|
|
15248
15332
|
triggers = triggers.filter((t) => t.name !== name);
|
|
15249
15333
|
}
|
|
@@ -15584,7 +15668,7 @@ var sessionMoments = [];
|
|
|
15584
15668
|
*/
|
|
15585
15669
|
function persistSession(get) {
|
|
15586
15670
|
const { storyData, currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
15587
|
-
if (!storyData) return;
|
|
15671
|
+
if (!storyData) return void 0;
|
|
15588
15672
|
if (sessionMoments.length > history.length) sessionMoments.length = history.length;
|
|
15589
15673
|
if (sessionMoments.length < history.length) if (history.length - sessionMoments.length === 1) {
|
|
15590
15674
|
const i = history.length - 1;
|
|
@@ -15592,14 +15676,15 @@ function persistSession(get) {
|
|
|
15592
15676
|
} else forEachRecorded(history.length, (vars, i) => {
|
|
15593
15677
|
if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
|
|
15594
15678
|
});
|
|
15595
|
-
saveSession(storyData.ifid, {
|
|
15679
|
+
return saveSession(storyData.ifid, {
|
|
15596
15680
|
passage: currentPassage,
|
|
15597
15681
|
variables,
|
|
15598
15682
|
history: sessionMoments,
|
|
15599
15683
|
historyIndex,
|
|
15600
15684
|
visitCounts,
|
|
15601
15685
|
renderCounts,
|
|
15602
|
-
prng: snapshotPRNG()
|
|
15686
|
+
prng: snapshotPRNG(),
|
|
15687
|
+
watchers: savedMacroWatchers()
|
|
15603
15688
|
});
|
|
15604
15689
|
}
|
|
15605
15690
|
/** What the page shows before a session write error (see RuntimeErrors). */
|
|
@@ -15618,7 +15703,8 @@ var NAVIGATION_ERROR_CONTEXT = "The story could not go to another passage:";
|
|
|
15618
15703
|
function persistSessionThen(get, after) {
|
|
15619
15704
|
let failure;
|
|
15620
15705
|
try {
|
|
15621
|
-
persistSession(get);
|
|
15706
|
+
const storageFailure = persistSession(get);
|
|
15707
|
+
if (storageFailure) showRuntimeError(SESSION_ERROR_CONTEXT, storageFailure.error);
|
|
15622
15708
|
} catch (error) {
|
|
15623
15709
|
failure = { error };
|
|
15624
15710
|
showRuntimeError(SESSION_ERROR_CONTEXT, error);
|
|
@@ -15983,6 +16069,15 @@ function restorePRNGFrom(prng) {
|
|
|
15983
16069
|
function restorePRNGFromMoment(moment) {
|
|
15984
16070
|
if (moment) restorePRNGFrom(moment.prng);
|
|
15985
16071
|
}
|
|
16072
|
+
/**
|
|
16073
|
+
* The first passage of `payload` (its current passage, then its history)
|
|
16074
|
+
* that `storyData` does not have, as when the story was updated since the
|
|
16075
|
+
* save; undefined if the story has them all (or is not loaded).
|
|
16076
|
+
*/
|
|
16077
|
+
function missingPassage(storyData, payload) {
|
|
16078
|
+
if (!storyData) return void 0;
|
|
16079
|
+
return [payload.passage, ...payload.history.map((m) => m.passage)].find((name) => !storyData.passages.has(name));
|
|
16080
|
+
}
|
|
15986
16081
|
var NAMESPACE_KEYS = [
|
|
15987
16082
|
"variables",
|
|
15988
16083
|
"temporary",
|
|
@@ -16312,7 +16407,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16312
16407
|
set((state) => {
|
|
16313
16408
|
state.saveError = null;
|
|
16314
16409
|
});
|
|
16315
|
-
await quickSave(storyData.ifid, playthrough, payload, slot, custom);
|
|
16410
|
+
await quickSave$1(storyData.ifid, playthrough, payload, slot, custom);
|
|
16316
16411
|
recordKnownSave(set, slot, true);
|
|
16317
16412
|
}), "spindle: failed to save", (err) => set((state) => {
|
|
16318
16413
|
state.saveError = errorMessage(err, "Failed to save");
|
|
@@ -16410,7 +16505,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16410
16505
|
historyIndex,
|
|
16411
16506
|
visitCounts: { ...visitCounts },
|
|
16412
16507
|
renderCounts: { ...renderCounts },
|
|
16413
|
-
prng: snapshotPRNG()
|
|
16508
|
+
prng: snapshotPRNG(),
|
|
16509
|
+
watchers: savedMacroWatchers()
|
|
16414
16510
|
};
|
|
16415
16511
|
},
|
|
16416
16512
|
beginSave: () => {
|
|
@@ -16422,12 +16518,15 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16422
16518
|
};
|
|
16423
16519
|
},
|
|
16424
16520
|
loadFromPayload: (payload, slot, playthroughId) => {
|
|
16425
|
-
const
|
|
16521
|
+
const applying = slotLoadApplying;
|
|
16426
16522
|
slotLoadApplying = null;
|
|
16523
|
+
const replacement = applying ?? ++stateReplacementsIssued;
|
|
16427
16524
|
if (payload.history.length === 0) {
|
|
16428
16525
|
console.warn("loadFromPayload: rejecting payload with empty history");
|
|
16429
16526
|
return;
|
|
16430
16527
|
}
|
|
16528
|
+
const missing = missingPassage(get().storyData, payload);
|
|
16529
|
+
if (missing !== void 0) throw new Error(`The save refers to the passage "${missing}", which this version of the story does not have`);
|
|
16431
16530
|
latestStateApplied = replacement;
|
|
16432
16531
|
emit("beforeload", slot);
|
|
16433
16532
|
const ifid = get().storyData?.ifid;
|
|
@@ -16453,6 +16552,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16453
16552
|
state.temporary = createNamespace();
|
|
16454
16553
|
state.transient = createNamespace(deepClone(get().transientDefaults));
|
|
16455
16554
|
});
|
|
16555
|
+
if (payload.watchers) restoreMacroWatchers(payload.watchers);
|
|
16456
16556
|
reinitTriggerState();
|
|
16457
16557
|
lastNavigationVars = reconstructVarsAt(get().historyIndex);
|
|
16458
16558
|
restorePRNGFrom(entry?.prng !== void 0 ? entry.prng : payload.prng);
|
|
@@ -16525,8 +16625,19 @@ var FOCUSABLE = [
|
|
|
16525
16625
|
"[contenteditable=\"true\"]",
|
|
16526
16626
|
"[tabindex]:not([tabindex=\"-1\"])"
|
|
16527
16627
|
].join(",");
|
|
16628
|
+
/**
|
|
16629
|
+
* Whether CSS leaves `el` in the tab sequence: not `visibility: hidden`
|
|
16630
|
+
* (inherited, so the computed value covers ancestors) and not inside a
|
|
16631
|
+
* `display: none` element.
|
|
16632
|
+
*/
|
|
16633
|
+
function isRendered(el) {
|
|
16634
|
+
const { visibility } = getComputedStyle(el);
|
|
16635
|
+
if (visibility === "hidden" || visibility === "collapse") return false;
|
|
16636
|
+
for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).display === "none") return false;
|
|
16637
|
+
return true;
|
|
16638
|
+
}
|
|
16528
16639
|
function focusables(root) {
|
|
16529
|
-
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]"));
|
|
16640
|
+
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]") && isRendered(el));
|
|
16530
16641
|
}
|
|
16531
16642
|
/** Open modal dialogs, innermost last. Only the topmost handles keys. */
|
|
16532
16643
|
var openModals = [];
|
|
@@ -16624,7 +16735,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16624
16735
|
}, []);
|
|
16625
16736
|
const handleBackdrop = (e) => {
|
|
16626
16737
|
if (!dismissible) return;
|
|
16627
|
-
if (e.target.
|
|
16738
|
+
if (e.target === e.currentTarget) stableOnClose();
|
|
16628
16739
|
};
|
|
16629
16740
|
const cls = panelClass ? `dialog-panel ${panelClass}` : "dialog-panel";
|
|
16630
16741
|
return /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
@@ -16639,6 +16750,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16639
16750
|
"aria-modal": "true",
|
|
16640
16751
|
tabIndex: -1,
|
|
16641
16752
|
children: [showCloseButton && /* @__PURE__ */ jsx("button", {
|
|
16753
|
+
type: "button",
|
|
16642
16754
|
class: "dialog-close",
|
|
16643
16755
|
"aria-label": "Close",
|
|
16644
16756
|
onClick: stableOnClose,
|
|
@@ -16742,18 +16854,39 @@ function RuntimeErrors() {
|
|
|
16742
16854
|
});
|
|
16743
16855
|
}
|
|
16744
16856
|
//#endregion
|
|
16857
|
+
//#region src/quick-actions.ts
|
|
16858
|
+
/**
|
|
16859
|
+
* QuickSave and QuickLoad as the built-in buttons and hotkeys perform them:
|
|
16860
|
+
* a failure is shown on the page (the Story API's save() and load() only
|
|
16861
|
+
* reject, for the code that awaits them).
|
|
16862
|
+
*/
|
|
16863
|
+
async function quickSave() {
|
|
16864
|
+
try {
|
|
16865
|
+
await useStoryStore.getState().save();
|
|
16866
|
+
} catch (error) {
|
|
16867
|
+
showRuntimeError("The game could not be saved:", error);
|
|
16868
|
+
}
|
|
16869
|
+
}
|
|
16870
|
+
async function quickLoad() {
|
|
16871
|
+
try {
|
|
16872
|
+
await useStoryStore.getState().load();
|
|
16873
|
+
} catch (error) {
|
|
16874
|
+
showRuntimeError("The saved game could not be loaded:", error);
|
|
16875
|
+
}
|
|
16876
|
+
}
|
|
16877
|
+
//#endregion
|
|
16745
16878
|
//#region src/components/App.tsx
|
|
16746
16879
|
function App() {
|
|
16747
16880
|
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
16748
16881
|
useEffect(() => {
|
|
16749
16882
|
const onKeyDown = (e) => {
|
|
16750
|
-
const { quickSaveKey, quickLoadKey
|
|
16883
|
+
const { quickSaveKey, quickLoadKey } = useStoryStore.getState();
|
|
16751
16884
|
if (quickSaveKey !== null && e.key === quickSaveKey) {
|
|
16752
16885
|
e.preventDefault();
|
|
16753
|
-
|
|
16886
|
+
quickSave();
|
|
16754
16887
|
} else if (quickLoadKey !== null && e.key === quickLoadKey) {
|
|
16755
16888
|
e.preventDefault();
|
|
16756
|
-
|
|
16889
|
+
quickLoad();
|
|
16757
16890
|
}
|
|
16758
16891
|
};
|
|
16759
16892
|
document.addEventListener("keydown", onKeyDown);
|
|
@@ -16931,7 +17064,9 @@ function storageKey() {
|
|
|
16931
17064
|
return `spindle.${useStoryStore.getState().storyData?.ifid || "unknown"}.settings`;
|
|
16932
17065
|
}
|
|
16933
17066
|
function persist() {
|
|
16934
|
-
|
|
17067
|
+
try {
|
|
17068
|
+
localStorage.setItem(storageKey(), JSON.stringify(values));
|
|
17069
|
+
} catch {}
|
|
16935
17070
|
}
|
|
16936
17071
|
function loadFromStorage() {
|
|
16937
17072
|
if (storageLoaded) return;
|
|
@@ -18215,7 +18350,13 @@ function widgetDef({ name = "", parameters = "" }) {
|
|
|
18215
18350
|
function parseWidgetDef(rawArgs) {
|
|
18216
18351
|
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
18217
18352
|
}
|
|
18218
|
-
|
|
18353
|
+
/**
|
|
18354
|
+
* A definition: its arguments (read like a {widget} macro's, see
|
|
18355
|
+
* parseWidgetDef) and its body. Macro names are not case-sensitive.
|
|
18356
|
+
*/
|
|
18357
|
+
var WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
|
|
18358
|
+
/** `{@children}`, also with selectors such as `{.highlight @children}`. */
|
|
18359
|
+
var CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
|
|
18219
18360
|
/**
|
|
18220
18361
|
* The names of the block widgets (those whose body renders `{@children}`)
|
|
18221
18362
|
* that StoryInit and the passages tagged `widget` define. They must be
|
|
@@ -18226,7 +18367,12 @@ function blockWidgetNames(passages) {
|
|
|
18226
18367
|
const names = [];
|
|
18227
18368
|
for (const passage of passages) {
|
|
18228
18369
|
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
18229
|
-
for (const match of passage.content.matchAll(
|
|
18370
|
+
for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
|
|
18371
|
+
if (!CHILDREN_PLACEHOLDER.test(match[2])) continue;
|
|
18372
|
+
try {
|
|
18373
|
+
names.push(parseWidgetDef(match[1]).name);
|
|
18374
|
+
} catch {}
|
|
18375
|
+
}
|
|
18230
18376
|
}
|
|
18231
18377
|
return names;
|
|
18232
18378
|
}
|
|
@@ -19030,7 +19176,7 @@ function keyOf(val, ancestors) {
|
|
|
19030
19176
|
const depth = ancestors.indexOf(obj);
|
|
19031
19177
|
if (depth !== -1) return `<cycle ${ancestors.length - depth}>`;
|
|
19032
19178
|
if (val instanceof Date) return `Date(${val.getTime()})`;
|
|
19033
|
-
if (val instanceof RegExp) return `RegExp(${String(val)})`;
|
|
19179
|
+
if (val instanceof RegExp) return `RegExp(${String(val)}@${val.lastIndex})`;
|
|
19034
19180
|
ancestors.push(obj);
|
|
19035
19181
|
try {
|
|
19036
19182
|
if (Array.isArray(val)) return `[${Array.from(val, (v) => keyOf(v, ancestors)).join(",")}]`;
|
|
@@ -19145,15 +19291,7 @@ defineMacro({
|
|
|
19145
19291
|
block: true,
|
|
19146
19292
|
render({ children = [] }, ctx) {
|
|
19147
19293
|
const code = ctx.collectText(children);
|
|
19148
|
-
|
|
19149
|
-
const location = ctx.sourceLocation();
|
|
19150
|
-
try {
|
|
19151
|
-
ctx.mutate(code);
|
|
19152
|
-
} catch (err) {
|
|
19153
|
-
logMacroError("do", err, location);
|
|
19154
|
-
}
|
|
19155
|
-
}, []);
|
|
19156
|
-
return null;
|
|
19294
|
+
return useRunOnce("do", "", () => ctx.mutate(code));
|
|
19157
19295
|
}
|
|
19158
19296
|
});
|
|
19159
19297
|
//#endregion
|
|
@@ -19220,7 +19358,10 @@ defineMacro({
|
|
|
19220
19358
|
* Return a function that runs a macro body once, outside the passage tree:
|
|
19221
19359
|
* it renders `children` into a detached node, so the body's macros ({set},
|
|
19222
19360
|
* {if}, {goto}, ...) fire their side effects through the normal Preact
|
|
19223
|
-
* pipeline
|
|
19361
|
+
* pipeline. Used by {button} and {link} on click. The body stays mounted
|
|
19362
|
+
* until the owning macro unmounts (its passage changes, say), so work it
|
|
19363
|
+
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
19364
|
+
* finish; the passage leaving cancels it.
|
|
19224
19365
|
*
|
|
19225
19366
|
* A detached render starts with no contexts, so the hook captures the ones
|
|
19226
19367
|
* the owning macro sees and provides them again: the locals scope (read
|
|
@@ -19241,6 +19382,10 @@ function useDetachedBody() {
|
|
|
19241
19382
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
19242
19383
|
const repeat = useContext(RepeatContext);
|
|
19243
19384
|
const closeDialog = useContext(DialogCloseContext);
|
|
19385
|
+
const mounted = useRef([]);
|
|
19386
|
+
useEffect(() => () => {
|
|
19387
|
+
for (const container of mounted.current.splice(0)) render(null, container);
|
|
19388
|
+
}, []);
|
|
19244
19389
|
const run = (children) => {
|
|
19245
19390
|
const locals = liveLocalsView(updater.getValues);
|
|
19246
19391
|
const container = document.createElement("div");
|
|
@@ -19270,7 +19415,7 @@ function useDetachedBody() {
|
|
|
19270
19415
|
})
|
|
19271
19416
|
})
|
|
19272
19417
|
}), container);
|
|
19273
|
-
|
|
19418
|
+
mounted.current.push(container);
|
|
19274
19419
|
};
|
|
19275
19420
|
return (children) => runWithCommittedMutations(() => run(children));
|
|
19276
19421
|
}
|
|
@@ -19298,6 +19443,7 @@ defineMacro({
|
|
|
19298
19443
|
perform: handleClick
|
|
19299
19444
|
});
|
|
19300
19445
|
return /* @__PURE__ */ jsx("button", {
|
|
19446
|
+
type: "button",
|
|
19301
19447
|
id: ctx.id,
|
|
19302
19448
|
class: ctx.cls,
|
|
19303
19449
|
onClick: handleClick,
|
|
@@ -19454,15 +19600,52 @@ defineMacro({
|
|
|
19454
19600
|
});
|
|
19455
19601
|
//#endregion
|
|
19456
19602
|
//#region src/components/macros/Type.tsx
|
|
19603
|
+
/**
|
|
19604
|
+
* The clip-path that shows the first `count` characters of `root`'s text in
|
|
19605
|
+
* reading order: the boxes the browser laid out for them, each wrapped line
|
|
19606
|
+
* as far as its last revealed character, as one path in the coordinates of
|
|
19607
|
+
* `inner`'s box. Undefined where there is no layout to measure.
|
|
19608
|
+
*/
|
|
19609
|
+
function revealedPath(root, inner, count) {
|
|
19610
|
+
const range = document.createRange();
|
|
19611
|
+
if (!range.getClientRects) return void 0;
|
|
19612
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
19613
|
+
let left = count;
|
|
19614
|
+
let end;
|
|
19615
|
+
for (let n = walker.nextNode(); n && left > 0; n = walker.nextNode()) {
|
|
19616
|
+
const length = n.nodeValue?.length ?? 0;
|
|
19617
|
+
end = {
|
|
19618
|
+
node: n,
|
|
19619
|
+
offset: Math.min(left, length)
|
|
19620
|
+
};
|
|
19621
|
+
left -= length;
|
|
19622
|
+
}
|
|
19623
|
+
if (!end) return "inset(0 100% 0 0)";
|
|
19624
|
+
range.setStart(root, 0);
|
|
19625
|
+
range.setEnd(end.node, end.offset);
|
|
19626
|
+
const rects = range.getClientRects();
|
|
19627
|
+
if (rects.length === 0) return void 0;
|
|
19628
|
+
const origin = inner.getBoundingClientRect();
|
|
19629
|
+
let path = "";
|
|
19630
|
+
for (const r of Array.from(rects)) {
|
|
19631
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
19632
|
+
const x = (r.left - origin.left).toFixed(2);
|
|
19633
|
+
const y = (r.top - origin.top).toFixed(2);
|
|
19634
|
+
path += `M${x} ${y}h${r.width.toFixed(2)}v${r.height.toFixed(2)}h-${r.width.toFixed(2)}z`;
|
|
19635
|
+
}
|
|
19636
|
+
return path ? `path("${path}")` : void 0;
|
|
19637
|
+
}
|
|
19457
19638
|
defineMacro({
|
|
19458
19639
|
name: "type",
|
|
19459
19640
|
block: true,
|
|
19460
19641
|
interpolate: true,
|
|
19461
19642
|
parameters: [DELAY_PARAMETER],
|
|
19462
19643
|
render({ children = [] }, ctx) {
|
|
19463
|
-
const { useState, useEffect, useRef } = ctx.hooks;
|
|
19644
|
+
const { useState, useEffect, useLayoutEffect, useRef } = ctx.hooks;
|
|
19464
19645
|
const speed = ctx.args.delay ?? 0;
|
|
19465
19646
|
const containerRef = useRef(null);
|
|
19647
|
+
const innerRef = useRef(null);
|
|
19648
|
+
const [clip, setClip] = useState();
|
|
19466
19649
|
const [totalChars, setTotalChars] = useState(0);
|
|
19467
19650
|
const [visibleChars, setVisibleChars] = useState(0);
|
|
19468
19651
|
const visibleCharsRef = useRef(0);
|
|
@@ -19485,6 +19668,22 @@ defineMacro({
|
|
|
19485
19668
|
return () => clearInterval(timer);
|
|
19486
19669
|
}, [totalChars, speed]);
|
|
19487
19670
|
const done = visibleChars >= totalChars && totalChars > 0;
|
|
19671
|
+
useLayoutEffect(() => {
|
|
19672
|
+
const root = containerRef.current;
|
|
19673
|
+
const inner = innerRef.current;
|
|
19674
|
+
if (!root || !inner || totalChars === 0 || done) {
|
|
19675
|
+
setClip(void 0);
|
|
19676
|
+
return;
|
|
19677
|
+
}
|
|
19678
|
+
const measure = () => setClip(revealedPath(root, inner, visibleChars));
|
|
19679
|
+
measure();
|
|
19680
|
+
window.addEventListener("resize", measure);
|
|
19681
|
+
return () => window.removeEventListener("resize", measure);
|
|
19682
|
+
}, [
|
|
19683
|
+
visibleChars,
|
|
19684
|
+
totalChars,
|
|
19685
|
+
done
|
|
19686
|
+
]);
|
|
19488
19687
|
const cls = [
|
|
19489
19688
|
"macro-type",
|
|
19490
19689
|
done ? "macro-type-done" : "",
|
|
@@ -19494,13 +19693,13 @@ defineMacro({
|
|
|
19494
19693
|
id: ctx.id,
|
|
19495
19694
|
class: cls,
|
|
19496
19695
|
ref: containerRef,
|
|
19497
|
-
style: { clipPath: totalChars > 0 && !done ? void 0 : void 0 },
|
|
19498
19696
|
children: [/* @__PURE__ */ jsx("span", {
|
|
19697
|
+
ref: innerRef,
|
|
19499
19698
|
class: "macro-type-inner",
|
|
19500
19699
|
style: {
|
|
19501
19700
|
display: "inline",
|
|
19502
19701
|
visibility: totalChars === 0 ? "hidden" : "visible",
|
|
19503
|
-
clipPath:
|
|
19702
|
+
clipPath: clip
|
|
19504
19703
|
},
|
|
19505
19704
|
children: ctx.renderInlineNodes(children)
|
|
19506
19705
|
}), !done && totalChars > 0 && /* @__PURE__ */ jsx("span", { class: "macro-type-cursor" })]
|
|
@@ -19548,6 +19747,7 @@ function defineMenubarAction(config) {
|
|
|
19548
19747
|
perform: config.dialog ? () => setDialogOpen(true) : setup.perform
|
|
19549
19748
|
});
|
|
19550
19749
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19750
|
+
type: "button",
|
|
19551
19751
|
id: ctx.id,
|
|
19552
19752
|
class: cls,
|
|
19553
19753
|
onClick: perform,
|
|
@@ -19587,9 +19787,9 @@ defineMenubarAction({
|
|
|
19587
19787
|
label: "QuickSave",
|
|
19588
19788
|
actionType: "save",
|
|
19589
19789
|
setup: () => {
|
|
19590
|
-
const {
|
|
19790
|
+
const { quickSaveKey } = useStoryFields("quickSaveKey");
|
|
19591
19791
|
return {
|
|
19592
|
-
perform:
|
|
19792
|
+
perform: quickSave,
|
|
19593
19793
|
title: hotkeyTitle("Quick Save", quickSaveKey)
|
|
19594
19794
|
};
|
|
19595
19795
|
}
|
|
@@ -19600,9 +19800,9 @@ defineMenubarAction({
|
|
|
19600
19800
|
actionType: "load",
|
|
19601
19801
|
confirm: "Load saved game? Current progress will be lost.",
|
|
19602
19802
|
setup: () => {
|
|
19603
|
-
const {
|
|
19803
|
+
const { hasSave, quickLoadKey } = useStoryFields("hasSave", "quickLoadKey", "knownSaves");
|
|
19604
19804
|
return {
|
|
19605
|
-
perform:
|
|
19805
|
+
perform: quickLoad,
|
|
19606
19806
|
disabled: !hasSave(),
|
|
19607
19807
|
title: hotkeyTitle("Quick Load", quickLoadKey)
|
|
19608
19808
|
};
|
|
@@ -19657,6 +19857,7 @@ defineMacro({
|
|
|
19657
19857
|
const label = ctx.resolve(ctx.args.label ?? "");
|
|
19658
19858
|
const passageName = ctx.collectText(children).trim().replace(/^["']|["']$/g, "");
|
|
19659
19859
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19860
|
+
type: "button",
|
|
19660
19861
|
id: ctx.id,
|
|
19661
19862
|
class: ctx.cls,
|
|
19662
19863
|
onClick: () => setOpen(true),
|
|
@@ -19696,15 +19897,19 @@ function SaveManagerContent() {
|
|
|
19696
19897
|
const [status, setStatus] = useState(null);
|
|
19697
19898
|
const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
|
|
19698
19899
|
const [renamingId, setRenamingId] = useState(null);
|
|
19900
|
+
const renameCancelled = useRef(false);
|
|
19699
19901
|
const [renameValue, setRenameValue] = useState("");
|
|
19700
19902
|
const renameInputRef = useRef(null);
|
|
19701
19903
|
const fileInputRef = useRef(null);
|
|
19702
19904
|
const closeDialog = useContext(DialogCloseContext);
|
|
19703
19905
|
const { storyData, playthroughId, beginSave, loadFromPayload } = useStoryFields("storyData", "playthroughId", "beginSave", "loadFromPayload");
|
|
19704
19906
|
const ifid = storyData?.ifid ?? "";
|
|
19907
|
+
const [temporary, setTemporary] = useState(false);
|
|
19705
19908
|
const refresh = useCallback(async () => {
|
|
19706
19909
|
if (!ifid) return;
|
|
19707
|
-
|
|
19910
|
+
const data = await getSavesGrouped(ifid);
|
|
19911
|
+
setTemporary(getBackendType() === "memory");
|
|
19912
|
+
setGroups(data);
|
|
19708
19913
|
setLoading(false);
|
|
19709
19914
|
}, [ifid]);
|
|
19710
19915
|
useEffect(() => {
|
|
@@ -19751,7 +19956,7 @@ function SaveManagerContent() {
|
|
|
19751
19956
|
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
19752
19957
|
return createSave(ifid, playthrough, payload);
|
|
19753
19958
|
});
|
|
19754
|
-
showStatus("Save created");
|
|
19959
|
+
showStatus(temporary ? "Save created (temporary)" : "Save created");
|
|
19755
19960
|
await refresh();
|
|
19756
19961
|
} catch {
|
|
19757
19962
|
showStatus("Failed to create save", "error");
|
|
@@ -19794,6 +19999,7 @@ function SaveManagerContent() {
|
|
|
19794
19999
|
}
|
|
19795
20000
|
};
|
|
19796
20001
|
const handleRenameStart = (save) => {
|
|
20002
|
+
renameCancelled.current = false;
|
|
19797
20003
|
setRenamingId(save.meta.id);
|
|
19798
20004
|
setRenameValue(save.meta.title);
|
|
19799
20005
|
};
|
|
@@ -19810,7 +20016,15 @@ function SaveManagerContent() {
|
|
|
19810
20016
|
};
|
|
19811
20017
|
const handleRenameKeyDown = (e) => {
|
|
19812
20018
|
if (e.key === "Enter") handleRenameConfirm();
|
|
19813
|
-
else if (e.key === "Escape")
|
|
20019
|
+
else if (e.key === "Escape") {
|
|
20020
|
+
e.preventDefault();
|
|
20021
|
+
e.stopPropagation();
|
|
20022
|
+
renameCancelled.current = true;
|
|
20023
|
+
setRenamingId(null);
|
|
20024
|
+
}
|
|
20025
|
+
};
|
|
20026
|
+
const handleRenameBlur = () => {
|
|
20027
|
+
if (!renameCancelled.current) handleRenameConfirm();
|
|
19814
20028
|
};
|
|
19815
20029
|
const handleExport = async (saveId) => {
|
|
19816
20030
|
try {
|
|
@@ -19858,10 +20072,12 @@ function SaveManagerContent() {
|
|
|
19858
20072
|
children: /* @__PURE__ */ jsxs("div", {
|
|
19859
20073
|
class: "saves-mode-toggle",
|
|
19860
20074
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20075
|
+
type: "button",
|
|
19861
20076
|
class: mode === "save" ? "active" : "",
|
|
19862
20077
|
onClick: () => setMode("save"),
|
|
19863
20078
|
children: "Save"
|
|
19864
20079
|
}), /* @__PURE__ */ jsx("button", {
|
|
20080
|
+
type: "button",
|
|
19865
20081
|
class: mode === "load" ? "active" : "",
|
|
19866
20082
|
onClick: () => setMode("load"),
|
|
19867
20083
|
children: "Load"
|
|
@@ -19869,9 +20085,15 @@ function SaveManagerContent() {
|
|
|
19869
20085
|
})
|
|
19870
20086
|
})
|
|
19871
20087
|
}),
|
|
20088
|
+
temporary && /* @__PURE__ */ jsx("div", {
|
|
20089
|
+
class: "saves-notice",
|
|
20090
|
+
role: "status",
|
|
20091
|
+
children: "Saves can not be kept in this browser: they are lost when the page is reloaded or closed. Export a save to keep it."
|
|
20092
|
+
}),
|
|
19872
20093
|
/* @__PURE__ */ jsxs("div", {
|
|
19873
20094
|
class: "saves-toolbar",
|
|
19874
20095
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20096
|
+
type: "button",
|
|
19875
20097
|
class: "saves-toolbar-button",
|
|
19876
20098
|
onClick: handleImport,
|
|
19877
20099
|
children: "Import"
|
|
@@ -19927,7 +20149,7 @@ function SaveManagerContent() {
|
|
|
19927
20149
|
value: renameValue,
|
|
19928
20150
|
onInput: (e) => setRenameValue(e.target.value),
|
|
19929
20151
|
onKeyDown: handleRenameKeyDown,
|
|
19930
|
-
onBlur:
|
|
20152
|
+
onBlur: handleRenameBlur
|
|
19931
20153
|
}) : /* @__PURE__ */ jsx("div", {
|
|
19932
20154
|
class: "save-slot-title",
|
|
19933
20155
|
children: save.meta.title
|
|
@@ -19939,25 +20161,30 @@ function SaveManagerContent() {
|
|
|
19939
20161
|
class: "save-slot-actions",
|
|
19940
20162
|
children: [
|
|
19941
20163
|
mode === "save" ? /* @__PURE__ */ jsx("button", {
|
|
20164
|
+
type: "button",
|
|
19942
20165
|
class: "save-slot-action primary",
|
|
19943
20166
|
onClick: () => handleOverwrite(save.meta.id),
|
|
19944
20167
|
children: "Save Here"
|
|
19945
20168
|
}) : /* @__PURE__ */ jsx("button", {
|
|
20169
|
+
type: "button",
|
|
19946
20170
|
class: "save-slot-action primary",
|
|
19947
20171
|
onClick: () => handleLoad(save),
|
|
19948
20172
|
children: "Load"
|
|
19949
20173
|
}),
|
|
19950
20174
|
/* @__PURE__ */ jsx("button", {
|
|
20175
|
+
type: "button",
|
|
19951
20176
|
class: "save-slot-action",
|
|
19952
20177
|
onClick: () => handleRenameStart(save),
|
|
19953
20178
|
children: "Rename"
|
|
19954
20179
|
}),
|
|
19955
20180
|
/* @__PURE__ */ jsx("button", {
|
|
20181
|
+
type: "button",
|
|
19956
20182
|
class: "save-slot-action",
|
|
19957
20183
|
onClick: () => handleExport(save.meta.id),
|
|
19958
20184
|
children: "Export"
|
|
19959
20185
|
}),
|
|
19960
20186
|
/* @__PURE__ */ jsx("button", {
|
|
20187
|
+
type: "button",
|
|
19961
20188
|
class: "save-slot-action danger",
|
|
19962
20189
|
onClick: () => handleDelete(save.meta.id),
|
|
19963
20190
|
children: "Delete"
|
|
@@ -19965,6 +20192,7 @@ function SaveManagerContent() {
|
|
|
19965
20192
|
]
|
|
19966
20193
|
})]
|
|
19967
20194
|
}, save.meta.id)), mode === "save" && isCurrentPt && /* @__PURE__ */ jsx("button", {
|
|
20195
|
+
type: "button",
|
|
19968
20196
|
class: "save-slot-new",
|
|
19969
20197
|
onClick: handleNewSave,
|
|
19970
20198
|
children: "+ New Save"
|
|
@@ -19976,6 +20204,7 @@ function SaveManagerContent() {
|
|
|
19976
20204
|
children: /* @__PURE__ */ jsx("div", {
|
|
19977
20205
|
class: "playthrough-saves",
|
|
19978
20206
|
children: /* @__PURE__ */ jsx("button", {
|
|
20207
|
+
type: "button",
|
|
19979
20208
|
class: "save-slot-new",
|
|
19980
20209
|
onClick: handleNewSave,
|
|
19981
20210
|
children: "+ New Save"
|
|
@@ -20347,6 +20576,7 @@ defineMacro({
|
|
|
20347
20576
|
}
|
|
20348
20577
|
});
|
|
20349
20578
|
return /* @__PURE__ */ jsx("button", {
|
|
20579
|
+
type: "button",
|
|
20350
20580
|
id: ctx.id,
|
|
20351
20581
|
class: ctx.cls,
|
|
20352
20582
|
onClick: handleClick,
|
|
@@ -20521,7 +20751,7 @@ function resolveTransition(targetTags, nextTransition, storeDefault) {
|
|
|
20521
20751
|
/**
|
|
20522
20752
|
* Create a snapshot clone of a .passage element for outgoing transitions.
|
|
20523
20753
|
* - Strips `id` attributes from the clone tree
|
|
20524
|
-
* - Disables interaction (pointer-events, user-select)
|
|
20754
|
+
* - Disables interaction (pointer-events, user-select, inert, aria-hidden)
|
|
20525
20755
|
* - Pauses/mutes any <audio>/<video>
|
|
20526
20756
|
* - Adds `.passage-snapshot` class and `data-transition` attribute
|
|
20527
20757
|
*/
|
|
@@ -20532,6 +20762,8 @@ function createSnapshot(passageEl, transitionType) {
|
|
|
20532
20762
|
for (let i = 0; i < idEls.length; i++) idEls[i].removeAttribute("id");
|
|
20533
20763
|
clone.style.pointerEvents = "none";
|
|
20534
20764
|
clone.style.userSelect = "none";
|
|
20765
|
+
clone.setAttribute("inert", "");
|
|
20766
|
+
clone.setAttribute("aria-hidden", "true");
|
|
20535
20767
|
const mediaEls = clone.querySelectorAll("audio, video");
|
|
20536
20768
|
for (let i = 0; i < mediaEls.length; i++) {
|
|
20537
20769
|
const media = mediaEls[i];
|
|
@@ -20570,6 +20802,19 @@ function cleanupSnapshots(containerEl) {
|
|
|
20570
20802
|
const snapshots = containerEl.querySelectorAll(".passage-snapshot");
|
|
20571
20803
|
for (let i = 0; i < snapshots.length; i++) snapshots[i].remove();
|
|
20572
20804
|
}
|
|
20805
|
+
/**
|
|
20806
|
+
* `config` for players who prefer reduced motion: the CSS shortens the
|
|
20807
|
+
* animations (see styles.css), so the waits that follow them shrink too,
|
|
20808
|
+
* leaving no empty passage area before the next passage.
|
|
20809
|
+
*/
|
|
20810
|
+
function reduceMotion(config) {
|
|
20811
|
+
if (!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return config;
|
|
20812
|
+
return {
|
|
20813
|
+
...config,
|
|
20814
|
+
duration: Math.min(config.duration, 10),
|
|
20815
|
+
pause: 0
|
|
20816
|
+
};
|
|
20817
|
+
}
|
|
20573
20818
|
defineMacro({
|
|
20574
20819
|
name: "passage",
|
|
20575
20820
|
interpolate: true,
|
|
@@ -20617,7 +20862,7 @@ defineMacro({
|
|
|
20617
20862
|
setDisplayedPassage(currentPassage);
|
|
20618
20863
|
return;
|
|
20619
20864
|
}
|
|
20620
|
-
const config = resolveTransition(targetPassage.tags, next, transitionConfig);
|
|
20865
|
+
const config = reduceMotion(resolveTransition(targetPassage.tags, next, transitionConfig));
|
|
20621
20866
|
resolvedTypeRef.current = config.type;
|
|
20622
20867
|
const containerEl = containerRef.current;
|
|
20623
20868
|
if (config.type === "none") {
|
|
@@ -20757,7 +21002,7 @@ defineMacro({
|
|
|
20757
21002
|
});
|
|
20758
21003
|
//#endregion
|
|
20759
21004
|
//#region src/styles.css?inline
|
|
20760
|
-
var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n/* Runtime errors shown on the page (see src/components/RuntimeErrors.tsx) */\n.spindle-error-banners {\n position: fixed;\n top: 0.75em;\n left: 50%;\n z-index: 300;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n width: min(42em, calc(100% - 1.5em));\n transform: translateX(-50%);\n}\n\n.spindle-error-banner {\n position: relative;\n padding: 0.75em 2.75em 0.75em 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: #2b1a1e;\n color: #ffcdd2;\n font-size: 0.9em;\n line-height: 1.5;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n}\n\n.spindle-error-banner-message {\n font-family: monospace;\n color: #ef9a9a;\n}\n\n.spindle-error-banner-dismiss {\n position: absolute;\n top: 0.5em;\n right: 0.5em;\n background: transparent;\n border: none;\n color: inherit;\n font-size: 1em;\n cursor: pointer;\n}\n\n.spindle-error-banner-dismiss:hover {\n color: #fff;\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
|
|
21005
|
+
var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n/* Runtime errors shown on the page (see src/components/RuntimeErrors.tsx) */\n.spindle-error-banners {\n position: fixed;\n top: 0.75em;\n left: 50%;\n z-index: 300;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n width: min(42em, calc(100% - 1.5em));\n transform: translateX(-50%);\n}\n\n.spindle-error-banner {\n position: relative;\n padding: 0.75em 2.75em 0.75em 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: #2b1a1e;\n color: #ffcdd2;\n font-size: 0.9em;\n line-height: 1.5;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n}\n\n.spindle-error-banner-message {\n font-family: monospace;\n color: #ef9a9a;\n}\n\n.spindle-error-banner-dismiss {\n position: absolute;\n top: 0.5em;\n right: 0.5em;\n background: transparent;\n border: none;\n color: inherit;\n font-size: 1em;\n cursor: pointer;\n}\n\n.spindle-error-banner-dismiss:hover {\n color: #fff;\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n/* Keep the first control clear of the close button above it */\n.dialog-settings .dialog-body {\n padding-top: 2.25em;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-notice {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #ffb74d;\n border-bottom: 1px solid rgba(255, 183, 77, 0.3);\n background: rgba(255, 183, 77, 0.08);\n flex-shrink: 0;\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
|
|
20761
21006
|
//#endregion
|
|
20762
21007
|
//#region src/index.tsx
|
|
20763
21008
|
function renderErrors(root, errors) {
|