@rohal12/spindle 0.30.0 → 0.31.1
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/package.json +1 -1
- package/src/components/TriggerDialogHost.tsx +36 -25
- package/src/components/macros/Checkbox.tsx +1 -1
- package/src/components/macros/Cycle.tsx +1 -2
- package/src/components/macros/Radiobutton.tsx +1 -1
- package/src/components/macros/option-utils.ts +3 -1
- package/src/define-macro.ts +43 -5
- package/src/story-api.ts +2 -3
- package/src/triggers.ts +14 -3
package/package.json
CHANGED
|
@@ -9,47 +9,58 @@ import {
|
|
|
9
9
|
import type { QueuedDialog } from '../triggers';
|
|
10
10
|
|
|
11
11
|
export function TriggerDialogHost() {
|
|
12
|
-
const [
|
|
13
|
-
const
|
|
14
|
-
|
|
12
|
+
const [stack, setStack] = useState<QueuedDialog[]>([]);
|
|
13
|
+
const stackRef = useRef(stack);
|
|
14
|
+
stackRef.current = stack;
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
setCurrent(next ?? null);
|
|
16
|
+
const push = useCallback((item: QueuedDialog) => {
|
|
17
|
+
setStack((prev) => [...prev, item]);
|
|
19
18
|
}, []);
|
|
20
19
|
|
|
21
20
|
const handleClose = useCallback(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}, [
|
|
21
|
+
setStack((prev) => prev.slice(0, -1));
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleCloseAll = useCallback(() => {
|
|
25
|
+
setStack([]);
|
|
26
|
+
}, []);
|
|
28
27
|
|
|
29
28
|
useEffect(() => {
|
|
30
29
|
return registerDialogHost({
|
|
31
30
|
close: handleClose,
|
|
32
|
-
|
|
31
|
+
closeAll: handleCloseAll,
|
|
32
|
+
push,
|
|
33
|
+
isOpen: () => stackRef.current.length > 0,
|
|
33
34
|
});
|
|
34
|
-
}, [handleClose]);
|
|
35
|
+
}, [handleClose, handleCloseAll, push]);
|
|
35
36
|
|
|
37
|
+
// Flush any dialogs that were queued before the host mounted
|
|
36
38
|
useEffect(() => {
|
|
37
39
|
return subscribeTriggerDialogs(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const items: QueuedDialog[] = [];
|
|
41
|
+
while (dialogQueueLength() > 0) {
|
|
42
|
+
const next = shiftDialogQueue();
|
|
43
|
+
if (next) items.push(next);
|
|
44
|
+
}
|
|
45
|
+
if (items.length > 0) {
|
|
46
|
+
setStack((prev) => [...prev, ...items]);
|
|
47
|
+
}
|
|
42
48
|
});
|
|
43
49
|
}, []);
|
|
44
50
|
|
|
45
|
-
if (
|
|
51
|
+
if (stack.length === 0) return null;
|
|
46
52
|
|
|
47
53
|
return (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
<>
|
|
55
|
+
{stack.map((dialog, i) => (
|
|
56
|
+
<PassageDialog
|
|
57
|
+
key={`${dialog.passageName}-${i}`}
|
|
58
|
+
passageName={dialog.passageName}
|
|
59
|
+
panelClass={dialog.panelClass}
|
|
60
|
+
onClose={handleClose}
|
|
61
|
+
showCloseButton={dialog.showCloseButton}
|
|
62
|
+
/>
|
|
63
|
+
))}
|
|
64
|
+
</>
|
|
54
65
|
);
|
|
55
66
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
|
|
3
3
|
function parseLabel(rawArgs: string): string {
|
|
4
|
-
const match = rawArgs.match(/^\s*["']
|
|
4
|
+
const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+["']?(.+?)["']?\s*$/);
|
|
5
5
|
return match?.[1] ?? '';
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
1
|
import { defineMacro } from '../../define-macro';
|
|
3
2
|
|
|
4
3
|
defineMacro({
|
|
@@ -9,7 +8,7 @@ defineMacro({
|
|
|
9
8
|
|
|
10
9
|
const handleClick = () => {
|
|
11
10
|
if (options.length === 0) return;
|
|
12
|
-
const current =
|
|
11
|
+
const current = ctx.getValue!();
|
|
13
12
|
const currentIndex = options.indexOf(String(current));
|
|
14
13
|
const nextIndex = (currentIndex + 1) % options.length;
|
|
15
14
|
ctx.setValue!(options[nextIndex]);
|
|
@@ -2,7 +2,7 @@ import { defineMacro } from '../../define-macro';
|
|
|
2
2
|
|
|
3
3
|
function parseRadioArgs(rawArgs: string): { value: string; label: string } {
|
|
4
4
|
const match = rawArgs.match(
|
|
5
|
-
/^\s*["']
|
|
5
|
+
/^\s*["']?\$?[\w.]+["']?\s+["'](.+?)["']\s+["']?(.+?)["']?\s*$/,
|
|
6
6
|
);
|
|
7
7
|
if (!match) {
|
|
8
8
|
const parts = rawArgs.trim().split(/\s+/).slice(1);
|
|
@@ -4,7 +4,9 @@ export function parseVarArgs(rawArgs: string): {
|
|
|
4
4
|
varName: string;
|
|
5
5
|
placeholder: string;
|
|
6
6
|
} {
|
|
7
|
-
const match = rawArgs.match(
|
|
7
|
+
const match = rawArgs.match(
|
|
8
|
+
/^\s*(["']?\$[\w.]+["']?)\s*(?:["'](.*)["'])?\s*$/,
|
|
9
|
+
);
|
|
8
10
|
if (!match) {
|
|
9
11
|
return { varName: rawArgs.trim(), placeholder: '' };
|
|
10
12
|
}
|
package/src/define-macro.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface MacroContext {
|
|
|
52
52
|
varName?: string;
|
|
53
53
|
value?: unknown;
|
|
54
54
|
setValue?: (value: unknown) => void;
|
|
55
|
+
getValue?: () => unknown;
|
|
55
56
|
evaluate?: (expr: string) => unknown;
|
|
56
57
|
collectText: typeof collectText;
|
|
57
58
|
sourceLocation: typeof currentSourceLocation;
|
|
@@ -93,6 +94,33 @@ const sharedHooks = {
|
|
|
93
94
|
useContext,
|
|
94
95
|
};
|
|
95
96
|
|
|
97
|
+
/** Traverse a dot-path on an object, returning the nested value. */
|
|
98
|
+
function getByPath(obj: Record<string, unknown>, segments: string[]): unknown {
|
|
99
|
+
let current: unknown = obj;
|
|
100
|
+
for (const seg of segments) {
|
|
101
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
102
|
+
current = (current as Record<string, unknown>)[seg];
|
|
103
|
+
}
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Set a nested value by dot-path segments on an (Immer draft) object. */
|
|
108
|
+
function setByPath(
|
|
109
|
+
obj: Record<string, unknown>,
|
|
110
|
+
segments: string[],
|
|
111
|
+
value: unknown,
|
|
112
|
+
): void {
|
|
113
|
+
let target: Record<string, unknown> = obj;
|
|
114
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
115
|
+
const seg = segments[i]!;
|
|
116
|
+
if (target[seg] == null || typeof target[seg] !== 'object') {
|
|
117
|
+
target[seg] = {};
|
|
118
|
+
}
|
|
119
|
+
target = target[seg] as Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
target[segments[segments.length - 1]!] = value;
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
export function defineMacro(config: MacroDefinition): void {
|
|
97
125
|
function Wrapper(props: MacroProps) {
|
|
98
126
|
// className/id resolved first (interpolate may transform them)
|
|
@@ -146,11 +174,21 @@ export function defineMacro(config: MacroDefinition): void {
|
|
|
146
174
|
|
|
147
175
|
if (config.storeVar) {
|
|
148
176
|
const firstToken = props.rawArgs.trim().split(/[\s"']+/)[0] ?? '';
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
ctx.
|
|
152
|
-
|
|
153
|
-
ctx.
|
|
177
|
+
const varExpr = firstToken.replace(/["']/g, '').replace(/^\$/, '');
|
|
178
|
+
const segments = varExpr.split('.');
|
|
179
|
+
ctx.varName = varExpr;
|
|
180
|
+
ctx.value = useStoryStore((s) => getByPath(s.variables, segments));
|
|
181
|
+
ctx.getValue = () =>
|
|
182
|
+
getByPath(useStoryStore.getState().variables, segments);
|
|
183
|
+
ctx.setValue = (value: unknown) => {
|
|
184
|
+
useStoryStore.setState((state) => {
|
|
185
|
+
setByPath(
|
|
186
|
+
state.variables as Record<string, unknown>,
|
|
187
|
+
segments,
|
|
188
|
+
value,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
154
192
|
}
|
|
155
193
|
|
|
156
194
|
return config.render(props, ctx);
|
package/src/story-api.ts
CHANGED
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
addTrigger,
|
|
26
26
|
removeTrigger,
|
|
27
27
|
pushDialog,
|
|
28
|
-
clearDialogQueue,
|
|
29
28
|
closeCurrentDialog,
|
|
29
|
+
closeAllOpenDialogs,
|
|
30
30
|
isDialogShowing,
|
|
31
31
|
} from './triggers';
|
|
32
32
|
import type { WatchOptions } from './triggers';
|
|
@@ -316,8 +316,7 @@ function createStoryAPI(): StoryAPI {
|
|
|
316
316
|
},
|
|
317
317
|
|
|
318
318
|
closeAllDialogs(): void {
|
|
319
|
-
|
|
320
|
-
closeCurrentDialog();
|
|
319
|
+
closeAllOpenDialogs();
|
|
321
320
|
},
|
|
322
321
|
|
|
323
322
|
isDialogOpen(): boolean {
|
package/src/triggers.ts
CHANGED
|
@@ -36,6 +36,8 @@ let dialogNotify: DialogCallback | null = null;
|
|
|
36
36
|
|
|
37
37
|
interface DialogHostCallbacks {
|
|
38
38
|
close: () => void;
|
|
39
|
+
closeAll: () => void;
|
|
40
|
+
push: (item: QueuedDialog) => void;
|
|
39
41
|
isOpen: () => boolean;
|
|
40
42
|
}
|
|
41
43
|
let dialogHostCallbacks: DialogHostCallbacks | null = null;
|
|
@@ -153,7 +155,7 @@ export function resetTriggers(): void {
|
|
|
153
155
|
triggers = [];
|
|
154
156
|
dialogQueue = [];
|
|
155
157
|
nextId = 0;
|
|
156
|
-
dialogHostCallbacks?.
|
|
158
|
+
dialogHostCallbacks?.closeAll();
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
export function subscribeTriggerDialogs(cb: () => void): () => void {
|
|
@@ -174,8 +176,12 @@ export function dialogQueueLength(): number {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
export function pushDialog(item: QueuedDialog): void {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
if (dialogHostCallbacks) {
|
|
180
|
+
dialogHostCallbacks.push(item);
|
|
181
|
+
} else {
|
|
182
|
+
dialogQueue.push(item);
|
|
183
|
+
dialogNotify?.();
|
|
184
|
+
}
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
export function clearDialogQueue(): void {
|
|
@@ -193,6 +199,11 @@ export function closeCurrentDialog(): void {
|
|
|
193
199
|
dialogHostCallbacks?.close();
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
export function closeAllOpenDialogs(): void {
|
|
203
|
+
clearDialogQueue();
|
|
204
|
+
dialogHostCallbacks?.closeAll();
|
|
205
|
+
}
|
|
206
|
+
|
|
196
207
|
export function isDialogShowing(): boolean {
|
|
197
208
|
return dialogHostCallbacks?.isOpen() ?? false;
|
|
198
209
|
}
|