@rohal12/spindle 0.59.8 → 0.59.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +197 -38
- 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 +5 -1
- package/src/components/macros/SaveManager.tsx +27 -1
- package/src/components/macros/Type.tsx +57 -8
- package/src/saves/format.ts +10 -1
- package/src/saves/save-manager.ts +52 -15
- package/src/saves/types.ts +3 -0
- package/src/store.ts +33 -2
- 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
|
@@ -11143,7 +11143,7 @@ function useRunOnce(macro, rawArgs, effect) {
|
|
|
11143
11143
|
effect();
|
|
11144
11144
|
} catch (error) {
|
|
11145
11145
|
failure.current = { error };
|
|
11146
|
-
logMacroError(`${macro} ${rawArgs}
|
|
11146
|
+
logMacroError(`${macro} ${rawArgs}`.trim(), error, location);
|
|
11147
11147
|
}
|
|
11148
11148
|
}
|
|
11149
11149
|
if (!failure.current) return null;
|
|
@@ -12548,6 +12548,7 @@ var CLASS_PREFIX = "c:";
|
|
|
12548
12548
|
var ERROR_PREFIX = "E:";
|
|
12549
12549
|
var SYMBOL_TAG = "S";
|
|
12550
12550
|
var KEYS_TAG = "K";
|
|
12551
|
+
var REGEXP_TAG = "R";
|
|
12551
12552
|
var BUILTIN_ERRORS = new Map([
|
|
12552
12553
|
Error,
|
|
12553
12554
|
EvalError,
|
|
@@ -12609,6 +12610,11 @@ function reducers() {
|
|
|
12609
12610
|
const key = Symbol.keyFor(v);
|
|
12610
12611
|
return key !== void 0 && [key];
|
|
12611
12612
|
};
|
|
12613
|
+
out[REGEXP_TAG] = (v) => v instanceof RegExp && v.lastIndex !== 0 && [
|
|
12614
|
+
v.source,
|
|
12615
|
+
v.flags,
|
|
12616
|
+
v.lastIndex
|
|
12617
|
+
];
|
|
12612
12618
|
out[KEYS_TAG] = (v) => {
|
|
12613
12619
|
if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
|
|
12614
12620
|
const keys = Object.keys(v);
|
|
@@ -12682,6 +12688,12 @@ function reviverFor(key) {
|
|
|
12682
12688
|
return Symbol.for(data[0]);
|
|
12683
12689
|
};
|
|
12684
12690
|
if (key === KEYS_TAG) return reviveKeyed;
|
|
12691
|
+
if (key === REGEXP_TAG) return (data) => {
|
|
12692
|
+
if (!Array.isArray(data) || typeof data[0] !== "string" || typeof data[1] !== "string" || !Number.isSafeInteger(data[2])) throw new TypeError("spindle: Malformed regular expression");
|
|
12693
|
+
const re = new RegExp(data[0], data[1]);
|
|
12694
|
+
re.lastIndex = data[2];
|
|
12695
|
+
return re;
|
|
12696
|
+
};
|
|
12685
12697
|
if (key.startsWith(CLASS_PREFIX)) return reviveClass(key.slice(2));
|
|
12686
12698
|
if (key.startsWith(ERROR_PREFIX)) return reviveError(key.slice(2));
|
|
12687
12699
|
}
|
|
@@ -12793,6 +12805,7 @@ function isOptionalPRNGSnapshot(value) {
|
|
|
12793
12805
|
return isRecord$1(value) && typeof value.seed === "string" && typeof value.pull === "number" && Number.isInteger(value.pull) && value.pull >= 0;
|
|
12794
12806
|
}
|
|
12795
12807
|
var isMoment = (value) => isRecord$1(value) && typeof value.passage === "string" && isRecord$1(value.variables) && typeof value.timestamp === "number" && isOptionalPRNGSnapshot(value.prng);
|
|
12808
|
+
var isWatchers = (value) => value === void 0 || Array.isArray(value) && Array.from(value).every((w) => isRecord$1(w) && typeof w.condition === "string" && isRecord$1(w.options));
|
|
12796
12809
|
var isCountsMap = (value) => value === void 0 || value instanceof Map && [...value.keys()].every((k) => typeof k === "string");
|
|
12797
12810
|
/**
|
|
12798
12811
|
* Whether a deserialized payload is well formed: every history moment is,
|
|
@@ -12806,7 +12819,7 @@ function isPayload(value) {
|
|
|
12806
12819
|
if (!Array.isArray(history) || history.length === 0) return false;
|
|
12807
12820
|
if (!Array.from(history).every(isMoment)) return false;
|
|
12808
12821
|
if (typeof historyIndex !== "number" || !Number.isInteger(historyIndex) || historyIndex < 0 || historyIndex >= history.length || history[historyIndex].passage !== value.passage) return false;
|
|
12809
|
-
return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng);
|
|
12822
|
+
return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng) && isWatchers(value.watchers);
|
|
12810
12823
|
}
|
|
12811
12824
|
var mapToCounts = (counts) => counts instanceof Map ? createCounts(Object.fromEntries(counts)) : void 0;
|
|
12812
12825
|
/**
|
|
@@ -13406,7 +13419,11 @@ function deepClone(value, options = {}) {
|
|
|
13406
13419
|
}
|
|
13407
13420
|
function cloneBuiltin(val) {
|
|
13408
13421
|
if (val instanceof Date) return keep(val, new Date(val.getTime()));
|
|
13409
|
-
if (val instanceof RegExp)
|
|
13422
|
+
if (val instanceof RegExp) {
|
|
13423
|
+
const copy = new RegExp(val.source, val.flags);
|
|
13424
|
+
copy.lastIndex = val.lastIndex;
|
|
13425
|
+
return keep(val, copy);
|
|
13426
|
+
}
|
|
13410
13427
|
if (val instanceof ArrayBuffer) return keep(val, val.slice(0));
|
|
13411
13428
|
if (ArrayBuffer.isView(val)) {
|
|
13412
13429
|
const buffer = clone(val.buffer);
|
|
@@ -13589,7 +13606,7 @@ function equalBytes(a, b) {
|
|
|
13589
13606
|
/** Equality of the content of atomic built-ins (see isAtomic) but Map/Set. */
|
|
13590
13607
|
function equalBuiltin(a, b, assumed) {
|
|
13591
13608
|
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
13592
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
13609
|
+
if (a instanceof RegExp) return String(a) === String(b) && a.lastIndex === b.lastIndex;
|
|
13593
13610
|
if (ArrayBuffer.isView(a)) {
|
|
13594
13611
|
const v = b;
|
|
13595
13612
|
return a.byteOffset === v.byteOffset && a.byteLength === v.byteLength && equal(a.buffer, v.buffer, assumed);
|
|
@@ -14148,15 +14165,17 @@ var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(i
|
|
|
14148
14165
|
var startNewPlaythrough = queued(startNewPlaythroughNow);
|
|
14149
14166
|
async function startNewPlaythroughNow(ifid, id = randomUUID()) {
|
|
14150
14167
|
const backend = await getBackend();
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14168
|
+
await withTabLock(`spindle-playthrough-count:${ifid}`, async () => {
|
|
14169
|
+
const num = await nextPlaythroughNumber(ifid);
|
|
14170
|
+
const record = {
|
|
14171
|
+
id,
|
|
14172
|
+
ifid,
|
|
14173
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14174
|
+
label: `Playthrough ${num}`
|
|
14175
|
+
};
|
|
14176
|
+
await backend.putPlaythrough(record);
|
|
14177
|
+
await backend.setMeta(playthroughCountKey(ifid), num);
|
|
14178
|
+
});
|
|
14160
14179
|
await backend.setMeta(currentPlaythroughKey(ifid), id);
|
|
14161
14180
|
return id;
|
|
14162
14181
|
}
|
|
@@ -14409,12 +14428,41 @@ function slotMetaKey(ifid, slot) {
|
|
|
14409
14428
|
return isNamedSlot(slot) ? `${SLOT_KEY_PREFIX}${slot}.${ifid}` : `${AUTOSAVE_KEY_PREFIX}${ifid}`;
|
|
14410
14429
|
}
|
|
14411
14430
|
var slotIndexKey = (ifid) => `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
14412
|
-
/**
|
|
14431
|
+
/**
|
|
14432
|
+
* Run `op` holding the browser's lock `name`, so other tabs of the story
|
|
14433
|
+
* (the operation queue only orders this document's operations) wait for it.
|
|
14434
|
+
* Runs `op` without a lock where the browser offers none (or refuses it, as
|
|
14435
|
+
* in a sandboxed frame, which has no other tabs sharing its storage).
|
|
14436
|
+
*/
|
|
14437
|
+
async function withTabLock(name, op) {
|
|
14438
|
+
let locks;
|
|
14439
|
+
try {
|
|
14440
|
+
locks = navigator.locks;
|
|
14441
|
+
} catch {}
|
|
14442
|
+
if (!locks) return op();
|
|
14443
|
+
let started = false;
|
|
14444
|
+
try {
|
|
14445
|
+
return await locks.request(name, () => {
|
|
14446
|
+
started = true;
|
|
14447
|
+
return op();
|
|
14448
|
+
});
|
|
14449
|
+
} catch (err) {
|
|
14450
|
+
if (started) throw err;
|
|
14451
|
+
return op();
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
/**
|
|
14455
|
+
* Read-modify-write the per-story slot index. Under a lock shared by the
|
|
14456
|
+
* story's tabs: two tabs adding a slot at once would both read the old index
|
|
14457
|
+
* and the later write would drop the other's slot.
|
|
14458
|
+
*/
|
|
14413
14459
|
async function updateSlotIndex(ifid, update) {
|
|
14414
14460
|
const backend = await getBackend();
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14461
|
+
await withTabLock(`spindle-slot-index:${ifid}`, async () => {
|
|
14462
|
+
const existing = await backend.getMeta(slotIndexKey(ifid)) ?? [];
|
|
14463
|
+
const updated = update(existing);
|
|
14464
|
+
if (updated !== existing) await backend.setMeta(slotIndexKey(ifid), updated);
|
|
14465
|
+
});
|
|
14418
14466
|
}
|
|
14419
14467
|
/** Every slot: the default one (undefined), then those in the slot index. */
|
|
14420
14468
|
async function allSlots(ifid) {
|
|
@@ -14761,6 +14809,11 @@ function executeStoryInit() {
|
|
|
14761
14809
|
*/
|
|
14762
14810
|
function initializeStory(session) {
|
|
14763
14811
|
executeStoryInit();
|
|
14812
|
+
const missing = session ? missingPassage(useStoryStore.getState().storyData, session) : void 0;
|
|
14813
|
+
if (missing !== void 0) {
|
|
14814
|
+
console.warn(`spindle: discarding the saved session: the story no longer has the passage "${missing}"`);
|
|
14815
|
+
session = void 0;
|
|
14816
|
+
}
|
|
14764
14817
|
if (session) useStoryStore.getState().loadFromPayload(session);
|
|
14765
14818
|
emit("storyinit");
|
|
14766
14819
|
if (!session) recordStoryInitState();
|
|
@@ -15254,6 +15307,27 @@ function addMacroTrigger(condition, options) {
|
|
|
15254
15307
|
if (triggers.some((t) => t.macroKey === macroKey)) return;
|
|
15255
15308
|
registerTrigger(condition, options, macroKey);
|
|
15256
15309
|
}
|
|
15310
|
+
/**
|
|
15311
|
+
* The {watch} macro watchers now registered, for a save or the session:
|
|
15312
|
+
* watchers outlive the passage that declared them, so a game restored
|
|
15313
|
+
* where it stopped needs the ones its earlier passages registered (and
|
|
15314
|
+
* not the ones {unwatch} or `once` removed).
|
|
15315
|
+
*/
|
|
15316
|
+
function savedMacroWatchers() {
|
|
15317
|
+
return triggers.filter((t) => t.macroKey !== void 0 && t.options).map((t) => ({
|
|
15318
|
+
condition: t.condition,
|
|
15319
|
+
options: { ...t.options }
|
|
15320
|
+
}));
|
|
15321
|
+
}
|
|
15322
|
+
/**
|
|
15323
|
+
* Make the {watch} macro watchers those of a loaded game: the ones of
|
|
15324
|
+
* `watchers` replace the registered ones (watchers added by Story.watch
|
|
15325
|
+
* code stay). The passage shown mounts its own {watch} macros again.
|
|
15326
|
+
*/
|
|
15327
|
+
function restoreMacroWatchers(watchers) {
|
|
15328
|
+
triggers = triggers.filter((t) => t.macroKey === void 0);
|
|
15329
|
+
for (const { condition, options } of watchers) addMacroTrigger(condition, options);
|
|
15330
|
+
}
|
|
15257
15331
|
function removeTrigger(name) {
|
|
15258
15332
|
triggers = triggers.filter((t) => t.name !== name);
|
|
15259
15333
|
}
|
|
@@ -15609,7 +15683,8 @@ function persistSession(get) {
|
|
|
15609
15683
|
historyIndex,
|
|
15610
15684
|
visitCounts,
|
|
15611
15685
|
renderCounts,
|
|
15612
|
-
prng: snapshotPRNG()
|
|
15686
|
+
prng: snapshotPRNG(),
|
|
15687
|
+
watchers: savedMacroWatchers()
|
|
15613
15688
|
});
|
|
15614
15689
|
}
|
|
15615
15690
|
/** What the page shows before a session write error (see RuntimeErrors). */
|
|
@@ -15994,6 +16069,15 @@ function restorePRNGFrom(prng) {
|
|
|
15994
16069
|
function restorePRNGFromMoment(moment) {
|
|
15995
16070
|
if (moment) restorePRNGFrom(moment.prng);
|
|
15996
16071
|
}
|
|
16072
|
+
/**
|
|
16073
|
+
* The first passage of `payload` (its current passage, then its history)
|
|
16074
|
+
* that `storyData` does not have, as when the story was updated since the
|
|
16075
|
+
* save; undefined if the story has them all (or is not loaded).
|
|
16076
|
+
*/
|
|
16077
|
+
function missingPassage(storyData, payload) {
|
|
16078
|
+
if (!storyData) return void 0;
|
|
16079
|
+
return [payload.passage, ...payload.history.map((m) => m.passage)].find((name) => !storyData.passages.has(name));
|
|
16080
|
+
}
|
|
15997
16081
|
var NAMESPACE_KEYS = [
|
|
15998
16082
|
"variables",
|
|
15999
16083
|
"temporary",
|
|
@@ -16421,7 +16505,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16421
16505
|
historyIndex,
|
|
16422
16506
|
visitCounts: { ...visitCounts },
|
|
16423
16507
|
renderCounts: { ...renderCounts },
|
|
16424
|
-
prng: snapshotPRNG()
|
|
16508
|
+
prng: snapshotPRNG(),
|
|
16509
|
+
watchers: savedMacroWatchers()
|
|
16425
16510
|
};
|
|
16426
16511
|
},
|
|
16427
16512
|
beginSave: () => {
|
|
@@ -16433,12 +16518,15 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16433
16518
|
};
|
|
16434
16519
|
},
|
|
16435
16520
|
loadFromPayload: (payload, slot, playthroughId) => {
|
|
16436
|
-
const
|
|
16521
|
+
const applying = slotLoadApplying;
|
|
16437
16522
|
slotLoadApplying = null;
|
|
16523
|
+
const replacement = applying ?? ++stateReplacementsIssued;
|
|
16438
16524
|
if (payload.history.length === 0) {
|
|
16439
16525
|
console.warn("loadFromPayload: rejecting payload with empty history");
|
|
16440
16526
|
return;
|
|
16441
16527
|
}
|
|
16528
|
+
const missing = missingPassage(get().storyData, payload);
|
|
16529
|
+
if (missing !== void 0) throw new Error(`The save refers to the passage "${missing}", which this version of the story does not have`);
|
|
16442
16530
|
latestStateApplied = replacement;
|
|
16443
16531
|
emit("beforeload", slot);
|
|
16444
16532
|
const ifid = get().storyData?.ifid;
|
|
@@ -16464,6 +16552,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16464
16552
|
state.temporary = createNamespace();
|
|
16465
16553
|
state.transient = createNamespace(deepClone(get().transientDefaults));
|
|
16466
16554
|
});
|
|
16555
|
+
if (payload.watchers) restoreMacroWatchers(payload.watchers);
|
|
16467
16556
|
reinitTriggerState();
|
|
16468
16557
|
lastNavigationVars = reconstructVarsAt(get().historyIndex);
|
|
16469
16558
|
restorePRNGFrom(entry?.prng !== void 0 ? entry.prng : payload.prng);
|
|
@@ -16646,7 +16735,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16646
16735
|
}, []);
|
|
16647
16736
|
const handleBackdrop = (e) => {
|
|
16648
16737
|
if (!dismissible) return;
|
|
16649
|
-
if (e.target.
|
|
16738
|
+
if (e.target === e.currentTarget) stableOnClose();
|
|
16650
16739
|
};
|
|
16651
16740
|
const cls = panelClass ? `dialog-panel ${panelClass}` : "dialog-panel";
|
|
16652
16741
|
return /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
@@ -16661,6 +16750,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16661
16750
|
"aria-modal": "true",
|
|
16662
16751
|
tabIndex: -1,
|
|
16663
16752
|
children: [showCloseButton && /* @__PURE__ */ jsx("button", {
|
|
16753
|
+
type: "button",
|
|
16664
16754
|
class: "dialog-close",
|
|
16665
16755
|
"aria-label": "Close",
|
|
16666
16756
|
onClick: stableOnClose,
|
|
@@ -19086,7 +19176,7 @@ function keyOf(val, ancestors) {
|
|
|
19086
19176
|
const depth = ancestors.indexOf(obj);
|
|
19087
19177
|
if (depth !== -1) return `<cycle ${ancestors.length - depth}>`;
|
|
19088
19178
|
if (val instanceof Date) return `Date(${val.getTime()})`;
|
|
19089
|
-
if (val instanceof RegExp) return `RegExp(${String(val)})`;
|
|
19179
|
+
if (val instanceof RegExp) return `RegExp(${String(val)}@${val.lastIndex})`;
|
|
19090
19180
|
ancestors.push(obj);
|
|
19091
19181
|
try {
|
|
19092
19182
|
if (Array.isArray(val)) return `[${Array.from(val, (v) => keyOf(v, ancestors)).join(",")}]`;
|
|
@@ -19201,15 +19291,7 @@ defineMacro({
|
|
|
19201
19291
|
block: true,
|
|
19202
19292
|
render({ children = [] }, ctx) {
|
|
19203
19293
|
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;
|
|
19294
|
+
return useRunOnce("do", "", () => ctx.mutate(code));
|
|
19213
19295
|
}
|
|
19214
19296
|
});
|
|
19215
19297
|
//#endregion
|
|
@@ -19361,6 +19443,7 @@ defineMacro({
|
|
|
19361
19443
|
perform: handleClick
|
|
19362
19444
|
});
|
|
19363
19445
|
return /* @__PURE__ */ jsx("button", {
|
|
19446
|
+
type: "button",
|
|
19364
19447
|
id: ctx.id,
|
|
19365
19448
|
class: ctx.cls,
|
|
19366
19449
|
onClick: handleClick,
|
|
@@ -19517,15 +19600,52 @@ defineMacro({
|
|
|
19517
19600
|
});
|
|
19518
19601
|
//#endregion
|
|
19519
19602
|
//#region src/components/macros/Type.tsx
|
|
19603
|
+
/**
|
|
19604
|
+
* The clip-path that shows the first `count` characters of `root`'s text in
|
|
19605
|
+
* reading order: the boxes the browser laid out for them, each wrapped line
|
|
19606
|
+
* as far as its last revealed character, as one path in the coordinates of
|
|
19607
|
+
* `inner`'s box. Undefined where there is no layout to measure.
|
|
19608
|
+
*/
|
|
19609
|
+
function revealedPath(root, inner, count) {
|
|
19610
|
+
const range = document.createRange();
|
|
19611
|
+
if (!range.getClientRects) return void 0;
|
|
19612
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
19613
|
+
let left = count;
|
|
19614
|
+
let end;
|
|
19615
|
+
for (let n = walker.nextNode(); n && left > 0; n = walker.nextNode()) {
|
|
19616
|
+
const length = n.nodeValue?.length ?? 0;
|
|
19617
|
+
end = {
|
|
19618
|
+
node: n,
|
|
19619
|
+
offset: Math.min(left, length)
|
|
19620
|
+
};
|
|
19621
|
+
left -= length;
|
|
19622
|
+
}
|
|
19623
|
+
if (!end) return "inset(0 100% 0 0)";
|
|
19624
|
+
range.setStart(root, 0);
|
|
19625
|
+
range.setEnd(end.node, end.offset);
|
|
19626
|
+
const rects = range.getClientRects();
|
|
19627
|
+
if (rects.length === 0) return void 0;
|
|
19628
|
+
const origin = inner.getBoundingClientRect();
|
|
19629
|
+
let path = "";
|
|
19630
|
+
for (const r of Array.from(rects)) {
|
|
19631
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
19632
|
+
const x = (r.left - origin.left).toFixed(2);
|
|
19633
|
+
const y = (r.top - origin.top).toFixed(2);
|
|
19634
|
+
path += `M${x} ${y}h${r.width.toFixed(2)}v${r.height.toFixed(2)}h-${r.width.toFixed(2)}z`;
|
|
19635
|
+
}
|
|
19636
|
+
return path ? `path("${path}")` : void 0;
|
|
19637
|
+
}
|
|
19520
19638
|
defineMacro({
|
|
19521
19639
|
name: "type",
|
|
19522
19640
|
block: true,
|
|
19523
19641
|
interpolate: true,
|
|
19524
19642
|
parameters: [DELAY_PARAMETER],
|
|
19525
19643
|
render({ children = [] }, ctx) {
|
|
19526
|
-
const { useState, useEffect, useRef } = ctx.hooks;
|
|
19644
|
+
const { useState, useEffect, useLayoutEffect, useRef } = ctx.hooks;
|
|
19527
19645
|
const speed = ctx.args.delay ?? 0;
|
|
19528
19646
|
const containerRef = useRef(null);
|
|
19647
|
+
const innerRef = useRef(null);
|
|
19648
|
+
const [clip, setClip] = useState();
|
|
19529
19649
|
const [totalChars, setTotalChars] = useState(0);
|
|
19530
19650
|
const [visibleChars, setVisibleChars] = useState(0);
|
|
19531
19651
|
const visibleCharsRef = useRef(0);
|
|
@@ -19548,6 +19668,22 @@ defineMacro({
|
|
|
19548
19668
|
return () => clearInterval(timer);
|
|
19549
19669
|
}, [totalChars, speed]);
|
|
19550
19670
|
const done = visibleChars >= totalChars && totalChars > 0;
|
|
19671
|
+
useLayoutEffect(() => {
|
|
19672
|
+
const root = containerRef.current;
|
|
19673
|
+
const inner = innerRef.current;
|
|
19674
|
+
if (!root || !inner || totalChars === 0 || done) {
|
|
19675
|
+
setClip(void 0);
|
|
19676
|
+
return;
|
|
19677
|
+
}
|
|
19678
|
+
const measure = () => setClip(revealedPath(root, inner, visibleChars));
|
|
19679
|
+
measure();
|
|
19680
|
+
window.addEventListener("resize", measure);
|
|
19681
|
+
return () => window.removeEventListener("resize", measure);
|
|
19682
|
+
}, [
|
|
19683
|
+
visibleChars,
|
|
19684
|
+
totalChars,
|
|
19685
|
+
done
|
|
19686
|
+
]);
|
|
19551
19687
|
const cls = [
|
|
19552
19688
|
"macro-type",
|
|
19553
19689
|
done ? "macro-type-done" : "",
|
|
@@ -19557,13 +19693,13 @@ defineMacro({
|
|
|
19557
19693
|
id: ctx.id,
|
|
19558
19694
|
class: cls,
|
|
19559
19695
|
ref: containerRef,
|
|
19560
|
-
style: { clipPath: totalChars > 0 && !done ? void 0 : void 0 },
|
|
19561
19696
|
children: [/* @__PURE__ */ jsx("span", {
|
|
19697
|
+
ref: innerRef,
|
|
19562
19698
|
class: "macro-type-inner",
|
|
19563
19699
|
style: {
|
|
19564
19700
|
display: "inline",
|
|
19565
19701
|
visibility: totalChars === 0 ? "hidden" : "visible",
|
|
19566
|
-
clipPath:
|
|
19702
|
+
clipPath: clip
|
|
19567
19703
|
},
|
|
19568
19704
|
children: ctx.renderInlineNodes(children)
|
|
19569
19705
|
}), !done && totalChars > 0 && /* @__PURE__ */ jsx("span", { class: "macro-type-cursor" })]
|
|
@@ -19611,6 +19747,7 @@ function defineMenubarAction(config) {
|
|
|
19611
19747
|
perform: config.dialog ? () => setDialogOpen(true) : setup.perform
|
|
19612
19748
|
});
|
|
19613
19749
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19750
|
+
type: "button",
|
|
19614
19751
|
id: ctx.id,
|
|
19615
19752
|
class: cls,
|
|
19616
19753
|
onClick: perform,
|
|
@@ -19720,6 +19857,7 @@ defineMacro({
|
|
|
19720
19857
|
const label = ctx.resolve(ctx.args.label ?? "");
|
|
19721
19858
|
const passageName = ctx.collectText(children).trim().replace(/^["']|["']$/g, "");
|
|
19722
19859
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
19860
|
+
type: "button",
|
|
19723
19861
|
id: ctx.id,
|
|
19724
19862
|
class: ctx.cls,
|
|
19725
19863
|
onClick: () => setOpen(true),
|
|
@@ -19766,9 +19904,12 @@ function SaveManagerContent() {
|
|
|
19766
19904
|
const closeDialog = useContext(DialogCloseContext);
|
|
19767
19905
|
const { storyData, playthroughId, beginSave, loadFromPayload } = useStoryFields("storyData", "playthroughId", "beginSave", "loadFromPayload");
|
|
19768
19906
|
const ifid = storyData?.ifid ?? "";
|
|
19907
|
+
const [temporary, setTemporary] = useState(false);
|
|
19769
19908
|
const refresh = useCallback(async () => {
|
|
19770
19909
|
if (!ifid) return;
|
|
19771
|
-
|
|
19910
|
+
const data = await getSavesGrouped(ifid);
|
|
19911
|
+
setTemporary(getBackendType() === "memory");
|
|
19912
|
+
setGroups(data);
|
|
19772
19913
|
setLoading(false);
|
|
19773
19914
|
}, [ifid]);
|
|
19774
19915
|
useEffect(() => {
|
|
@@ -19815,7 +19956,7 @@ function SaveManagerContent() {
|
|
|
19815
19956
|
await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
|
|
19816
19957
|
return createSave(ifid, playthrough, payload);
|
|
19817
19958
|
});
|
|
19818
|
-
showStatus("Save created");
|
|
19959
|
+
showStatus(temporary ? "Save created (temporary)" : "Save created");
|
|
19819
19960
|
await refresh();
|
|
19820
19961
|
} catch {
|
|
19821
19962
|
showStatus("Failed to create save", "error");
|
|
@@ -19931,10 +20072,12 @@ function SaveManagerContent() {
|
|
|
19931
20072
|
children: /* @__PURE__ */ jsxs("div", {
|
|
19932
20073
|
class: "saves-mode-toggle",
|
|
19933
20074
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20075
|
+
type: "button",
|
|
19934
20076
|
class: mode === "save" ? "active" : "",
|
|
19935
20077
|
onClick: () => setMode("save"),
|
|
19936
20078
|
children: "Save"
|
|
19937
20079
|
}), /* @__PURE__ */ jsx("button", {
|
|
20080
|
+
type: "button",
|
|
19938
20081
|
class: mode === "load" ? "active" : "",
|
|
19939
20082
|
onClick: () => setMode("load"),
|
|
19940
20083
|
children: "Load"
|
|
@@ -19942,9 +20085,15 @@ function SaveManagerContent() {
|
|
|
19942
20085
|
})
|
|
19943
20086
|
})
|
|
19944
20087
|
}),
|
|
20088
|
+
temporary && /* @__PURE__ */ jsx("div", {
|
|
20089
|
+
class: "saves-notice",
|
|
20090
|
+
role: "status",
|
|
20091
|
+
children: "Saves can not be kept in this browser: they are lost when the page is reloaded or closed. Export a save to keep it."
|
|
20092
|
+
}),
|
|
19945
20093
|
/* @__PURE__ */ jsxs("div", {
|
|
19946
20094
|
class: "saves-toolbar",
|
|
19947
20095
|
children: [/* @__PURE__ */ jsx("button", {
|
|
20096
|
+
type: "button",
|
|
19948
20097
|
class: "saves-toolbar-button",
|
|
19949
20098
|
onClick: handleImport,
|
|
19950
20099
|
children: "Import"
|
|
@@ -20012,25 +20161,30 @@ function SaveManagerContent() {
|
|
|
20012
20161
|
class: "save-slot-actions",
|
|
20013
20162
|
children: [
|
|
20014
20163
|
mode === "save" ? /* @__PURE__ */ jsx("button", {
|
|
20164
|
+
type: "button",
|
|
20015
20165
|
class: "save-slot-action primary",
|
|
20016
20166
|
onClick: () => handleOverwrite(save.meta.id),
|
|
20017
20167
|
children: "Save Here"
|
|
20018
20168
|
}) : /* @__PURE__ */ jsx("button", {
|
|
20169
|
+
type: "button",
|
|
20019
20170
|
class: "save-slot-action primary",
|
|
20020
20171
|
onClick: () => handleLoad(save),
|
|
20021
20172
|
children: "Load"
|
|
20022
20173
|
}),
|
|
20023
20174
|
/* @__PURE__ */ jsx("button", {
|
|
20175
|
+
type: "button",
|
|
20024
20176
|
class: "save-slot-action",
|
|
20025
20177
|
onClick: () => handleRenameStart(save),
|
|
20026
20178
|
children: "Rename"
|
|
20027
20179
|
}),
|
|
20028
20180
|
/* @__PURE__ */ jsx("button", {
|
|
20181
|
+
type: "button",
|
|
20029
20182
|
class: "save-slot-action",
|
|
20030
20183
|
onClick: () => handleExport(save.meta.id),
|
|
20031
20184
|
children: "Export"
|
|
20032
20185
|
}),
|
|
20033
20186
|
/* @__PURE__ */ jsx("button", {
|
|
20187
|
+
type: "button",
|
|
20034
20188
|
class: "save-slot-action danger",
|
|
20035
20189
|
onClick: () => handleDelete(save.meta.id),
|
|
20036
20190
|
children: "Delete"
|
|
@@ -20038,6 +20192,7 @@ function SaveManagerContent() {
|
|
|
20038
20192
|
]
|
|
20039
20193
|
})]
|
|
20040
20194
|
}, save.meta.id)), mode === "save" && isCurrentPt && /* @__PURE__ */ jsx("button", {
|
|
20195
|
+
type: "button",
|
|
20041
20196
|
class: "save-slot-new",
|
|
20042
20197
|
onClick: handleNewSave,
|
|
20043
20198
|
children: "+ New Save"
|
|
@@ -20049,6 +20204,7 @@ function SaveManagerContent() {
|
|
|
20049
20204
|
children: /* @__PURE__ */ jsx("div", {
|
|
20050
20205
|
class: "playthrough-saves",
|
|
20051
20206
|
children: /* @__PURE__ */ jsx("button", {
|
|
20207
|
+
type: "button",
|
|
20052
20208
|
class: "save-slot-new",
|
|
20053
20209
|
onClick: handleNewSave,
|
|
20054
20210
|
children: "+ New Save"
|
|
@@ -20420,6 +20576,7 @@ defineMacro({
|
|
|
20420
20576
|
}
|
|
20421
20577
|
});
|
|
20422
20578
|
return /* @__PURE__ */ jsx("button", {
|
|
20579
|
+
type: "button",
|
|
20423
20580
|
id: ctx.id,
|
|
20424
20581
|
class: ctx.cls,
|
|
20425
20582
|
onClick: handleClick,
|
|
@@ -20594,7 +20751,7 @@ function resolveTransition(targetTags, nextTransition, storeDefault) {
|
|
|
20594
20751
|
/**
|
|
20595
20752
|
* Create a snapshot clone of a .passage element for outgoing transitions.
|
|
20596
20753
|
* - Strips `id` attributes from the clone tree
|
|
20597
|
-
* - Disables interaction (pointer-events, user-select)
|
|
20754
|
+
* - Disables interaction (pointer-events, user-select, inert, aria-hidden)
|
|
20598
20755
|
* - Pauses/mutes any <audio>/<video>
|
|
20599
20756
|
* - Adds `.passage-snapshot` class and `data-transition` attribute
|
|
20600
20757
|
*/
|
|
@@ -20605,6 +20762,8 @@ function createSnapshot(passageEl, transitionType) {
|
|
|
20605
20762
|
for (let i = 0; i < idEls.length; i++) idEls[i].removeAttribute("id");
|
|
20606
20763
|
clone.style.pointerEvents = "none";
|
|
20607
20764
|
clone.style.userSelect = "none";
|
|
20765
|
+
clone.setAttribute("inert", "");
|
|
20766
|
+
clone.setAttribute("aria-hidden", "true");
|
|
20608
20767
|
const mediaEls = clone.querySelectorAll("audio, video");
|
|
20609
20768
|
for (let i = 0; i < mediaEls.length; i++) {
|
|
20610
20769
|
const media = mediaEls[i];
|
|
@@ -20843,7 +21002,7 @@ defineMacro({
|
|
|
20843
21002
|
});
|
|
20844
21003
|
//#endregion
|
|
20845
21004
|
//#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";
|
|
21005
|
+
var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n/* Runtime errors shown on the page (see src/components/RuntimeErrors.tsx) */\n.spindle-error-banners {\n position: fixed;\n top: 0.75em;\n left: 50%;\n z-index: 300;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n width: min(42em, calc(100% - 1.5em));\n transform: translateX(-50%);\n}\n\n.spindle-error-banner {\n position: relative;\n padding: 0.75em 2.75em 0.75em 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: #2b1a1e;\n color: #ffcdd2;\n font-size: 0.9em;\n line-height: 1.5;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n}\n\n.spindle-error-banner-message {\n font-family: monospace;\n color: #ef9a9a;\n}\n\n.spindle-error-banner-dismiss {\n position: absolute;\n top: 0.5em;\n right: 0.5em;\n background: transparent;\n border: none;\n color: inherit;\n font-size: 1em;\n cursor: pointer;\n}\n\n.spindle-error-banner-dismiss:hover {\n color: #fff;\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n/* Keep the first control clear of the close button above it */\n.dialog-settings .dialog-body {\n padding-top: 2.25em;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-notice {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #ffb74d;\n border-bottom: 1px solid rgba(255, 183, 77, 0.3);\n background: rgba(255, 183, 77, 0.08);\n flex-shrink: 0;\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
|
|
20847
21006
|
//#endregion
|
|
20848
21007
|
//#region src/index.tsx
|
|
20849
21008
|
function renderErrors(root, errors) {
|
package/package.json
CHANGED
package/src/class-registry.ts
CHANGED
|
@@ -51,7 +51,9 @@ export function clearRegistry(): void {
|
|
|
51
51
|
// with its message, cause, errors and own enumerable keys (not its stack);
|
|
52
52
|
// - `S`: a symbol from the global registry (Symbol.for);
|
|
53
53
|
// - `K`: a plain object holding a one-character key that JSON escapes (see
|
|
54
|
-
// GUARD_V8_KEYS)
|
|
54
|
+
// GUARD_V8_KEYS);
|
|
55
|
+
// - `R`: a RegExp whose lastIndex is not zero (the scanning cursor of a
|
|
56
|
+
// global or sticky pattern), with its source, flags and lastIndex.
|
|
55
57
|
//
|
|
56
58
|
// Everything else is refused when saving, with the path to the value:
|
|
57
59
|
// functions, unique symbols, symbol keys, instances of unregistered classes,
|
|
@@ -61,6 +63,7 @@ const CLASS_PREFIX = 'c:';
|
|
|
61
63
|
const ERROR_PREFIX = 'E:';
|
|
62
64
|
const SYMBOL_TAG = 'S';
|
|
63
65
|
const KEYS_TAG = 'K';
|
|
66
|
+
const REGEXP_TAG = 'R';
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Work around Chromium issue 521080746 (V8 in Chrome/Chromium 14x-153+,
|
|
@@ -167,6 +170,9 @@ function reducers(): Record<string, (value: unknown) => unknown> {
|
|
|
167
170
|
const key = Symbol.keyFor(v);
|
|
168
171
|
return key !== undefined && [key];
|
|
169
172
|
};
|
|
173
|
+
out[REGEXP_TAG] = (v) =>
|
|
174
|
+
v instanceof RegExp &&
|
|
175
|
+
v.lastIndex !== 0 && [v.source, v.flags, v.lastIndex];
|
|
170
176
|
if (GUARD_V8_KEYS) {
|
|
171
177
|
out[KEYS_TAG] = (v) => {
|
|
172
178
|
if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) {
|
|
@@ -264,6 +270,21 @@ function reviverFor(key: string | symbol) {
|
|
|
264
270
|
};
|
|
265
271
|
}
|
|
266
272
|
if (key === KEYS_TAG) return reviveKeyed;
|
|
273
|
+
if (key === REGEXP_TAG) {
|
|
274
|
+
return (data: unknown) => {
|
|
275
|
+
if (
|
|
276
|
+
!Array.isArray(data) ||
|
|
277
|
+
typeof data[0] !== 'string' ||
|
|
278
|
+
typeof data[1] !== 'string' ||
|
|
279
|
+
!Number.isSafeInteger(data[2])
|
|
280
|
+
) {
|
|
281
|
+
throw new TypeError('spindle: Malformed regular expression');
|
|
282
|
+
}
|
|
283
|
+
const re = new RegExp(data[0], data[1]);
|
|
284
|
+
re.lastIndex = data[2] as number;
|
|
285
|
+
return re;
|
|
286
|
+
};
|
|
287
|
+
}
|
|
267
288
|
if (key.startsWith(CLASS_PREFIX)) {
|
|
268
289
|
return reviveClass(key.slice(CLASS_PREFIX.length));
|
|
269
290
|
}
|