Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,4 @@ jobs:
- Security issues
- Bug risks
- Code quality and adherence to project conventions
claude_args: --max-turns 100 --verbose
show_full_output: true
claude_args: --max-turns 100
525 changes: 350 additions & 175 deletions docs/custom-macros.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions docs/story-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ Load the quick save.

Returns `true` if a quick save exists for the current session.

### `Story.defineMacro(config)`

Register a custom macro. See [Custom Macros](custom-macros.md) for full details.

| Property | Type | Description |
| ------------- | ----------- | ------------------------------------------------------------------- |
| `name` | `string` | Macro name (case-insensitive) |
| `interpolate` | `boolean?` | Resolve variable interpolations in className/id |
| `merged` | `boolean?` | Provide `ctx.merged` variable 3-tuple + `ctx.evaluate()` |
| `storeVar` | `boolean?` | Bind to a `$variable`: `ctx.varName`, `ctx.value`, `ctx.setValue()` |
| `subMacros` | `string[]?` | Register sub-macro names for branching |
| `render` | `function` | `(props, ctx) => VNode \| null` — the render function |

```
{do}
Story.defineMacro({
name: "shout",
render: function(props, ctx) {
return ctx.h("span", null, props.rawArgs.toUpperCase());
}
});
{/do}
```

The `ctx` object provides `h`, `renderNodes`, `renderInlineNodes`, `collectText`, `sourceLocation`, `hooks`, and any values from the enabled feature flags. The `render` function runs inside a Preact component and can call hooks via `ctx.hooks`.

### `Story.registerClass(name, constructor)`

Register a class so its instances can be cloned, saved, and restored with their prototype intact.
Expand Down
3 changes: 2 additions & 1 deletion src/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type ActionType =
| 'forward'
| 'restart'
| 'save'
| 'load';
| 'load'
| 'dialog';

export interface StoryAction {
id: string;
Expand Down
69 changes: 69 additions & 0 deletions src/components/PassageDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createContext } from 'preact';
import { useMemo } from 'preact/hooks';
import { tokenize } from '../markup/tokenizer';
import { buildAST } from '../markup/ast';
import { renderNodes } from '../markup/render';
import { useStoryStore } from '../store';

export const DialogCloseContext = createContext<(() => void) | null>(null);

interface PassageDialogProps {
passageName?: string;
fallbackMarkup?: string;
panelClass?: string;
onClose: () => void;
}

export function PassageDialog({
passageName,
fallbackMarkup,
panelClass,
onClose,
}: PassageDialogProps) {
const storyData = useStoryStore((s) => s.storyData);

const passage = passageName
? storyData?.passages.get(passageName)
: undefined;
const markup = passage?.content ?? fallbackMarkup;

const content = useMemo(() => {
if (!markup) {
return <div class="error">Dialog: no content available</div>;
}
try {
const tokens = tokenize(markup);
const ast = buildAST(tokens);
return renderNodes(ast);
} catch (err) {
return <div class="error">Error in dialog: {(err as Error).message}</div>;
}
}, [markup]);

const handleBackdrop = (e: MouseEvent) => {
if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
onClose();
}
};

const cls = panelClass ? `dialog-panel ${panelClass}` : 'dialog-panel';

return (
<DialogCloseContext.Provider value={onClose}>
<div
class="dialog-overlay"
onClick={handleBackdrop}
>
<div class={cls}>
<button
class="dialog-close"
onClick={onClose}
>
</button>
<div class="dialog-body">{content}</div>
</div>
</div>
</DialogCloseContext.Provider>
);
}
49 changes: 0 additions & 49 deletions src/components/PassageLink.tsx

This file was deleted.

Loading