@ankhorage/zora-tabletop 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/cardLabel.d.ts +4 -0
- package/dist/cardLabel.d.ts.map +1 -0
- package/dist/cardLabel.js +9 -0
- package/dist/cardLabel.js.map +1 -0
- package/dist/cardSizing.d.ts +10 -0
- package/dist/cardSizing.d.ts.map +1 -0
- package/dist/cardSizing.js +27 -0
- package/dist/cardSizing.js.map +1 -0
- package/dist/colors.d.ts +44 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +20 -0
- package/dist/colors.js.map +1 -0
- package/dist/components/card-back/CardBack.d.ts +4 -0
- package/dist/components/card-back/CardBack.d.ts.map +1 -0
- package/dist/components/card-back/CardBack.js +44 -0
- package/dist/components/card-back/CardBack.js.map +1 -0
- package/dist/components/card-back/index.d.ts +3 -0
- package/dist/components/card-back/index.d.ts.map +1 -0
- package/dist/components/card-back/index.js +2 -0
- package/dist/components/card-back/index.js.map +1 -0
- package/dist/components/card-back/types.d.ts +10 -0
- package/dist/components/card-back/types.d.ts.map +1 -0
- package/dist/components/card-back/types.js +2 -0
- package/dist/components/card-back/types.js.map +1 -0
- package/dist/components/card-hand/CardHand.d.ts +4 -0
- package/dist/components/card-hand/CardHand.d.ts.map +1 -0
- package/dist/components/card-hand/CardHand.js +23 -0
- package/dist/components/card-hand/CardHand.js.map +1 -0
- package/dist/components/card-hand/index.d.ts +3 -0
- package/dist/components/card-hand/index.d.ts.map +1 -0
- package/dist/components/card-hand/index.js +2 -0
- package/dist/components/card-hand/index.js.map +1 -0
- package/dist/components/card-hand/types.d.ts +12 -0
- package/dist/components/card-hand/types.d.ts.map +1 -0
- package/dist/components/card-hand/types.js +2 -0
- package/dist/components/card-hand/types.js.map +1 -0
- package/dist/components/playing-card/PlayingCard.d.ts +4 -0
- package/dist/components/playing-card/PlayingCard.d.ts.map +1 -0
- package/dist/components/playing-card/PlayingCard.js +68 -0
- package/dist/components/playing-card/PlayingCard.js.map +1 -0
- package/dist/components/playing-card/index.d.ts +3 -0
- package/dist/components/playing-card/index.d.ts.map +1 -0
- package/dist/components/playing-card/index.js +2 -0
- package/dist/components/playing-card/index.js.map +1 -0
- package/dist/components/playing-card/types.d.ts +12 -0
- package/dist/components/playing-card/types.d.ts.map +1 -0
- package/dist/components/playing-card/types.js +2 -0
- package/dist/components/playing-card/types.js.map +1 -0
- package/dist/components/tabletop-table/TabletopTable.d.ts +4 -0
- package/dist/components/tabletop-table/TabletopTable.d.ts.map +1 -0
- package/dist/components/tabletop-table/TabletopTable.js +165 -0
- package/dist/components/tabletop-table/TabletopTable.js.map +1 -0
- package/dist/components/tabletop-table/index.d.ts +3 -0
- package/dist/components/tabletop-table/index.d.ts.map +1 -0
- package/dist/components/tabletop-table/index.js +2 -0
- package/dist/components/tabletop-table/index.js.map +1 -0
- package/dist/components/tabletop-table/types.d.ts +17 -0
- package/dist/components/tabletop-table/types.d.ts.map +1 -0
- package/dist/components/tabletop-table/types.js +2 -0
- package/dist/components/tabletop-table/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/tablePositions.d.ts +11 -0
- package/dist/tablePositions.d.ts.map +1 -0
- package/dist/tablePositions.js +85 -0
- package/dist/tablePositions.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
- package/src/cardLabel.ts +11 -0
- package/src/cardSizing.ts +37 -0
- package/src/colors.ts +67 -0
- package/src/components/card-back/CardBack.tsx +66 -0
- package/src/components/card-back/index.ts +2 -0
- package/src/components/card-back/types.ts +10 -0
- package/src/components/card-hand/CardHand.tsx +55 -0
- package/src/components/card-hand/index.ts +2 -0
- package/src/components/card-hand/types.ts +12 -0
- package/src/components/playing-card/PlayingCard.tsx +99 -0
- package/src/components/playing-card/index.ts +2 -0
- package/src/components/playing-card/types.ts +12 -0
- package/src/components/tabletop-table/TabletopTable.tsx +219 -0
- package/src/components/tabletop-table/index.ts +2 -0
- package/src/components/tabletop-table/types.ts +24 -0
- package/src/index.ts +18 -0
- package/src/tablePositions.test.ts +40 -0
- package/src/tablePositions.ts +97 -0
- package/src/types.ts +24 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useZoraTheme } from '@ankhorage/zora';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { StyleSheet, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { createHiddenCardLabel } from '../../cardLabel';
|
|
6
|
+
import { getTabletopCardDimensions } from '../../cardSizing';
|
|
7
|
+
import { createTabletopColorScheme } from '../../colors';
|
|
8
|
+
import type { CardBackProps } from './types';
|
|
9
|
+
|
|
10
|
+
export function CardBack({
|
|
11
|
+
size = 'medium',
|
|
12
|
+
muted = false,
|
|
13
|
+
accessibilityLabel,
|
|
14
|
+
colorScheme,
|
|
15
|
+
testID,
|
|
16
|
+
}: CardBackProps) {
|
|
17
|
+
const { theme } = useZoraTheme();
|
|
18
|
+
const colors = React.useMemo(
|
|
19
|
+
() => createTabletopColorScheme(theme, colorScheme),
|
|
20
|
+
[colorScheme, theme],
|
|
21
|
+
);
|
|
22
|
+
const dimensions = getTabletopCardDimensions(size);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
accessibilityLabel={accessibilityLabel ?? createHiddenCardLabel()}
|
|
27
|
+
accessibilityRole="image"
|
|
28
|
+
style={[
|
|
29
|
+
styles.card,
|
|
30
|
+
{
|
|
31
|
+
backgroundColor: colors.cardBack,
|
|
32
|
+
borderColor: colors.cardBackBorder,
|
|
33
|
+
borderRadius: dimensions.radius,
|
|
34
|
+
height: dimensions.height,
|
|
35
|
+
opacity: muted ? 0.48 : 1,
|
|
36
|
+
width: dimensions.width,
|
|
37
|
+
},
|
|
38
|
+
]}
|
|
39
|
+
testID={testID}
|
|
40
|
+
>
|
|
41
|
+
<View
|
|
42
|
+
style={[
|
|
43
|
+
styles.inner,
|
|
44
|
+
{
|
|
45
|
+
borderColor: colors.cardBackBorder,
|
|
46
|
+
borderRadius: Math.max(2, dimensions.radius - 3),
|
|
47
|
+
},
|
|
48
|
+
]}
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
card: {
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
},
|
|
60
|
+
inner: {
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
height: '68%',
|
|
63
|
+
opacity: 0.48,
|
|
64
|
+
width: '68%',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TabletopColorOverrides } from '../../colors';
|
|
2
|
+
import type { TabletopCardSize } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface CardBackProps {
|
|
5
|
+
readonly size?: TabletopCardSize;
|
|
6
|
+
readonly muted?: boolean;
|
|
7
|
+
readonly accessibilityLabel?: string;
|
|
8
|
+
readonly colorScheme?: TabletopColorOverrides;
|
|
9
|
+
readonly testID?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { CardBack } from '../card-back';
|
|
5
|
+
import { PlayingCard } from '../playing-card';
|
|
6
|
+
import type { CardHandProps } from './types';
|
|
7
|
+
|
|
8
|
+
function createFaceDownCards(count: number): readonly number[] {
|
|
9
|
+
return Array.from({ length: Math.max(0, count) }, (_, index) => index);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CardHand({
|
|
13
|
+
cards = [],
|
|
14
|
+
faceDownCards = 0,
|
|
15
|
+
size = 'medium',
|
|
16
|
+
muted = false,
|
|
17
|
+
colorScheme,
|
|
18
|
+
accessibilityLabel,
|
|
19
|
+
testID,
|
|
20
|
+
}: CardHandProps) {
|
|
21
|
+
const hiddenCards = createFaceDownCards(faceDownCards);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View accessibilityLabel={accessibilityLabel} style={styles.hand} testID={testID}>
|
|
25
|
+
{cards.map((card, index) => (
|
|
26
|
+
<PlayingCard
|
|
27
|
+
card={card}
|
|
28
|
+
colorScheme={colorScheme}
|
|
29
|
+
key={`${card.rank}-${card.suit}-${index}`}
|
|
30
|
+
muted={muted}
|
|
31
|
+
size={size}
|
|
32
|
+
testID={testID ? `${testID}-card-${index}` : undefined}
|
|
33
|
+
/>
|
|
34
|
+
))}
|
|
35
|
+
{hiddenCards.map((index) => (
|
|
36
|
+
<CardBack
|
|
37
|
+
colorScheme={colorScheme}
|
|
38
|
+
key={`hidden-${index}`}
|
|
39
|
+
muted={muted}
|
|
40
|
+
size={size}
|
|
41
|
+
testID={testID ? `${testID}-hidden-${index}` : undefined}
|
|
42
|
+
/>
|
|
43
|
+
))}
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const styles = StyleSheet.create({
|
|
49
|
+
hand: {
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
flexDirection: 'row',
|
|
52
|
+
gap: 4,
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TabletopColorOverrides } from '../../colors';
|
|
2
|
+
import type { PlayingCardValue, TabletopCardSize } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface CardHandProps {
|
|
5
|
+
readonly cards?: readonly PlayingCardValue[];
|
|
6
|
+
readonly faceDownCards?: number;
|
|
7
|
+
readonly size?: TabletopCardSize;
|
|
8
|
+
readonly muted?: boolean;
|
|
9
|
+
readonly colorScheme?: TabletopColorOverrides;
|
|
10
|
+
readonly accessibilityLabel?: string;
|
|
11
|
+
readonly testID?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useZoraTheme } from '@ankhorage/zora';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { getPlayingCardLabel } from '../../cardLabel';
|
|
6
|
+
import { getTabletopCardDimensions } from '../../cardSizing';
|
|
7
|
+
import { createTabletopColorScheme } from '../../colors';
|
|
8
|
+
import type { PlayingCardSuit } from '../../types';
|
|
9
|
+
import type { PlayingCardProps } from './types';
|
|
10
|
+
|
|
11
|
+
const suitMarks: Readonly<Record<PlayingCardSuit, string>> = {
|
|
12
|
+
clubs: 'C',
|
|
13
|
+
diamonds: 'D',
|
|
14
|
+
hearts: 'H',
|
|
15
|
+
spades: 'S',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isRedSuit(suit: PlayingCardSuit): boolean {
|
|
19
|
+
return suit === 'diamonds' || suit === 'hearts';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function PlayingCard({
|
|
23
|
+
card,
|
|
24
|
+
size = 'medium',
|
|
25
|
+
selected = false,
|
|
26
|
+
muted = false,
|
|
27
|
+
accessibilityLabel,
|
|
28
|
+
colorScheme,
|
|
29
|
+
testID,
|
|
30
|
+
}: PlayingCardProps) {
|
|
31
|
+
const { theme } = useZoraTheme();
|
|
32
|
+
const colors = React.useMemo(
|
|
33
|
+
() => createTabletopColorScheme(theme, colorScheme),
|
|
34
|
+
[colorScheme, theme],
|
|
35
|
+
);
|
|
36
|
+
const dimensions = getTabletopCardDimensions(size);
|
|
37
|
+
const suitColor = isRedSuit(card.suit) ? colors.redSuitText : colors.cardText;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
accessibilityLabel={accessibilityLabel ?? getPlayingCardLabel(card)}
|
|
42
|
+
accessibilityRole="image"
|
|
43
|
+
style={[
|
|
44
|
+
styles.card,
|
|
45
|
+
{
|
|
46
|
+
backgroundColor: colors.cardSurface,
|
|
47
|
+
borderColor: selected ? colors.seatSelectedBorder : colors.cardBorder,
|
|
48
|
+
borderRadius: dimensions.radius,
|
|
49
|
+
height: dimensions.height,
|
|
50
|
+
opacity: muted ? 0.48 : 1,
|
|
51
|
+
width: dimensions.width,
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
testID={testID}
|
|
55
|
+
>
|
|
56
|
+
<Text
|
|
57
|
+
selectable={false}
|
|
58
|
+
style={[
|
|
59
|
+
styles.rank,
|
|
60
|
+
{
|
|
61
|
+
color: suitColor,
|
|
62
|
+
fontSize: dimensions.rankFontSize,
|
|
63
|
+
lineHeight: dimensions.rankFontSize + 4,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
{card.rank}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text
|
|
70
|
+
selectable={false}
|
|
71
|
+
style={[
|
|
72
|
+
styles.suit,
|
|
73
|
+
{
|
|
74
|
+
color: suitColor,
|
|
75
|
+
fontSize: dimensions.suitFontSize,
|
|
76
|
+
lineHeight: dimensions.suitFontSize + 3,
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
79
|
+
>
|
|
80
|
+
{suitMarks[card.suit]}
|
|
81
|
+
</Text>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
card: {
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
},
|
|
92
|
+
rank: {
|
|
93
|
+
fontWeight: '800',
|
|
94
|
+
},
|
|
95
|
+
suit: {
|
|
96
|
+
fontWeight: '700',
|
|
97
|
+
marginTop: -2,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TabletopColorOverrides } from '../../colors';
|
|
2
|
+
import type { PlayingCardValue, TabletopCardSize } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface PlayingCardProps {
|
|
5
|
+
readonly card: PlayingCardValue;
|
|
6
|
+
readonly size?: TabletopCardSize;
|
|
7
|
+
readonly selected?: boolean;
|
|
8
|
+
readonly muted?: boolean;
|
|
9
|
+
readonly accessibilityLabel?: string;
|
|
10
|
+
readonly colorScheme?: TabletopColorOverrides;
|
|
11
|
+
readonly testID?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useZoraTheme } from '@ankhorage/zora';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { createTabletopColorScheme } from '../../colors';
|
|
6
|
+
import { getTabletopSeatPosition } from '../../tablePositions';
|
|
7
|
+
import type { TabletopSeatState } from '../../types';
|
|
8
|
+
import { CardHand } from '../card-hand';
|
|
9
|
+
import type { TabletopTableProps } from './types';
|
|
10
|
+
|
|
11
|
+
function createSeatAccessibilityLabel(seat: TabletopSeatState): string | undefined {
|
|
12
|
+
if (seat.accessibilityLabel !== undefined) return seat.accessibilityLabel;
|
|
13
|
+
if (typeof seat.label !== 'string') return undefined;
|
|
14
|
+
|
|
15
|
+
const parts = [seat.label];
|
|
16
|
+
if (typeof seat.sublabel === 'string') parts.push(seat.sublabel);
|
|
17
|
+
if (typeof seat.tokenLabel === 'string') parts.push(seat.tokenLabel);
|
|
18
|
+
if (seat.selected) parts.push('selected');
|
|
19
|
+
if (seat.muted) parts.push('muted');
|
|
20
|
+
|
|
21
|
+
return parts.join(', ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TabletopTable({
|
|
25
|
+
seats,
|
|
26
|
+
centerCards = [],
|
|
27
|
+
centerLabel,
|
|
28
|
+
centerSublabel,
|
|
29
|
+
shape = 'oval',
|
|
30
|
+
seatCount,
|
|
31
|
+
cardSize = 'small',
|
|
32
|
+
disabled = false,
|
|
33
|
+
colorScheme,
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
testID,
|
|
36
|
+
}: TabletopTableProps) {
|
|
37
|
+
const { theme } = useZoraTheme();
|
|
38
|
+
const colors = React.useMemo(
|
|
39
|
+
() => createTabletopColorScheme(theme, colorScheme),
|
|
40
|
+
[colorScheme, theme],
|
|
41
|
+
);
|
|
42
|
+
const resolvedSeatCount = seatCount ?? seats.length;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View
|
|
46
|
+
accessibilityLabel={accessibilityLabel}
|
|
47
|
+
style={[styles.root, { opacity: disabled ? 0.56 : 1 }]}
|
|
48
|
+
testID={testID}
|
|
49
|
+
>
|
|
50
|
+
<View
|
|
51
|
+
style={[
|
|
52
|
+
styles.surface,
|
|
53
|
+
shape === 'circle' ? styles.circleSurface : styles.ovalSurface,
|
|
54
|
+
shape === 'rounded' ? styles.roundedSurface : null,
|
|
55
|
+
{
|
|
56
|
+
backgroundColor: colors.tableFelt,
|
|
57
|
+
borderColor: colors.tableBorder,
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
>
|
|
61
|
+
<View
|
|
62
|
+
pointerEvents="none"
|
|
63
|
+
style={[
|
|
64
|
+
styles.innerSurface,
|
|
65
|
+
shape === 'circle' ? styles.circleSurface : styles.ovalSurface,
|
|
66
|
+
shape === 'rounded' ? styles.roundedSurface : null,
|
|
67
|
+
{ borderColor: colors.tableInnerBorder },
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
<View style={styles.centerContent}>
|
|
71
|
+
{centerCards.length > 0 ? (
|
|
72
|
+
<CardHand cards={centerCards} colorScheme={colorScheme} size={cardSize} />
|
|
73
|
+
) : null}
|
|
74
|
+
{centerLabel !== undefined ? (
|
|
75
|
+
<View style={[styles.centerLabel, { backgroundColor: colors.seatSurface }]}>
|
|
76
|
+
<Text style={[styles.centerLabelText, { color: colors.cardText }]}>
|
|
77
|
+
{centerLabel}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
) : null}
|
|
81
|
+
{centerSublabel !== undefined ? (
|
|
82
|
+
<Text style={[styles.centerSublabel, { color: colors.mutedText }]}>
|
|
83
|
+
{centerSublabel}
|
|
84
|
+
</Text>
|
|
85
|
+
) : null}
|
|
86
|
+
</View>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
{seats.map((seat, index) => {
|
|
90
|
+
const position = getTabletopSeatPosition(index, resolvedSeatCount);
|
|
91
|
+
return (
|
|
92
|
+
<View
|
|
93
|
+
accessibilityLabel={createSeatAccessibilityLabel(seat)}
|
|
94
|
+
accessibilityRole="summary"
|
|
95
|
+
key={seat.id}
|
|
96
|
+
style={[
|
|
97
|
+
styles.seat,
|
|
98
|
+
{
|
|
99
|
+
borderColor: seat.selected ? colors.seatSelectedBorder : colors.seatBorder,
|
|
100
|
+
backgroundColor: colors.seatSurface,
|
|
101
|
+
left: position.left,
|
|
102
|
+
opacity: seat.muted ? 0.48 : 1,
|
|
103
|
+
top: position.top,
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
testID={testID ? `${testID}-seat-${seat.id}` : undefined}
|
|
107
|
+
>
|
|
108
|
+
{seat.cards !== undefined || seat.faceDownCards !== undefined ? (
|
|
109
|
+
<CardHand
|
|
110
|
+
cards={seat.cards}
|
|
111
|
+
colorScheme={colorScheme}
|
|
112
|
+
faceDownCards={seat.faceDownCards}
|
|
113
|
+
muted={seat.muted ?? seat.disabled}
|
|
114
|
+
size={cardSize}
|
|
115
|
+
/>
|
|
116
|
+
) : null}
|
|
117
|
+
<Text style={[styles.seatLabel, { color: colors.cardText }]}>{seat.label}</Text>
|
|
118
|
+
{seat.sublabel !== undefined ? (
|
|
119
|
+
<Text style={[styles.seatSublabel, { color: colors.mutedText }]}>
|
|
120
|
+
{seat.sublabel}
|
|
121
|
+
</Text>
|
|
122
|
+
) : null}
|
|
123
|
+
{seat.tokenLabel !== undefined ? (
|
|
124
|
+
<View style={[styles.token, { backgroundColor: colors.tokenSurface }]}>
|
|
125
|
+
<Text style={[styles.tokenText, { color: colors.tokenText }]}>
|
|
126
|
+
{seat.tokenLabel}
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
) : null}
|
|
130
|
+
</View>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const styles = StyleSheet.create({
|
|
138
|
+
centerContent: {
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
gap: 6,
|
|
141
|
+
left: 0,
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
right: 0,
|
|
144
|
+
top: '35%',
|
|
145
|
+
},
|
|
146
|
+
centerLabel: {
|
|
147
|
+
borderRadius: 999,
|
|
148
|
+
paddingHorizontal: 10,
|
|
149
|
+
paddingVertical: 4,
|
|
150
|
+
},
|
|
151
|
+
centerLabelText: {
|
|
152
|
+
fontSize: 12,
|
|
153
|
+
fontWeight: '800',
|
|
154
|
+
},
|
|
155
|
+
centerSublabel: {
|
|
156
|
+
fontSize: 11,
|
|
157
|
+
fontWeight: '600',
|
|
158
|
+
},
|
|
159
|
+
circleSurface: {
|
|
160
|
+
borderRadius: 999,
|
|
161
|
+
},
|
|
162
|
+
innerSurface: {
|
|
163
|
+
borderWidth: 1,
|
|
164
|
+
bottom: 10,
|
|
165
|
+
left: 10,
|
|
166
|
+
opacity: 0.44,
|
|
167
|
+
position: 'absolute',
|
|
168
|
+
right: 10,
|
|
169
|
+
top: 10,
|
|
170
|
+
},
|
|
171
|
+
ovalSurface: {
|
|
172
|
+
borderRadius: 999,
|
|
173
|
+
},
|
|
174
|
+
root: {
|
|
175
|
+
aspectRatio: 1.6,
|
|
176
|
+
maxWidth: 760,
|
|
177
|
+
position: 'relative',
|
|
178
|
+
width: '100%',
|
|
179
|
+
},
|
|
180
|
+
roundedSurface: {
|
|
181
|
+
borderRadius: 28,
|
|
182
|
+
},
|
|
183
|
+
seat: {
|
|
184
|
+
alignItems: 'center',
|
|
185
|
+
borderRadius: 10,
|
|
186
|
+
borderWidth: 1,
|
|
187
|
+
gap: 3,
|
|
188
|
+
minWidth: 64,
|
|
189
|
+
paddingHorizontal: 6,
|
|
190
|
+
paddingVertical: 5,
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
transform: [{ translateX: -32 }, { translateY: -28 }],
|
|
193
|
+
},
|
|
194
|
+
seatLabel: {
|
|
195
|
+
fontSize: 11,
|
|
196
|
+
fontWeight: '800',
|
|
197
|
+
},
|
|
198
|
+
seatSublabel: {
|
|
199
|
+
fontSize: 10,
|
|
200
|
+
fontWeight: '600',
|
|
201
|
+
},
|
|
202
|
+
surface: {
|
|
203
|
+
borderWidth: 8,
|
|
204
|
+
bottom: 28,
|
|
205
|
+
left: 24,
|
|
206
|
+
position: 'absolute',
|
|
207
|
+
right: 24,
|
|
208
|
+
top: 24,
|
|
209
|
+
},
|
|
210
|
+
token: {
|
|
211
|
+
borderRadius: 999,
|
|
212
|
+
paddingHorizontal: 6,
|
|
213
|
+
paddingVertical: 2,
|
|
214
|
+
},
|
|
215
|
+
tokenText: {
|
|
216
|
+
fontSize: 10,
|
|
217
|
+
fontWeight: '800',
|
|
218
|
+
},
|
|
219
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { TabletopColorOverrides } from '../../colors';
|
|
4
|
+
import type {
|
|
5
|
+
PlayingCardValue,
|
|
6
|
+
TabletopCardSize,
|
|
7
|
+
TabletopSeatCount,
|
|
8
|
+
TabletopSeatState,
|
|
9
|
+
TabletopShape,
|
|
10
|
+
} from '../../types';
|
|
11
|
+
|
|
12
|
+
export interface TabletopTableProps {
|
|
13
|
+
readonly seats: readonly TabletopSeatState[];
|
|
14
|
+
readonly centerCards?: readonly PlayingCardValue[];
|
|
15
|
+
readonly centerLabel?: React.ReactNode;
|
|
16
|
+
readonly centerSublabel?: React.ReactNode;
|
|
17
|
+
readonly shape?: TabletopShape;
|
|
18
|
+
readonly seatCount?: TabletopSeatCount;
|
|
19
|
+
readonly cardSize?: TabletopCardSize;
|
|
20
|
+
readonly disabled?: boolean;
|
|
21
|
+
readonly colorScheme?: TabletopColorOverrides;
|
|
22
|
+
readonly accessibilityLabel?: string;
|
|
23
|
+
readonly testID?: string;
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type { TabletopColorOverrides, TabletopColorScheme } from './colors';
|
|
2
|
+
export { createTabletopColorScheme } from './colors';
|
|
3
|
+
export type { CardBackProps } from './components/card-back';
|
|
4
|
+
export { CardBack } from './components/card-back';
|
|
5
|
+
export type { CardHandProps } from './components/card-hand';
|
|
6
|
+
export { CardHand } from './components/card-hand';
|
|
7
|
+
export type { PlayingCardProps } from './components/playing-card';
|
|
8
|
+
export { PlayingCard } from './components/playing-card';
|
|
9
|
+
export type { TabletopTableProps } from './components/tabletop-table';
|
|
10
|
+
export { TabletopTable } from './components/tabletop-table';
|
|
11
|
+
export type {
|
|
12
|
+
PlayingCardSuit,
|
|
13
|
+
PlayingCardValue,
|
|
14
|
+
TabletopCardSize,
|
|
15
|
+
TabletopSeatCount,
|
|
16
|
+
TabletopSeatState,
|
|
17
|
+
TabletopShape,
|
|
18
|
+
} from './types';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getTabletopSeatPosition,
|
|
5
|
+
getTabletopSeatPositions,
|
|
6
|
+
normalizeTabletopSeatCount,
|
|
7
|
+
} from './tablePositions';
|
|
8
|
+
|
|
9
|
+
describe('normalizeTabletopSeatCount', () => {
|
|
10
|
+
test('clamps low counts to two seats', () => {
|
|
11
|
+
expect(normalizeTabletopSeatCount(0)).toBe(2);
|
|
12
|
+
expect(normalizeTabletopSeatCount(1)).toBe(2);
|
|
13
|
+
expect(normalizeTabletopSeatCount(2)).toBe(2);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('preserves supported middle counts', () => {
|
|
17
|
+
expect(normalizeTabletopSeatCount(6)).toBe(6);
|
|
18
|
+
expect(normalizeTabletopSeatCount(9)).toBe(9);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('clamps high counts to ten seats', () => {
|
|
22
|
+
expect(normalizeTabletopSeatCount(11)).toBe(10);
|
|
23
|
+
expect(normalizeTabletopSeatCount(20)).toBe(10);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getTabletopSeatPositions', () => {
|
|
28
|
+
test('returns the expected number of positions', () => {
|
|
29
|
+
expect(getTabletopSeatPositions(2)).toHaveLength(2);
|
|
30
|
+
expect(getTabletopSeatPositions(9)).toHaveLength(9);
|
|
31
|
+
expect(getTabletopSeatPositions(10)).toHaveLength(10);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('getTabletopSeatPosition', () => {
|
|
36
|
+
test('wraps indexes within the normalized seat count', () => {
|
|
37
|
+
expect(getTabletopSeatPosition(0, 2)).toEqual({ top: '82%', left: '50%' });
|
|
38
|
+
expect(getTabletopSeatPosition(2, 2)).toEqual({ top: '82%', left: '50%' });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { TabletopSeatCount } from './types';
|
|
2
|
+
|
|
3
|
+
type TabletopSeatPositionValue = `${number}%`;
|
|
4
|
+
|
|
5
|
+
export interface TabletopSeatPosition {
|
|
6
|
+
readonly top: TabletopSeatPositionValue;
|
|
7
|
+
readonly left: TabletopSeatPositionValue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const fallbackSeatPosition: TabletopSeatPosition = { top: '82%', left: '50%' };
|
|
11
|
+
|
|
12
|
+
const seatPositions: Readonly<Record<TabletopSeatCount, readonly TabletopSeatPosition[]>> = {
|
|
13
|
+
2: [fallbackSeatPosition, { top: '18%', left: '50%' }],
|
|
14
|
+
3: [fallbackSeatPosition, { top: '28%', left: '18%' }, { top: '28%', left: '82%' }],
|
|
15
|
+
4: [
|
|
16
|
+
fallbackSeatPosition,
|
|
17
|
+
{ top: '50%', left: '12%' },
|
|
18
|
+
{ top: '18%', left: '50%' },
|
|
19
|
+
{ top: '50%', left: '88%' },
|
|
20
|
+
],
|
|
21
|
+
5: [
|
|
22
|
+
{ top: '84%', left: '50%' },
|
|
23
|
+
{ top: '62%', left: '12%' },
|
|
24
|
+
{ top: '18%', left: '24%' },
|
|
25
|
+
{ top: '18%', left: '76%' },
|
|
26
|
+
{ top: '62%', left: '88%' },
|
|
27
|
+
],
|
|
28
|
+
6: [
|
|
29
|
+
{ top: '84%', left: '50%' },
|
|
30
|
+
{ top: '68%', left: '16%' },
|
|
31
|
+
{ top: '26%', left: '16%' },
|
|
32
|
+
{ top: '16%', left: '50%' },
|
|
33
|
+
{ top: '26%', left: '84%' },
|
|
34
|
+
{ top: '68%', left: '84%' },
|
|
35
|
+
],
|
|
36
|
+
7: [
|
|
37
|
+
{ top: '86%', left: '50%' },
|
|
38
|
+
{ top: '70%', left: '14%' },
|
|
39
|
+
{ top: '34%', left: '10%' },
|
|
40
|
+
{ top: '14%', left: '34%' },
|
|
41
|
+
{ top: '14%', left: '66%' },
|
|
42
|
+
{ top: '34%', left: '90%' },
|
|
43
|
+
{ top: '70%', left: '86%' },
|
|
44
|
+
],
|
|
45
|
+
8: [
|
|
46
|
+
{ top: '86%', left: '50%' },
|
|
47
|
+
{ top: '72%', left: '16%' },
|
|
48
|
+
{ top: '50%', left: '8%' },
|
|
49
|
+
{ top: '20%', left: '20%' },
|
|
50
|
+
{ top: '14%', left: '50%' },
|
|
51
|
+
{ top: '20%', left: '80%' },
|
|
52
|
+
{ top: '50%', left: '92%' },
|
|
53
|
+
{ top: '72%', left: '84%' },
|
|
54
|
+
],
|
|
55
|
+
9: [
|
|
56
|
+
{ top: '88%', left: '50%' },
|
|
57
|
+
{ top: '78%', left: '12%' },
|
|
58
|
+
{ top: '50%', left: '4%' },
|
|
59
|
+
{ top: '18%', left: '10%' },
|
|
60
|
+
{ top: '8%', left: '28%' },
|
|
61
|
+
{ top: '8%', left: '50%' },
|
|
62
|
+
{ top: '8%', left: '72%' },
|
|
63
|
+
{ top: '18%', left: '90%' },
|
|
64
|
+
{ top: '50%', left: '96%' },
|
|
65
|
+
],
|
|
66
|
+
10: [
|
|
67
|
+
{ top: '88%', left: '50%' },
|
|
68
|
+
{ top: '80%', left: '20%' },
|
|
69
|
+
{ top: '58%', left: '6%' },
|
|
70
|
+
{ top: '30%', left: '8%' },
|
|
71
|
+
{ top: '10%', left: '30%' },
|
|
72
|
+
{ top: '8%', left: '50%' },
|
|
73
|
+
{ top: '10%', left: '70%' },
|
|
74
|
+
{ top: '30%', left: '92%' },
|
|
75
|
+
{ top: '58%', left: '94%' },
|
|
76
|
+
{ top: '80%', left: '80%' },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function normalizeTabletopSeatCount(count: number): TabletopSeatCount {
|
|
81
|
+
const rounded = Math.round(count);
|
|
82
|
+
if (rounded <= 2) return 2;
|
|
83
|
+
if (rounded >= 10) return 10;
|
|
84
|
+
|
|
85
|
+
return rounded as TabletopSeatCount;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getTabletopSeatPositions(count: number): readonly TabletopSeatPosition[] {
|
|
89
|
+
return seatPositions[normalizeTabletopSeatCount(count)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getTabletopSeatPosition(index: number, count: number): TabletopSeatPosition {
|
|
93
|
+
const positions = getTabletopSeatPositions(count);
|
|
94
|
+
const position = positions[index % positions.length];
|
|
95
|
+
|
|
96
|
+
return position ?? fallbackSeatPosition;
|
|
97
|
+
}
|