diff --git a/example/src/client/actions.ts b/example/src/client/actions.ts
index 78bdd44..5484da1 100644
--- a/example/src/client/actions.ts
+++ b/example/src/client/actions.ts
@@ -1,6 +1,6 @@
// DO NOT MODIFY!
// This file is generated by pyre.
-// Generated: 2026-01-24 09:09:49.
+// Generated: 2026-01-26 13:25:18.
import { send } from 'pyre/client'
@@ -12,6 +12,14 @@ export const playAgain = () => {
})
}
+export const emojiChange = (player: 'x' | 'o', emoji: string) => {
+ send({
+ type: 'action',
+ action: 'emojiChange',
+ args: [player, emoji]
+ })
+}
+
export const nameChange = (name: string) => {
send({
type: 'action',
diff --git a/example/src/client/board.tsx b/example/src/client/board.tsx
index 0d321a8..91fb9ea 100644
--- a/example/src/client/board.tsx
+++ b/example/src/client/board.tsx
@@ -1,5 +1,6 @@
import { define, createThemes } from 'forge'
import { move, nameChange, playAgain } from './actions'
+import { EmojiModal, openEmojiModal } from './emojiModal'
import type { Game } from '../types'
import { sessionId } from 'pyre/client'
@@ -33,6 +34,10 @@ const Player = define('Player', {
parts: {
Symbol: {
fontSize: 32,
+ cursor: 'pointer',
+ render({ children }) {
+ return
{children}
+ }
},
Indicator: {
width: 8,
@@ -314,14 +319,14 @@ const BoardWrapper = define('BoardWrapper', {
position: 'relative',
})
-const symbol = (tile: string): string =>
- tile === 'x' ? '❌' : tile === 'o' ? '⭕️' : ''
+const symbol = (game: Game, tile: string): string =>
+ tile === 'x' ? game.emoji.x : tile === 'o' ? game.emoji.o : ''
const PlayerO = ({ game, myPlayer }: { game: Game, myPlayer: string }) =>
-
+
const PlayerX = ({ game, myPlayer }: { game: Game, myPlayer: string }) =>
-
+
const tryMove = (game: Game, player: 'x' | 'o', x: number, y: number) => {
return game.turn === player && game.phase === 'play' ? () => move(x, y) : () => { }
@@ -335,6 +340,7 @@ export default ({ game }: { game: Game }) => {
return (
+
{game.winLine && (
{
{player === 'o' && }
- {symbol(game.board[0][0])}
- {symbol(game.board[0][1])}
- {symbol(game.board[0][2])}
+ {symbol(game, game.board[0][0])}
+ {symbol(game, game.board[0][1])}
+ {symbol(game, game.board[0][2])}
- {symbol(game.board[1][0])}
- {symbol(game.board[1][1])}
- {symbol(game.board[1][2])}
+ {symbol(game, game.board[1][0])}
+ {symbol(game, game.board[1][1])}
+ {symbol(game, game.board[1][2])}
- {symbol(game.board[2][0])}
- {symbol(game.board[2][1])}
- {symbol(game.board[2][2])}
+ {symbol(game, game.board[2][0])}
+ {symbol(game, game.board[2][1])}
+ {symbol(game, game.board[2][2])}
{game.winLine && (
{
+ emojiModalOpen = true
+ redraw()
+}
+
+export const closeEmojiModal = () => {
+ emojiModalOpen = false
+ redraw()
+}
+
+const chooseEmoji = (game: Game, player: xo, emoji: Emoji) => {
+ closeEmojiModal()
+ emojiChange(player, emoji)
+}
+
+const ModalBackdrop = define('ModalBackdrop', {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 100,
+
+ render({ parts: { Root }, props }) {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') closeEmojiModal()
+ }
+
+ const onMount = () => {
+ document.addEventListener('keydown', handleKeyDown)
+ }
+
+ return (
+
+ {props.children}
+
+ )
+ },
+})
+
+const ModalContent = define('ModalContent', {
+ background: theme('bg'),
+ borderRadius: 16,
+ padding: 24,
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
+
+ render({ parts: { Root }, props }) {
+ return (
+ e.stopPropagation()}>
+ {props.children}
+
+ )
+ },
+})
+
+const EmojiGrid = define('EmojiGrid', {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(3, 1fr)',
+ gap: 8,
+})
+
+const EmojiCell = define('EmojiCell', {
+ base: 'button',
+ width: 72,
+ height: 72,
+ fontSize: 40,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ background: theme('hoverBg'),
+ border: 'none',
+ borderRadius: 12,
+ cursor: 'pointer',
+ transition: 'transform 0.15s ease, background 0.15s ease',
+
+ states: {
+ hover: {
+ background: theme('inputBorder'),
+ transform: 'scale(1.1)',
+ },
+ },
+})
+
+type EmojiPickerProps = {
+ game: Game
+ player: xo
+}
+
+export const EmojiModal = define('EmojiModal', {
+ render({ props }: { props: EmojiPickerProps }) {
+ const { game, player } = props
+ return (
+ emojiModalOpen &&
+
+
+
+ {EMOJI_OPTIONS.map((emoji) => (
+ chooseEmoji(game, player, emoji)}>{emoji}
+ ))}
+
+
+
+ )
+ },
+})
diff --git a/example/src/game.ts b/example/src/game.ts
index be63cad..cf2d274 100644
--- a/example/src/game.ts
+++ b/example/src/game.ts
@@ -8,6 +8,7 @@ const newGame: Game = {
turn: 'x',
players: {},
names: { x: 'Player X', o: 'Player O' },
+ emoji: { x: '❌', o: '⭕️' },
board: [
['', '', ''],
['', '', ''],
@@ -31,7 +32,7 @@ const action = setup(newGame, {
}
})
-action('playAgain', (ctx) => {
+action('playAgain', ctx => {
const game = ctx.game as Game
game.phase = 'play'
game.turn = 'x'
@@ -44,6 +45,12 @@ action('playAgain', (ctx) => {
return game
})
+action('emojiChange', (ctx, player: 'x' | 'o', emoji: string) => {
+ const game = ctx.game as Game
+ if (game.emoji[player]) game.emoji[player] = emoji
+ return game
+})
+
action('nameChange', (ctx, name: string) => {
const game = ctx.game as Game
const player = game.players[ctx.session]
diff --git a/example/src/types.ts b/example/src/types.ts
index 56d7005..61861d0 100644
--- a/example/src/types.ts
+++ b/example/src/types.ts
@@ -8,6 +8,7 @@ export type Game = {
board: [Row, Row, Row]
players: Record
names: Record
+ emoji: Record
winLine?: WinLine
}