Skip to content
Open
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
11 changes: 8 additions & 3 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { appMachine } from './machines/app-machine/app-machine.ts'
import { type Card as CardType } from './types/cards.ts'
import coinsIcon from './images/gold-coins.png'
import { AppPreloader } from './components/app-preloader.tsx'
import { CharacterCreation } from './components/character-creation.tsx'
import { CharacterCreationScreen } from './components/character-creation-screen.tsx'
import { ModeSelectScreen } from './components/mode-select-screen.tsx'
import { Avatar } from './components/avatar.tsx'
import { Card } from './components/card.tsx'
import { Deck } from './components/deck.tsx'
Expand All @@ -17,10 +18,10 @@ import { Button } from './components/button.tsx'
import { ItemShopCard, type ItemShopCardStatus } from './components/item-shop-card.tsx'
import { ItemShopItem } from './components/item-shop-item.tsx'
import { StatsRow, StatIcon, StatVal } from './components/stats.tsx'
import { Banner } from './components/banner.tsx'
import { cardUseSound } from './machines/app-machine/app-machine.ts'
import { getItem } from './helpers/item.ts'
import css from './app.module.css'
import { Banner } from './components/banner.tsx'

export function App() {
const [{ context, value }, send] = useMachine(appMachine)
Expand All @@ -29,9 +30,13 @@ export function App() {
return <AppPreloader />
}

if (value === 'ModeSelect') {
return <ModeSelectScreen />
}

if (value === 'CharacterCreation') {
return (
<CharacterCreation
<CharacterCreationScreen
onCreate={(formData) => {
send({
type: 'CREATE_CHARACTER',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type SyntheticEvent } from 'react'
import { useMachine } from '@xstate/react'
import { appMachine } from '../machines/app-machine/app-machine'
import { Button } from './button'
import css from './character-creation.module.css'
import css from './character-creation-screen.module.css'
import { resolveModules } from '../helpers/vite'
import { Stack } from './stack'

Expand All @@ -15,7 +15,7 @@ const PLAYER_PORTRAIT_MODULES = import.meta.glob('../images/player-portraits/*.(
const PLAYER_PORTRAITS = resolveModules<string>(PLAYER_PORTRAIT_MODULES)

/** UI view for character creation */
export function CharacterCreation({
export function CharacterCreationScreen({
onCreate,
}: {
onCreate: (data: Record<string, FormDataEntryValue>) => void
Expand Down
129 changes: 129 additions & 0 deletions src/components/mode-select-screen.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* -------- base layout (unchanged) --------------------------- */
.container {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
font-family: system-ui, sans-serif;
}

.label {
font-weight: 600;
}

.checkbox {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}

.switch {
position: relative;
display: inline-block;
width: 4rem;
height: 2.2rem;
cursor: pointer;
}

/* -------- track -------------------------------------------- */
.slider {
position: absolute;
inset: 0;
border-radius: 9999px;
background: #c5c5c5; /* dad mode track color */
transition: background 0.3s;
}

/* rainbow gradient border (still hides until checked) */
.slider::after {
content: '';
position: absolute;
inset: -0.15rem;
border-radius: inherit;
background: linear-gradient(90deg, #ff004d, #ff7c00, #f7ff00, #00ff87, #0094ff, #8e00ff, #ff004d);
z-index: -1;
opacity: 0;
transition: opacity 0.3s;
}

/* -------- thumb -------------------------------------------- */
.slider::before {
content: '';
position: absolute;
left: 0.2rem;
top: 0.2rem;
width: 1.8rem;
height: 1.8rem;
background: #ffffff;
border-radius: 50%;

/* NEW: subtle drop shadow for depth */
box-shadow: 0 0.15rem 0.35rem rgba(0, 0, 0, 0.25);

/* bouncy slide */
transition:
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s;
}

/* -------- shiny glint overlay ------------------------------ */
.glint {
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
overflow: hidden; /* mask the moving strip */
opacity: 0; /* hidden until rainbow mode */
}

/* moving highlight strip */
.glint::before {
content: '';
position: absolute;
top: 0;
left: -150%; /* start well off‑screen */
width: 50%; /* width of the strip */
height: 100%;
transform: skewX(-20deg);
background: linear-gradient(
120deg,
transparent 0%,
rgba(255, 255, 255, 0.85) 50%,
transparent 100%
);
}

/* animation */
@keyframes glintSweep {
to {
left: 150%;
}
}

/* -------- checked state tweaks ----------------------------- */
.checkbox:checked + .slider {
background: #ffd6f6; /* pastel track */
}
.checkbox:checked + .slider::before {
transform: translateX(1.8rem);
/* add gentle glow so the thumb feels lit */
box-shadow:
0 0.15rem 0.35rem rgba(0, 0, 0, 0.25),
0 0 0.6rem rgba(255, 255, 255, 0.6);
}
.checkbox:checked + .slider::after {
opacity: 1; /* show rainbow border */
}
.checkbox:checked + .slider .glint {
opacity: 1; /* enable glint layer */
}
.checkbox:checked + .slider .glint::before {
animation: glintSweep 1.2s linear infinite;
}

/* -------- keyboard focus ring ------------------------------ */
.checkbox:focus-visible + .slider {
outline: 3px solid #000;
outline-offset: 3px;
}
58 changes: 58 additions & 0 deletions src/components/mode-select-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from 'react'
import css from './mode-select-screen.module.css'

export function ModeSelectScreen() {
return (
<div>
<ModeSelect />
</div>
)
}

type ModeSelectProps = {
/** Fires whenever the user toggles the switch. */
onChange?: (mode: 'dad' | 'rainbow') => void
}

/**
* Visually‑custom switch that still relies on a native checkbox for
* a11y, keyboard support, form compatibility, and reduced JS.
*/
const ModeSelect: React.FC<ModeSelectProps> = ({ onChange }) => {
/** `false` → Dad mode, `true` → Rainbow mode */
const [isRainbow, setIsRainbow] = useState(false)

/** Mirror checkbox state into React state and lift via `onChange` */
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
const checked = e.target.checked
setIsRainbow(checked)
onChange?.(checked ? 'rainbow' : 'dad')
}

return (
<div className={css.container}>
{/* Text label updates live for additional clarity */}
<span className={css.label} aria-hidden="true">
{isRainbow ? '🌈 Rainbow mode' : '👨 Dad mode'}
</span>

{/* Label ties the visual slider to the hidden checkbox */}
<label className={css.switch}>
{/* The real control (visually hidden, but still tabbable/clickable) */}
<input
type="checkbox"
className={css.checkbox}
checked={isRainbow}
onChange={handleToggle}
aria-label="Toggle rainbow mode"
/>

{/* Styled track + thumb (CSS handles :checked visuals) */}
<span className={css.slider} aria-hidden="true">
{/* sparkling overlay */}
<span className={css.glint} />
</span>
</label>
</div>
)
}
25 changes: 24 additions & 1 deletion src/machines/app-machine/app-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const cashRegisterSound = getSound({ src: cashRegisterSfx, volume: 0.5 })

const coinsSound = getSound({ src: coinsSfx, volume: 0.7 })

type GameMode = 'rainbow' | 'dad'

/** Prefetches assets from multiple sources returned by `import.meta.glob` */
async function prefetchAssets() {
return Promise.all([
Expand Down Expand Up @@ -101,6 +103,7 @@ export type AppMachineContext = {
cards: Array<Card>
items: Array<Item>
}
mode: GameMode
monsters: Array<Monster>
items: Array<Item>
currentHand: Array<Card>
Expand Down Expand Up @@ -148,6 +151,7 @@ type AppMachineEvent =
| { type: 'DESTRUCTION_SHOP_CARD_CLICK'; data: { card: Card } }
| { type: 'ITEM_SHOP_ITEM_CLICK'; data: { item: Item } }
| { type: 'INVENTORY_ITEM_CLICK'; data: { item: Item } }
| { type: 'MODE_SELECT'; data: { mode: GameMode } }

export const appMachine = setup({
types: {
Expand Down Expand Up @@ -424,6 +428,7 @@ export const appMachine = setup({
},
inventory: [],
},
mode: 'dad',
monsters: [],
items: [],
shop: {
Expand All @@ -440,11 +445,29 @@ export const appMachine = setup({
LoadingAssets: {
invoke: {
src: 'loadAllAssets',
onDone: 'CharacterCreation',
onDone: 'ModeSelect',
onError: 'LoadingAssetsError',
},
},
LoadingAssetsError: {},
ModeSelect: {
on: {
MODE_SELECT: {
target: 'CharacterCreation',
actions: assign({
game: (args) => {
const { context, event } = args
const mode = event.data.mode

return {
...context.game,
mode,
}
},
}),
},
},
},
CharacterCreation: {
on: {
CREATE_CHARACTER: {
Expand Down