@rohal12/spindle 0.26.2 → 0.28.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.2",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -12,6 +12,7 @@ interface PassageDialogProps {
12
12
  fallbackMarkup?: string;
13
13
  panelClass?: string;
14
14
  onClose: () => void;
15
+ showCloseButton?: boolean;
15
16
  }
16
17
 
17
18
  export function PassageDialog({
@@ -19,6 +20,7 @@ export function PassageDialog({
19
20
  fallbackMarkup,
20
21
  panelClass,
21
22
  onClose,
23
+ showCloseButton = true,
22
24
  }: PassageDialogProps) {
23
25
  const storyData = useStoryStore((s) => s.storyData);
24
26
 
@@ -55,12 +57,14 @@ export function PassageDialog({
55
57
  onClick={handleBackdrop}
56
58
  >
57
59
  <div class={cls}>
58
- <button
59
- class="dialog-close"
60
- onClick={onClose}
61
- >
62
- ✕
63
- </button>
60
+ {showCloseButton && (
61
+ <button
62
+ class="dialog-close"
63
+ onClick={onClose}
64
+ >
65
+ ✕
66
+ </button>
67
+ )}
64
68
  <div class="dialog-body">{content}</div>
65
69
  </div>
66
70
  </div>
@@ -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,20 +42,14 @@ 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}
52
+ showCloseButton={current.showCloseButton}
40
53
  />
41
54
  );
42
55
  }
@@ -3,11 +3,15 @@ import { PassageDialog } from '../PassageDialog';
3
3
 
4
4
  defineMacro({
5
5
  name: 'dialog',
6
+ block: true,
6
7
  interpolate: true,
7
8
  render({ rawArgs, children = [] }, ctx) {
8
9
  const [open, setOpen] = ctx.hooks.useState(false);
9
10
 
10
- const label = ctx.resolve?.(rawArgs.replace(/^["']|["']$/g, '')) ?? rawArgs;
11
+ const noclose = /\bnoclose\s*$/.test(rawArgs);
12
+ const labelRaw = rawArgs.replace(/\bnoclose\s*$/, '').trim();
13
+ const label =
14
+ ctx.resolve?.(labelRaw.replace(/^["']|["']$/g, '')) ?? labelRaw;
11
15
  const passageName = ctx
12
16
  .collectText(children)
13
17
  .trim()
@@ -26,6 +30,7 @@ defineMacro({
26
30
  <PassageDialog
27
31
  passageName={passageName}
28
32
  onClose={() => setOpen(false)}
33
+ showCloseButton={!noclose}
29
34
  />
30
35
  )}
31
36
  </>
@@ -18,6 +18,7 @@ interface MenubarActionConfig {
18
18
  passageName: string;
19
19
  fallbackMarkup?: string;
20
20
  panelClass?: string;
21
+ showCloseButton?: boolean;
21
22
  };
22
23
  }
23
24
 
@@ -72,6 +73,7 @@ export function defineMenubarAction(config: MenubarActionConfig) {
72
73
  fallbackMarkup={config.dialog.fallbackMarkup}
73
74
  panelClass={config.dialog.panelClass}
74
75
  onClose={() => setDialogOpen(false)}
76
+ showCloseButton={config.dialog.showCloseButton}
75
77
  />
76
78
  )}
77
79
  </>
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,13 @@ export interface StoryAPI {
73
80
  callbackOrOptions: (() => void) | WatchOptions,
74
81
  ): () => void;
75
82
  unwatch(name: string): void;
83
+ openDialog(
84
+ passageName: string,
85
+ options?: { panelClass?: string; showCloseButton?: boolean },
86
+ ): void;
87
+ closeDialog(): void;
88
+ closeAllDialogs(): void;
89
+ isDialogOpen(): boolean;
76
90
  setNobr(enabled: boolean): void;
77
91
  setCSS(enabled: boolean): void;
78
92
  setTransition(config: TransitionConfig | null): void;
@@ -286,6 +300,30 @@ function createStoryAPI(): StoryAPI {
286
300
  removeTrigger(name);
287
301
  },
288
302
 
303
+ openDialog(
304
+ passageName: string,
305
+ options?: { panelClass?: string; showCloseButton?: boolean },
306
+ ): void {
307
+ pushDialog({
308
+ passageName,
309
+ panelClass: options?.panelClass,
310
+ showCloseButton: options?.showCloseButton,
311
+ });
312
+ },
313
+
314
+ closeDialog(): void {
315
+ closeCurrentDialog();
316
+ },
317
+
318
+ closeAllDialogs(): void {
319
+ clearDialogQueue();
320
+ closeCurrentDialog();
321
+ },
322
+
323
+ isDialogOpen(): boolean {
324
+ return isDialogShowing();
325
+ },
326
+
289
327
  setNobr(enabled: boolean): void {
290
328
  useStoryStore.setState({ nobr: enabled });
291
329
  },
package/src/triggers.ts CHANGED
@@ -10,6 +10,12 @@ export interface WatchOptions {
10
10
  priority?: number;
11
11
  }
12
12
 
13
+ export interface QueuedDialog {
14
+ passageName: string;
15
+ panelClass?: string;
16
+ showCloseButton?: boolean;
17
+ }
18
+
13
19
  interface Trigger {
14
20
  id: number;
15
21
  name?: string;
@@ -25,9 +31,15 @@ let triggers: Trigger[] = [];
25
31
  let checking = false;
26
32
 
27
33
  type DialogCallback = () => void;
28
- let dialogQueue: string[] = [];
34
+ let dialogQueue: QueuedDialog[] = [];
29
35
  let dialogNotify: DialogCallback | null = null;
30
36
 
37
+ interface DialogHostCallbacks {
38
+ close: () => void;
39
+ isOpen: () => boolean;
40
+ }
41
+ let dialogHostCallbacks: DialogHostCallbacks | null = null;
42
+
31
43
  function evalCondition(condition: string): boolean {
32
44
  const state = useStoryStore.getState();
33
45
  try {
@@ -84,7 +96,7 @@ function fireTrigger(trigger: Trigger): void {
84
96
  }
85
97
 
86
98
  if (options.dialog) {
87
- dialogQueue.push(options.dialog);
99
+ dialogQueue.push({ passageName: options.dialog });
88
100
  dialogNotify?.();
89
101
  }
90
102
 
@@ -141,6 +153,7 @@ export function resetTriggers(): void {
141
153
  triggers = [];
142
154
  dialogQueue = [];
143
155
  nextId = 0;
156
+ dialogHostCallbacks?.close();
144
157
  }
145
158
 
146
159
  export function subscribeTriggerDialogs(cb: () => void): () => void {
@@ -152,10 +165,34 @@ export function subscribeTriggerDialogs(cb: () => void): () => void {
152
165
  };
153
166
  }
154
167
 
155
- export function shiftDialogQueue(): string | undefined {
168
+ export function shiftDialogQueue(): QueuedDialog | undefined {
156
169
  return dialogQueue.shift();
157
170
  }
158
171
 
159
172
  export function dialogQueueLength(): number {
160
173
  return dialogQueue.length;
161
174
  }
175
+
176
+ export function pushDialog(item: QueuedDialog): void {
177
+ dialogQueue.push(item);
178
+ dialogNotify?.();
179
+ }
180
+
181
+ export function clearDialogQueue(): void {
182
+ dialogQueue = [];
183
+ }
184
+
185
+ export function registerDialogHost(callbacks: DialogHostCallbacks): () => void {
186
+ dialogHostCallbacks = callbacks;
187
+ return () => {
188
+ if (dialogHostCallbacks === callbacks) dialogHostCallbacks = null;
189
+ };
190
+ }
191
+
192
+ export function closeCurrentDialog(): void {
193
+ dialogHostCallbacks?.close();
194
+ }
195
+
196
+ export function isDialogShowing(): boolean {
197
+ return dialogHostCallbacks?.isOpen() ?? false;
198
+ }