feat: кубики/таймер/кисть/спонсоры и задел игровых систем
Показ анимации кубиков игрокам, закрытие оверлеев с окнами, ручной ввод количества, меньший радиус кисти, блок спонсоров в «О программе»; плюс черновик game systems / character sheet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { ipcChannels } from '../../shared/ipc/contracts';
|
||||
import { DICE_SIDES, type DiceSides } from '../../shared/types/diceRoll';
|
||||
import {
|
||||
DICE_COUNT_MAX,
|
||||
DICE_COUNT_MIN,
|
||||
DICE_SIDES,
|
||||
clampDiceCount,
|
||||
type DiceSides,
|
||||
} from '../../shared/types/diceRoll';
|
||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||
import { getDndApi } from '../shared/dndApi';
|
||||
import { useDiceRollState } from '../shared/dice/useDiceRollState';
|
||||
@@ -15,6 +21,7 @@ export function DiceApp() {
|
||||
const [state, diceApi] = useDiceRollState();
|
||||
const [sides, setSides] = useState<DiceSides>(20);
|
||||
const [count, setCount] = useState(1);
|
||||
const [countDraft, setCountDraft] = useState('1');
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -26,6 +33,32 @@ export function DiceApp() {
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [api]);
|
||||
|
||||
const commitCountDraft = () => {
|
||||
const trimmed = countDraft.trim();
|
||||
if (trimmed === '') {
|
||||
setCount(DICE_COUNT_MIN);
|
||||
setCountDraft(String(DICE_COUNT_MIN));
|
||||
return;
|
||||
}
|
||||
const parsed = Number(trimmed);
|
||||
if (!Number.isFinite(parsed) || parsed < DICE_COUNT_MIN) {
|
||||
setCount(DICE_COUNT_MIN);
|
||||
setCountDraft(String(DICE_COUNT_MIN));
|
||||
return;
|
||||
}
|
||||
const next = clampDiceCount(parsed);
|
||||
setCount(next);
|
||||
setCountDraft(String(next));
|
||||
};
|
||||
|
||||
const setCountClamped = (n: number) => {
|
||||
const next = clampDiceCount(n);
|
||||
setCount(next);
|
||||
setCountDraft(String(next));
|
||||
};
|
||||
|
||||
const showToPlayers = state?.visibleToPlayers ?? true;
|
||||
|
||||
return (
|
||||
<div className={styles.page} data-testid="dice-app">
|
||||
<div className={styles.sectionLabel}>{t('dice.sides')}</div>
|
||||
@@ -46,27 +79,80 @@ export function DiceApp() {
|
||||
<div className={styles.countRow}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setCount((c) => Math.max(1, c - 1))}
|
||||
onClick={() => setCountClamped(count - 1)}
|
||||
ariaLabel={t('dice.dec')}
|
||||
disabled={count <= DICE_COUNT_MIN}
|
||||
>
|
||||
−
|
||||
</Button>
|
||||
<div className={styles.countValue} data-testid="dice-count">
|
||||
{count}
|
||||
</div>
|
||||
<input
|
||||
className={styles.countValue}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={countDraft}
|
||||
onChange={(e) => {
|
||||
const digits = e.target.value.replace(/\D/g, '');
|
||||
if (digits === '') {
|
||||
setCountDraft('');
|
||||
return;
|
||||
}
|
||||
const n = Number(digits);
|
||||
if (!Number.isFinite(n)) {
|
||||
setCountDraft(String(DICE_COUNT_MIN));
|
||||
setCount(DICE_COUNT_MIN);
|
||||
return;
|
||||
}
|
||||
if (n > DICE_COUNT_MAX) {
|
||||
setCountDraft(String(DICE_COUNT_MAX));
|
||||
setCount(DICE_COUNT_MAX);
|
||||
return;
|
||||
}
|
||||
setCountDraft(digits);
|
||||
}}
|
||||
onBlur={() => commitCountDraft()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commitCountDraft();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
aria-label={t('dice.count')}
|
||||
data-testid="dice-count"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setCount((c) => Math.min(20, c + 1))}
|
||||
onClick={() => setCountClamped(count + 1)}
|
||||
ariaLabel={t('dice.inc')}
|
||||
disabled={count >= DICE_COUNT_MAX}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<label className={styles.checkRow}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showToPlayers}
|
||||
onChange={(e) =>
|
||||
void diceApi.dispatch({ kind: 'setVisibleToPlayers', visible: e.target.checked })
|
||||
}
|
||||
data-testid="dice-show-to-players"
|
||||
/>
|
||||
<span>{t('dice.showAnimationToPlayers')}</span>
|
||||
</label>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void diceApi.dispatch({ kind: 'roll', count, sides })}
|
||||
disabled={clampDiceCount(Number(countDraft.trim()) || count) < 1}
|
||||
onClick={() => {
|
||||
const next = clampDiceCount(Number(countDraft.trim()));
|
||||
setCount(next);
|
||||
setCountDraft(String(next));
|
||||
if (next < 1) return;
|
||||
void diceApi.dispatch({ kind: 'roll', count: next, sides });
|
||||
}}
|
||||
data-testid="dice-roll"
|
||||
>
|
||||
{t('dice.roll')}
|
||||
|
||||
Reference in New Issue
Block a user