@rohal12/spindle 0.59.7 → 0.59.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +136 -50
- package/dist/pkg/story-variables.js +13 -2
- package/package.json +1 -1
- package/src/components/App.tsx +4 -4
- package/src/components/macros/MenubarButtons.tsx +8 -5
- package/src/components/macros/PassageDisplay.tsx +14 -4
- package/src/components/macros/SaveManager.tsx +15 -2
- package/src/components/macros/detached-body.tsx +16 -3
- package/src/expression.ts +9 -21
- package/src/hooks/use-modal-focus.ts +15 -1
- package/src/quick-actions.ts +23 -0
- package/src/saves/save-manager.ts +14 -4
- package/src/saves/storage.ts +10 -8
- package/src/settings.ts +5 -1
- package/src/store.ts +8 -4
- package/src/styles.css +5 -0
- package/src/widgets/widget-def.ts +15 -3
package/dist/pkg/headless.js
CHANGED
|
@@ -10096,18 +10096,18 @@ function historyQueries(visitCounts, renderCounts) {
|
|
|
10096
10096
|
};
|
|
10097
10097
|
}
|
|
10098
10098
|
var cachedFns = null;
|
|
10099
|
-
|
|
10100
|
-
|
|
10099
|
+
/**
|
|
10100
|
+
* The functions compiled code calls. The history queries read the counters
|
|
10101
|
+
* when called, not when the functions were built: mutation code that
|
|
10102
|
+
* navigates (`Story.goto()`) goes on with the counters of the new state,
|
|
10103
|
+
* as the Story API does.
|
|
10104
|
+
*/
|
|
10101
10105
|
function buildExpressionFns() {
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
cachedFns = {
|
|
10105
|
-
...historyQueries(() => visitCounts, () => renderCounts),
|
|
10106
|
+
cachedFns ??= {
|
|
10107
|
+
...historyQueries(() => useStoryStore.getState().visitCounts, () => useStoryStore.getState().renderCounts),
|
|
10106
10108
|
random,
|
|
10107
10109
|
randomInt
|
|
10108
10110
|
};
|
|
10109
|
-
cachedVisitCounts = visitCounts;
|
|
10110
|
-
cachedRenderCounts = renderCounts;
|
|
10111
10111
|
return cachedFns;
|
|
10112
10112
|
}
|
|
10113
10113
|
/**
|
|
@@ -13176,24 +13176,28 @@ function createIDBBackend() {
|
|
|
13176
13176
|
}
|
|
13177
13177
|
var _backend = null;
|
|
13178
13178
|
async function detectBackend() {
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
req.
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13179
|
+
try {
|
|
13180
|
+
if (typeof indexedDB !== "undefined") {
|
|
13181
|
+
await new Promise((resolve, reject) => {
|
|
13182
|
+
const req = indexedDB.open("__spindle_probe__", 1);
|
|
13183
|
+
req.onsuccess = () => {
|
|
13184
|
+
req.result.close();
|
|
13185
|
+
indexedDB.deleteDatabase("__spindle_probe__");
|
|
13186
|
+
resolve();
|
|
13187
|
+
};
|
|
13188
|
+
req.onerror = () => reject(req.error);
|
|
13189
|
+
});
|
|
13190
|
+
return createIDBBackend();
|
|
13191
|
+
}
|
|
13190
13192
|
} catch {}
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13193
|
+
try {
|
|
13194
|
+
if (typeof localStorage !== "undefined") {
|
|
13195
|
+
const probeKey = "__spindle_probe__";
|
|
13196
|
+
localStorage.setItem(probeKey, "1");
|
|
13197
|
+
const val = localStorage.getItem(probeKey);
|
|
13198
|
+
localStorage.removeItem(probeKey);
|
|
13199
|
+
if (val === "1") return createLocalStorageBackend();
|
|
13200
|
+
}
|
|
13197
13201
|
} catch {}
|
|
13198
13202
|
return createMemoryBackend();
|
|
13199
13203
|
}
|
|
@@ -14463,7 +14467,7 @@ function toSaveInfo(record, slot) {
|
|
|
14463
14467
|
* (settled by operations issued before this call), so the save takes its
|
|
14464
14468
|
* place in the order of operations when it is called.
|
|
14465
14469
|
*/
|
|
14466
|
-
function quickSave(ifid, playthroughId, payload, slot, custom) {
|
|
14470
|
+
function quickSave$1(ifid, playthroughId, payload, slot, custom) {
|
|
14467
14471
|
return inOrder(async () => {
|
|
14468
14472
|
const ptId = await playthroughId;
|
|
14469
14473
|
const existingId = await slotSaveId(ifid, slot);
|
|
@@ -14549,12 +14553,18 @@ var SESSION_KEY_PREFIX = "spindle.session.";
|
|
|
14549
14553
|
* encodePayload). Throws, like a save, when the state holds a value a save
|
|
14550
14554
|
* cannot (a function, an instance of an unregistered class, a unique
|
|
14551
14555
|
* symbol); the session then keeps its previous copy.
|
|
14556
|
+
*
|
|
14557
|
+
* A storage that is unavailable or full is returned, not thrown: the session
|
|
14558
|
+
* keeps its previous copy then too, but the story can go on, so the caller
|
|
14559
|
+
* decides how to tell the player.
|
|
14552
14560
|
*/
|
|
14553
14561
|
function saveSession(ifid, payload) {
|
|
14554
14562
|
const text = JSON.stringify(encodePayload(payload));
|
|
14555
14563
|
try {
|
|
14556
14564
|
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, text);
|
|
14557
|
-
} catch {
|
|
14565
|
+
} catch (error) {
|
|
14566
|
+
return { error };
|
|
14567
|
+
}
|
|
14558
14568
|
}
|
|
14559
14569
|
/**
|
|
14560
14570
|
* The live payload of the stored session, if there is one that loads. A
|
|
@@ -14671,14 +14681,14 @@ var clearAllData = queued(clearAllDataNow);
|
|
|
14671
14681
|
async function clearAllDataNow() {
|
|
14672
14682
|
await (await getBackend()).destroy();
|
|
14673
14683
|
resetBackend();
|
|
14674
|
-
|
|
14684
|
+
try {
|
|
14675
14685
|
const toRemove = [];
|
|
14676
14686
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
14677
14687
|
const key = sessionStorage.key(i);
|
|
14678
14688
|
if (key?.startsWith("spindle.session.")) toRemove.push(key);
|
|
14679
14689
|
}
|
|
14680
14690
|
for (const key of toRemove) sessionStorage.removeItem(key);
|
|
14681
|
-
}
|
|
14691
|
+
} catch {}
|
|
14682
14692
|
}
|
|
14683
14693
|
/**
|
|
14684
14694
|
* Delete a playthrough with its saves, emptying the slots that held them.
|
|
@@ -15584,7 +15594,7 @@ var sessionMoments = [];
|
|
|
15584
15594
|
*/
|
|
15585
15595
|
function persistSession(get) {
|
|
15586
15596
|
const { storyData, currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
15587
|
-
if (!storyData) return;
|
|
15597
|
+
if (!storyData) return void 0;
|
|
15588
15598
|
if (sessionMoments.length > history.length) sessionMoments.length = history.length;
|
|
15589
15599
|
if (sessionMoments.length < history.length) if (history.length - sessionMoments.length === 1) {
|
|
15590
15600
|
const i = history.length - 1;
|
|
@@ -15592,7 +15602,7 @@ function persistSession(get) {
|
|
|
15592
15602
|
} else forEachRecorded(history.length, (vars, i) => {
|
|
15593
15603
|
if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
|
|
15594
15604
|
});
|
|
15595
|
-
saveSession(storyData.ifid, {
|
|
15605
|
+
return saveSession(storyData.ifid, {
|
|
15596
15606
|
passage: currentPassage,
|
|
15597
15607
|
variables,
|
|
15598
15608
|
history: sessionMoments,
|
|
@@ -15618,7 +15628,8 @@ var NAVIGATION_ERROR_CONTEXT = "The story could not go to another passage:";
|
|
|
15618
15628
|
function persistSessionThen(get, after) {
|
|
15619
15629
|
let failure;
|
|
15620
15630
|
try {
|
|
15621
|
-
persistSession(get);
|
|
15631
|
+
const storageFailure = persistSession(get);
|
|
15632
|
+
if (storageFailure) showRuntimeError(SESSION_ERROR_CONTEXT, storageFailure.error);
|
|
15622
15633
|
} catch (error) {
|
|
15623
15634
|
failure = { error };
|
|
15624
15635
|
showRuntimeError(SESSION_ERROR_CONTEXT, error);
|
|
@@ -16312,7 +16323,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
16312
16323
|
set((state) => {
|
|
16313
16324
|
state.saveError = null;
|
|
16314
16325
|
});
|
|
16315
|
-
await quickSave(storyData.ifid, playthrough, payload, slot, custom);
|
|
16326
|
+
await quickSave$1(storyData.ifid, playthrough, payload, slot, custom);
|
|
16316
16327
|
recordKnownSave(set, slot, true);
|
|
16317
16328
|
}), "spindle: failed to save", (err) => set((state) => {
|
|
16318
16329
|
state.saveError = errorMessage(err, "Failed to save");
|
|
@@ -16525,8 +16536,19 @@ var FOCUSABLE = [
|
|
|
16525
16536
|
"[contenteditable=\"true\"]",
|
|
16526
16537
|
"[tabindex]:not([tabindex=\"-1\"])"
|
|
16527
16538
|
].join(",");
|
|
16539
|
+
/**
|
|
16540
|
+
* Whether CSS leaves `el` in the tab sequence: not `visibility: hidden`
|
|
16541
|
+
* (inherited, so the computed value covers ancestors) and not inside a
|
|
16542
|
+
* `display: none` element.
|
|
16543
|
+
*/
|
|
16544
|
+
function isRendered(el) {
|
|
16545
|
+
const { visibility } = getComputedStyle(el);
|
|
16546
|
+
if (visibility === "hidden" || visibility === "collapse") return false;
|
|
16547
|
+
for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).display === "none") return false;
|
|
16548
|
+
return true;
|
|
16549
|
+
}
|
|
16528
16550
|
function focusables(root) {
|
|
16529
|
-
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]"));
|
|
16551
|
+
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && !el.closest("[hidden], [inert]") && isRendered(el));
|
|
16530
16552
|
}
|
|
16531
16553
|
/** Open modal dialogs, innermost last. Only the topmost handles keys. */
|
|
16532
16554
|
var openModals = [];
|
|
@@ -16742,18 +16764,39 @@ function RuntimeErrors() {
|
|
|
16742
16764
|
});
|
|
16743
16765
|
}
|
|
16744
16766
|
//#endregion
|
|
16767
|
+
//#region src/quick-actions.ts
|
|
16768
|
+
/**
|
|
16769
|
+
* QuickSave and QuickLoad as the built-in buttons and hotkeys perform them:
|
|
16770
|
+
* a failure is shown on the page (the Story API's save() and load() only
|
|
16771
|
+
* reject, for the code that awaits them).
|
|
16772
|
+
*/
|
|
16773
|
+
async function quickSave() {
|
|
16774
|
+
try {
|
|
16775
|
+
await useStoryStore.getState().save();
|
|
16776
|
+
} catch (error) {
|
|
16777
|
+
showRuntimeError("The game could not be saved:", error);
|
|
16778
|
+
}
|
|
16779
|
+
}
|
|
16780
|
+
async function quickLoad() {
|
|
16781
|
+
try {
|
|
16782
|
+
await useStoryStore.getState().load();
|
|
16783
|
+
} catch (error) {
|
|
16784
|
+
showRuntimeError("The saved game could not be loaded:", error);
|
|
16785
|
+
}
|
|
16786
|
+
}
|
|
16787
|
+
//#endregion
|
|
16745
16788
|
//#region src/components/App.tsx
|
|
16746
16789
|
function App() {
|
|
16747
16790
|
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
16748
16791
|
useEffect(() => {
|
|
16749
16792
|
const onKeyDown = (e) => {
|
|
16750
|
-
const { quickSaveKey, quickLoadKey
|
|
16793
|
+
const { quickSaveKey, quickLoadKey } = useStoryStore.getState();
|
|
16751
16794
|
if (quickSaveKey !== null && e.key === quickSaveKey) {
|
|
16752
16795
|
e.preventDefault();
|
|
16753
|
-
|
|
16796
|
+
quickSave();
|
|
16754
16797
|
} else if (quickLoadKey !== null && e.key === quickLoadKey) {
|
|
16755
16798
|
e.preventDefault();
|
|
16756
|
-
|
|
16799
|
+
quickLoad();
|
|
16757
16800
|
}
|
|
16758
16801
|
};
|
|
16759
16802
|
document.addEventListener("keydown", onKeyDown);
|
|
@@ -16931,7 +16974,9 @@ function storageKey() {
|
|
|
16931
16974
|
return `spindle.${useStoryStore.getState().storyData?.ifid || "unknown"}.settings`;
|
|
16932
16975
|
}
|
|
16933
16976
|
function persist() {
|
|
16934
|
-
|
|
16977
|
+
try {
|
|
16978
|
+
localStorage.setItem(storageKey(), JSON.stringify(values));
|
|
16979
|
+
} catch {}
|
|
16935
16980
|
}
|
|
16936
16981
|
function loadFromStorage() {
|
|
16937
16982
|
if (storageLoaded) return;
|
|
@@ -18215,7 +18260,13 @@ function widgetDef({ name = "", parameters = "" }) {
|
|
|
18215
18260
|
function parseWidgetDef(rawArgs) {
|
|
18216
18261
|
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
18217
18262
|
}
|
|
18218
|
-
|
|
18263
|
+
/**
|
|
18264
|
+
* A definition: its arguments (read like a {widget} macro's, see
|
|
18265
|
+
* parseWidgetDef) and its body. Macro names are not case-sensitive.
|
|
18266
|
+
*/
|
|
18267
|
+
var WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
|
|
18268
|
+
/** `{@children}`, also with selectors such as `{.highlight @children}`. */
|
|
18269
|
+
var CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
|
|
18219
18270
|
/**
|
|
18220
18271
|
* The names of the block widgets (those whose body renders `{@children}`)
|
|
18221
18272
|
* that StoryInit and the passages tagged `widget` define. They must be
|
|
@@ -18226,7 +18277,12 @@ function blockWidgetNames(passages) {
|
|
|
18226
18277
|
const names = [];
|
|
18227
18278
|
for (const passage of passages) {
|
|
18228
18279
|
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
18229
|
-
for (const match of passage.content.matchAll(
|
|
18280
|
+
for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
|
|
18281
|
+
if (!CHILDREN_PLACEHOLDER.test(match[2])) continue;
|
|
18282
|
+
try {
|
|
18283
|
+
names.push(parseWidgetDef(match[1]).name);
|
|
18284
|
+
} catch {}
|
|
18285
|
+
}
|
|
18230
18286
|
}
|
|
18231
18287
|
return names;
|
|
18232
18288
|
}
|
|
@@ -19220,7 +19276,10 @@ defineMacro({
|
|
|
19220
19276
|
* Return a function that runs a macro body once, outside the passage tree:
|
|
19221
19277
|
* it renders `children` into a detached node, so the body's macros ({set},
|
|
19222
19278
|
* {if}, {goto}, ...) fire their side effects through the normal Preact
|
|
19223
|
-
* pipeline
|
|
19279
|
+
* pipeline. Used by {button} and {link} on click. The body stays mounted
|
|
19280
|
+
* until the owning macro unmounts (its passage changes, say), so work it
|
|
19281
|
+
* starts in an effect, such as the timer of a {timed} or {repeat}, can
|
|
19282
|
+
* finish; the passage leaving cancels it.
|
|
19224
19283
|
*
|
|
19225
19284
|
* A detached render starts with no contexts, so the hook captures the ones
|
|
19226
19285
|
* the owning macro sees and provides them again: the locals scope (read
|
|
@@ -19241,6 +19300,10 @@ function useDetachedBody() {
|
|
|
19241
19300
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
19242
19301
|
const repeat = useContext(RepeatContext);
|
|
19243
19302
|
const closeDialog = useContext(DialogCloseContext);
|
|
19303
|
+
const mounted = useRef([]);
|
|
19304
|
+
useEffect(() => () => {
|
|
19305
|
+
for (const container of mounted.current.splice(0)) render(null, container);
|
|
19306
|
+
}, []);
|
|
19244
19307
|
const run = (children) => {
|
|
19245
19308
|
const locals = liveLocalsView(updater.getValues);
|
|
19246
19309
|
const container = document.createElement("div");
|
|
@@ -19270,7 +19333,7 @@ function useDetachedBody() {
|
|
|
19270
19333
|
})
|
|
19271
19334
|
})
|
|
19272
19335
|
}), container);
|
|
19273
|
-
|
|
19336
|
+
mounted.current.push(container);
|
|
19274
19337
|
};
|
|
19275
19338
|
return (children) => runWithCommittedMutations(() => run(children));
|
|
19276
19339
|
}
|
|
@@ -19587,9 +19650,9 @@ defineMenubarAction({
|
|
|
19587
19650
|
label: "QuickSave",
|
|
19588
19651
|
actionType: "save",
|
|
19589
19652
|
setup: () => {
|
|
19590
|
-
const {
|
|
19653
|
+
const { quickSaveKey } = useStoryFields("quickSaveKey");
|
|
19591
19654
|
return {
|
|
19592
|
-
perform:
|
|
19655
|
+
perform: quickSave,
|
|
19593
19656
|
title: hotkeyTitle("Quick Save", quickSaveKey)
|
|
19594
19657
|
};
|
|
19595
19658
|
}
|
|
@@ -19600,9 +19663,9 @@ defineMenubarAction({
|
|
|
19600
19663
|
actionType: "load",
|
|
19601
19664
|
confirm: "Load saved game? Current progress will be lost.",
|
|
19602
19665
|
setup: () => {
|
|
19603
|
-
const {
|
|
19666
|
+
const { hasSave, quickLoadKey } = useStoryFields("hasSave", "quickLoadKey", "knownSaves");
|
|
19604
19667
|
return {
|
|
19605
|
-
perform:
|
|
19668
|
+
perform: quickLoad,
|
|
19606
19669
|
disabled: !hasSave(),
|
|
19607
19670
|
title: hotkeyTitle("Quick Load", quickLoadKey)
|
|
19608
19671
|
};
|
|
@@ -19696,6 +19759,7 @@ function SaveManagerContent() {
|
|
|
19696
19759
|
const [status, setStatus] = useState(null);
|
|
19697
19760
|
const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
|
|
19698
19761
|
const [renamingId, setRenamingId] = useState(null);
|
|
19762
|
+
const renameCancelled = useRef(false);
|
|
19699
19763
|
const [renameValue, setRenameValue] = useState("");
|
|
19700
19764
|
const renameInputRef = useRef(null);
|
|
19701
19765
|
const fileInputRef = useRef(null);
|
|
@@ -19794,6 +19858,7 @@ function SaveManagerContent() {
|
|
|
19794
19858
|
}
|
|
19795
19859
|
};
|
|
19796
19860
|
const handleRenameStart = (save) => {
|
|
19861
|
+
renameCancelled.current = false;
|
|
19797
19862
|
setRenamingId(save.meta.id);
|
|
19798
19863
|
setRenameValue(save.meta.title);
|
|
19799
19864
|
};
|
|
@@ -19810,7 +19875,15 @@ function SaveManagerContent() {
|
|
|
19810
19875
|
};
|
|
19811
19876
|
const handleRenameKeyDown = (e) => {
|
|
19812
19877
|
if (e.key === "Enter") handleRenameConfirm();
|
|
19813
|
-
else if (e.key === "Escape")
|
|
19878
|
+
else if (e.key === "Escape") {
|
|
19879
|
+
e.preventDefault();
|
|
19880
|
+
e.stopPropagation();
|
|
19881
|
+
renameCancelled.current = true;
|
|
19882
|
+
setRenamingId(null);
|
|
19883
|
+
}
|
|
19884
|
+
};
|
|
19885
|
+
const handleRenameBlur = () => {
|
|
19886
|
+
if (!renameCancelled.current) handleRenameConfirm();
|
|
19814
19887
|
};
|
|
19815
19888
|
const handleExport = async (saveId) => {
|
|
19816
19889
|
try {
|
|
@@ -19927,7 +20000,7 @@ function SaveManagerContent() {
|
|
|
19927
20000
|
value: renameValue,
|
|
19928
20001
|
onInput: (e) => setRenameValue(e.target.value),
|
|
19929
20002
|
onKeyDown: handleRenameKeyDown,
|
|
19930
|
-
onBlur:
|
|
20003
|
+
onBlur: handleRenameBlur
|
|
19931
20004
|
}) : /* @__PURE__ */ jsx("div", {
|
|
19932
20005
|
class: "save-slot-title",
|
|
19933
20006
|
children: save.meta.title
|
|
@@ -20570,6 +20643,19 @@ function cleanupSnapshots(containerEl) {
|
|
|
20570
20643
|
const snapshots = containerEl.querySelectorAll(".passage-snapshot");
|
|
20571
20644
|
for (let i = 0; i < snapshots.length; i++) snapshots[i].remove();
|
|
20572
20645
|
}
|
|
20646
|
+
/**
|
|
20647
|
+
* `config` for players who prefer reduced motion: the CSS shortens the
|
|
20648
|
+
* animations (see styles.css), so the waits that follow them shrink too,
|
|
20649
|
+
* leaving no empty passage area before the next passage.
|
|
20650
|
+
*/
|
|
20651
|
+
function reduceMotion(config) {
|
|
20652
|
+
if (!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return config;
|
|
20653
|
+
return {
|
|
20654
|
+
...config,
|
|
20655
|
+
duration: Math.min(config.duration, 10),
|
|
20656
|
+
pause: 0
|
|
20657
|
+
};
|
|
20658
|
+
}
|
|
20573
20659
|
defineMacro({
|
|
20574
20660
|
name: "passage",
|
|
20575
20661
|
interpolate: true,
|
|
@@ -20617,7 +20703,7 @@ defineMacro({
|
|
|
20617
20703
|
setDisplayedPassage(currentPassage);
|
|
20618
20704
|
return;
|
|
20619
20705
|
}
|
|
20620
|
-
const config = resolveTransition(targetPassage.tags, next, transitionConfig);
|
|
20706
|
+
const config = reduceMotion(resolveTransition(targetPassage.tags, next, transitionConfig));
|
|
20621
20707
|
resolvedTypeRef.current = config.type;
|
|
20622
20708
|
const containerEl = containerRef.current;
|
|
20623
20709
|
if (config.type === "none") {
|
|
@@ -20757,7 +20843,7 @@ defineMacro({
|
|
|
20757
20843
|
});
|
|
20758
20844
|
//#endregion
|
|
20759
20845
|
//#region src/styles.css?inline
|
|
20760
|
-
var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n/* Runtime errors shown on the page (see src/components/RuntimeErrors.tsx) */\n.spindle-error-banners {\n position: fixed;\n top: 0.75em;\n left: 50%;\n z-index: 300;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n width: min(42em, calc(100% - 1.5em));\n transform: translateX(-50%);\n}\n\n.spindle-error-banner {\n position: relative;\n padding: 0.75em 2.75em 0.75em 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: #2b1a1e;\n color: #ffcdd2;\n font-size: 0.9em;\n line-height: 1.5;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n}\n\n.spindle-error-banner-message {\n font-family: monospace;\n color: #ef9a9a;\n}\n\n.spindle-error-banner-dismiss {\n position: absolute;\n top: 0.5em;\n right: 0.5em;\n background: transparent;\n border: none;\n color: inherit;\n font-size: 1em;\n cursor: pointer;\n}\n\n.spindle-error-banner-dismiss:hover {\n color: #fff;\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
|
|
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";
|
|
20761
20847
|
//#endregion
|
|
20762
20848
|
//#region src/index.tsx
|
|
20763
20849
|
function renderErrors(root, errors) {
|
|
@@ -10269,7 +10269,13 @@ function widgetDef({ name = "", parameters = "" }) {
|
|
|
10269
10269
|
function parseWidgetDef(rawArgs) {
|
|
10270
10270
|
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
10271
10271
|
}
|
|
10272
|
-
|
|
10272
|
+
/**
|
|
10273
|
+
* A definition: its arguments (read like a {widget} macro's, see
|
|
10274
|
+
* parseWidgetDef) and its body. Macro names are not case-sensitive.
|
|
10275
|
+
*/
|
|
10276
|
+
var WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
|
|
10277
|
+
/** `{@children}`, also with selectors such as `{.highlight @children}`. */
|
|
10278
|
+
var CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
|
|
10273
10279
|
/**
|
|
10274
10280
|
* The names of the block widgets (those whose body renders `{@children}`)
|
|
10275
10281
|
* that StoryInit and the passages tagged `widget` define. They must be
|
|
@@ -10280,7 +10286,12 @@ function blockWidgetNames(passages) {
|
|
|
10280
10286
|
const names = [];
|
|
10281
10287
|
for (const passage of passages) {
|
|
10282
10288
|
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
10283
|
-
for (const match of passage.content.matchAll(
|
|
10289
|
+
for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
|
|
10290
|
+
if (!CHILDREN_PLACEHOLDER.test(match[2])) continue;
|
|
10291
|
+
try {
|
|
10292
|
+
names.push(parseWidgetDef(match[1]).name);
|
|
10293
|
+
} catch {}
|
|
10294
|
+
}
|
|
10284
10295
|
}
|
|
10285
10296
|
return names;
|
|
10286
10297
|
}
|
package/package.json
CHANGED
package/src/components/App.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { NobrContext } from '../markup/render';
|
|
|
5
5
|
import { StoryInterface } from './StoryInterface';
|
|
6
6
|
import { TriggerDialogHost } from './TriggerDialogHost';
|
|
7
7
|
import { RuntimeErrors } from './RuntimeErrors';
|
|
8
|
+
import { quickLoad, quickSave } from '../quick-actions';
|
|
8
9
|
|
|
9
10
|
export function App() {
|
|
10
11
|
const { storyData, currentPassage, nobr } = useStoryFields(
|
|
@@ -15,14 +16,13 @@ export function App() {
|
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
18
|
-
const { quickSaveKey, quickLoadKey
|
|
19
|
-
useStoryStore.getState();
|
|
19
|
+
const { quickSaveKey, quickLoadKey } = useStoryStore.getState();
|
|
20
20
|
if (quickSaveKey !== null && e.key === quickSaveKey) {
|
|
21
21
|
e.preventDefault();
|
|
22
|
-
|
|
22
|
+
quickSave();
|
|
23
23
|
} else if (quickLoadKey !== null && e.key === quickLoadKey) {
|
|
24
24
|
e.preventDefault();
|
|
25
|
-
|
|
25
|
+
quickLoad();
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
document.addEventListener('keydown', onKeyDown);
|
|
@@ -2,6 +2,7 @@ import { useState } from 'preact/hooks';
|
|
|
2
2
|
import { useStoryStore } from '../../store';
|
|
3
3
|
import { useStoryFields } from '../../hooks/use-story-fields';
|
|
4
4
|
import { settings } from '../../settings';
|
|
5
|
+
import { quickLoad, quickSave } from '../../quick-actions';
|
|
5
6
|
import { defineMacro } from '../../define-macro';
|
|
6
7
|
import { PassageDialog } from '../PassageDialog';
|
|
7
8
|
import type { ActionType } from '../../action-registry';
|
|
@@ -110,8 +111,11 @@ defineMenubarAction({
|
|
|
110
111
|
label: 'QuickSave',
|
|
111
112
|
actionType: 'save',
|
|
112
113
|
setup: () => {
|
|
113
|
-
const {
|
|
114
|
-
return {
|
|
114
|
+
const { quickSaveKey } = useStoryFields('quickSaveKey');
|
|
115
|
+
return {
|
|
116
|
+
perform: quickSave,
|
|
117
|
+
title: hotkeyTitle('Quick Save', quickSaveKey),
|
|
118
|
+
};
|
|
115
119
|
},
|
|
116
120
|
});
|
|
117
121
|
|
|
@@ -122,14 +126,13 @@ defineMenubarAction({
|
|
|
122
126
|
confirm: 'Load saved game? Current progress will be lost.',
|
|
123
127
|
setup: () => {
|
|
124
128
|
// knownSaves: re-render when the saves change, as hasSave() reads them
|
|
125
|
-
const {
|
|
126
|
-
'load',
|
|
129
|
+
const { hasSave, quickLoadKey } = useStoryFields(
|
|
127
130
|
'hasSave',
|
|
128
131
|
'quickLoadKey',
|
|
129
132
|
'knownSaves',
|
|
130
133
|
);
|
|
131
134
|
return {
|
|
132
|
-
perform:
|
|
135
|
+
perform: quickLoad,
|
|
133
136
|
disabled: !hasSave(),
|
|
134
137
|
title: hotkeyTitle('Quick Load', quickLoadKey),
|
|
135
138
|
};
|
|
@@ -80,6 +80,18 @@ function cleanupSnapshots(containerEl: Element | null): void {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* `config` for players who prefer reduced motion: the CSS shortens the
|
|
85
|
+
* animations (see styles.css), so the waits that follow them shrink too,
|
|
86
|
+
* leaving no empty passage area before the next passage.
|
|
87
|
+
*/
|
|
88
|
+
function reduceMotion(config: ResolvedTransition): ResolvedTransition {
|
|
89
|
+
if (!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
return { ...config, duration: Math.min(config.duration, 10), pause: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
defineMacro({
|
|
84
96
|
name: 'passage',
|
|
85
97
|
interpolate: true,
|
|
@@ -173,10 +185,8 @@ defineMacro({
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
// Resolve transition from tags → nextTransition → store default → built-in
|
|
176
|
-
const config =
|
|
177
|
-
targetPassage.tags,
|
|
178
|
-
next,
|
|
179
|
-
transitionConfig,
|
|
188
|
+
const config = reduceMotion(
|
|
189
|
+
resolveTransition(targetPassage.tags, next, transitionConfig),
|
|
180
190
|
);
|
|
181
191
|
|
|
182
192
|
resolvedTypeRef.current = config.type;
|
|
@@ -56,6 +56,7 @@ export function SaveManagerContent() {
|
|
|
56
56
|
} | null>(null);
|
|
57
57
|
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set());
|
|
58
58
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
59
|
+
const renameCancelled = useRef(false);
|
|
59
60
|
const [renameValue, setRenameValue] = useState('');
|
|
60
61
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
|
61
62
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -191,6 +192,7 @@ export function SaveManagerContent() {
|
|
|
191
192
|
};
|
|
192
193
|
|
|
193
194
|
const handleRenameStart = (save: SaveRecord) => {
|
|
195
|
+
renameCancelled.current = false;
|
|
194
196
|
setRenamingId(save.meta.id);
|
|
195
197
|
setRenameValue(save.meta.title);
|
|
196
198
|
};
|
|
@@ -209,7 +211,18 @@ export function SaveManagerContent() {
|
|
|
209
211
|
|
|
210
212
|
const handleRenameKeyDown = (e: KeyboardEvent) => {
|
|
211
213
|
if (e.key === 'Enter') handleRenameConfirm();
|
|
212
|
-
else if (e.key === 'Escape')
|
|
214
|
+
else if (e.key === 'Escape') {
|
|
215
|
+
// Cancel the edit only: the dialog stays open, and the blur that
|
|
216
|
+
// follows removing the input must not confirm it
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
renameCancelled.current = true;
|
|
220
|
+
setRenamingId(null);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleRenameBlur = () => {
|
|
225
|
+
if (!renameCancelled.current) handleRenameConfirm();
|
|
213
226
|
};
|
|
214
227
|
|
|
215
228
|
const handleExport = async (saveId: string) => {
|
|
@@ -350,7 +363,7 @@ export function SaveManagerContent() {
|
|
|
350
363
|
)
|
|
351
364
|
}
|
|
352
365
|
onKeyDown={handleRenameKeyDown}
|
|
353
|
-
onBlur={
|
|
366
|
+
onBlur={handleRenameBlur}
|
|
354
367
|
/>
|
|
355
368
|
) : (
|
|
356
369
|
<div class="save-slot-title">{save.meta.title}</div>
|