Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
a710bc5
BL-15642 Intro Page Settings
hatton Dec 30, 2025
476a196
simplify
hatton Mar 27, 2026
f52fe7a
UseEffect reduction from https://github.com/softaworks/agent-toolkit
hatton Mar 27, 2026
cb894db
Merge remote-tracking branch 'origin/master' into BL-15642PageColor
hatton Mar 27, 2026
c98b222
fix
hatton Mar 28, 2026
c62e7a2
Add required useEffect justification comment per AGENTS.md
hatton Mar 28, 2026
31f7ab6
Ensure isOpenAlready flag is always reset even if page element restor…
hatton Mar 28, 2026
9a8409b
Ensure page settings dialog save always clears open flag
hatton Mar 28, 2026
e25d518
Merge remote-tracking branch 'origin/BL-15642PageColor' into BL-15642…
hatton Mar 28, 2026
47214c6
BL-15642 Intro Page Settings
hatton Dec 30, 2025
5c50810
simplify
hatton Mar 27, 2026
b8b4daf
UseEffect reduction from https://github.com/softaworks/agent-toolkit
hatton Mar 27, 2026
4e63659
fix
hatton Mar 28, 2026
6f1f773
Add required useEffect justification comment per AGENTS.md
hatton Mar 28, 2026
f9645df
Ensure page settings dialog save always clears open flag
hatton Mar 28, 2026
b1fc170
Ensure isOpenAlready flag is always reset even if page element restor…
hatton Mar 28, 2026
b672c02
Clarify labels for "Collection Settings" and "Book and Page" settings
hatton Mar 28, 2026
8c48170
don't allow page setting on cover for now
hatton Mar 28, 2026
f9a7053
normalize text display of all the controls that show above the page i…
hatton Mar 28, 2026
822af2b
fix background color when in rounded theme
hatton Mar 29, 2026
ff0b8cf
Merge remote-tracking branch 'origin/BL-15642PageColor' into BL-15642…
hatton Mar 29, 2026
3d5e700
Fix localization of origami choices
hatton Mar 29, 2026
456a868
Remove backend involvement in opening book and page settings
hatton Mar 29, 2026
db8200f
Render game controls through AbovePageControls
hatton Mar 30, 2026
a7791b7
Remove unused edit-tab About wrapper
hatton Mar 30, 2026
c2692b6
test: add regression test for nested Configr initial page selection
hatton Mar 30, 2026
388736d
Clarify/comment on some changes
hatton Mar 30, 2026
414e665
enable crowdin translations on new labels
hatton Mar 30, 2026
ffb4e2f
Preserve page number background theme
hatton Mar 30, 2026
302e281
Refactor game page above-page controls
hatton Mar 30, 2026
ed8e2d9
Handle slow toolbox accordion initialization.
hatton Mar 30, 2026
4c47d76
Simplify toolbox accordion retry handling.
hatton Mar 30, 2026
b6d1560
obsolete l10n key
hatton Mar 30, 2026
dbf7f9d
Handle early Book Settings click safely
hatton Mar 30, 2026
93d0ac1
Make color picker hide safe before show
hatton Mar 30, 2026
cde4844
hide page settings button, relabel "page"-->"Current Page"
hatton Mar 30, 2026
28cf31f
Update .github/prompts/bloom-l10.prompt.md
hatton Mar 30, 2026
53173ec
Update .github/skills/bloom-automation/SKILL.md
hatton Mar 30, 2026
09bcf50
fix: extract `size` prop before spreading onto SvgIcon DOM element
Mar 30, 2026
b6cf39a
docs: fix react-useeffect resource count
hatton Mar 30, 2026
437def8
docs: correct bloom l10n prompt typos
hatton Mar 30, 2026
6441666
docs: fix bloom automation wording
hatton Mar 30, 2026
703bea3
fix: stop CogIcon size prop leaking
hatton Mar 30, 2026
f1077b2
fix: localize color picker sample title
hatton Mar 30, 2026
1d643e8
test: reset head in page settings spec
hatton Mar 30, 2026
e183d8d
localization
hatton Mar 30, 2026
d8b8a76
fixes for different themes
hatton Mar 30, 2026
e6ebfe2
Merge remote-tracking branch 'origin/BL-15642PageColor' into BL-15642…
hatton Mar 30, 2026
7d7e507
comment
hatton Mar 30, 2026
259515d
Fix unresolved toolbox PR comments
hatton Mar 30, 2026
f283c8f
update automation to the new react-window layout
hatton Mar 31, 2026
62bf013
Inherit the running Bloom Vite port in watch mode.
hatton Mar 31, 2026
16a746a
Stop keeping track of normal themes when it comes to knowing that the…
hatton Mar 31, 2026
da803e9
Revert unrelated toolbox.ts changes
hatton Apr 2, 2026
b2a44b5
Match page label typography in edit bar
hatton Apr 2, 2026
cb0e7c7
review fixes
hatton Apr 2, 2026
4ac3d4b
rename
hatton Apr 2, 2026
91ad094
fix merge damage
hatton Apr 2, 2026
4a0cfcc
Merge remote-tracking branch 'origin/master' into BL-15642PageColor
hatton Apr 2, 2026
4fdd3b9
fix dialog middle height
hatton Apr 2, 2026
6225888
remove de-zooming during eyedropper
hatton Apr 3, 2026
658d03d
Fix page settings dialog save behavior
hatton Apr 3, 2026
b83a48b
Move bloom-page readiness into shared utils
hatton Apr 3, 2026
0c80116
Some by-hand tweaks to theme interaction with the new page number pro…
hatton Apr 3, 2026
71ae997
Refactor page background and frame colors
hatton Apr 3, 2026
30ddfa7
Fix page number outline theme override
hatton Apr 3, 2026
108cf00
Enable transparency for text color pickers
hatton Apr 3, 2026
ea6c3aa
Add missing page settings English localizations
hatton Apr 3, 2026
1745a08
Remove unwanted rule in rounded-border
hatton Apr 3, 2026
390a0a6
provisionally enable the page number color settings
hatton Apr 3, 2026
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
4 changes: 4 additions & 0 deletions .github/prompts/bloom-l10.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ the string id may be used by translators as they try to understand context or tr
## Expose ID to translators
Add a note like this: `<note>ID: LinkTargetChooser.URL.Paste.Tooltip</note>`

## Legacy strings
Our localization build system is such that if we are no longer using a string ID in the next version, we cannot remove the ID from the XLF file immediately. This is because if we release a new version of the previous release, we will still need that old localization ID. The fact that its code base still has it will not be sufficient. Somehow the actual Crowdin database will lose the translations when it sees the string ID removed from a newer version. Therefore when we stop using a string ID we just add a note like this: "<note>Obsolete as of 6.2</note>". You can figure out the current version from the `Version` property on build/Bloom.proj.

## Add comments for translators
Although we don't want to fill in l10nComment in useL10n, we do want to fill in the note field to give context to translators. They don't know where the string appears in the UI, they also might need some explanation of what it means. For example, for the above string, we might add a note like `<note>This is the text on a button in the Foobar dialog that brightens all images in the current book.</note>`

# Tips
* Never use the word "Aria" in ids or comments. Translators don't know what that means.
* Stop processing immediately if I haven't told you what priority we want. After you have the priority, then you can continue.

4 changes: 4 additions & 0 deletions .github/prompts/bloom-test-CURRENTPAGE.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
description: use browser tools to test and debug
---
The backend should already be running and serving a page at http://localhost:<port>/bloom/CURRENTPAGE. <port> is usually 8089. If i include a port number, use that, otherwise use 8089. You may use chrome-devtools-mcp, playwright-mcp, or other browser management tools. If you can't find any, use askQuestions tool to ask me to enable something for you to use.
13 changes: 7 additions & 6 deletions .github/skills/bloom-automation/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: bloom-automation
description: Use when an agent needs to determine if Bloom is already running, detect whether the running Bloom came from a different worktree, kill Bloom or dotnet-watch parents, start Bloom from the current worktree, attach to the embedded WebView2 over CDP, inspect DOM/console/network, or run Playwright tests against the actual exe instead of CURRENTPAGE.
description: Use when an agent needs to determine if Bloom is already running, detect whether the running Bloom came from a different worktree, kill Bloom or dotnet-watch parents, start Bloom from the current worktree, attach to the embedded WebView2 over CDP, inspect DOM/console/network, use dev-browser to inspect or run e2e tests against the actual exe instead of CURRENTPAGE.
argument-hint: "repo root or worktree, task such as status, restart, attach, run exe-backed tests"
user-invocable: true
---
Expand Down Expand Up @@ -219,8 +219,9 @@ These tests attach to the real Bloom.exe target over CDP and verify tab switchin
- Exact-target cleanup is intentionally strict: `killBloomProcess.mjs --http-port <port>` should only kill the instance that actually reports that HTTP port, and should fail without killing anything if that target cannot be resolved.
- When reporting work, include the helper commands you used so reviewers can confirm the workflow stayed on the supported path.
- Wrong-worktree detection is authoritative when a real `Bloom.exe` child exists or when `dotnet watch` was started with an absolute `--project` path.
- A standalone `dotnet watch` started with a relative project path may not expose enough information to attribute it to a worktree. For current-worktree automation, start Bloom through `node scripts/watchBloomExe.mjs`, which always uses an absolute path. For the already-running Bloom workflow, use `--running-bloom` instead of trying to infer a worktree.
- When more than one Bloom is running from the same worktree, repo-root matching is not enough. Use the explicit HTTP port workflow.
- For ad hoc local debugging in this workspace, `dev-browser --connect http://localhost:<cdpPort>` can attach directly to the existing Bloom WebView2 target. Use it as a low-friction inspection client.
- After attaching to Bloom's WebView2 target, if Bloom is on the Edit tab, the editable page content lives inside the iframe named `page`; the top-level document mostly hosts shell UI plus the root dialog container.

## Completion Checks
- Bloom's status is known: not running, running from current worktree, or running from different worktree.
Expand All @@ -243,7 +244,7 @@ Report:
- what browser-native evidence you collected: DOM state, console output, network request, tab state, or test results

## Example Prompts
- `Use bloom-automation to determine whether Bloom is already running from this worktree and attach Playwright to the embedded browser.`
- `Use bloom-automation to switch the already-running Bloom to the Edit tab.`
- `Use bloom-automation to kill the wrong-worktree Bloom and start the current checkout with dotnet watch.`
- `Use bloom-automation to run the exe-backed Playwright top bar smoke tests against the actual Bloom.exe window.`
- `troubleshoot why the page is refreshing when we open page settings`

## Debugging tips
Use node or bash scripts. Avoid powershell. Use the "dev-browser" cli instead of playwright for interactive debugging/driving Bloom. Use "dev-browser --help" to see the available commands and options. If the user hasn't installed dev-browser, ask them for permission to install it (https://github.com/SawyerHood/dev-browser).
6 changes: 6 additions & 0 deletions .github/skills/bloom-automation/bloomProcessCommon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,19 @@ export const extractRepoRoot = (text) => {
export const normalizeBloomInstanceInfo = (info, discoveredViaPort) => {
const httpPort = toTcpPort(info?.httpPort) ?? discoveredViaPort;
const cdpPort = toTcpPort(info?.cdpPort);
const executablePath = normalizePath(info?.executablePath);
const detectedRepoRoot = extractRepoRoot(executablePath);
const vitePort = toTcpPort(info?.vitePort);

return {
processId: toPositiveInteger(info?.processId),
discoveredViaPort,
httpPort,
origin: toLocalOrigin(httpPort),
cdpPort,
executablePath,
detectedRepoRoot,
vitePort,
};
};

Expand Down
320 changes: 320 additions & 0 deletions .github/skills/react-useeffect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
# React useEffect Best Practices

A comprehensive guide teaching when to use `useEffect` in React, and more importantly, when NOT to use it. This skill is based on official React documentation and provides practical alternatives to common useEffect anti-patterns.

## Purpose

Effects are an **escape hatch** from React's reactive paradigm. They let you synchronize with external systems like browser APIs, third-party widgets, or network requests. However, many developers overuse Effects for tasks that React handles better through other means.

This skill helps you:
- Identify when you truly need an Effect vs. when you don't
- Recognize common anti-patterns and their fixes
- Apply better alternatives like `useMemo`, `key` prop, and event handlers
- Write Effects that are clean, maintainable, and free from race conditions

## When to Use This Skill

Use this skill when you're:
- Writing or reviewing `useEffect` code
- Using `useState` to store derived values
- Implementing data fetching or subscriptions
- Synchronizing state between components
- Facing bugs with stale data or race conditions
- Wondering if your Effect is necessary

**Trigger phrases:**
- "Should I use useEffect for this?"
- "How do I fix this useEffect?"
- "My Effect is causing too many re-renders"
- "Data fetching with useEffect"
- "Reset state when props change"
- "Derived state from props"

## How It Works

This skill provides guidance through four key resources:

1. **Quick Reference Table** - Fast lookup for common scenarios with DO/DON'T patterns
2. **Decision Tree** - Visual flowchart to determine the right approach
3. **Detailed Anti-Patterns** - 9 common mistakes with explanations and fixes
4. **Better Alternatives** - 8 proven patterns to replace unnecessary Effects

The skill teaches you to ask the right questions:
- Is there an external system involved?
- Am I responding to a user event or component appearance?
- Can this value be calculated during render?
- Do I need to reset state when a prop changes?

## Key Features

### 1. Quick Reference Guide

Visual table showing the DO/DON'T for common scenarios:
- Derived state from props/state
- Expensive calculations
- Resetting state on prop change
- User event responses
- Notifying parent components
- Data fetching

### 2. Decision Tree

Clear flowchart that guides you from "Need to respond to something?" to the correct solution:
- User interaction → Event handler
- Component appeared → Effect (for external sync/analytics)
- Derived value needed → Calculate during render (+ useMemo if expensive)
- Reset state on prop change → Key prop

### 3. Anti-Pattern Recognition

Detailed examples of 9 common mistakes:
1. Redundant state for derived values
2. Filtering/transforming data in Effect
3. Resetting state on prop change
4. Event-specific logic in Effect
5. Chains of Effects
6. Notifying parent via Effect
7. Passing data up to parent
8. Fetching without cleanup (race conditions)
9. App initialization in Effect

Each anti-pattern includes:
- Bad example with explanation
- Good example with fix
- Why the anti-pattern is problematic

### 4. Better Alternatives

8 proven patterns to replace unnecessary Effects:
1. Calculate during render for derived state
2. `useMemo` for expensive calculations
3. `key` prop to reset state
4. Store ID instead of object for stable references
5. Event handlers for user actions
6. `useSyncExternalStore` for external stores
7. Lifting state up for shared state
8. Custom hooks for data fetching with cleanup

## Usage Examples

### Example 1: Derived State

**Bad - Unnecessary Effect:**
```tsx
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');

useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
}
```

**Good - Calculate during render:**
```tsx
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName; // Just compute it
}
```

### Example 2: Resetting State

**Bad - Effect to reset:**
```tsx
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

useEffect(() => {
setComment('');
}, [userId]);
}
```

**Good - Key prop:**
```tsx
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
const [comment, setComment] = useState(''); // Resets automatically
}
```

### Example 3: Data Fetching with Cleanup

**Bad - Race condition:**
```tsx
function SearchResults({ query }) {
const [results, setResults] = useState([]);

useEffect(() => {
fetchResults(query).then(json => {
setResults(json); // "hello" response may arrive after "hell"
});
}, [query]);
}
```

**Good - Cleanup flag:**
```tsx
function SearchResults({ query }) {
const [results, setResults] = useState([]);

useEffect(() => {
let ignore = false;

fetchResults(query).then(json => {
if (!ignore) setResults(json);
});

return () => { ignore = true; };
}, [query]);
}
```

### Example 4: Event Handler Instead of Effect

**Bad - Effect watching state:**
```tsx
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name}!`);
}
}, [product]);

function handleBuyClick() {
addToCart(product);
}
}
```

**Good - Handle in event:**
```tsx
function ProductPage({ product, addToCart }) {
function handleBuyClick() {
addToCart(product);
showNotification(`Added ${product.name}!`);
}
}
```

## When You DO Need Effects

Effects are appropriate for:

- **Synchronizing with external systems** - Browser APIs, third-party widgets, non-React code
- **Subscriptions** - WebSocket connections, global event listeners (prefer `useSyncExternalStore`)
- **Analytics/logging** - Code that needs to run because the component displayed
- **Data fetching** - With proper cleanup (or use your framework's built-in mechanism)

## When You DON'T Need Effects

Avoid Effects for:

1. **Transforming data for rendering** - Calculate at the top level instead
2. **Handling user events** - Use event handlers where you know exactly what happened
3. **Deriving state** - Just compute it: `const fullName = firstName + ' ' + lastName`
4. **Chaining state updates** - Calculate all next state in the event handler
5. **Notifying parent components** - Call the callback in the same event handler
6. **Resetting state** - Use the `key` prop to create a fresh component instance

## Best Practices

### 1. Start Without an Effect

Before adding an Effect, ask: "Is there an external system involved?" If no, you probably don't need an Effect.

### 2. Prefer Derived State

If you can calculate a value from props or state, don't store it in state with an Effect updating it.

### 3. Use the Right Tool

- Expensive calculation → `useMemo`
- User interaction → Event handler
- Reset on prop change → `key` prop
- External subscription → `useSyncExternalStore`
- Shared state → Lift state up

### 4. Always Clean Up

If your Effect subscribes, fetches, or sets timers, return a cleanup function to prevent memory leaks and race conditions.

### 5. Avoid Effect Chains

Multiple Effects triggering each other causes unnecessary re-renders and makes code hard to follow. Calculate everything in one place (usually an event handler).

### 6. Test in Strict Mode

React 18+ Strict Mode mounts components twice in development to expose missing cleanup. If your Effect breaks, you need cleanup.

### 7. Consider Framework Solutions

For data fetching, prefer your framework's built-in solution (Next.js, Remix) or libraries (React Query, SWR) over manual Effects.

## Reference Files

This skill includes three detailed reference documents:

1. **SKILL.md** - Quick reference table and decision tree
2. **anti-patterns.md** - 9 common mistakes with detailed explanations
3. **alternatives.md** - 8 better alternatives with code examples

## Common Pitfalls

### Multiple Re-renders

**Symptom:** Component re-renders many times in quick succession.

**Cause:** Effect that sets state based on state it depends on, creating a loop.

**Fix:** Calculate the final value in an event handler or during render.

### Stale Data

**Symptom:** UI shows outdated values briefly before updating.

**Cause:** Using Effect to update derived state causes an extra render pass.

**Fix:** Calculate derived values during render instead of in state.

### Race Conditions

**Symptom:** Fast typing shows results for old queries after new ones.

**Cause:** Missing cleanup in data fetching Effect.

**Fix:** Use cleanup flag (`ignore` variable) or AbortController.

### Runs Twice in Development

**Symptom:** Effect runs twice on component mount in development.

**Cause:** React 18 Strict Mode intentionally mounts components twice to expose bugs.

**Fix:** Add proper cleanup. If it's app initialization that shouldn't run twice, use a module-level guard.

## Resources

This skill is based on:
- [React Official Docs: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
- [React Official Docs: Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects)
- [React Official Docs: Lifecycle of Reactive Effects](https://react.dev/learn/lifecycle-of-reactive-effects)

## Summary

The golden rule: **Effects are an escape hatch from React.** If you're not synchronizing with an external system, you probably don't need an Effect.

Before writing `useEffect`, ask yourself:
1. Is this responding to a user interaction? → Use event handler
2. Is this a value I can calculate from props/state? → Calculate during render
3. Is this resetting state when a prop changes? → Use key prop
4. Is this synchronizing with an external system? → Use Effect with cleanup

Follow these patterns, and your React code will be more maintainable, performant, and bug-free.
Loading