@rohal12/spindle 0.59.8 → 0.59.10
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 +321 -74
- package/package.json +1 -1
- package/src/class-registry.ts +22 -1
- 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 +1 -0
- package/src/components/macros/PassageDisplay.tsx +61 -11
- package/src/components/macros/SaveManager.tsx +27 -1
- package/src/components/macros/Type.tsx +57 -8
- package/src/components/macros/WidgetInvocation.tsx +20 -1
- package/src/components/macros/detached-body.tsx +22 -16
- package/src/components/macros/input-macro.ts +18 -3
- package/src/hooks/use-merged-locals.ts +5 -2
- package/src/hooks/use-story-fields.ts +16 -1
- package/src/markup/render.tsx +9 -4
- package/src/saves/format.ts +10 -1
- package/src/saves/save-manager.ts +99 -18
- package/src/saves/types.ts +3 -0
- package/src/store.ts +44 -3
- package/src/story-init.ts +13 -1
- package/src/structural.ts +6 -2
- package/src/styles.css +9 -0
- package/src/triggers.ts +30 -0
- package/src/utils/stable-key.ts +1 -1
package/dist/pkg/headless.js
CHANGED
|
@@ -9735,13 +9735,24 @@ function tokenizeMarkup(source, options = {}) {
|
|
|
9735
9735
|
//#endregion
|
|
9736
9736
|
//#region src/hooks/use-story-fields.ts
|
|
9737
9737
|
/**
|
|
9738
|
+
* Tells the components below when to keep the state they last rendered with,
|
|
9739
|
+
* given the store's current state: an owner (a click body, see
|
|
9740
|
+
* useDetachedBody; a passage on its way out, see PassageDisplay) provides it
|
|
9741
|
+
* so that a branch skipped earlier cannot run against later state (#351,
|
|
9742
|
+
* #352). It runs inside the store's own notification, before the owner could
|
|
9743
|
+
* re-render, which is why it is a predicate and not a flag.
|
|
9744
|
+
*/
|
|
9745
|
+
var FrozenStateContext = createContext(null);
|
|
9746
|
+
/**
|
|
9738
9747
|
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
9739
9748
|
* useStoryStore() selector per field would). The result keeps its identity
|
|
9740
9749
|
* while the fields don't change, as the store's snapshot must.
|
|
9741
9750
|
*/
|
|
9742
9751
|
function useStoryFields(...keys) {
|
|
9743
9752
|
const prev = useRef();
|
|
9753
|
+
const freeze = useContext(FrozenStateContext);
|
|
9744
9754
|
return useStoryStore((s) => {
|
|
9755
|
+
if (prev.current && freeze?.(s)) return prev.current;
|
|
9745
9756
|
const next = {};
|
|
9746
9757
|
for (const key of keys) next[key] = s[key];
|
|
9747
9758
|
return shallow(prev.current, next) ? prev.current : prev.current = next;
|
|
@@ -9754,7 +9765,7 @@ function useStoryFields(...keys) {
|
|
|
9754
9765
|
* All four dicts use unprefixed keys suitable for evaluate/execute.
|
|
9755
9766
|
*/
|
|
9756
9767
|
function useMergedLocals() {
|
|
9757
|
-
const { variables, temporary, transient } = useStoryFields("variables", "temporary", "transient");
|
|
9768
|
+
const { variables, temporary, transient, renderCounts } = useStoryFields("variables", "temporary", "transient", "renderCounts");
|
|
9758
9769
|
const localsValues = useContext(LocalsValuesContext);
|
|
9759
9770
|
return useMemo(() => {
|
|
9760
9771
|
return [
|
|
@@ -9767,7 +9778,8 @@ function useMergedLocals() {
|
|
|
9767
9778
|
variables,
|
|
9768
9779
|
temporary,
|
|
9769
9780
|
localsValues,
|
|
9770
|
-
transient
|
|
9781
|
+
transient,
|
|
9782
|
+
renderCounts
|
|
9771
9783
|
]);
|
|
9772
9784
|
}
|
|
9773
9785
|
//#endregion
|
|
@@ -10887,7 +10899,17 @@ function LocalsScope({ body, parentValues, ownKeys }) {
|
|
|
10887
10899
|
}
|
|
10888
10900
|
//#endregion
|
|
10889
10901
|
//#region src/components/macros/WidgetInvocation.tsx
|
|
10890
|
-
function WidgetInvocation(
|
|
10902
|
+
function WidgetInvocation(props) {
|
|
10903
|
+
const { className, id } = props;
|
|
10904
|
+
const content = /* @__PURE__ */ jsx(WidgetContent, { ...props });
|
|
10905
|
+
if (!className && !id) return content;
|
|
10906
|
+
return /* @__PURE__ */ jsx("span", {
|
|
10907
|
+
class: className,
|
|
10908
|
+
id,
|
|
10909
|
+
children: content
|
|
10910
|
+
});
|
|
10911
|
+
}
|
|
10912
|
+
function WidgetContent({ body, params, rawArgs, invocationChildren, className, id }) {
|
|
10891
10913
|
const renderOptions = useRenderOptions();
|
|
10892
10914
|
const parentValues = renderOptions.locals;
|
|
10893
10915
|
const [mergedVars, mergedTemps, mergedLocals, mergedTrans] = useMergedLocals();
|
|
@@ -11143,7 +11165,7 @@ function useRunOnce(macro, rawArgs, effect) {
|
|
|
11143
11165
|
effect();
|
|
11144
11166
|
} catch (error) {
|
|
11145
11167
|
failure.current = { error };
|
|
11146
|
-
logMacroError(`${macro} ${rawArgs}
|
|
11168
|
+
logMacroError(`${macro} ${rawArgs}`.trim(), error, location);
|
|
11147
11169
|
}
|
|
11148
11170
|
}
|
|
11149
11171
|
if (!failure.current) return null;
|
|
@@ -11643,18 +11665,21 @@ function nodeKey(node) {
|
|
|
11643
11665
|
}
|
|
11644
11666
|
function renderMacro(node, key) {
|
|
11645
11667
|
if (isSubMacro(node.name)) return null;
|
|
11668
|
+
const invocation = {
|
|
11669
|
+
rawArgs: node.rawArgs,
|
|
11670
|
+
className: node.className,
|
|
11671
|
+
id: node.id
|
|
11672
|
+
};
|
|
11646
11673
|
const widget = getWidget(node.name);
|
|
11647
11674
|
if (widget) return /* @__PURE__ */ jsx(WidgetInvocation, {
|
|
11675
|
+
...invocation,
|
|
11648
11676
|
body: widget.body,
|
|
11649
11677
|
params: widget.params,
|
|
11650
|
-
rawArgs: node.rawArgs,
|
|
11651
11678
|
invocationChildren: node.children
|
|
11652
11679
|
}, key);
|
|
11653
11680
|
const Component = getMacro(node.name);
|
|
11654
11681
|
if (Component) return /* @__PURE__ */ jsx(Component, {
|
|
11655
|
-
|
|
11656
|
-
className: node.className,
|
|
11657
|
-
id: node.id,
|
|
11682
|
+
...invocation,
|
|
11658
11683
|
children: node.children,
|
|
11659
11684
|
branches: node.branches
|
|
11660
11685
|
}, key);
|
|
@@ -12548,6 +12573,7 @@ var CLASS_PREFIX = "c:";
|
|
|
12548
12573
|
var ERROR_PREFIX = "E:";
|
|
12549
12574
|
var SYMBOL_TAG = "S";
|
|
12550
12575
|
var KEYS_TAG = "K";
|
|
12576
|
+
var REGEXP_TAG = "R";
|
|
12551
12577
|
var BUILTIN_ERRORS = new Map([
|
|
12552
12578
|
Error,
|
|
12553
12579
|
EvalError,
|
|
@@ -12609,6 +12635,11 @@ function reducers() {
|
|
|
12609
12635
|
const key = Symbol.keyFor(v);
|
|
12610
12636
|
return key !== void 0 && [key];
|
|
12611
12637
|
};
|
|
12638
|
+
out[REGEXP_TAG] = (v) => v instanceof RegExp && v.lastIndex !== 0 && [
|
|
12639
|
+
v.source,
|
|
12640
|
+
v.flags,
|
|
12641
|
+
v.lastIndex
|
|
12642
|
+
];
|
|
12612
12643
|
out[KEYS_TAG] = (v) => {
|
|
12613
12644
|
if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
|
|
12614
12645
|
const keys = Object.keys(v);
|
|
@@ -12682,6 +12713,12 @@ function reviverFor(key) {
|
|
|
12682
12713
|
return Symbol.for(data[0]);
|
|
12683
12714
|
};
|
|
12684
12715
|
if (key === KEYS_TAG) return reviveKeyed;
|
|
12716
|
+
if (key === REGEXP_TAG) return (data) => {
|
|
12717
|
+
if (!Array.isArray(data) || typeof data[0] !== "string" || typeof data[1] !== "string" || !Number.isSafeInteger(data[2])) throw new TypeError("spindle: Malformed regular expression");
|
|
12718
|
+
const re = new RegExp(data[0], data[1]);
|
|
12719
|
+
re.lastIndex = data[2];
|
|
12720
|
+
return re;
|
|
12721
|
+
};
|
|
12685
12722
|
if (key.startsWith(CLASS_PREFIX)) return reviveClass(key.slice(2));
|
|
12686
12723
|
if (key.startsWith(ERROR_PREFIX)) return reviveError(key.slice(2));
|
|
12687
12724
|
}
|
|
@@ -12793,6 +12830,7 @@ function isOptionalPRNGSnapshot(value) {
|
|
|
12793
12830
|
return isRecord$1(value) && typeof value.seed === "string" && typeof value.pull === "number" && Number.isInteger(value.pull) && value.pull >= 0;
|
|
12794
12831
|
}
|
|
12795
12832
|
var isMoment = (value) => isRecord$1(value) && typeof value.passage === "string" && isRecord$1(value.variables) && typeof value.timestamp === "number" && isOptionalPRNGSnapshot(value.prng);
|
|
12833
|
+
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
12834
|
var isCountsMap = (value) => value === void 0 || value instanceof Map && [...value.keys()].every((k) => typeof k === "string");
|
|
12797
12835
|
/**
|
|
12798
12836
|
* Whether a deserialized payload is well formed: every history moment is,
|
|
@@ -12806,7 +12844,7 @@ function isPayload(value) {
|
|
|
12806
12844
|
if (!Array.isArray(history) || history.length === 0) return false;
|
|
12807
12845
|
if (!Array.from(history).every(isMoment)) return false;
|
|
12808
12846
|
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);
|
|
12847
|
+
return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng) && isWatchers(value.watchers);
|
|
12810
12848
|
}
|
|
12811
12849
|
var mapToCounts = (counts) => counts instanceof Map ? createCounts(Object.fromEntries(counts)) : void 0;
|
|
12812
12850
|
/**
|
|
@@ -13406,7 +13444,11 @@ function deepClone(value, options = {}) {
|
|
|
13406
13444
|
}
|
|
13407
13445
|
function cloneBuiltin(val) {
|
|
13408
13446
|
if (val instanceof Date) return keep(val, new Date(val.getTime()));
|
|
13409
|
-
if (val instanceof RegExp)
|
|
13447
|
+
if (val instanceof RegExp) {
|
|
13448
|
+
const copy = new RegExp(val.source, val.flags);
|
|
13449
|
+
copy.lastIndex = val.lastIndex;
|
|
13450
|
+
return keep(val, copy);
|
|
13451
|
+
}
|
|
13410
13452
|
if (val instanceof ArrayBuffer) return keep(val, val.slice(0));
|
|
13411
13453
|
if (ArrayBuffer.isView(val)) {
|
|
13412
13454
|
const buffer = clone(val.buffer);
|
|
@@ -13589,7 +13631,7 @@ function equalBytes(a, b) {
|
|
|
13589
13631
|
/** Equality of the content of atomic built-ins (see isAtomic) but Map/Set. */
|
|
13590
13632
|
function equalBuiltin(a, b, assumed) {
|
|
13591
13633
|
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
13592
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
13634
|
+
if (a instanceof RegExp) return String(a) === String(b) && a.lastIndex === b.lastIndex;
|
|
13593
13635
|
if (ArrayBuffer.isView(a)) {
|
|
13594
13636
|
const v = b;
|
|
13595
13637
|
return a.byteOffset === v.byteOffset && a.byteLength === v.byteLength && equal(a.buffer, v.buffer, assumed);
|
|
@@ -14148,16 +14190,19 @@ var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(i
|
|
|
14148
14190
|
var startNewPlaythrough = queued(startNewPlaythroughNow);
|
|
14149
14191
|
async function startNewPlaythroughNow(ifid, id = randomUUID()) {
|
|
14150
14192
|
const backend = await getBackend();
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14193
|
+
await withTabLock(`spindle-playthrough-count:${ifid}`, async () => {
|
|
14194
|
+
const num = await nextPlaythroughNumber(ifid);
|
|
14195
|
+
const record = {
|
|
14196
|
+
id,
|
|
14197
|
+
ifid,
|
|
14198
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14199
|
+
label: `Playthrough ${num}`
|
|
14200
|
+
};
|
|
14201
|
+
await backend.putPlaythrough(record);
|
|
14202
|
+
await backend.setMeta(playthroughCountKey(ifid), num);
|
|
14203
|
+
});
|
|
14160
14204
|
await backend.setMeta(currentPlaythroughKey(ifid), id);
|
|
14205
|
+
saveSessionPlaythrough(ifid, id);
|
|
14161
14206
|
return id;
|
|
14162
14207
|
}
|
|
14163
14208
|
var PLAYTHROUGH_LABEL = /^Playthrough (\d+)$/;
|
|
@@ -14197,6 +14242,7 @@ async function adoptPlaythroughNow(ifid, id) {
|
|
|
14197
14242
|
await ensurePlaythrough(ifid, id, (/* @__PURE__ */ new Date()).toISOString());
|
|
14198
14243
|
const key = currentPlaythroughKey(ifid);
|
|
14199
14244
|
if (await backend.getMeta(key) !== id) await backend.setMeta(key, id);
|
|
14245
|
+
saveSessionPlaythrough(ifid, id);
|
|
14200
14246
|
}
|
|
14201
14247
|
/**
|
|
14202
14248
|
* Record the playthrough `id` of the story as "Imported", created at
|
|
@@ -14219,8 +14265,12 @@ async function ensurePlaythrough(ifid, id, createdAt) {
|
|
|
14219
14265
|
*/
|
|
14220
14266
|
function establishPlaythrough(ifid) {
|
|
14221
14267
|
return inOrder(async () => {
|
|
14268
|
+
const backend = await getBackend();
|
|
14269
|
+
const own = loadSessionPlaythrough(ifid);
|
|
14270
|
+
const id = own !== void 0 && (await backend.getPlaythroughsByIfid(ifid)).some((p) => p.id === own) ? own : await backend.getMeta(currentPlaythroughKey(ifid)) ?? await startNewPlaythroughNow(ifid);
|
|
14271
|
+
saveSessionPlaythrough(ifid, id);
|
|
14222
14272
|
return {
|
|
14223
|
-
id
|
|
14273
|
+
id,
|
|
14224
14274
|
knownSaves: await populateKnownSavesNow(ifid)
|
|
14225
14275
|
};
|
|
14226
14276
|
});
|
|
@@ -14409,12 +14459,41 @@ function slotMetaKey(ifid, slot) {
|
|
|
14409
14459
|
return isNamedSlot(slot) ? `${SLOT_KEY_PREFIX}${slot}.${ifid}` : `${AUTOSAVE_KEY_PREFIX}${ifid}`;
|
|
14410
14460
|
}
|
|
14411
14461
|
var slotIndexKey = (ifid) => `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
14412
|
-
/**
|
|
14462
|
+
/**
|
|
14463
|
+
* Run `op` holding the browser's lock `name`, so other tabs of the story
|
|
14464
|
+
* (the operation queue only orders this document's operations) wait for it.
|
|
14465
|
+
* Runs `op` without a lock where the browser offers none (or refuses it, as
|
|
14466
|
+
* in a sandboxed frame, which has no other tabs sharing its storage).
|
|
14467
|
+
*/
|
|
14468
|
+
async function withTabLock(name, op) {
|
|
14469
|
+
let locks;
|
|
14470
|
+
try {
|
|
14471
|
+
locks = navigator.locks;
|
|
14472
|
+
} catch {}
|
|
14473
|
+
if (!locks) return op();
|
|
14474
|
+
let started = false;
|
|
14475
|
+
try {
|
|
14476
|
+
return await locks.request(name, () => {
|
|
14477
|
+
started = true;
|
|
14478
|
+
return op();
|
|
14479
|
+
});
|
|
14480
|
+
} catch (err) {
|
|
14481
|
+
if (started) throw err;
|
|
14482
|
+
return op();
|
|
14483
|
+
}
|
|
14484
|
+
}
|
|
14485
|
+
/**
|
|
14486
|
+
* Read-modify-write the per-story slot index. Under a lock shared by the
|
|
14487
|
+
* story's tabs: two tabs adding a slot at once would both read the old index
|
|
14488
|
+
* and the later write would drop the other's slot.
|
|
14489
|
+
*/
|
|
14413
14490
|
async function updateSlotIndex(ifid, update) {
|
|
14414
14491
|
const backend = await getBackend();
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14492
|
+
await withTabLock(`spindle-slot-index:${ifid}`, async () => {
|
|
14493
|
+
const existing = await backend.getMeta(slotIndexKey(ifid)) ?? [];
|
|
14494
|
+
const updated = update(existing);
|
|
14495
|
+
if (updated !== existing) await backend.setMeta(slotIndexKey(ifid), updated);
|
|
14496
|
+
});
|
|
14418
14497
|
}
|
|
14419
14498
|
/** Every slot: the default one (undefined), then those in the slot index. */
|
|
14420
14499
|
async function allSlots(ifid) {
|
|
@@ -14494,12 +14573,16 @@ readingSlot(livePayload);
|
|
|
14494
14573
|
* (see adoptPlaythrough) in the same operation: operations issued after the
|
|
14495
14574
|
* load take effect in the loaded playthrough. Resolves to the live payload
|
|
14496
14575
|
* and its playthrough, or undefined if the slot is empty.
|
|
14576
|
+
*
|
|
14577
|
+
* `check` is run on the payload first: when it throws, the load rejects and
|
|
14578
|
+
* the game stays in its playthrough (#357).
|
|
14497
14579
|
*/
|
|
14498
|
-
function loadSlotSave(ifid, slot) {
|
|
14580
|
+
function loadSlotSave(ifid, slot, check) {
|
|
14499
14581
|
return inOrder(async () => {
|
|
14500
14582
|
const record = await slotRecord(ifid, slot);
|
|
14501
14583
|
if (!record) return void 0;
|
|
14502
14584
|
const payload = decodePayload(record.payload);
|
|
14585
|
+
check?.(payload);
|
|
14503
14586
|
const { playthroughId } = record.meta;
|
|
14504
14587
|
if (playthroughId) await adoptPlaythroughNow(ifid, playthroughId);
|
|
14505
14588
|
return {
|
|
@@ -14589,6 +14672,26 @@ function clearSession(ifid) {
|
|
|
14589
14672
|
sessionStorage.removeItem(`${SESSION_KEY_PREFIX}${ifid}`);
|
|
14590
14673
|
} catch {}
|
|
14591
14674
|
}
|
|
14675
|
+
/**
|
|
14676
|
+
* The playthrough this tab's game is in, kept with its session: the shared
|
|
14677
|
+
* current playthrough (a stored meta value) can be changed by another tab.
|
|
14678
|
+
* Written where this tab's storage operations set the current playthrough,
|
|
14679
|
+
* so it follows them in call order. Under the session prefix, so clearing
|
|
14680
|
+
* all data clears it.
|
|
14681
|
+
*/
|
|
14682
|
+
var SESSION_PLAYTHROUGH_PREFIX = `${SESSION_KEY_PREFIX}playthrough.`;
|
|
14683
|
+
function saveSessionPlaythrough(ifid, id) {
|
|
14684
|
+
try {
|
|
14685
|
+
sessionStorage.setItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`, id);
|
|
14686
|
+
} catch {}
|
|
14687
|
+
}
|
|
14688
|
+
function loadSessionPlaythrough(ifid) {
|
|
14689
|
+
try {
|
|
14690
|
+
return sessionStorage.getItem(`${SESSION_PLAYTHROUGH_PREFIX}${ifid}`) || void 0;
|
|
14691
|
+
} catch {
|
|
14692
|
+
return;
|
|
14693
|
+
}
|
|
14694
|
+
}
|
|
14592
14695
|
/** The export of a stored save, if there is one. */
|
|
14593
14696
|
function toExport(record) {
|
|
14594
14697
|
if (!record) return void 0;
|
|
@@ -14761,6 +14864,11 @@ function executeStoryInit() {
|
|
|
14761
14864
|
*/
|
|
14762
14865
|
function initializeStory(session) {
|
|
14763
14866
|
executeStoryInit();
|
|
14867
|
+
const missing = session ? missingPassage(useStoryStore.getState().storyData, session) : void 0;
|
|
14868
|
+
if (missing !== void 0) {
|
|
14869
|
+
console.warn(`spindle: discarding the saved session: the story no longer has the passage "${missing}"`);
|
|
14870
|
+
session = void 0;
|
|
14871
|
+
}
|
|
14764
14872
|
if (session) useStoryStore.getState().loadFromPayload(session);
|
|
14765
14873
|
emit("storyinit");
|
|
14766
14874
|
if (!session) recordStoryInitState();
|
|
@@ -15254,6 +15362,27 @@ function addMacroTrigger(condition, options) {
|
|
|
15254
15362
|
if (triggers.some((t) => t.macroKey === macroKey)) return;
|
|
15255
15363
|
registerTrigger(condition, options, macroKey);
|
|
15256
15364
|
}
|
|
15365
|
+
/**
|
|
15366
|
+
* The {watch} macro watchers now registered, for a save or the session:
|
|
15367
|
+
* watchers outlive the passage that declared them, so a game restored
|
|
15368
|
+
* where it stopped needs the ones its earlier passages registered (and
|
|
15369
|
+
* not the ones {unwatch} or `once` removed).
|
|
15370
|
+
*/
|
|
15371
|
+
function savedMacroWatchers() {
|
|
15372
|
+
return triggers.filter((t) => t.macroKey !== void 0 && t.options).map((t) => ({
|
|
15373
|
+
condition: t.condition,
|
|
15374
|
+
options: { ...t.options }
|
|
15375
|
+
}));
|
|
15376
|
+
}
|
|
15377
|
+
/**
|
|
15378
|
+
* Make the {watch} macro watchers those of a loaded game: the ones of
|
|
15379
|
+
* `watchers` replace the registered ones (watchers added by Story.watch
|
|
15380
|
+
* code stay). The passage shown mounts its own {watch} macros again.
|
|
15381
|
+
*/
|
|
15382
|
+
function restoreMacroWatchers(watchers) {
|
|
15383
|
+
triggers = triggers.filter((t) => t.macroKey === void 0);
|
|
15384
|
+
for (const { condition, options } of watchers) addMacroTrigger(condition, options);
|
|
15385
|
+
}
|
|
15257
15386
|
function removeTrigger(name) {
|
|
15258
15387
|
triggers = triggers.filter((t) => t.name !== name);
|
|
15259
15388
|
}
|
|
@@ -15609,7 +15738,8 @@ function persistSession(get) {
|
|
|
15609
15738
|
historyIndex,
|
|
15610
15739
|
visitCounts,
|
|
15611
15740
|
renderCounts,
|
|
15612
|
-
prng: snapshotPRNG()
|
|
15741
|
+
prng: snapshotPRNG(),
|
|
15742
|
+
watchers: savedMacroWatchers()
|
|
15613
15743
|
});
|
|
15614
15744
|
}
|
|
15615
15745
|
/** What the page shows before a session write error (see RuntimeErrors). */
|
|
@@ -15994,6 +16124,20 @@ function restorePRNGFrom(prng) {
|
|
|
15994
16124
|
function restorePRNGFromMoment(moment) {
|
|
15995
16125
|
if (moment) restorePRNGFrom(moment.prng);
|
|
15996
16126
|
}
|
|
16127
|
+
/**
|
|
16128
|
+
* The first passage of `payload` (its current passage, then its history)
|
|
16129
|
+
* that `storyData` does not have, as when the story was updated since the
|
|
16130
|
+
* save; undefined if the story has them all (or is not loaded).
|
|
16131
|
+
*/
|
|
16132
|
+
function missingPassage(storyData, payload) {
|
|
16133
|
+
if (!storyData) return void 0;
|
|
16134
|
+
return [payload.passage, ...payload.history.map((m) => m.passage)].find((name) => !storyData.passages.has(name));
|
|
16135
|
+
}
|
|
16136
|
+
/** Throw if `payload` refers to a passage `storyData` does not have. */
|
|
16137
|
+
function assertPassagesExist(storyData, payload) {
|
|
16138
|
+
const missing = missingPassage(storyData, payload);
|
|
16139
|
+
if (missing !== void 0) throw new Error(`The save refers to the passage "${missing}", which this version of the story does not have`);
|
|
16140
|
+
}
|
|
15997
16141
|
var NAMESPACE_KEYS = [
|
|
15998
16142
|
"variables",
|
|
15999
16143
|
"temporary",
|
|
@@ -16336,7 +16480,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16336
16480
|
state.loadError = null;
|
|
16337
16481
|
});
|
|
16338
16482
|
const previous = resolvePlaythroughId();
|
|
16339
|
-
const read = loadSlotSave(storyData.ifid, slot);
|
|
16483
|
+
const read = loadSlotSave(storyData.ifid, slot, (payload) => assertPassagesExist(storyData, payload));
|
|
16340
16484
|
const switched = switchToLookedUpPlaythrough(Promise.all([previous, read.catch(() => void 0)]).then(([prev, loaded]) => loaded?.playthroughId || prev));
|
|
16341
16485
|
const replacement = ++stateReplacementsIssued;
|
|
16342
16486
|
return reported(read.then(async (loaded) => {
|
|
@@ -16421,7 +16565,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16421
16565
|
historyIndex,
|
|
16422
16566
|
visitCounts: { ...visitCounts },
|
|
16423
16567
|
renderCounts: { ...renderCounts },
|
|
16424
|
-
prng: snapshotPRNG()
|
|
16568
|
+
prng: snapshotPRNG(),
|
|
16569
|
+
watchers: savedMacroWatchers()
|
|
16425
16570
|
};
|
|
16426
16571
|
},
|
|
16427
16572
|
beginSave: () => {
|
|
@@ -16433,12 +16578,14 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16433
16578
|
};
|
|
16434
16579
|
},
|
|
16435
16580
|
loadFromPayload: (payload, slot, playthroughId) => {
|
|
16436
|
-
const
|
|
16581
|
+
const applying = slotLoadApplying;
|
|
16437
16582
|
slotLoadApplying = null;
|
|
16583
|
+
const replacement = applying ?? ++stateReplacementsIssued;
|
|
16438
16584
|
if (payload.history.length === 0) {
|
|
16439
16585
|
console.warn("loadFromPayload: rejecting payload with empty history");
|
|
16440
16586
|
return;
|
|
16441
16587
|
}
|
|
16588
|
+
assertPassagesExist(get().storyData, payload);
|
|
16442
16589
|
latestStateApplied = replacement;
|
|
16443
16590
|
emit("beforeload", slot);
|
|
16444
16591
|
const ifid = get().storyData?.ifid;
|
|
@@ -16464,6 +16611,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16464
16611
|
state.temporary = createNamespace();
|
|
16465
16612
|
state.transient = createNamespace(deepClone(get().transientDefaults));
|
|
16466
16613
|
});
|
|
16614
|
+
if (payload.watchers) restoreMacroWatchers(payload.watchers);
|
|
16467
16615
|
reinitTriggerState();
|
|
16468
16616
|
lastNavigationVars = reconstructVarsAt(get().historyIndex);
|
|
16469
16617
|
restorePRNGFrom(entry?.prng !== void 0 ? entry.prng : payload.prng);
|
|
@@ -16646,7 +16794,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16646
16794
|
}, []);
|
|
16647
16795
|
const handleBackdrop = (e) => {
|
|
16648
16796
|
if (!dismissible) return;
|
|
16649
|
-
if (e.target.
|
|
16797
|
+
if (e.target === e.currentTarget) stableOnClose();
|
|
16650
16798
|
};
|
|
16651
16799
|
const cls = panelClass ? `dialog-panel ${panelClass}` : "dialog-panel";
|
|
16652
16800
|
return /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
@@ -16661,6 +16809,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16661
16809
|
"aria-modal": "true",
|
|
16662
16810
|
tabIndex: -1,
|
|
16663
16811
|
children: [showCloseButton && /* @__PURE__ */ jsx("button", {
|
|
16812
|
+
type: "button",
|
|
16664
16813
|
class: "dialog-close",
|
|
16665
16814
|
"aria-label": "Close",
|
|
16666
16815
|
onClick: stableOnClose,
|
|
@@ -19086,7 +19235,7 @@ function keyOf(val, ancestors) {
|
|
|
19086
19235
|
const depth = ancestors.indexOf(obj);
|
|
19087
19236
|
if (depth !== -1) return `<cycle ${ancestors.length - depth}>`;
|
|
19088
19237
|
if (val instanceof Date) return `Date(${val.getTime()})`;
|
|
19089
|
-
if (val instanceof RegExp) return `RegExp(${String(val)})`;
|
|
19238
|
+
if (val instanceof RegExp) return `RegExp(${String(val)}@${val.lastIndex})`;
|
|
19090
19239
|
ancestors.push(obj);
|
|
19091
19240
|
try {
|
|
19092
19241
|
if (Array.isArray(val)) return `[${Array.from(val, (v) => keyOf(v, ancestors)).join(",")}]`;
|
|
@@ -19201,15 +19350,7 @@ defineMacro({
|
|
|
19201
19350
|
block: true,
|
|
19202
19351
|
render({ children = [] }, ctx) {
|
|
19203
19352
|
const code = ctx.collectText(children);
|
|
19204
|
-
|
|
19205
|
-
const location = ctx.sourceLocation();
|
|
19206
|
-
try {
|
|
19207
|
-
ctx.mutate(code);
|
|
19208
|
-
} catch (err) {
|
|
19209
|
-
logMacroError("do", err, location);
|
|
19210
|
-
}
|
|
19211
|
-
}, []);
|
|
19212
|
-
return null;
|
|
19353
|
+
return useRunOnce("do", "", () => ctx.mutate(code));
|
|
19213
19354
|
}
|
|
19214
19355
|
});
|
|
19215
19356
|
//#endregion
|
|
@@ -19272,11 +19413,13 @@ defineMacro({
|
|
|
19272
19413
|
});
|
|
19273
19414
|
//#endregion
|
|
19274
19415
|
//#region src/components/macros/detached-body.tsx
|
|
19416
|
+
var frozenAlways = () => true;
|
|
19275
19417
|
/**
|
|
19276
19418
|
* Return a function that runs a macro body once, outside the passage tree:
|
|
19277
19419
|
* it renders `children` into a detached node, so the body's macros ({set},
|
|
19278
19420
|
* {if}, {goto}, ...) fire their side effects through the normal Preact
|
|
19279
|
-
* pipeline. Used by {button} and {link} on click. The body
|
|
19421
|
+
* pipeline. Used by {button} and {link} on click. The body's state is frozen
|
|
19422
|
+
* once it has rendered, so it runs once per click. It stays mounted
|
|
19280
19423
|
* until the owning macro unmounts (its passage changes, say), so work it
|
|
19281
19424
|
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
19282
19425
|
* finish; the passage leaving cancels it.
|
|
@@ -19307,24 +19450,27 @@ function useDetachedBody() {
|
|
|
19307
19450
|
const run = (children) => {
|
|
19308
19451
|
const locals = liveLocalsView(updater.getValues);
|
|
19309
19452
|
const container = document.createElement("div");
|
|
19310
|
-
render(/* @__PURE__ */ jsx(
|
|
19311
|
-
value:
|
|
19312
|
-
children: /* @__PURE__ */ jsx(
|
|
19313
|
-
value:
|
|
19314
|
-
children: /* @__PURE__ */ jsx(
|
|
19315
|
-
value:
|
|
19316
|
-
children: /* @__PURE__ */ jsx(
|
|
19317
|
-
value:
|
|
19318
|
-
children: /* @__PURE__ */ jsx(
|
|
19319
|
-
value:
|
|
19320
|
-
children: /* @__PURE__ */ jsx(
|
|
19321
|
-
value:
|
|
19322
|
-
children: /* @__PURE__ */ jsx(
|
|
19323
|
-
value:
|
|
19324
|
-
children:
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19453
|
+
render(/* @__PURE__ */ jsx(FrozenStateContext.Provider, {
|
|
19454
|
+
value: frozenAlways,
|
|
19455
|
+
children: /* @__PURE__ */ jsx(LocalsUpdateContext.Provider, {
|
|
19456
|
+
value: updater,
|
|
19457
|
+
children: /* @__PURE__ */ jsx(LocalsValuesContext.Provider, {
|
|
19458
|
+
value: locals,
|
|
19459
|
+
children: /* @__PURE__ */ jsx(NobrContext.Provider, {
|
|
19460
|
+
value: nobr,
|
|
19461
|
+
children: /* @__PURE__ */ jsx(InlineContext.Provider, {
|
|
19462
|
+
value: inline,
|
|
19463
|
+
children: /* @__PURE__ */ jsx(WidgetChildrenContext.Provider, {
|
|
19464
|
+
value: widgetChildren,
|
|
19465
|
+
children: /* @__PURE__ */ jsx(RepeatContext.Provider, {
|
|
19466
|
+
value: repeat,
|
|
19467
|
+
children: /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
19468
|
+
value: closeDialog,
|
|
19469
|
+
children: renderNodes(children, {
|
|
19470
|
+
nobr,
|
|
19471
|
+
inline,
|
|
19472
|
+
locals
|
|
19473
|
+
})
|
|
19328
19474
|
})
|
|
19329
19475
|
})
|
|
19330
19476
|
})
|
|
@@ -19361,6 +19507,7 @@ defineMacro({
|
|
|
19361
19507
|
perform: handleClick
|
|
19362
19508
|
});
|
|
19363
19509
|
return /* @__PURE__ */ jsx("button", {
|
|
19510
|
+
type: "button",
|
|
19364
19511
|
id: ctx.id,
|
|
19365
19512
|
class: ctx.cls,
|
|
19366
19513
|
onClick: handleClick,
|
|
@@ -19517,15 +19664,52 @@ defineMacro({
|
|
|
19517
19664
|
});
|
|
19518
19665
|
//#endregion
|
|
19519
19666
|
//#region src/components/macros/Type.tsx
|
|
19667
|
+
/**
|
|
19668
|
+
* The clip-path that shows the first `count` characters of `root`'s text in
|
|
19669
|
+
* reading order: the boxes the browser laid out for them, each wrapped line
|
|
19670
|
+
* as far as its last revealed character, as one path in the coordinates of
|
|
19671
|
+
* `inner`'s box. Undefined where there is no layout to measure.
|
|
19672
|
+
*/
|
|
19673
|
+
function revealedPath(root, inner, count) {
|
|
19674
|
+
const range = document.createRange();
|
|
19675
|
+
if (!range.getClientRects) return void 0;
|
|
19676
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
19677
|
+
let left = count;
|
|
19678
|
+
let end;
|
|
19679
|
+
for (let n = walker.nextNode(); n && left > 0; n = walker.nextNode()) {
|
|
19680
|
+
const length = n.nodeValue?.length ?? 0;
|
|
19681
|
+
end = {
|
|
19682
|
+
node: n,
|
|
19683
|
+
offset: Math.min(left, length)
|
|
19684
|
+
};
|
|
19685
|
+
left -= length;
|
|
19686
|
+
}
|
|
19687
|
+
if (!end) return "inset(0 100% 0 0)";
|
|
19688
|
+
range.setStart(root, 0);
|
|
19689
|
+
range.setEnd(end.node, end.offset);
|
|
19690
|
+
const rects = range.getClientRects();
|
|
19691
|
+
if (rects.length === 0) return void 0;
|
|
19692
|
+
const origin = inner.getBoundingClientRect();
|
|
19693
|
+
let path = "";
|
|
19694
|
+
for (const r of Array.from(rects)) {
|
|
19695
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
19696
|
+
const x = (r.left - origin.left).toFixed(2);
|
|
19697
|
+
const y = (r.top - origin.top).toFixed(2);
|
|
19698
|
+
path += `M${x} ${y}h${r.width.toFixed(2)}v${r.height.toFixed(2)}h-${r.width.toFixed(2)}z`;
|
|
19699
|
+
}
|
|
19700
|
+
return path ? `path("${path}")` : void 0;
|
|
19701
|
+
}
|
|
19520
19702
|
defineMacro({
|
|
19521
19703
|
name: "type",
|
|
19522
19704
|
block: true,
|
|
19523
19705
|
interpolate: true,
|
|
19524
19706
|
parameters: [DELAY_PARAMETER],
|
|
19525
19707
|
render({ children = [] }, ctx) {
|
|
19526
|
-
const { useState, useEffect, useRef } = ctx.hooks;
|
|
19708
|
+
const { useState, useEffect, useLayoutEffect, useRef } = ctx.hooks;
|
|
19527
19709
|
const speed = ctx.args.delay ?? 0;
|
|
19528
19710
|
const containerRef = useRef(null);
|
|
19711
|
+
const innerRef = useRef(null);
|
|
19712
|
+
const [clip, setClip] = useState();
|
|
19529
19713
|
const [totalChars, setTotalChars] = useState(0);
|
|
19530
19714
|
const [visibleChars, setVisibleChars] = useState(0);
|
|
19531
19715
|
const visibleCharsRef = useRef(0);
|
|
@@ -19548,6 +19732,22 @@ defineMacro({
|
|
|
19548
19732
|
return () => clearInterval(timer);
|
|
19549
19733
|
}, [totalChars, speed]);
|
|
19550
19734
|
const done = visibleChars >= totalChars && totalChars > 0;
|
|
19735
|
+
useLayoutEffect(() => {
|
|
19736
|
+
const root = containerRef.current;
|
|
19737
|
+
const inner = innerRef.current;
|
|
19738
|
+
if (!root || !inner || totalChars === 0 || done) {
|
|
19739
|
+
setClip(void 0);
|
|
19740
|
+
return;
|
|
19741
|
+
}
|
|
19742
|
+
const measure = () => setClip(revealedPath(root, inner, visibleChars));
|
|
19743
|
+
measure();
|
|
19744
|
+
window.addEventListener("resize", measure);
|
|
19745
|
+
return () => window.removeEventListener("resize", measure);
|
|
19746
|
+
}, [
|
|
19747
|
+
visibleChars,
|
|
19748
|
+
totalChars,
|
|
19749
|
+
done
|
|
19750
|
+
]);
|
|
19551
19751
|
const cls = [
|
|
19552
19752
|
"macro-type",
|
|
19553
19753
|
done ? "macro-type-done" : "",
|
|
@@ -19557,13 +19757,13 @@ defineMacro({
|
|
|
19557
19757
|
id: ctx.id,
|
|
19558
19758
|
class: cls,
|
|
19559
19759
|
ref: containerRef,
|
|
19560
|
-
style: { clipPath: totalChars > 0 && !done ? void 0 : void 0 },
|
|
19561
19760
|
children: [/* @__PURE__ */ jsx("span", {
|
|
19761
|
+
ref: innerRef,
|
|
19562
19762
|
class: "macro-type-inner",
|
|
19563
19763
|
style: {
|
|
19564
19764
|
display: "inline",
|
|
19565
19765
|
visibility: totalChars === 0 ? "hidden" : "visible",
|
|
19566
|
-
clipPath:
|
|
19766
|
+
clipPath: clip
|
|
19567
19767
|
},
|
|
19568
19768
|
children: ctx.renderInlineNodes(children)
|
|
19569
19769
|
}), !done && totalChars > 0 && /* @__PURE__ */ jsx("span", { class: "macro-type-cursor" })]
|
|
@@ -19611,6 +19811,7 @@ function defineMenubarAction(config) {
|
|
|
19611
19811
|
perform: config.dialog ? () => setDialogOpen(true) : setup.perform
|
|
19612
19812
|
});
|
|
19613
19813
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19814
|
+
type: "button",
|
|
19614
19815
|
id: ctx.id,
|
|
19615
19816
|
class: cls,
|
|
19616
19817
|
onClick: perform,
|
|
@@ -19720,6 +19921,7 @@ defineMacro({
|
|
|
19720
19921
|
const label = ctx.resolve(ctx.args.label ?? "");
|
|
19721
19922
|
const passageName = ctx.collectText(children).trim().replace(/^["']|["']$/g, "");
|
|
19722
19923
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19924
|
+
type: "button",
|
|
19723
19925
|
id: ctx.id,
|
|
19724
19926
|
class: ctx.cls,
|
|
19725
19927
|
onClick: () => setOpen(true),
|
|
@@ -19766,9 +19968,12 @@ function SaveManagerContent() {
|
|
|
19766
19968
|
const closeDialog = useContext(DialogCloseContext);
|
|
19767
19969
|
const { storyData, playthroughId, beginSave, loadFromPayload } = useStoryFields("storyData", "playthroughId", "beginSave", "loadFromPayload");
|
|
19768
19970
|
const ifid = storyData?.ifid ?? "";
|
|
19971
|
+
const [temporary, setTemporary] = useState(false);
|
|
19769
19972
|
const refresh = useCallback(async () => {
|
|
19770
19973
|
if (!ifid) return;
|
|
19771
|
-
|
|
19974
|
+
const data = await getSavesGrouped(ifid);
|
|
19975
|
+
setTemporary(getBackendType() === "memory");
|
|
19976
|
+
setGroups(data);
|
|
19772
19977
|
setLoading(false);
|
|
19773
19978
|
}, [ifid]);
|
|
19774
19979
|
useEffect(() => {
|
|
@@ -19815,7 +20020,7 @@ function SaveManagerContent() {
|
|
|
19815
20020
|
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
19816
20021
|
return createSave(ifid, playthrough, payload);
|
|
19817
20022
|
});
|
|
19818
|
-
showStatus("Save created");
|
|
20023
|
+
showStatus(temporary ? "Save created (temporary)" : "Save created");
|
|
19819
20024
|
await refresh();
|
|
19820
20025
|
} catch {
|
|
19821
20026
|
showStatus("Failed to create save", "error");
|
|
@@ -19931,10 +20136,12 @@ function SaveManagerContent() {
|
|
|
19931
20136
|
children: /* @__PURE__ */ jsxs("div", {
|
|
19932
20137
|
class: "saves-mode-toggle",
|
|
19933
20138
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20139
|
+
type: "button",
|
|
19934
20140
|
class: mode === "save" ? "active" : "",
|
|
19935
20141
|
onClick: () => setMode("save"),
|
|
19936
20142
|
children: "Save"
|
|
19937
20143
|
}), /* @__PURE__ */ jsx("button", {
|
|
20144
|
+
type: "button",
|
|
19938
20145
|
class: mode === "load" ? "active" : "",
|
|
19939
20146
|
onClick: () => setMode("load"),
|
|
19940
20147
|
children: "Load"
|
|
@@ -19942,9 +20149,15 @@ function SaveManagerContent() {
|
|
|
19942
20149
|
})
|
|
19943
20150
|
})
|
|
19944
20151
|
}),
|
|
20152
|
+
temporary && /* @__PURE__ */ jsx("div", {
|
|
20153
|
+
class: "saves-notice",
|
|
20154
|
+
role: "status",
|
|
20155
|
+
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."
|
|
20156
|
+
}),
|
|
19945
20157
|
/* @__PURE__ */ jsxs("div", {
|
|
19946
20158
|
class: "saves-toolbar",
|
|
19947
20159
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20160
|
+
type: "button",
|
|
19948
20161
|
class: "saves-toolbar-button",
|
|
19949
20162
|
onClick: handleImport,
|
|
19950
20163
|
children: "Import"
|
|
@@ -20012,25 +20225,30 @@ function SaveManagerContent() {
|
|
|
20012
20225
|
class: "save-slot-actions",
|
|
20013
20226
|
children: [
|
|
20014
20227
|
mode === "save" ? /* @__PURE__ */ jsx("button", {
|
|
20228
|
+
type: "button",
|
|
20015
20229
|
class: "save-slot-action primary",
|
|
20016
20230
|
onClick: () => handleOverwrite(save.meta.id),
|
|
20017
20231
|
children: "Save Here"
|
|
20018
20232
|
}) : /* @__PURE__ */ jsx("button", {
|
|
20233
|
+
type: "button",
|
|
20019
20234
|
class: "save-slot-action primary",
|
|
20020
20235
|
onClick: () => handleLoad(save),
|
|
20021
20236
|
children: "Load"
|
|
20022
20237
|
}),
|
|
20023
20238
|
/* @__PURE__ */ jsx("button", {
|
|
20239
|
+
type: "button",
|
|
20024
20240
|
class: "save-slot-action",
|
|
20025
20241
|
onClick: () => handleRenameStart(save),
|
|
20026
20242
|
children: "Rename"
|
|
20027
20243
|
}),
|
|
20028
20244
|
/* @__PURE__ */ jsx("button", {
|
|
20245
|
+
type: "button",
|
|
20029
20246
|
class: "save-slot-action",
|
|
20030
20247
|
onClick: () => handleExport(save.meta.id),
|
|
20031
20248
|
children: "Export"
|
|
20032
20249
|
}),
|
|
20033
20250
|
/* @__PURE__ */ jsx("button", {
|
|
20251
|
+
type: "button",
|
|
20034
20252
|
class: "save-slot-action danger",
|
|
20035
20253
|
onClick: () => handleDelete(save.meta.id),
|
|
20036
20254
|
children: "Delete"
|
|
@@ -20038,6 +20256,7 @@ function SaveManagerContent() {
|
|
|
20038
20256
|
]
|
|
20039
20257
|
})]
|
|
20040
20258
|
}, save.meta.id)), mode === "save" && isCurrentPt && /* @__PURE__ */ jsx("button", {
|
|
20259
|
+
type: "button",
|
|
20041
20260
|
class: "save-slot-new",
|
|
20042
20261
|
onClick: handleNewSave,
|
|
20043
20262
|
children: "+ New Save"
|
|
@@ -20049,6 +20268,7 @@ function SaveManagerContent() {
|
|
|
20049
20268
|
children: /* @__PURE__ */ jsx("div", {
|
|
20050
20269
|
class: "playthrough-saves",
|
|
20051
20270
|
children: /* @__PURE__ */ jsx("button", {
|
|
20271
|
+
type: "button",
|
|
20052
20272
|
class: "save-slot-new",
|
|
20053
20273
|
onClick: handleNewSave,
|
|
20054
20274
|
children: "+ New Save"
|
|
@@ -20277,13 +20497,19 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
|
|
|
20277
20497
|
label: placeholder || ctx.varName,
|
|
20278
20498
|
perform: (v) => ctx.setValue(toValue(v))
|
|
20279
20499
|
});
|
|
20500
|
+
const typed = ctx.hooks.useRef(null);
|
|
20501
|
+
const shown = typed.current !== null && toValue(typed.current) === ctx.value && type === "number" ? typed.current : display(ctx.value);
|
|
20280
20502
|
return h(type ? "input" : "textarea", {
|
|
20281
20503
|
type,
|
|
20282
20504
|
id: ctx.id,
|
|
20283
20505
|
class: ctx.cls,
|
|
20284
|
-
value:
|
|
20506
|
+
value: shown,
|
|
20285
20507
|
placeholder,
|
|
20286
|
-
onInput: (e) =>
|
|
20508
|
+
onInput: (e) => {
|
|
20509
|
+
const text = e.target.value;
|
|
20510
|
+
typed.current = text;
|
|
20511
|
+
ctx.setValue(toValue(text));
|
|
20512
|
+
}
|
|
20287
20513
|
});
|
|
20288
20514
|
}
|
|
20289
20515
|
});
|
|
@@ -20420,6 +20646,7 @@ defineMacro({
|
|
|
20420
20646
|
}
|
|
20421
20647
|
});
|
|
20422
20648
|
return /* @__PURE__ */ jsx("button", {
|
|
20649
|
+
type: "button",
|
|
20423
20650
|
id: ctx.id,
|
|
20424
20651
|
class: ctx.cls,
|
|
20425
20652
|
onClick: handleClick,
|
|
@@ -20594,7 +20821,7 @@ function resolveTransition(targetTags, nextTransition, storeDefault) {
|
|
|
20594
20821
|
/**
|
|
20595
20822
|
* Create a snapshot clone of a .passage element for outgoing transitions.
|
|
20596
20823
|
* - Strips `id` attributes from the clone tree
|
|
20597
|
-
* - Disables interaction (pointer-events, user-select)
|
|
20824
|
+
* - Disables interaction (pointer-events, user-select, inert, aria-hidden)
|
|
20598
20825
|
* - Pauses/mutes any <audio>/<video>
|
|
20599
20826
|
* - Adds `.passage-snapshot` class and `data-transition` attribute
|
|
20600
20827
|
*/
|
|
@@ -20605,6 +20832,8 @@ function createSnapshot(passageEl, transitionType) {
|
|
|
20605
20832
|
for (let i = 0; i < idEls.length; i++) idEls[i].removeAttribute("id");
|
|
20606
20833
|
clone.style.pointerEvents = "none";
|
|
20607
20834
|
clone.style.userSelect = "none";
|
|
20835
|
+
clone.setAttribute("inert", "");
|
|
20836
|
+
clone.setAttribute("aria-hidden", "true");
|
|
20608
20837
|
const mediaEls = clone.querySelectorAll("audio, video");
|
|
20609
20838
|
for (let i = 0; i < mediaEls.length; i++) {
|
|
20610
20839
|
const media = mediaEls[i];
|
|
@@ -20672,6 +20901,9 @@ defineMacro({
|
|
|
20672
20901
|
});
|
|
20673
20902
|
const resolvedTypeRef = useRef("fade");
|
|
20674
20903
|
const prevHistoryLenRef = useRef(0);
|
|
20904
|
+
const displayedId = displayed.id;
|
|
20905
|
+
const isOutgoing = useCallback((state) => state.navigationId !== displayedId, [displayedId]);
|
|
20906
|
+
const lastElement = useRef(null);
|
|
20675
20907
|
const containerRef = useRef(null);
|
|
20676
20908
|
const timeoutsRef = useRef([]);
|
|
20677
20909
|
/** Cancel any in-progress transition. */
|
|
@@ -20740,6 +20972,16 @@ defineMacro({
|
|
|
20740
20972
|
cleanupSnapshots(containerEl);
|
|
20741
20973
|
}, config.duration)];
|
|
20742
20974
|
}, [navigationId]);
|
|
20975
|
+
const shownId = displayedPassage && !renderDeferred ? displayed.id : null;
|
|
20976
|
+
const lastShownId = useRef(shownId);
|
|
20977
|
+
useLayoutEffect(() => {
|
|
20978
|
+
if (shownId === null) return;
|
|
20979
|
+
const previous = lastShownId.current;
|
|
20980
|
+
lastShownId.current = shownId;
|
|
20981
|
+
if (previous === null || previous === shownId) return;
|
|
20982
|
+
const el = containerRef.current?.querySelector(".passage");
|
|
20983
|
+
if (el && el.getBoundingClientRect().top < 0) el.scrollIntoView?.({ block: "start" });
|
|
20984
|
+
}, [shownId]);
|
|
20743
20985
|
const effectivePassage = renderDeferred ? storyData?.passages.get("StoryLoading") ?? null : displayedPassage ? storyData?.passages.get(displayedPassage) : null;
|
|
20744
20986
|
if (!effectivePassage && displayedPassage && !renderDeferred) return /* @__PURE__ */ jsxs("div", {
|
|
20745
20987
|
class: "error",
|
|
@@ -20750,6 +20992,12 @@ defineMacro({
|
|
|
20750
20992
|
]
|
|
20751
20993
|
});
|
|
20752
20994
|
const readyPassage = storyData?.passages.get("PassageReady");
|
|
20995
|
+
const passageElement = !renderDeferred && displayed.id !== navigationId && lastElement.current ? lastElement.current : effectivePassage && /* @__PURE__ */ jsx(Passage, {
|
|
20996
|
+
passage: effectivePassage,
|
|
20997
|
+
dataTransition: renderDeferred ? "none" : resolvedTypeRef.current,
|
|
20998
|
+
navigationId: renderDeferred ? void 0 : displayed.id
|
|
20999
|
+
}, renderDeferred ? "loading" : `nav-${displayed.id}`);
|
|
21000
|
+
lastElement.current = passageElement || null;
|
|
20753
21001
|
return /* @__PURE__ */ jsxs("div", {
|
|
20754
21002
|
id: ctx.id ?? "story",
|
|
20755
21003
|
class: ctx.className ?? "story",
|
|
@@ -20759,11 +21007,10 @@ defineMacro({
|
|
|
20759
21007
|
}, `ready-${navigationId}`), /* @__PURE__ */ jsx("div", {
|
|
20760
21008
|
class: "passage-container",
|
|
20761
21009
|
ref: containerRef,
|
|
20762
|
-
children:
|
|
20763
|
-
|
|
20764
|
-
|
|
20765
|
-
|
|
20766
|
-
}, renderDeferred ? "loading" : `nav-${displayed.id}`)
|
|
21010
|
+
children: passageElement && /* @__PURE__ */ jsx(FrozenStateContext.Provider, {
|
|
21011
|
+
value: isOutgoing,
|
|
21012
|
+
children: passageElement
|
|
21013
|
+
})
|
|
20767
21014
|
})]
|
|
20768
21015
|
});
|
|
20769
21016
|
}
|
|
@@ -20843,7 +21090,7 @@ defineMacro({
|
|
|
20843
21090
|
});
|
|
20844
21091
|
//#endregion
|
|
20845
21092
|
//#region src/styles.css?inline
|
|
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";
|
|
21093
|
+
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";
|
|
20847
21094
|
//#endregion
|
|
20848
21095
|
//#region src/index.tsx
|
|
20849
21096
|
function renderErrors(root, errors) {
|