@rohal12/spindle 0.1.0 → 0.2.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.
@@ -46,16 +46,74 @@ export interface HtmlToken {
46
46
  end: number;
47
47
  }
48
48
 
49
- export type Token = TextToken | LinkToken | MacroToken | VariableToken | HtmlToken;
49
+ export type Token =
50
+ | TextToken
51
+ | LinkToken
52
+ | MacroToken
53
+ | VariableToken
54
+ | HtmlToken;
50
55
 
51
56
  const HTML_TAGS = new Set([
52
- 'a', 'article', 'aside', 'b', 'blockquote', 'br', 'caption', 'code',
53
- 'col', 'colgroup', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt',
54
- 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5',
55
- 'h6', 'header', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
56
- 'nav', 'ol', 'p', 'pre', 'q', 's', 'samp', 'section', 'small', 'span',
57
- 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
58
- 'thead', 'tr', 'u', 'ul', 'wbr',
57
+ 'a',
58
+ 'article',
59
+ 'aside',
60
+ 'b',
61
+ 'blockquote',
62
+ 'br',
63
+ 'caption',
64
+ 'code',
65
+ 'col',
66
+ 'colgroup',
67
+ 'dd',
68
+ 'del',
69
+ 'details',
70
+ 'dfn',
71
+ 'div',
72
+ 'dl',
73
+ 'dt',
74
+ 'em',
75
+ 'figcaption',
76
+ 'figure',
77
+ 'footer',
78
+ 'h1',
79
+ 'h2',
80
+ 'h3',
81
+ 'h4',
82
+ 'h5',
83
+ 'h6',
84
+ 'header',
85
+ 'hr',
86
+ 'i',
87
+ 'img',
88
+ 'ins',
89
+ 'kbd',
90
+ 'li',
91
+ 'main',
92
+ 'mark',
93
+ 'nav',
94
+ 'ol',
95
+ 'p',
96
+ 'pre',
97
+ 'q',
98
+ 's',
99
+ 'samp',
100
+ 'section',
101
+ 'small',
102
+ 'span',
103
+ 'strong',
104
+ 'sub',
105
+ 'summary',
106
+ 'sup',
107
+ 'table',
108
+ 'tbody',
109
+ 'td',
110
+ 'tfoot',
111
+ 'th',
112
+ 'thead',
113
+ 'tr',
114
+ 'u',
115
+ 'ul',
116
+ 'wbr',
59
117
  ]);
60
118
 
61
119
  const HTML_VOID_TAGS = new Set(['br', 'col', 'hr', 'img', 'wbr']);
@@ -15,6 +15,7 @@ import {
15
15
  getMeta,
16
16
  setMeta,
17
17
  } from './idb';
18
+ import { deepClone, serialize, deserialize } from '../class-registry';
18
19
 
19
20
  type TitleGenerator = (payload: SavePayload) => string;
20
21
 
@@ -118,7 +119,13 @@ export async function createSave(
118
119
  custom,
119
120
  };
120
121
 
121
- const record: SaveRecord = { meta, payload: structuredClone(payload) };
122
+ const serializedPayload = deepClone(payload);
123
+ serializedPayload.variables = serialize(serializedPayload.variables);
124
+ serializedPayload.history = serializedPayload.history.map((m) => ({
125
+ ...m,
126
+ variables: serialize(m.variables),
127
+ }));
128
+ const record: SaveRecord = { meta, payload: serializedPayload };
122
129
  await putSave(record);
123
130
  return record;
124
131
  }
@@ -132,7 +139,13 @@ export async function overwriteSave(
132
139
 
133
140
  existing.meta.updatedAt = new Date().toISOString();
134
141
  existing.meta.passage = payload.passage;
135
- existing.payload = structuredClone(payload);
142
+ const serializedPayload = deepClone(payload);
143
+ serializedPayload.variables = serialize(serializedPayload.variables);
144
+ serializedPayload.history = serializedPayload.history.map((m) => ({
145
+ ...m,
146
+ variables: serialize(m.variables),
147
+ }));
148
+ existing.payload = serializedPayload;
136
149
  await putSave(existing);
137
150
  return existing;
138
151
  }
@@ -141,7 +154,14 @@ export async function loadSave(
141
154
  saveId: string,
142
155
  ): Promise<SavePayload | undefined> {
143
156
  const record = await getSave(saveId);
144
- return record?.payload;
157
+ if (!record) return undefined;
158
+ const payload = record.payload;
159
+ payload.variables = deserialize(payload.variables);
160
+ payload.history = payload.history.map((m) => ({
161
+ ...m,
162
+ variables: deserialize(m.variables),
163
+ }));
164
+ return payload;
145
165
  }
146
166
 
147
167
  export async function deleteSaveById(saveId: string): Promise<void> {
@@ -294,7 +314,7 @@ export async function importSave(
294
314
  }
295
315
 
296
316
  // Re-assign a new ID to avoid collisions
297
- const record = structuredClone(data.save);
317
+ const record = deepClone(data.save);
298
318
  record.meta.id = crypto.randomUUID();
299
319
  record.meta.updatedAt = new Date().toISOString();
300
320
 
package/src/store.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  quickSave,
11
11
  loadQuickSave,
12
12
  } from './saves/save-manager';
13
+ import { deepClone, deserialize } from './class-registry';
13
14
 
14
15
  export interface HistoryMoment {
15
16
  passage: string;
@@ -75,7 +76,7 @@ export const useStoryStore = create<StoryState>()(
75
76
  );
76
77
  }
77
78
 
78
- const initialVars = structuredClone(variableDefaults);
79
+ const initialVars = deepClone(variableDefaults);
79
80
 
80
81
  set((state) => {
81
82
  state.storyData = storyData as StoryData;
@@ -86,7 +87,7 @@ export const useStoryStore = create<StoryState>()(
86
87
  state.history = [
87
88
  {
88
89
  passage: startPassage.name,
89
- variables: structuredClone(initialVars),
90
+ variables: deepClone(initialVars),
90
91
  timestamp: Date.now(),
91
92
  },
92
93
  ];
@@ -130,7 +131,7 @@ export const useStoryStore = create<StoryState>()(
130
131
 
131
132
  state.history.push({
132
133
  passage: passageName,
133
- variables: { ...state.variables },
134
+ variables: deepClone(state.variables),
134
135
  timestamp: Date.now(),
135
136
  });
136
137
  state.historyIndex = state.history.length - 1;
@@ -147,7 +148,7 @@ export const useStoryStore = create<StoryState>()(
147
148
  state.historyIndex--;
148
149
  const moment = state.history[state.historyIndex];
149
150
  state.currentPassage = moment.passage;
150
- state.variables = { ...moment.variables };
151
+ state.variables = deepClone(moment.variables);
151
152
  state.temporary = {};
152
153
  });
153
154
  },
@@ -158,7 +159,7 @@ export const useStoryStore = create<StoryState>()(
158
159
  state.historyIndex++;
159
160
  const moment = state.history[state.historyIndex];
160
161
  state.currentPassage = moment.passage;
161
- state.variables = { ...moment.variables };
162
+ state.variables = deepClone(moment.variables);
162
163
  state.temporary = {};
163
164
  });
164
165
  },
@@ -201,7 +202,7 @@ export const useStoryStore = create<StoryState>()(
201
202
  const startPassage = storyData.passagesById.get(storyData.startNode);
202
203
  if (!startPassage) return;
203
204
 
204
- const initialVars = structuredClone(variableDefaults);
205
+ const initialVars = deepClone(variableDefaults);
205
206
 
206
207
  set((state) => {
207
208
  state.currentPassage = startPassage.name;
@@ -210,7 +211,7 @@ export const useStoryStore = create<StoryState>()(
210
211
  state.history = [
211
212
  {
212
213
  passage: startPassage.name,
213
- variables: structuredClone(initialVars),
214
+ variables: deepClone(initialVars),
214
215
  timestamp: Date.now(),
215
216
  },
216
217
  ];
@@ -244,8 +245,8 @@ export const useStoryStore = create<StoryState>()(
244
245
 
245
246
  const payload: SavePayload = {
246
247
  passage: currentPassage,
247
- variables: structuredClone(variables),
248
- history: structuredClone(history),
248
+ variables: deepClone(variables),
249
+ history: deepClone(history),
249
250
  historyIndex,
250
251
  visitCounts: { ...visitCounts },
251
252
  renderCounts: { ...renderCounts },
@@ -294,8 +295,8 @@ export const useStoryStore = create<StoryState>()(
294
295
  } = get();
295
296
  return {
296
297
  passage: currentPassage,
297
- variables: structuredClone(variables),
298
- history: structuredClone(history),
298
+ variables: deepClone(variables),
299
+ history: deepClone(history),
299
300
  historyIndex,
300
301
  visitCounts: { ...visitCounts },
301
302
  renderCounts: { ...renderCounts },
@@ -305,8 +306,11 @@ export const useStoryStore = create<StoryState>()(
305
306
  loadFromPayload: (payload: SavePayload) => {
306
307
  set((state) => {
307
308
  state.currentPassage = payload.passage;
308
- state.variables = payload.variables;
309
- state.history = payload.history;
309
+ state.variables = deserialize(payload.variables);
310
+ state.history = payload.history.map((m) => ({
311
+ ...m,
312
+ variables: deserialize(m.variables),
313
+ }));
310
314
  state.historyIndex = payload.historyIndex;
311
315
  state.visitCounts = payload.visitCounts ?? {};
312
316
  state.renderCounts = payload.renderCounts ?? {};
package/src/story-api.ts CHANGED
@@ -2,6 +2,7 @@ import { useStoryStore } from './store';
2
2
  import { settings } from './settings';
3
3
  import type { SavePayload } from './saves/types';
4
4
  import { setTitleGenerator } from './saves/save-manager';
5
+ import { registerClass } from './class-registry';
5
6
 
6
7
  export interface StoryAPI {
7
8
  get(name: string): unknown;
@@ -24,6 +25,7 @@ export interface StoryAPI {
24
25
  hasRenderedAll(...names: string[]): boolean;
25
26
  readonly title: string;
26
27
  readonly settings: typeof settings;
28
+ registerClass(name: string, ctor: new (...args: any[]) => any): void;
27
29
  readonly saves: {
28
30
  setTitleGenerator(fn: (payload: SavePayload) => string): void;
29
31
  };
@@ -116,6 +118,10 @@ function createStoryAPI(): StoryAPI {
116
118
 
117
119
  settings,
118
120
 
121
+ registerClass(name: string, ctor: new (...args: any[]) => any): void {
122
+ registerClass(name, ctor);
123
+ },
124
+
119
125
  saves: {
120
126
  setTitleGenerator(fn: (payload: SavePayload) => string): void {
121
127
  setTitleGenerator(fn);
package/src/story-init.ts CHANGED
@@ -4,6 +4,7 @@ import { buildAST } from './markup/ast';
4
4
  import { execute } from './expression';
5
5
  import type { ASTNode } from './markup/ast';
6
6
  import { setSaveTitlePassage } from './saves/save-manager';
7
+ import { deepClone } from './class-registry';
7
8
 
8
9
  /**
9
10
  * Walk AST nodes from StoryInit and execute {set} and {do} imperatively
@@ -42,8 +43,8 @@ export function executeStoryInit() {
42
43
  const tokens = tokenize(storyInit.content);
43
44
  const ast = buildAST(tokens);
44
45
 
45
- const vars = { ...state.variables };
46
- const temps = { ...state.temporary };
46
+ const vars = deepClone(state.variables);
47
+ const temps = deepClone(state.temporary);
47
48
 
48
49
  walkAndExecute(ast, vars, temps);
49
50
 
package/src/styles.css CHANGED
@@ -629,6 +629,36 @@ tw-storydata {
629
629
  border-color: #90caf9;
630
630
  }
631
631
 
632
+ /* Macro: meter */
633
+
634
+ .macro-meter {
635
+ position: relative;
636
+ width: 100%;
637
+ height: 1.4em;
638
+ background: #2a2a3e;
639
+ border-radius: 0.3em;
640
+ overflow: hidden;
641
+ border: 1px solid #444;
642
+ }
643
+
644
+ .macro-meter-fill {
645
+ height: 100%;
646
+ background: #64b5f6;
647
+ transition: width 0.3s ease;
648
+ }
649
+
650
+ .macro-meter-label {
651
+ position: absolute;
652
+ inset: 0;
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: center;
656
+ font-size: 0.85em;
657
+ color: #e0e0e0;
658
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
659
+ pointer-events: none;
660
+ }
661
+
632
662
  /* Markdown wrapper — invisible to layout */
633
663
 
634
664
  .md {
@@ -777,4 +807,4 @@ tw-storydata {
777
807
  50% {
778
808
  opacity: 0;
779
809
  }
780
- }
810
+ }