@rohal12/spindle 0.1.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.
Files changed (65) hide show
  1. package/README.md +66 -0
  2. package/dist/pkg/format.js +1 -0
  3. package/dist/pkg/index.js +12 -0
  4. package/dist/pkg/types/globals.d.ts +18 -0
  5. package/dist/pkg/types/index.d.ts +158 -0
  6. package/package.json +71 -0
  7. package/src/components/App.tsx +53 -0
  8. package/src/components/Passage.tsx +36 -0
  9. package/src/components/PassageLink.tsx +35 -0
  10. package/src/components/SaveLoadDialog.tsx +403 -0
  11. package/src/components/SettingsDialog.tsx +106 -0
  12. package/src/components/StoryInterface.tsx +31 -0
  13. package/src/components/macros/Back.tsx +23 -0
  14. package/src/components/macros/Button.tsx +49 -0
  15. package/src/components/macros/Checkbox.tsx +41 -0
  16. package/src/components/macros/Computed.tsx +100 -0
  17. package/src/components/macros/Cycle.tsx +39 -0
  18. package/src/components/macros/Do.tsx +46 -0
  19. package/src/components/macros/For.tsx +113 -0
  20. package/src/components/macros/Forward.tsx +25 -0
  21. package/src/components/macros/Goto.tsx +23 -0
  22. package/src/components/macros/If.tsx +63 -0
  23. package/src/components/macros/Include.tsx +52 -0
  24. package/src/components/macros/Listbox.tsx +42 -0
  25. package/src/components/macros/MacroLink.tsx +107 -0
  26. package/src/components/macros/Numberbox.tsx +43 -0
  27. package/src/components/macros/Print.tsx +48 -0
  28. package/src/components/macros/QuickLoad.tsx +33 -0
  29. package/src/components/macros/QuickSave.tsx +22 -0
  30. package/src/components/macros/Radiobutton.tsx +59 -0
  31. package/src/components/macros/Repeat.tsx +53 -0
  32. package/src/components/macros/Restart.tsx +27 -0
  33. package/src/components/macros/Saves.tsx +25 -0
  34. package/src/components/macros/Set.tsx +36 -0
  35. package/src/components/macros/SettingsButton.tsx +29 -0
  36. package/src/components/macros/Stop.tsx +12 -0
  37. package/src/components/macros/StoryTitle.tsx +20 -0
  38. package/src/components/macros/Switch.tsx +69 -0
  39. package/src/components/macros/Textarea.tsx +41 -0
  40. package/src/components/macros/Textbox.tsx +40 -0
  41. package/src/components/macros/Timed.tsx +63 -0
  42. package/src/components/macros/Type.tsx +83 -0
  43. package/src/components/macros/Unset.tsx +25 -0
  44. package/src/components/macros/VarDisplay.tsx +44 -0
  45. package/src/components/macros/Widget.tsx +18 -0
  46. package/src/components/macros/option-utils.ts +14 -0
  47. package/src/expression.ts +93 -0
  48. package/src/index.tsx +120 -0
  49. package/src/markup/ast.ts +284 -0
  50. package/src/markup/markdown.ts +21 -0
  51. package/src/markup/render.tsx +537 -0
  52. package/src/markup/tokenizer.ts +581 -0
  53. package/src/parser.ts +72 -0
  54. package/src/registry.ts +21 -0
  55. package/src/saves/idb.ts +165 -0
  56. package/src/saves/save-manager.ts +317 -0
  57. package/src/saves/types.ts +40 -0
  58. package/src/settings.ts +96 -0
  59. package/src/store.ts +317 -0
  60. package/src/story-api.ts +129 -0
  61. package/src/story-init.ts +67 -0
  62. package/src/story-variables.ts +166 -0
  63. package/src/styles.css +780 -0
  64. package/src/utils/parse-delay.ts +14 -0
  65. package/src/widgets/widget-registry.ts +15 -0
@@ -0,0 +1,158 @@
1
+ // Format metadata (used by twee-ts)
2
+ export declare const name: string;
3
+ export declare const version: string;
4
+ export declare const source: string;
5
+ export declare const proofing: boolean;
6
+
7
+ // --- Format-specific API types (used by story authors) ---
8
+
9
+ /**
10
+ * A moment in the story history, capturing the state at a specific navigation point.
11
+ * @see {@link ../../src/store.ts} for the implementation.
12
+ */
13
+ export interface HistoryMoment {
14
+ passage: string;
15
+ variables: Record<string, unknown>;
16
+ timestamp: number;
17
+ }
18
+
19
+ /**
20
+ * Payload stored in a save slot.
21
+ * @see {@link ../../src/saves/types.ts} for the implementation.
22
+ */
23
+ export interface SavePayload {
24
+ passage: string;
25
+ variables: Record<string, unknown>;
26
+ history: HistoryMoment[];
27
+ historyIndex: number;
28
+ visitCounts?: Record<string, number>;
29
+ renderCounts?: Record<string, number>;
30
+ }
31
+
32
+ /**
33
+ * Configuration for a toggle (boolean) setting.
34
+ * @see {@link ../../src/settings.ts} for the implementation.
35
+ */
36
+ export interface ToggleConfig {
37
+ label: string;
38
+ default: boolean;
39
+ }
40
+
41
+ /**
42
+ * Configuration for a list (dropdown) setting.
43
+ * @see {@link ../../src/settings.ts} for the implementation.
44
+ */
45
+ export interface ListConfig {
46
+ label: string;
47
+ options: string[];
48
+ default: string;
49
+ }
50
+
51
+ /**
52
+ * Configuration for a range (slider) setting.
53
+ * @see {@link ../../src/settings.ts} for the implementation.
54
+ */
55
+ export interface RangeConfig {
56
+ label: string;
57
+ min: number;
58
+ max: number;
59
+ step: number;
60
+ default: number;
61
+ }
62
+
63
+ /**
64
+ * Discriminated union of setting definitions.
65
+ * @see {@link ../../src/settings.ts} for the implementation.
66
+ */
67
+ export type SettingDef =
68
+ | { type: 'toggle'; config: ToggleConfig }
69
+ | { type: 'list'; config: ListConfig }
70
+ | { type: 'range'; config: RangeConfig };
71
+
72
+ /**
73
+ * The settings API for registering and managing story settings.
74
+ * Settings appear in the built-in settings dialog.
75
+ * @see {@link ../../src/settings.ts} for the implementation.
76
+ */
77
+ export interface SettingsAPI {
78
+ addToggle(name: string, config: ToggleConfig): void;
79
+ addList(name: string, config: ListConfig): void;
80
+ addRange(name: string, config: RangeConfig): void;
81
+ get(name: string): unknown;
82
+ set(name: string, value: unknown): void;
83
+ getAll(): Record<string, unknown>;
84
+ getDefinitions(): Map<string, SettingDef>;
85
+ hasAny(): boolean;
86
+ }
87
+
88
+ /**
89
+ * The main Story API available as `window.Story` at runtime.
90
+ * Provides access to variables, navigation, save/load, and visit tracking.
91
+ * @see {@link ../../src/story-api.ts} for the implementation.
92
+ */
93
+ export interface StoryAPI {
94
+ /** Get the value of a story variable. */
95
+ get(name: string): unknown;
96
+
97
+ /** Set a single story variable. */
98
+ set(name: string, value: unknown): void;
99
+ /** Set multiple story variables at once. */
100
+ set(vars: Record<string, unknown>): void;
101
+
102
+ /** Navigate to a passage by name. */
103
+ goto(passageName: string): void;
104
+
105
+ /** Go back one step in history. */
106
+ back(): void;
107
+
108
+ /** Go forward one step in history. */
109
+ forward(): void;
110
+
111
+ /** Restart the story from the beginning. */
112
+ restart(): void;
113
+
114
+ /** Save the current state (quick save). */
115
+ save(slot?: string): void;
116
+
117
+ /** Load a saved state (quick load). */
118
+ load(slot?: string): void;
119
+
120
+ /** Check whether a save exists. */
121
+ hasSave(slot?: string): boolean;
122
+
123
+ /** Return the number of times a passage has been visited. */
124
+ visited(name: string): number;
125
+
126
+ /** Check if a passage has been visited at least once. */
127
+ hasVisited(name: string): boolean;
128
+
129
+ /** Check if any of the given passages have been visited. */
130
+ hasVisitedAny(...names: string[]): boolean;
131
+
132
+ /** Check if all of the given passages have been visited. */
133
+ hasVisitedAll(...names: string[]): boolean;
134
+
135
+ /** Return the number of times a passage has been rendered. */
136
+ rendered(name: string): number;
137
+
138
+ /** Check if a passage has been rendered at least once. */
139
+ hasRendered(name: string): boolean;
140
+
141
+ /** Check if any of the given passages have been rendered. */
142
+ hasRenderedAny(...names: string[]): boolean;
143
+
144
+ /** Check if all of the given passages have been rendered. */
145
+ hasRenderedAll(...names: string[]): boolean;
146
+
147
+ /** The story title. */
148
+ readonly title: string;
149
+
150
+ /** The settings API. */
151
+ readonly settings: SettingsAPI;
152
+
153
+ /** Save system configuration. */
154
+ readonly saves: {
155
+ /** Set a custom function to generate save titles. */
156
+ setTitleGenerator(fn: (payload: SavePayload) => string): void;
157
+ };
158
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@rohal12/spindle",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A Preact-based story format for Twine 2.",
6
+ "license": "Unlicense",
7
+ "author": "Rohal12",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/rohal12/spindle.git"
14
+ },
15
+ "homepage": "https://rohal12.github.io/spindle/",
16
+ "bugs": "https://github.com/rohal12/spindle/issues",
17
+ "keywords": [
18
+ "twine-story-format",
19
+ "twine",
20
+ "interactive-fiction",
21
+ "story-format",
22
+ "preact"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/pkg/types/index.d.ts",
27
+ "import": "./dist/pkg/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist/pkg/index.js",
32
+ "dist/pkg/format.js",
33
+ "dist/pkg/types",
34
+ "src"
35
+ ],
36
+ "scripts": {
37
+ "build": "vite build && bun run scripts/build-format.ts",
38
+ "compile": "bun run scripts/compile-story.ts",
39
+ "preview": "bun run build && bun run compile",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check .",
45
+ "docs:dev": "vitepress dev docs",
46
+ "docs:build": "vitepress build docs",
47
+ "docs:preview": "vitepress preview docs",
48
+ "prepublishOnly": "bun run test && bun run build"
49
+ },
50
+ "dependencies": {
51
+ "immer": "^10.1.0",
52
+ "micromark": "^4.0.2",
53
+ "micromark-extension-gfm-strikethrough": "^2.1.0",
54
+ "micromark-extension-gfm-table": "^2.1.1",
55
+ "preact": "^10.25.0",
56
+ "zustand": "^5.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@preact/preset-vite": "^2.9.0",
60
+ "@vitest/coverage-v8": "^4.0.18",
61
+ "happy-dom": "^20.7.0",
62
+ "playwright": "^1.58.2",
63
+ "prettier": "^3.8.1",
64
+ "tweenode": "^0.3.0",
65
+ "typescript": "^5.7.0",
66
+ "vite": "^6.0.0",
67
+ "vite-plugin-singlefile": "^2.0.0",
68
+ "vitepress": "^1.6.4",
69
+ "vitest": "^4.0.18"
70
+ }
71
+ }
@@ -0,0 +1,53 @@
1
+ import { useEffect } from 'preact/hooks';
2
+ import { useStoryStore } from '../store';
3
+ import { Passage } from './Passage';
4
+ import { StoryInterface } from './StoryInterface';
5
+
6
+ export function App() {
7
+ const currentPassage = useStoryStore((s) => s.currentPassage);
8
+ const storyData = useStoryStore((s) => s.storyData);
9
+
10
+ useEffect(() => {
11
+ const onKeyDown = (e: KeyboardEvent) => {
12
+ if (e.key === 'F6') {
13
+ e.preventDefault();
14
+ useStoryStore.getState().save();
15
+ } else if (e.key === 'F9') {
16
+ e.preventDefault();
17
+ useStoryStore.getState().load();
18
+ }
19
+ };
20
+ document.addEventListener('keydown', onKeyDown);
21
+ return () => document.removeEventListener('keydown', onKeyDown);
22
+ }, []);
23
+
24
+ if (!storyData || !currentPassage) {
25
+ return <div class="loading">Loading...</div>;
26
+ }
27
+
28
+ const passage = storyData.passages.get(currentPassage);
29
+ if (!passage) {
30
+ return (
31
+ <div class="error">
32
+ Error: Passage &ldquo;{currentPassage}&rdquo; not found.
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <>
39
+ <header class="story-menubar">
40
+ <StoryInterface />
41
+ </header>
42
+ <div
43
+ id="story"
44
+ class="story"
45
+ >
46
+ <Passage
47
+ passage={passage}
48
+ key={currentPassage}
49
+ />
50
+ </div>
51
+ </>
52
+ );
53
+ }
@@ -0,0 +1,36 @@
1
+ import { useMemo } from 'preact/hooks';
2
+ import { tokenize } from '../markup/tokenizer';
3
+ import { buildAST } from '../markup/ast';
4
+ import { renderNodes } from '../markup/render';
5
+ import type { Passage as PassageData } from '../parser';
6
+
7
+ interface PassageProps {
8
+ passage: PassageData;
9
+ }
10
+
11
+ export function Passage({ passage }: PassageProps) {
12
+ const content = useMemo(() => {
13
+ try {
14
+ const tokens = tokenize(passage.content);
15
+ const ast = buildAST(tokens);
16
+ return renderNodes(ast);
17
+ } catch (err) {
18
+ return (
19
+ <div class="error">
20
+ Error parsing passage &ldquo;{passage.name}&rdquo;:{' '}
21
+ {(err as Error).message}
22
+ </div>
23
+ );
24
+ }
25
+ }, [passage.content, passage.name]);
26
+
27
+ return (
28
+ <div
29
+ class="passage"
30
+ data-passage={passage.name}
31
+ data-tags={passage.tags.join(' ')}
32
+ >
33
+ {content}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,35 @@
1
+ import { useStoryStore } from '../store';
2
+
3
+ interface PassageLinkProps {
4
+ target: string;
5
+ className?: string;
6
+ id?: string;
7
+ children: preact.ComponentChildren;
8
+ }
9
+
10
+ export function PassageLink({
11
+ target,
12
+ className,
13
+ id,
14
+ children,
15
+ }: PassageLinkProps) {
16
+ const navigate = useStoryStore((s) => s.navigate);
17
+
18
+ const handleClick = (e: MouseEvent) => {
19
+ e.preventDefault();
20
+ navigate(target);
21
+ };
22
+
23
+ const cls = className ? `passage-link ${className}` : 'passage-link';
24
+
25
+ return (
26
+ <a
27
+ href="#"
28
+ id={id}
29
+ class={cls}
30
+ onClick={handleClick}
31
+ >
32
+ {children}
33
+ </a>
34
+ );
35
+ }