@rohal12/spindle 0.26.1 → 0.27.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,19 +1,38 @@
1
- import { useState, useEffect, useCallback } from 'preact/hooks';
1
+ import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
2
2
  import { PassageDialog } from './PassageDialog';
3
3
  import {
4
4
  subscribeTriggerDialogs,
5
5
  shiftDialogQueue,
6
6
  dialogQueueLength,
7
+ registerDialogHost,
7
8
  } from '../triggers';
9
+ import type { QueuedDialog } from '../triggers';
8
10
 
9
11
  export function TriggerDialogHost() {
10
- const [current, setCurrent] = useState<string | null>(null);
12
+ const [current, setCurrent] = useState<QueuedDialog | null>(null);
13
+ const currentRef = useRef(current);
14
+ currentRef.current = current;
11
15
 
12
16
  const advance = useCallback(() => {
13
17
  const next = shiftDialogQueue();
14
18
  setCurrent(next ?? null);
15
19
  }, []);
16
20
 
21
+ const handleClose = useCallback(() => {
22
+ setCurrent(null);
23
+ // Show next queued dialog after a tick
24
+ if (dialogQueueLength() > 0) {
25
+ requestAnimationFrame(advance);
26
+ }
27
+ }, [advance]);
28
+
29
+ useEffect(() => {
30
+ return registerDialogHost({
31
+ close: handleClose,
32
+ isOpen: () => currentRef.current !== null,
33
+ });
34
+ }, [handleClose]);
35
+
17
36
  useEffect(() => {
18
37
  return subscribeTriggerDialogs(() => {
19
38
  setCurrent((prev) => {
@@ -23,19 +42,12 @@ export function TriggerDialogHost() {
23
42
  });
24
43
  }, []);
25
44
 
26
- const handleClose = useCallback(() => {
27
- setCurrent(null);
28
- // Show next queued dialog after a tick
29
- if (dialogQueueLength() > 0) {
30
- requestAnimationFrame(advance);
31
- }
32
- }, [advance]);
33
-
34
45
  if (!current) return null;
35
46
 
36
47
  return (
37
48
  <PassageDialog
38
- passageName={current}
49
+ passageName={current.passageName}
50
+ panelClass={current.panelClass}
39
51
  onClose={handleClose}
40
52
  />
41
53
  );
@@ -81,17 +81,19 @@ function WidgetBody({
81
81
  ownKeys: Record<string, unknown>;
82
82
  }) {
83
83
  const nobr = useContext(NobrContext);
84
- const [localState, setLocalState] = useState<Record<string, unknown>>(() => ({
85
- ...parentValues,
86
- ...ownKeys,
87
- }));
84
+ const [localMutations, setLocalMutations] = useState<Record<string, unknown>>(
85
+ {},
86
+ );
87
+
88
+ // Recomputed every render — picks up new ownKeys from parent
89
+ const localState = { ...parentValues, ...ownKeys, ...localMutations };
88
90
 
89
91
  const valuesRef = useRef(localState);
90
92
  valuesRef.current = localState;
91
93
 
92
94
  const getValues = useCallback(() => valuesRef.current, []);
93
95
  const update = useCallback((key: string, value: unknown) => {
94
- setLocalState((prev) => ({ ...prev, [key]: value }));
96
+ setLocalMutations((prev) => ({ ...prev, [key]: value }));
95
97
  }, []);
96
98
  const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
97
99
 
package/src/story-api.ts CHANGED
@@ -21,7 +21,14 @@ import {
21
21
  randomInt,
22
22
  snapshotPRNG,
23
23
  } from './prng';
24
- import { addTrigger, removeTrigger } from './triggers';
24
+ import {
25
+ addTrigger,
26
+ removeTrigger,
27
+ pushDialog,
28
+ clearDialogQueue,
29
+ closeCurrentDialog,
30
+ isDialogShowing,
31
+ } from './triggers';
25
32
  import type { WatchOptions } from './triggers';
26
33
  import type { TransitionConfig } from './transition';
27
34
 
@@ -73,6 +80,10 @@ export interface StoryAPI {
73
80
  callbackOrOptions: (() => void) | WatchOptions,
74
81
  ): () => void;
75
82
  unwatch(name: string): void;
83
+ openDialog(passageName: string, options?: { panelClass?: string }): void;
84
+ closeDialog(): void;
85
+ closeAllDialogs(): void;
86
+ isDialogOpen(): boolean;
76
87
  setNobr(enabled: boolean): void;
77
88
  setCSS(enabled: boolean): void;
78
89
  setTransition(config: TransitionConfig | null): void;
@@ -286,6 +297,23 @@ function createStoryAPI(): StoryAPI {
286
297
  removeTrigger(name);
287
298
  },
288
299
 
300
+ openDialog(passageName: string, options?: { panelClass?: string }): void {
301
+ pushDialog({ passageName, panelClass: options?.panelClass });
302
+ },
303
+
304
+ closeDialog(): void {
305
+ closeCurrentDialog();
306
+ },
307
+
308
+ closeAllDialogs(): void {
309
+ clearDialogQueue();
310
+ closeCurrentDialog();
311
+ },
312
+
313
+ isDialogOpen(): boolean {
314
+ return isDialogShowing();
315
+ },
316
+
289
317
  setNobr(enabled: boolean): void {
290
318
  useStoryStore.setState({ nobr: enabled });
291
319
  },
package/src/triggers.ts CHANGED
@@ -10,6 +10,11 @@ export interface WatchOptions {
10
10
  priority?: number;
11
11
  }
12
12
 
13
+ export interface QueuedDialog {
14
+ passageName: string;
15
+ panelClass?: string;
16
+ }
17
+
13
18
  interface Trigger {
14
19
  id: number;
15
20
  name?: string;
@@ -25,9 +30,15 @@ let triggers: Trigger[] = [];
25
30
  let checking = false;
26
31
 
27
32
  type DialogCallback = () => void;
28
- let dialogQueue: string[] = [];
33
+ let dialogQueue: QueuedDialog[] = [];
29
34
  let dialogNotify: DialogCallback | null = null;
30
35
 
36
+ interface DialogHostCallbacks {
37
+ close: () => void;
38
+ isOpen: () => boolean;
39
+ }
40
+ let dialogHostCallbacks: DialogHostCallbacks | null = null;
41
+
31
42
  function evalCondition(condition: string): boolean {
32
43
  const state = useStoryStore.getState();
33
44
  try {
@@ -84,7 +95,7 @@ function fireTrigger(trigger: Trigger): void {
84
95
  }
85
96
 
86
97
  if (options.dialog) {
87
- dialogQueue.push(options.dialog);
98
+ dialogQueue.push({ passageName: options.dialog });
88
99
  dialogNotify?.();
89
100
  }
90
101
 
@@ -141,6 +152,7 @@ export function resetTriggers(): void {
141
152
  triggers = [];
142
153
  dialogQueue = [];
143
154
  nextId = 0;
155
+ dialogHostCallbacks?.close();
144
156
  }
145
157
 
146
158
  export function subscribeTriggerDialogs(cb: () => void): () => void {
@@ -152,10 +164,34 @@ export function subscribeTriggerDialogs(cb: () => void): () => void {
152
164
  };
153
165
  }
154
166
 
155
- export function shiftDialogQueue(): string | undefined {
167
+ export function shiftDialogQueue(): QueuedDialog | undefined {
156
168
  return dialogQueue.shift();
157
169
  }
158
170
 
159
171
  export function dialogQueueLength(): number {
160
172
  return dialogQueue.length;
161
173
  }
174
+
175
+ export function pushDialog(item: QueuedDialog): void {
176
+ dialogQueue.push(item);
177
+ dialogNotify?.();
178
+ }
179
+
180
+ export function clearDialogQueue(): void {
181
+ dialogQueue = [];
182
+ }
183
+
184
+ export function registerDialogHost(callbacks: DialogHostCallbacks): () => void {
185
+ dialogHostCallbacks = callbacks;
186
+ return () => {
187
+ if (dialogHostCallbacks === callbacks) dialogHostCallbacks = null;
188
+ };
189
+ }
190
+
191
+ export function closeCurrentDialog(): void {
192
+ dialogHostCallbacks?.close();
193
+ }
194
+
195
+ export function isDialogShowing(): boolean {
196
+ return dialogHostCallbacks?.isOpen() ?? false;
197
+ }