@rohal12/spindle 0.3.2 → 0.4.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.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -46,7 +46,8 @@
46
46
  "docs:dev": "vitepress dev docs",
47
47
  "docs:build": "vitepress build docs",
48
48
  "docs:preview": "vitepress preview docs",
49
- "prepublishOnly": "bun run test && bun run build"
49
+ "prepublishOnly": "bun run test && bun run build",
50
+ "prepare": "husky"
50
51
  },
51
52
  "dependencies": {
52
53
  "immer": "^11.1.4",
@@ -56,13 +57,18 @@
56
57
  "preact": "^10.28.4",
57
58
  "zustand": "^5.0.11"
58
59
  },
60
+ "lint-staged": {
61
+ "*": "prettier --write --ignore-unknown"
62
+ },
59
63
  "devDependencies": {
60
64
  "@preact/preset-vite": "^2.10.3",
61
65
  "@rohal12/twee-ts": "^1.1.2",
62
66
  "@types/js-yaml": "^4.0.9",
63
67
  "@vitest/coverage-v8": "^4.0.18",
64
68
  "happy-dom": "^20.8.3",
69
+ "husky": "^9.1.7",
65
70
  "js-yaml": "^4.1.1",
71
+ "lint-staged": "^16.3.2",
66
72
  "playwright": "^1.58.2",
67
73
  "prettier": "^3.8.1",
68
74
  "typescript": "^5.9.3",
@@ -27,7 +27,7 @@ export async function runAutomation(
27
27
  }
28
28
 
29
29
  for (let i = 0; i < script.steps.length; i++) {
30
- const step = script.steps[i];
30
+ const step = script.steps[i]!;
31
31
  options.onStep?.(i, step);
32
32
 
33
33
  try {
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
2
2
  import { useStoryStore } from '../store';
3
- import type { SaveRecord } from '../saves/types';
3
+ import { isSaveExport, type SaveRecord } from '../saves/types';
4
4
  import {
5
5
  getSavesGrouped,
6
6
  createSave,
@@ -45,7 +45,7 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
45
45
  text: string;
46
46
  type: 'success' | 'error';
47
47
  } | null>(null);
48
- const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
48
+ const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set());
49
49
  const [renamingId, setRenamingId] = useState<string | null>(null);
50
50
  const [renameValue, setRenameValue] = useState('');
51
51
  const renameInputRef = useRef<HTMLInputElement>(null);
@@ -75,9 +75,11 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
75
75
  }
76
76
  }, [renamingId]);
77
77
 
78
+ const statusTimer = useRef<number>();
78
79
  const showStatus = (text: string, type: 'success' | 'error' = 'success') => {
80
+ clearTimeout(statusTimer.current);
79
81
  setStatus({ text, type });
80
- setTimeout(() => setStatus(null), 3000);
82
+ statusTimer.current = window.setTimeout(() => setStatus(null), 3000);
81
83
  };
82
84
 
83
85
  const toggleCollapse = (id: string) => {
@@ -187,6 +189,9 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
187
189
  try {
188
190
  const text = await file.text();
189
191
  const data = JSON.parse(text);
192
+ if (!isSaveExport(data)) {
193
+ throw new Error('Invalid save file format');
194
+ }
190
195
  await importSave(data, ifid);
191
196
  showStatus('Save imported');
192
197
  await refresh();
@@ -12,8 +12,8 @@ function parseArgs(rawArgs: string): { varName: string; label: string } {
12
12
  if (!match) {
13
13
  return { varName: rawArgs.trim(), label: '' };
14
14
  }
15
- const varName = match[1].replace(/["']/g, '');
16
- const label = match[2];
15
+ const varName = match[1]!.replace(/["']/g, '');
16
+ const label = match[2]!;
17
17
  return { varName, label };
18
18
  }
19
19
 
@@ -1,8 +1,7 @@
1
1
  import { useLayoutEffect } from 'preact/hooks';
2
- import { useContext } from 'preact/hooks';
3
2
  import { useStoryStore } from '../../store';
4
3
  import { evaluate } from '../../expression';
5
- import { LocalsContext } from '../../markup/render';
4
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
6
5
 
7
6
  interface ComputedProps {
8
7
  rawArgs: string;
@@ -50,18 +49,32 @@ function valuesEqual(a: unknown, b: unknown): boolean {
50
49
  typeof b === 'object' &&
51
50
  b !== null
52
51
  ) {
53
- return JSON.stringify(a) === JSON.stringify(b);
52
+ try {
53
+ return JSON.stringify(a) === JSON.stringify(b);
54
+ } catch {
55
+ return false;
56
+ }
54
57
  }
55
58
  return false;
56
59
  }
57
60
 
58
61
  export function Computed({ rawArgs }: ComputedProps) {
59
- // Subscribe to trigger re-renders when store changes
60
- useStoryStore((s) => s.variables);
61
- useStoryStore((s) => s.temporary);
62
- const locals = useContext(LocalsContext);
63
-
64
- const { target, expr } = parseComputedArgs(rawArgs);
62
+ const [mergedVars, mergedTemps] = useMergedLocals();
63
+
64
+ let target: string;
65
+ let expr: string;
66
+ try {
67
+ ({ target, expr } = parseComputedArgs(rawArgs));
68
+ } catch (err) {
69
+ return (
70
+ <span
71
+ class="error"
72
+ title={String(err)}
73
+ >
74
+ {`{computed error: ${err instanceof Error ? err.message : String(err)}}`}
75
+ </span>
76
+ );
77
+ }
65
78
  const isTemp = target.startsWith('_');
66
79
  const name = target.slice(1);
67
80
 
@@ -69,14 +82,6 @@ export function Computed({ rawArgs }: ComputedProps) {
69
82
  useLayoutEffect(() => {
70
83
  const state = useStoryStore.getState();
71
84
 
72
- // Merge locals from for-loops
73
- const mergedVars = { ...state.variables };
74
- const mergedTemps = { ...state.temporary };
75
- for (const [key, val] of Object.entries(locals)) {
76
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
77
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
78
- }
79
-
80
85
  let newValue: unknown;
81
86
  try {
82
87
  newValue = evaluate(expr, mergedVars, mergedTemps);
@@ -1,8 +1,7 @@
1
- import { useStoryStore } from '../../store';
2
1
  import { useContext } from 'preact/hooks';
3
2
  import { evaluate } from '../../expression';
4
- import { LocalsContext } from '../../markup/render';
5
- import { renderNodes } from '../../markup/render';
3
+ import { LocalsContext, renderNodes } from '../../markup/render';
4
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
6
5
  import type { ASTNode } from '../../markup/ast';
7
6
 
8
7
  interface ForProps {
@@ -29,24 +28,15 @@ function parseForArgs(rawArgs: string): {
29
28
  const listExpr = rawArgs.slice(ofIdx + 4).trim();
30
29
 
31
30
  const vars = varsPart.split(',').map((v) => v.trim());
32
- const itemVar = vars[0];
33
- const indexVar = vars.length > 1 ? vars[1] : null;
31
+ const itemVar = vars[0]!;
32
+ const indexVar = vars.length > 1 ? vars[1]! : null;
34
33
 
35
34
  return { itemVar, indexVar, listExpr };
36
35
  }
37
36
 
38
37
  export function For({ rawArgs, children, className, id }: ForProps) {
39
- const variables = useStoryStore((s) => s.variables);
40
- const temporary = useStoryStore((s) => s.temporary);
41
38
  const parentLocals = useContext(LocalsContext);
42
-
43
- // Merge parent locals for expression evaluation
44
- const mergedVars = { ...variables };
45
- const mergedTemps = { ...temporary };
46
- for (const [key, val] of Object.entries(parentLocals)) {
47
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
48
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
49
- }
39
+ const [mergedVars, mergedTemps] = useMergedLocals();
50
40
 
51
41
  let parsed: ReturnType<typeof parseForArgs>;
52
42
  try {
@@ -57,7 +47,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
57
47
  class="error"
58
48
  title={String(err)}
59
49
  >
60
- {`{for error: ${(err as Error).message}}`}
50
+ {`{for error: ${err instanceof Error ? err.message : String(err)}}`}
61
51
  </span>
62
52
  );
63
53
  }
@@ -81,14 +71,17 @@ export function For({ rawArgs, children, className, id }: ForProps) {
81
71
  class="error"
82
72
  title={String(err)}
83
73
  >
84
- {`{for error: ${(err as Error).message}}`}
74
+ {`{for error: ${err instanceof Error ? err.message : String(err)}}`}
85
75
  </span>
86
76
  );
87
77
  }
88
78
 
89
79
  const content = list.map((item, i) => {
90
- const locals = { ...parentLocals, [itemVar]: item };
91
- if (indexVar) locals[indexVar] = i;
80
+ const locals = {
81
+ ...parentLocals,
82
+ [itemVar]: item,
83
+ ...(indexVar ? { [indexVar]: i } : undefined),
84
+ };
92
85
 
93
86
  return (
94
87
  <LocalsContext.Provider
@@ -1,8 +1,6 @@
1
- import { useStoryStore } from '../../store';
2
- import { useContext } from 'preact/hooks';
3
1
  import { evaluate } from '../../expression';
4
- import { LocalsContext } from '../../markup/render';
5
2
  import { renderNodes } from '../../markup/render';
3
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
6
4
  import type { Branch } from '../../markup/ast';
7
5
 
8
6
  interface IfProps {
@@ -10,17 +8,7 @@ interface IfProps {
10
8
  }
11
9
 
12
10
  export function If({ branches }: IfProps) {
13
- const variables = useStoryStore((s) => s.variables);
14
- const temporary = useStoryStore((s) => s.temporary);
15
- const locals = useContext(LocalsContext);
16
-
17
- // Merge locals for expression evaluation
18
- const mergedVars = { ...variables };
19
- const mergedTemps = { ...temporary };
20
- for (const [key, val] of Object.entries(locals)) {
21
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
22
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
23
- }
11
+ const [mergedVars, mergedTemps] = useMergedLocals();
24
12
 
25
13
  function renderBranch(branch: Branch) {
26
14
  const children = renderNodes(branch.children);
@@ -53,7 +41,7 @@ export function If({ branches }: IfProps) {
53
41
  class="error"
54
42
  title={String(err)}
55
43
  >
56
- {`{if error: ${(err as Error).message}}`}
44
+ {`{if error: ${err instanceof Error ? err.message : String(err)}}`}
57
45
  </span>
58
46
  );
59
47
  }
@@ -19,13 +19,13 @@ function parseArgs(rawArgs: string): {
19
19
  const re = /["']([^"']+)["']/g;
20
20
  let m;
21
21
  while ((m = re.exec(rawArgs)) !== null) {
22
- parts.push(m[1]);
22
+ parts.push(m[1]!);
23
23
  }
24
24
  if (parts.length >= 2) {
25
- return { display: parts[0], passage: parts[1] };
25
+ return { display: parts[0]!, passage: parts[1]! };
26
26
  }
27
27
  if (parts.length === 1) {
28
- return { display: parts[0], passage: null };
28
+ return { display: parts[0]!, passage: null };
29
29
  }
30
30
  // Fallback: treat entire rawArgs as display text
31
31
  return { display: rawArgs.trim(), passage: null };
@@ -1,7 +1,5 @@
1
- import { useStoryStore } from '../../store';
2
- import { useContext } from 'preact/hooks';
3
1
  import { evaluate } from '../../expression';
4
- import { LocalsContext } from '../../markup/render';
2
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
5
3
 
6
4
  interface MeterProps {
7
5
  rawArgs: string;
@@ -25,8 +23,8 @@ function parseArgs(rawArgs: string): {
25
23
  let rest = trimmed;
26
24
  const quoteMatch = rest.match(/\s+(?:"([^"]*)"|'([^']*)')$/);
27
25
  if (quoteMatch) {
28
- labelMode = quoteMatch[1] ?? quoteMatch[2];
29
- rest = rest.slice(0, quoteMatch.index!).trim();
26
+ labelMode = quoteMatch[1] ?? quoteMatch[2] ?? '';
27
+ rest = rest.slice(0, quoteMatch.index ?? rest.length).trim();
30
28
  }
31
29
 
32
30
  // Split remaining into two expressions.
@@ -40,7 +38,7 @@ function parseArgs(rawArgs: string): {
40
38
  }
41
39
 
42
40
  return {
43
- currentExpr: exprs[0],
41
+ currentExpr: exprs[0]!,
44
42
  maxExpr: exprs.slice(1).join(' '),
45
43
  labelMode,
46
44
  };
@@ -52,7 +50,7 @@ function splitExpressions(input: string): string[] {
52
50
  let depth = 0;
53
51
 
54
52
  for (let i = 0; i < input.length; i++) {
55
- const ch = input[i];
53
+ const ch = input[i]!;
56
54
  if (ch === '(' || ch === '[') {
57
55
  depth++;
58
56
  current += ch;
@@ -63,7 +61,7 @@ function splitExpressions(input: string): string[] {
63
61
  result.push(current);
64
62
  current = '';
65
63
  // Skip additional whitespace
66
- while (i + 1 < input.length && /\s/.test(input[i + 1])) i++;
64
+ while (i + 1 < input.length && /\s/.test(input[i + 1]!)) i++;
67
65
  } else {
68
66
  current += ch;
69
67
  }
@@ -78,29 +76,21 @@ function formatLabel(
78
76
  labelMode: string,
79
77
  ): string | null {
80
78
  if (labelMode === 'none') return null;
81
- if (labelMode === '%') return `${Math.round((current / max) * 100)}%`;
79
+ if (labelMode === '%')
80
+ return `${max === 0 ? 0 : Math.round((current / max) * 100)}%`;
82
81
  if (labelMode) return `${current} ${labelMode} / ${max} ${labelMode}`;
83
82
  return `${current} / ${max}`;
84
83
  }
85
84
 
86
85
  export function Meter({ rawArgs, className, id }: MeterProps) {
87
- const variables = useStoryStore((s) => s.variables);
88
- const temporary = useStoryStore((s) => s.temporary);
89
- const locals = useContext(LocalsContext);
90
-
91
- // Merge locals into variables for expression evaluation
92
- const mergedVars = { ...variables };
93
- const mergedTemps = { ...temporary };
94
- for (const [key, val] of Object.entries(locals)) {
95
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
96
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
97
- }
86
+ const [mergedVars, mergedTemps] = useMergedLocals();
98
87
 
99
88
  try {
100
89
  const { currentExpr, maxExpr, labelMode } = parseArgs(rawArgs);
101
90
  const current = Number(evaluate(currentExpr, mergedVars, mergedTemps));
102
91
  const max = Number(evaluate(maxExpr, mergedVars, mergedTemps));
103
- const pct = Math.max(0, Math.min(100, (current / max) * 100));
92
+ const pct =
93
+ max === 0 ? 0 : Math.max(0, Math.min(100, (current / max) * 100));
104
94
  const label = formatLabel(current, max, labelMode);
105
95
 
106
96
  const classes = ['macro-meter', className].filter(Boolean).join(' ');
@@ -123,7 +113,7 @@ export function Meter({ rawArgs, className, id }: MeterProps) {
123
113
  class="error"
124
114
  title={String(err)}
125
115
  >
126
- {`{meter error: ${(err as Error).message}}`}
116
+ {`{meter error: ${err instanceof Error ? err.message : String(err)}}`}
127
117
  </span>
128
118
  );
129
119
  }
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
12
12
  if (!match) {
13
13
  return { varName: rawArgs.trim(), placeholder: '' };
14
14
  }
15
- const varName = match[1].replace(/["']/g, '');
15
+ const varName = match[1]!.replace(/["']/g, '');
16
16
  const placeholder = match[2] || '';
17
17
  return { varName, placeholder };
18
18
  }
@@ -1,7 +1,5 @@
1
- import { useStoryStore } from '../../store';
2
- import { useContext } from 'preact/hooks';
3
1
  import { evaluate } from '../../expression';
4
- import { LocalsContext } from '../../markup/render';
2
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
5
3
 
6
4
  interface PrintProps {
7
5
  rawArgs: string;
@@ -10,17 +8,7 @@ interface PrintProps {
10
8
  }
11
9
 
12
10
  export function Print({ rawArgs, className, id }: PrintProps) {
13
- const variables = useStoryStore((s) => s.variables);
14
- const temporary = useStoryStore((s) => s.temporary);
15
- const locals = useContext(LocalsContext);
16
-
17
- // Merge locals into variables for expression evaluation
18
- const mergedVars = { ...variables };
19
- const mergedTemps = { ...temporary };
20
- for (const [key, val] of Object.entries(locals)) {
21
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
22
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
23
- }
11
+ const [mergedVars, mergedTemps] = useMergedLocals();
24
12
 
25
13
  try {
26
14
  const result = evaluate(rawArgs, mergedVars, mergedTemps);
@@ -41,7 +29,7 @@ export function Print({ rawArgs, className, id }: PrintProps) {
41
29
  class="error"
42
30
  title={String(err)}
43
31
  >
44
- {`{print error: ${(err as Error).message}}`}
32
+ {`{print error: ${err instanceof Error ? err.message : String(err)}}`}
45
33
  </span>
46
34
  );
47
35
  }
@@ -20,15 +20,15 @@ function parseArgs(rawArgs: string): {
20
20
  // Try simpler: $var value label
21
21
  const parts = rawArgs.trim().split(/\s+/);
22
22
  return {
23
- varName: (parts[0] || '').replace(/["']/g, ''),
24
- value: parts[1] || '',
23
+ varName: (parts[0] ?? '').replace(/["']/g, ''),
24
+ value: parts[1] ?? '',
25
25
  label: parts.slice(2).join(' '),
26
26
  };
27
27
  }
28
28
  return {
29
- varName: match[1].replace(/["']/g, ''),
30
- value: match[2],
31
- label: match[3],
29
+ varName: match[1]!.replace(/["']/g, ''),
30
+ value: match[2]!,
31
+ label: match[3]!,
32
32
  };
33
33
  }
34
34
 
@@ -1,7 +1,6 @@
1
- import { useStoryStore } from '../../store';
2
- import { useContext } from 'preact/hooks';
3
1
  import { evaluate } from '../../expression';
4
- import { LocalsContext, renderNodes } from '../../markup/render';
2
+ import { renderNodes } from '../../markup/render';
3
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
5
4
  import type { Branch } from '../../markup/ast';
6
5
 
7
6
  interface SwitchProps {
@@ -10,16 +9,7 @@ interface SwitchProps {
10
9
  }
11
10
 
12
11
  export function Switch({ rawArgs, branches }: SwitchProps) {
13
- const variables = useStoryStore((s) => s.variables);
14
- const temporary = useStoryStore((s) => s.temporary);
15
- const locals = useContext(LocalsContext);
16
-
17
- const mergedVars = { ...variables };
18
- const mergedTemps = { ...temporary };
19
- for (const [key, val] of Object.entries(locals)) {
20
- if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
21
- else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
22
- }
12
+ const [mergedVars, mergedTemps] = useMergedLocals();
23
13
 
24
14
  let switchValue: unknown;
25
15
  try {
@@ -30,7 +20,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
30
20
  class="error"
31
21
  title={String(err)}
32
22
  >
33
- {`{switch error: ${(err as Error).message}}`}
23
+ {`{switch error: ${err instanceof Error ? err.message : String(err)}}`}
34
24
  </span>
35
25
  );
36
26
  }
@@ -55,7 +45,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
55
45
  class="error"
56
46
  title={String(err)}
57
47
  >
58
- {`{case error: ${(err as Error).message}}`}
48
+ {`{case error: ${err instanceof Error ? err.message : String(err)}}`}
59
49
  </span>
60
50
  );
61
51
  }
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
12
12
  if (!match) {
13
13
  return { varName: rawArgs.trim(), placeholder: '' };
14
14
  }
15
- const varName = match[1].replace(/["']/g, '');
15
+ const varName = match[1]!.replace(/["']/g, '');
16
16
  const placeholder = match[2] || '';
17
17
  return { varName, placeholder };
18
18
  }
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
12
12
  if (!match) {
13
13
  return { varName: rawArgs.trim(), placeholder: '' };
14
14
  }
15
- const varName = match[1].replace(/["']/g, '');
15
+ const varName = match[1]!.replace(/["']/g, '');
16
16
  const placeholder = match[2] || '';
17
17
  return { varName, placeholder };
18
18
  }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'preact/hooks';
1
+ import { useState, useEffect, useMemo } from 'preact/hooks';
2
2
  import { renderNodes } from '../../markup/render';
3
3
  import { parseDelay } from '../../utils/parse-delay';
4
4
  import type { ASTNode, Branch } from '../../markup/ast';
@@ -20,16 +20,15 @@ export function Timed({
20
20
  }: TimedProps) {
21
21
  // Section 0 = initial children, sections 1..N = {next} branches
22
22
  // Each section has its own delay
23
- const sections: { delay: number; nodes: ASTNode[] }[] = [];
24
-
25
- // Initial content with the timed delay
26
- sections.push({ delay: parseDelay(rawArgs), nodes: children });
27
-
28
- // {next} branches
29
- for (const branch of branches) {
30
- const delay = branch.rawArgs ? parseDelay(branch.rawArgs) : 0;
31
- sections.push({ delay, nodes: branch.children });
32
- }
23
+ const sections = useMemo(() => {
24
+ const result: { delay: number; nodes: ASTNode[] }[] = [];
25
+ result.push({ delay: parseDelay(rawArgs), nodes: children });
26
+ for (const branch of branches) {
27
+ const delay = branch.rawArgs ? parseDelay(branch.rawArgs) : 0;
28
+ result.push({ delay, nodes: branch.children });
29
+ }
30
+ return result;
31
+ }, [rawArgs, children, branches]);
33
32
 
34
33
  const [visibleIndex, setVisibleIndex] = useState(-1);
35
34
 
@@ -37,18 +36,18 @@ export function Timed({
37
36
  if (visibleIndex >= sections.length - 1) return;
38
37
 
39
38
  const nextIndex = visibleIndex + 1;
40
- const delay = sections[nextIndex].delay;
39
+ const delay = sections[nextIndex]!.delay;
41
40
 
42
41
  const timer = setTimeout(() => {
43
42
  setVisibleIndex(nextIndex);
44
43
  }, delay);
45
44
 
46
45
  return () => clearTimeout(timer);
47
- }, [visibleIndex, sections.length]);
46
+ }, [visibleIndex, sections]);
48
47
 
49
48
  if (visibleIndex < 0) return null;
50
49
 
51
- const content = renderNodes(sections[visibleIndex].nodes);
50
+ const content = renderNodes(sections[visibleIndex]!.nodes);
52
51
 
53
52
  if (className || id)
54
53
  return (
@@ -12,7 +12,7 @@ interface VarDisplayProps {
12
12
  export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
13
13
  const locals = useContext(LocalsContext);
14
14
  const parts = name.split('.');
15
- const root = parts[0];
15
+ const root = parts[0]!;
16
16
  const storeValue = useStoryStore((s) =>
17
17
  scope === 'variable' ? s.variables[root] : s.temporary[root],
18
18
  );
@@ -27,7 +27,8 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
27
27
  value = undefined;
28
28
  break;
29
29
  }
30
- value = (value as Record<string, unknown>)[parts[i]];
30
+ const part = parts[i] as string;
31
+ value = (value as Record<string, unknown>)[part];
31
32
  }
32
33
 
33
34
  const display = value == null ? '' : String(value);