@rohal12/spindle 0.59.6 → 0.59.8
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 +197 -71
- package/dist/pkg/story-variables.js +13 -2
- package/package.json +1 -1
- package/src/class-registry.ts +12 -0
- package/src/components/App.tsx +7 -9
- package/src/components/macros/Computed.tsx +3 -2
- package/src/components/macros/MenubarButtons.tsx +8 -5
- package/src/components/macros/PassageDisplay.tsx +18 -7
- package/src/components/macros/SaveManager.tsx +18 -7
- package/src/components/macros/detached-body.tsx +16 -3
- package/src/execute-mutation.ts +3 -3
- package/src/expression.ts +9 -21
- package/src/hooks/use-modal-focus.ts +15 -1
- package/src/index.tsx +22 -7
- package/src/quick-actions.ts +23 -0
- package/src/saves/save-manager.ts +23 -6
- package/src/saves/storage.ts +10 -8
- package/src/settings.ts +5 -1
- package/src/store.ts +8 -4
- package/src/structural.ts +11 -5
- package/src/styles.css +5 -0
- 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
|
/**
|
|
@@ -12575,10 +12575,18 @@ var defineData = (target, key, value) => {
|
|
|
12575
12575
|
});
|
|
12576
12576
|
};
|
|
12577
12577
|
/**
|
|
12578
|
+
* A reducer extracts string keys, so devalue never sees the symbol keys of
|
|
12579
|
+
* the original object: refuse them here, as it does for a plain object.
|
|
12580
|
+
*/
|
|
12581
|
+
function refuseSymbolKeys(value) {
|
|
12582
|
+
if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError("spindle: Cannot save an object with symbol keys");
|
|
12583
|
+
}
|
|
12584
|
+
/**
|
|
12578
12585
|
* Own enumerable keys of `value`, as a plain object; for an error also its
|
|
12579
12586
|
* message, cause and (AggregateError) errors, which are not enumerable.
|
|
12580
12587
|
*/
|
|
12581
12588
|
function ownData(value) {
|
|
12589
|
+
refuseSymbolKeys(value);
|
|
12582
12590
|
const data = {};
|
|
12583
12591
|
if (value instanceof Error) {
|
|
12584
12592
|
for (const key of ERROR_HIDDEN_KEYS$1) if (hasOwn(value, key)) defineData(data, key, value[key]);
|
|
@@ -12605,6 +12613,7 @@ function reducers() {
|
|
|
12605
12613
|
if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
|
|
12606
12614
|
const keys = Object.keys(v);
|
|
12607
12615
|
if (!keys.some(isEscapedOneCharKey)) return false;
|
|
12616
|
+
refuseSymbolKeys(v);
|
|
12608
12617
|
if (keys.includes("__proto__")) throw new TypeError("spindle: Cannot save a property named \"__proto__\"");
|
|
12609
12618
|
return keys.flatMap((k) => [k, v[k]]);
|
|
12610
12619
|
};
|
|
@@ -13167,24 +13176,28 @@ function createIDBBackend() {
|
|
|
13167
13176
|
}
|
|
13168
13177
|
var _backend = null;
|
|
13169
13178
|
async function detectBackend() {
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
req.
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13179
|
+
try {
|
|
13180
|
+
if (typeof indexedDB !== "undefined") {
|
|
13181
|
+
await new Promise((resolve, reject) => {
|
|
13182
|
+
const req = indexedDB.open("__spindle_probe__", 1);
|
|
13183
|
+
req.onsuccess = () => {
|
|
13184
|
+
req.result.close();
|
|
13185
|
+
indexedDB.deleteDatabase("__spindle_probe__");
|
|
13186
|
+
resolve();
|
|
13187
|
+
};
|
|
13188
|
+
req.onerror = () => reject(req.error);
|
|
13189
|
+
});
|
|
13190
|
+
return createIDBBackend();
|
|
13191
|
+
}
|
|
13181
13192
|
} catch {}
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13193
|
+
try {
|
|
13194
|
+
if (typeof localStorage !== "undefined") {
|
|
13195
|
+
const probeKey = "__spindle_probe__";
|
|
13196
|
+
localStorage.setItem(probeKey, "1");
|
|
13197
|
+
const val = localStorage.getItem(probeKey);
|
|
13198
|
+
localStorage.removeItem(probeKey);
|
|
13199
|
+
if (val === "1") return createLocalStorageBackend();
|
|
13200
|
+
}
|
|
13188
13201
|
} catch {}
|
|
13189
13202
|
return createMemoryBackend();
|
|
13190
13203
|
}
|
|
@@ -13496,6 +13509,13 @@ function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Ma
|
|
|
13496
13509
|
return pairs;
|
|
13497
13510
|
}
|
|
13498
13511
|
/**
|
|
13512
|
+
* Whether `a` and `b` are equal as deepEqual() has it and also share
|
|
13513
|
+
* references at the same places (a save of one loads as the other).
|
|
13514
|
+
*/
|
|
13515
|
+
function deepEqualStrict(a, b) {
|
|
13516
|
+
return equal(a, b, strictPairs());
|
|
13517
|
+
}
|
|
13518
|
+
/**
|
|
13499
13519
|
* `curr` with each part that equals (see deepEqualStrict) the part of
|
|
13500
13520
|
* `prev` at the same place replaced by `prev`'s: the history moments of a
|
|
13501
13521
|
* save then share what did not change between them, which a save stores
|
|
@@ -13765,13 +13785,14 @@ function underChange(path, keys) {
|
|
|
13765
13785
|
}
|
|
13766
13786
|
/**
|
|
13767
13787
|
* The segment standing for the entry `index` of a Map or Set in a path, or
|
|
13768
|
-
* for the `cause` or `errors` of an Error ('c', 'e')
|
|
13788
|
+
* for the `cause` or `errors` of an Error ('c', 'e'), or for the buffer a
|
|
13789
|
+
* typed array or DataView views ('b').
|
|
13769
13790
|
*/
|
|
13770
13791
|
var entryKey = (kind, index) => `\0${kind}${index}`;
|
|
13771
13792
|
var isEntryKey = (segment) => segment.startsWith("\0");
|
|
13772
13793
|
/**
|
|
13773
|
-
* What a Map, Set or
|
|
13774
|
-
* members, `cause` and `errors
|
|
13794
|
+
* What a Map, Set, Error or view holds in place of properties (keys and
|
|
13795
|
+
* values, members, `cause` and `errors`, the buffer), each with the segment standing for it in
|
|
13775
13796
|
* a path (see entryKey); undefined for any other value.
|
|
13776
13797
|
*/
|
|
13777
13798
|
function entryChildren(value) {
|
|
@@ -13787,7 +13808,8 @@ function entryChildren(value) {
|
|
|
13787
13808
|
for (const member of value) children.push([entryKey("s", index++), member]);
|
|
13788
13809
|
} else if (value instanceof Error) {
|
|
13789
13810
|
for (const key of ["cause", "errors"]) if (hasOwn(value, key)) children.push([entryKey(key === "cause" ? "c" : "e", 0), value[key]]);
|
|
13790
|
-
} else
|
|
13811
|
+
} else if (ArrayBuffer.isView(value)) children.push([entryKey("b", 0), value.buffer]);
|
|
13812
|
+
else return;
|
|
13791
13813
|
return children;
|
|
13792
13814
|
}
|
|
13793
13815
|
/**
|
|
@@ -14245,7 +14267,9 @@ function storedRecord(meta, payload) {
|
|
|
14245
14267
|
};
|
|
14246
14268
|
}
|
|
14247
14269
|
var createSave = queued(createSaveNow);
|
|
14248
|
-
async function createSaveNow(ifid,
|
|
14270
|
+
async function createSaveNow(ifid, playthroughPromise, payload, custom = {}) {
|
|
14271
|
+
const playthroughId = await playthroughPromise;
|
|
14272
|
+
if (!playthroughId) throw new Error("No playthrough");
|
|
14249
14273
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14250
14274
|
const record = storedRecord({
|
|
14251
14275
|
id: randomUUID(),
|
|
@@ -14261,6 +14285,10 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
|
|
|
14261
14285
|
return record;
|
|
14262
14286
|
}
|
|
14263
14287
|
/**
|
|
14288
|
+
* A playthrough passed as a promise (see resolvePlaythroughId) is awaited
|
|
14289
|
+
* inside the operation, so the call takes its place in the order of
|
|
14290
|
+
* operations when it is made.
|
|
14291
|
+
*
|
|
14264
14292
|
* Replace the payload of an existing save, keeping its ID and `createdAt`.
|
|
14265
14293
|
* Pass the current `playthroughId` when the new payload comes from the
|
|
14266
14294
|
* running game: the save then holds that playthrough's state, so it is
|
|
@@ -14276,7 +14304,8 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
|
|
|
14276
14304
|
* before, so slot keys and metadata not passed again are kept.
|
|
14277
14305
|
*/
|
|
14278
14306
|
var overwriteSave = queued(overwriteSaveNow);
|
|
14279
|
-
async function overwriteSaveNow(saveId, payload, custom,
|
|
14307
|
+
async function overwriteSaveNow(saveId, payload, custom, playthroughPromise) {
|
|
14308
|
+
const playthroughId = await playthroughPromise;
|
|
14280
14309
|
const backend = await getBackend();
|
|
14281
14310
|
const existing = await backend.getSave(saveId);
|
|
14282
14311
|
if (!existing) return void 0;
|
|
@@ -14438,7 +14467,7 @@ function toSaveInfo(record, slot) {
|
|
|
14438
14467
|
* (settled by operations issued before this call), so the save takes its
|
|
14439
14468
|
* place in the order of operations when it is called.
|
|
14440
14469
|
*/
|
|
14441
|
-
function quickSave(ifid, playthroughId, payload, slot, custom) {
|
|
14470
|
+
function quickSave$1(ifid, playthroughId, payload, slot, custom) {
|
|
14442
14471
|
return inOrder(async () => {
|
|
14443
14472
|
const ptId = await playthroughId;
|
|
14444
14473
|
const existingId = await slotSaveId(ifid, slot);
|
|
@@ -14524,12 +14553,18 @@ var SESSION_KEY_PREFIX = "spindle.session.";
|
|
|
14524
14553
|
* encodePayload). Throws, like a save, when the state holds a value a save
|
|
14525
14554
|
* cannot (a function, an instance of an unregistered class, a unique
|
|
14526
14555
|
* symbol); the session then keeps its previous copy.
|
|
14556
|
+
*
|
|
14557
|
+
* A storage that is unavailable or full is returned, not thrown: the session
|
|
14558
|
+
* keeps its previous copy then too, but the story can go on, so the caller
|
|
14559
|
+
* decides how to tell the player.
|
|
14527
14560
|
*/
|
|
14528
14561
|
function saveSession(ifid, payload) {
|
|
14529
14562
|
const text = JSON.stringify(encodePayload(payload));
|
|
14530
14563
|
try {
|
|
14531
14564
|
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, text);
|
|
14532
|
-
} catch {
|
|
14565
|
+
} catch (error) {
|
|
14566
|
+
return { error };
|
|
14567
|
+
}
|
|
14533
14568
|
}
|
|
14534
14569
|
/**
|
|
14535
14570
|
* The live payload of the stored session, if there is one that loads. A
|
|
@@ -14646,14 +14681,14 @@ var clearAllData = queued(clearAllDataNow);
|
|
|
14646
14681
|
async function clearAllDataNow() {
|
|
14647
14682
|
await (await getBackend()).destroy();
|
|
14648
14683
|
resetBackend();
|
|
14649
|
-
|
|
14684
|
+
try {
|
|
14650
14685
|
const toRemove = [];
|
|
14651
14686
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
14652
14687
|
const key = sessionStorage.key(i);
|
|
14653
14688
|
if (key?.startsWith("spindle.session.")) toRemove.push(key);
|
|
14654
14689
|
}
|
|
14655
14690
|
for (const key of toRemove) sessionStorage.removeItem(key);
|
|
14656
|
-
}
|
|
14691
|
+
} catch {}
|
|
14657
14692
|
}
|
|
14658
14693
|
/**
|
|
14659
14694
|
* Delete a playthrough with its saves, emptying the slots that held them.
|
|
@@ -14983,9 +15018,18 @@ function relink(draft, work, changes) {
|
|
|
14983
15018
|
function cloneNamespaces(from) {
|
|
14984
15019
|
const seen = /* @__PURE__ */ new Map();
|
|
14985
15020
|
return {
|
|
14986
|
-
variables: deepClone(from.variables, {
|
|
14987
|
-
|
|
14988
|
-
|
|
15021
|
+
variables: deepClone(from.variables, {
|
|
15022
|
+
keepUnregistered: true,
|
|
15023
|
+
seen
|
|
15024
|
+
}),
|
|
15025
|
+
temporary: deepClone(from.temporary, {
|
|
15026
|
+
keepUnregistered: true,
|
|
15027
|
+
seen
|
|
15028
|
+
}),
|
|
15029
|
+
transient: deepClone(from.transient, {
|
|
15030
|
+
keepUnregistered: true,
|
|
15031
|
+
seen
|
|
15032
|
+
})
|
|
14989
15033
|
};
|
|
14990
15034
|
}
|
|
14991
15035
|
/** The three variable namespaces of `state`, as an object to walk. */
|
|
@@ -15550,7 +15594,7 @@ var sessionMoments = [];
|
|
|
15550
15594
|
*/
|
|
15551
15595
|
function persistSession(get) {
|
|
15552
15596
|
const { storyData, currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
15553
|
-
if (!storyData) return;
|
|
15597
|
+
if (!storyData) return void 0;
|
|
15554
15598
|
if (sessionMoments.length > history.length) sessionMoments.length = history.length;
|
|
15555
15599
|
if (sessionMoments.length < history.length) if (history.length - sessionMoments.length === 1) {
|
|
15556
15600
|
const i = history.length - 1;
|
|
@@ -15558,7 +15602,7 @@ function persistSession(get) {
|
|
|
15558
15602
|
} else forEachRecorded(history.length, (vars, i) => {
|
|
15559
15603
|
if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
|
|
15560
15604
|
});
|
|
15561
|
-
saveSession(storyData.ifid, {
|
|
15605
|
+
return saveSession(storyData.ifid, {
|
|
15562
15606
|
passage: currentPassage,
|
|
15563
15607
|
variables,
|
|
15564
15608
|
history: sessionMoments,
|
|
@@ -15584,7 +15628,8 @@ var NAVIGATION_ERROR_CONTEXT = "The story could not go to another passage:";
|
|
|
15584
15628
|
function persistSessionThen(get, after) {
|
|
15585
15629
|
let failure;
|
|
15586
15630
|
try {
|
|
15587
|
-
persistSession(get);
|
|
15631
|
+
const storageFailure = persistSession(get);
|
|
15632
|
+
if (storageFailure) showRuntimeError(SESSION_ERROR_CONTEXT, storageFailure.error);
|
|
15588
15633
|
} catch (error) {
|
|
15589
15634
|
failure = { error };
|
|
15590
15635
|
showRuntimeError(SESSION_ERROR_CONTEXT, error);
|
|
@@ -16278,7 +16323,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16278
16323
|
set((state) => {
|
|
16279
16324
|
state.saveError = null;
|
|
16280
16325
|
});
|
|
16281
|
-
await quickSave(storyData.ifid, playthrough, payload, slot, custom);
|
|
16326
|
+
await quickSave$1(storyData.ifid, playthrough, payload, slot, custom);
|
|
16282
16327
|
recordKnownSave(set, slot, true);
|
|
16283
16328
|
}), "spindle: failed to save", (err) => set((state) => {
|
|
16284
16329
|
state.saveError = errorMessage(err, "Failed to save");
|
|
@@ -16491,8 +16536,19 @@ var FOCUSABLE = [
|
|
|
16491
16536
|
"[contenteditable=\"true\"]",
|
|
16492
16537
|
"[tabindex]:not([tabindex=\"-1\"])"
|
|
16493
16538
|
].join(",");
|
|
16539
|
+
/**
|
|
16540
|
+
* Whether CSS leaves `el` in the tab sequence: not `visibility: hidden`
|
|
16541
|
+
* (inherited, so the computed value covers ancestors) and not inside a
|
|
16542
|
+
* `display: none` element.
|
|
16543
|
+
*/
|
|
16544
|
+
function isRendered(el) {
|
|
16545
|
+
const { visibility } = getComputedStyle(el);
|
|
16546
|
+
if (visibility === "hidden" || visibility === "collapse") return false;
|
|
16547
|
+
for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).display === "none") return false;
|
|
16548
|
+
return true;
|
|
16549
|
+
}
|
|
16494
16550
|
function focusables(root) {
|
|
16495
|
-
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]"));
|
|
16551
|
+
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]") && isRendered(el));
|
|
16496
16552
|
}
|
|
16497
16553
|
/** Open modal dialogs, innermost last. Only the topmost handles keys. */
|
|
16498
16554
|
var openModals = [];
|
|
@@ -16708,18 +16764,39 @@ function RuntimeErrors() {
|
|
|
16708
16764
|
});
|
|
16709
16765
|
}
|
|
16710
16766
|
//#endregion
|
|
16767
|
+
//#region src/quick-actions.ts
|
|
16768
|
+
/**
|
|
16769
|
+
* QuickSave and QuickLoad as the built-in buttons and hotkeys perform them:
|
|
16770
|
+
* a failure is shown on the page (the Story API's save() and load() only
|
|
16771
|
+
* reject, for the code that awaits them).
|
|
16772
|
+
*/
|
|
16773
|
+
async function quickSave() {
|
|
16774
|
+
try {
|
|
16775
|
+
await useStoryStore.getState().save();
|
|
16776
|
+
} catch (error) {
|
|
16777
|
+
showRuntimeError("The game could not be saved:", error);
|
|
16778
|
+
}
|
|
16779
|
+
}
|
|
16780
|
+
async function quickLoad() {
|
|
16781
|
+
try {
|
|
16782
|
+
await useStoryStore.getState().load();
|
|
16783
|
+
} catch (error) {
|
|
16784
|
+
showRuntimeError("The saved game could not be loaded:", error);
|
|
16785
|
+
}
|
|
16786
|
+
}
|
|
16787
|
+
//#endregion
|
|
16711
16788
|
//#region src/components/App.tsx
|
|
16712
16789
|
function App() {
|
|
16713
16790
|
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
16714
16791
|
useEffect(() => {
|
|
16715
16792
|
const onKeyDown = (e) => {
|
|
16716
|
-
const { quickSaveKey, quickLoadKey
|
|
16793
|
+
const { quickSaveKey, quickLoadKey } = useStoryStore.getState();
|
|
16717
16794
|
if (quickSaveKey !== null && e.key === quickSaveKey) {
|
|
16718
16795
|
e.preventDefault();
|
|
16719
|
-
|
|
16796
|
+
quickSave();
|
|
16720
16797
|
} else if (quickLoadKey !== null && e.key === quickLoadKey) {
|
|
16721
16798
|
e.preventDefault();
|
|
16722
|
-
|
|
16799
|
+
quickLoad();
|
|
16723
16800
|
}
|
|
16724
16801
|
};
|
|
16725
16802
|
document.addEventListener("keydown", onKeyDown);
|
|
@@ -16734,10 +16811,10 @@ function App() {
|
|
|
16734
16811
|
/* @__PURE__ */ jsx(TriggerDialogHost, {}),
|
|
16735
16812
|
/* @__PURE__ */ jsx(RuntimeErrors, {})
|
|
16736
16813
|
] });
|
|
16737
|
-
return
|
|
16738
|
-
value:
|
|
16814
|
+
return /* @__PURE__ */ jsx(NobrContext.Provider, {
|
|
16815
|
+
value: nobr,
|
|
16739
16816
|
children: content
|
|
16740
|
-
})
|
|
16817
|
+
});
|
|
16741
16818
|
}
|
|
16742
16819
|
//#endregion
|
|
16743
16820
|
//#region src/parser.ts
|
|
@@ -16897,7 +16974,9 @@ function storageKey() {
|
|
|
16897
16974
|
return `spindle.${useStoryStore.getState().storyData?.ifid || "unknown"}.settings`;
|
|
16898
16975
|
}
|
|
16899
16976
|
function persist() {
|
|
16900
|
-
|
|
16977
|
+
try {
|
|
16978
|
+
localStorage.setItem(storageKey(), JSON.stringify(values));
|
|
16979
|
+
} catch {}
|
|
16901
16980
|
}
|
|
16902
16981
|
function loadFromStorage() {
|
|
16903
16982
|
if (storageLoaded) return;
|
|
@@ -18181,7 +18260,13 @@ function widgetDef({ name = "", parameters = "" }) {
|
|
|
18181
18260
|
function parseWidgetDef(rawArgs) {
|
|
18182
18261
|
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
18183
18262
|
}
|
|
18184
|
-
|
|
18263
|
+
/**
|
|
18264
|
+
* A definition: its arguments (read like a {widget} macro's, see
|
|
18265
|
+
* parseWidgetDef) and its body. Macro names are not case-sensitive.
|
|
18266
|
+
*/
|
|
18267
|
+
var WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
|
|
18268
|
+
/** `{@children}`, also with selectors such as `{.highlight @children}`. */
|
|
18269
|
+
var CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
|
|
18185
18270
|
/**
|
|
18186
18271
|
* The names of the block widgets (those whose body renders `{@children}`)
|
|
18187
18272
|
* that StoryInit and the passages tagged `widget` define. They must be
|
|
@@ -18192,7 +18277,12 @@ function blockWidgetNames(passages) {
|
|
|
18192
18277
|
const names = [];
|
|
18193
18278
|
for (const passage of passages) {
|
|
18194
18279
|
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
18195
|
-
for (const match of passage.content.matchAll(
|
|
18280
|
+
for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
|
|
18281
|
+
if (!CHILDREN_PLACEHOLDER.test(match[2])) continue;
|
|
18282
|
+
try {
|
|
18283
|
+
names.push(parseWidgetDef(match[1]).name);
|
|
18284
|
+
} catch {}
|
|
18285
|
+
}
|
|
18196
18286
|
}
|
|
18197
18287
|
return names;
|
|
18198
18288
|
}
|
|
@@ -18779,7 +18869,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
|
|
|
18779
18869
|
logMacroError(`computed ${rawArgs}`, err, location);
|
|
18780
18870
|
return;
|
|
18781
18871
|
}
|
|
18782
|
-
if (!
|
|
18872
|
+
if (!deepEqualStrict(prevRef.current, newValue)) {
|
|
18783
18873
|
prevRef.current = newValue;
|
|
18784
18874
|
if (isLocal) try {
|
|
18785
18875
|
localsUpdate(name, newValue);
|
|
@@ -19186,7 +19276,10 @@ defineMacro({
|
|
|
19186
19276
|
* Return a function that runs a macro body once, outside the passage tree:
|
|
19187
19277
|
* it renders `children` into a detached node, so the body's macros ({set},
|
|
19188
19278
|
* {if}, {goto}, ...) fire their side effects through the normal Preact
|
|
19189
|
-
* pipeline
|
|
19279
|
+
* pipeline. Used by {button} and {link} on click. The body stays mounted
|
|
19280
|
+
* until the owning macro unmounts (its passage changes, say), so work it
|
|
19281
|
+
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
19282
|
+
* finish; the passage leaving cancels it.
|
|
19190
19283
|
*
|
|
19191
19284
|
* A detached render starts with no contexts, so the hook captures the ones
|
|
19192
19285
|
* the owning macro sees and provides them again: the locals scope (read
|
|
@@ -19207,6 +19300,10 @@ function useDetachedBody() {
|
|
|
19207
19300
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
19208
19301
|
const repeat = useContext(RepeatContext);
|
|
19209
19302
|
const closeDialog = useContext(DialogCloseContext);
|
|
19303
|
+
const mounted = useRef([]);
|
|
19304
|
+
useEffect(() => () => {
|
|
19305
|
+
for (const container of mounted.current.splice(0)) render(null, container);
|
|
19306
|
+
}, []);
|
|
19210
19307
|
const run = (children) => {
|
|
19211
19308
|
const locals = liveLocalsView(updater.getValues);
|
|
19212
19309
|
const container = document.createElement("div");
|
|
@@ -19236,7 +19333,7 @@ function useDetachedBody() {
|
|
|
19236
19333
|
})
|
|
19237
19334
|
})
|
|
19238
19335
|
}), container);
|
|
19239
|
-
|
|
19336
|
+
mounted.current.push(container);
|
|
19240
19337
|
};
|
|
19241
19338
|
return (children) => runWithCommittedMutations(() => run(children));
|
|
19242
19339
|
}
|
|
@@ -19553,9 +19650,9 @@ defineMenubarAction({
|
|
|
19553
19650
|
label: "QuickSave",
|
|
19554
19651
|
actionType: "save",
|
|
19555
19652
|
setup: () => {
|
|
19556
|
-
const {
|
|
19653
|
+
const { quickSaveKey } = useStoryFields("quickSaveKey");
|
|
19557
19654
|
return {
|
|
19558
|
-
perform:
|
|
19655
|
+
perform: quickSave,
|
|
19559
19656
|
title: hotkeyTitle("Quick Save", quickSaveKey)
|
|
19560
19657
|
};
|
|
19561
19658
|
}
|
|
@@ -19566,9 +19663,9 @@ defineMenubarAction({
|
|
|
19566
19663
|
actionType: "load",
|
|
19567
19664
|
confirm: "Load saved game? Current progress will be lost.",
|
|
19568
19665
|
setup: () => {
|
|
19569
|
-
const {
|
|
19666
|
+
const { hasSave, quickLoadKey } = useStoryFields("hasSave", "quickLoadKey", "knownSaves");
|
|
19570
19667
|
return {
|
|
19571
|
-
perform:
|
|
19668
|
+
perform: quickLoad,
|
|
19572
19669
|
disabled: !hasSave(),
|
|
19573
19670
|
title: hotkeyTitle("Quick Load", quickLoadKey)
|
|
19574
19671
|
};
|
|
@@ -19662,6 +19759,7 @@ function SaveManagerContent() {
|
|
|
19662
19759
|
const [status, setStatus] = useState(null);
|
|
19663
19760
|
const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
|
|
19664
19761
|
const [renamingId, setRenamingId] = useState(null);
|
|
19762
|
+
const renameCancelled = useRef(false);
|
|
19665
19763
|
const [renameValue, setRenameValue] = useState("");
|
|
19666
19764
|
const renameInputRef = useRef(null);
|
|
19667
19765
|
const fileInputRef = useRef(null);
|
|
@@ -19715,9 +19813,7 @@ function SaveManagerContent() {
|
|
|
19715
19813
|
const playthrough = resolvePlaythroughId();
|
|
19716
19814
|
try {
|
|
19717
19815
|
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
19718
|
-
|
|
19719
|
-
if (!ptId) throw new Error("No playthrough");
|
|
19720
|
-
return createSave(ifid, ptId, payload);
|
|
19816
|
+
return createSave(ifid, playthrough, payload);
|
|
19721
19817
|
});
|
|
19722
19818
|
showStatus("Save created");
|
|
19723
19819
|
await refresh();
|
|
@@ -19729,7 +19825,7 @@ function SaveManagerContent() {
|
|
|
19729
19825
|
const playthrough = resolvePlaythroughId();
|
|
19730
19826
|
try {
|
|
19731
19827
|
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
19732
|
-
if (!await overwriteSave(saveId, payload, void 0,
|
|
19828
|
+
if (!await overwriteSave(saveId, payload, void 0, playthrough)) throw new Error("Save not found");
|
|
19733
19829
|
});
|
|
19734
19830
|
showStatus("Save overwritten");
|
|
19735
19831
|
await refresh();
|
|
@@ -19762,6 +19858,7 @@ function SaveManagerContent() {
|
|
|
19762
19858
|
}
|
|
19763
19859
|
};
|
|
19764
19860
|
const handleRenameStart = (save) => {
|
|
19861
|
+
renameCancelled.current = false;
|
|
19765
19862
|
setRenamingId(save.meta.id);
|
|
19766
19863
|
setRenameValue(save.meta.title);
|
|
19767
19864
|
};
|
|
@@ -19778,7 +19875,15 @@ function SaveManagerContent() {
|
|
|
19778
19875
|
};
|
|
19779
19876
|
const handleRenameKeyDown = (e) => {
|
|
19780
19877
|
if (e.key === "Enter") handleRenameConfirm();
|
|
19781
|
-
else if (e.key === "Escape")
|
|
19878
|
+
else if (e.key === "Escape") {
|
|
19879
|
+
e.preventDefault();
|
|
19880
|
+
e.stopPropagation();
|
|
19881
|
+
renameCancelled.current = true;
|
|
19882
|
+
setRenamingId(null);
|
|
19883
|
+
}
|
|
19884
|
+
};
|
|
19885
|
+
const handleRenameBlur = () => {
|
|
19886
|
+
if (!renameCancelled.current) handleRenameConfirm();
|
|
19782
19887
|
};
|
|
19783
19888
|
const handleExport = async (saveId) => {
|
|
19784
19889
|
try {
|
|
@@ -19895,7 +20000,7 @@ function SaveManagerContent() {
|
|
|
19895
20000
|
value: renameValue,
|
|
19896
20001
|
onInput: (e) => setRenameValue(e.target.value),
|
|
19897
20002
|
onKeyDown: handleRenameKeyDown,
|
|
19898
|
-
onBlur:
|
|
20003
|
+
onBlur: handleRenameBlur
|
|
19899
20004
|
}) : /* @__PURE__ */ jsx("div", {
|
|
19900
20005
|
class: "save-slot-title",
|
|
19901
20006
|
children: save.meta.title
|
|
@@ -20538,6 +20643,19 @@ function cleanupSnapshots(containerEl) {
|
|
|
20538
20643
|
const snapshots = containerEl.querySelectorAll(".passage-snapshot");
|
|
20539
20644
|
for (let i = 0; i < snapshots.length; i++) snapshots[i].remove();
|
|
20540
20645
|
}
|
|
20646
|
+
/**
|
|
20647
|
+
* `config` for players who prefer reduced motion: the CSS shortens the
|
|
20648
|
+
* animations (see styles.css), so the waits that follow them shrink too,
|
|
20649
|
+
* leaving no empty passage area before the next passage.
|
|
20650
|
+
*/
|
|
20651
|
+
function reduceMotion(config) {
|
|
20652
|
+
if (!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return config;
|
|
20653
|
+
return {
|
|
20654
|
+
...config,
|
|
20655
|
+
duration: Math.min(config.duration, 10),
|
|
20656
|
+
pause: 0
|
|
20657
|
+
};
|
|
20658
|
+
}
|
|
20541
20659
|
defineMacro({
|
|
20542
20660
|
name: "passage",
|
|
20543
20661
|
interpolate: true,
|
|
@@ -20566,13 +20684,13 @@ defineMacro({
|
|
|
20566
20684
|
useEffect(() => {
|
|
20567
20685
|
const store = useStoryStore.getState();
|
|
20568
20686
|
const { history, historyIndex, transitionConfig } = store;
|
|
20569
|
-
const next = store.consumeNextTransition();
|
|
20570
20687
|
const prevLen = prevHistoryLenRef.current;
|
|
20571
20688
|
prevHistoryLenRef.current = history.length;
|
|
20572
20689
|
const isFirstLoad = !displayedPassage;
|
|
20573
20690
|
const isRestart = history.length === 1 && historyIndex === 0 && prevLen > 1;
|
|
20574
20691
|
const isLoad = history.length > 1 && historyIndex === history.length - 1 && prevLen > 1 && Math.abs(history.length - prevLen) > 1;
|
|
20575
20692
|
if (navigationId === displayed.id) return;
|
|
20693
|
+
const next = store.consumeNextTransition();
|
|
20576
20694
|
cancelTransition();
|
|
20577
20695
|
const targetPassage = storyData?.passages.get(currentPassage);
|
|
20578
20696
|
if (!targetPassage) {
|
|
@@ -20585,7 +20703,7 @@ defineMacro({
|
|
|
20585
20703
|
setDisplayedPassage(currentPassage);
|
|
20586
20704
|
return;
|
|
20587
20705
|
}
|
|
20588
|
-
const config = resolveTransition(targetPassage.tags, next, transitionConfig);
|
|
20706
|
+
const config = reduceMotion(resolveTransition(targetPassage.tags, next, transitionConfig));
|
|
20589
20707
|
resolvedTypeRef.current = config.type;
|
|
20590
20708
|
const containerEl = containerRef.current;
|
|
20591
20709
|
if (config.type === "none") {
|
|
@@ -20725,7 +20843,7 @@ defineMacro({
|
|
|
20725
20843
|
});
|
|
20726
20844
|
//#endregion
|
|
20727
20845
|
//#region src/styles.css?inline
|
|
20728
|
-
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";
|
|
20846
|
+
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-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";
|
|
20729
20847
|
//#endregion
|
|
20730
20848
|
//#region src/index.tsx
|
|
20731
20849
|
function renderErrors(root, errors) {
|
|
@@ -20753,6 +20871,15 @@ function stopWithErrors(errors) {
|
|
|
20753
20871
|
throw new Error(`spindle: ${errors.length} validation error(s):\n${errors.join("\n")}`);
|
|
20754
20872
|
}
|
|
20755
20873
|
/**
|
|
20874
|
+
* The undeclared variables the passages refer to, with the macros known now:
|
|
20875
|
+
* the input macros that bind a variable (registered by author JS or
|
|
20876
|
+
* StoryInit) and the roles of their parameters decide what is a reference.
|
|
20877
|
+
*/
|
|
20878
|
+
function variableErrors(storyData, schema) {
|
|
20879
|
+
const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
|
|
20880
|
+
return validatePassages(storyData.passages, schema, storeVarMacros);
|
|
20881
|
+
}
|
|
20882
|
+
/**
|
|
20756
20883
|
* The markup errors (malformed markup, unknown macros, syntax errors in
|
|
20757
20884
|
* code) of the passages for which `only` holds, with the macros and widgets
|
|
20758
20885
|
* known now.
|
|
@@ -20801,9 +20928,8 @@ function boot() {
|
|
|
20801
20928
|
throw new Error(`spindle: ${msg}`);
|
|
20802
20929
|
}
|
|
20803
20930
|
const schema = parseStoryVariables(storyVarsPassage.content);
|
|
20804
|
-
const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
|
|
20805
20931
|
for (const name of blockWidgetNames(storyData.passages.values())) registerBlockMacro(name);
|
|
20806
|
-
const errors =
|
|
20932
|
+
const errors = variableErrors(storyData, schema);
|
|
20807
20933
|
errors.push(...markupErrors(storyData, (p) => p.name === "StoryInit"));
|
|
20808
20934
|
let transientDefaults = {};
|
|
20809
20935
|
const storyTransientsPassage = storyData.passages.get("StoryTransients");
|
|
@@ -20818,7 +20944,7 @@ function boot() {
|
|
|
20818
20944
|
useStoryStore.getState().init(storyData, defaults, transientDefaults);
|
|
20819
20945
|
enterRuntimePhase();
|
|
20820
20946
|
initializeStory(loadSession(storyData.ifid));
|
|
20821
|
-
const markup = markupErrors(storyData, (p) => p.name !== "StoryInit");
|
|
20947
|
+
const markup = [...variableErrors(storyData, schema), ...markupErrors(storyData, (p) => p.name !== "StoryInit")];
|
|
20822
20948
|
if (markup.length > 0) stopWithErrors(markup);
|
|
20823
20949
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
20824
20950
|
const widgetAST = parseMarkup(passage.content);
|