@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/cardLabel.d.ts +4 -0
  4. package/dist/cardLabel.d.ts.map +1 -0
  5. package/dist/cardLabel.js +9 -0
  6. package/dist/cardLabel.js.map +1 -0
  7. package/dist/cardSizing.d.ts +10 -0
  8. package/dist/cardSizing.d.ts.map +1 -0
  9. package/dist/cardSizing.js +27 -0
  10. package/dist/cardSizing.js.map +1 -0
  11. package/dist/colors.d.ts +44 -0
  12. package/dist/colors.d.ts.map +1 -0
  13. package/dist/colors.js +20 -0
  14. package/dist/colors.js.map +1 -0
  15. package/dist/components/card-back/CardBack.d.ts +4 -0
  16. package/dist/components/card-back/CardBack.d.ts.map +1 -0
  17. package/dist/components/card-back/CardBack.js +44 -0
  18. package/dist/components/card-back/CardBack.js.map +1 -0
  19. package/dist/components/card-back/index.d.ts +3 -0
  20. package/dist/components/card-back/index.d.ts.map +1 -0
  21. package/dist/components/card-back/index.js +2 -0
  22. package/dist/components/card-back/index.js.map +1 -0
  23. package/dist/components/card-back/types.d.ts +10 -0
  24. package/dist/components/card-back/types.d.ts.map +1 -0
  25. package/dist/components/card-back/types.js +2 -0
  26. package/dist/components/card-back/types.js.map +1 -0
  27. package/dist/components/card-hand/CardHand.d.ts +4 -0
  28. package/dist/components/card-hand/CardHand.d.ts.map +1 -0
  29. package/dist/components/card-hand/CardHand.js +23 -0
  30. package/dist/components/card-hand/CardHand.js.map +1 -0
  31. package/dist/components/card-hand/index.d.ts +3 -0
  32. package/dist/components/card-hand/index.d.ts.map +1 -0
  33. package/dist/components/card-hand/index.js +2 -0
  34. package/dist/components/card-hand/index.js.map +1 -0
  35. package/dist/components/card-hand/types.d.ts +12 -0
  36. package/dist/components/card-hand/types.d.ts.map +1 -0
  37. package/dist/components/card-hand/types.js +2 -0
  38. package/dist/components/card-hand/types.js.map +1 -0
  39. package/dist/components/playing-card/PlayingCard.d.ts +4 -0
  40. package/dist/components/playing-card/PlayingCard.d.ts.map +1 -0
  41. package/dist/components/playing-card/PlayingCard.js +68 -0
  42. package/dist/components/playing-card/PlayingCard.js.map +1 -0
  43. package/dist/components/playing-card/index.d.ts +3 -0
  44. package/dist/components/playing-card/index.d.ts.map +1 -0
  45. package/dist/components/playing-card/index.js +2 -0
  46. package/dist/components/playing-card/index.js.map +1 -0
  47. package/dist/components/playing-card/types.d.ts +12 -0
  48. package/dist/components/playing-card/types.d.ts.map +1 -0
  49. package/dist/components/playing-card/types.js +2 -0
  50. package/dist/components/playing-card/types.js.map +1 -0
  51. package/dist/components/tabletop-table/TabletopTable.d.ts +4 -0
  52. package/dist/components/tabletop-table/TabletopTable.d.ts.map +1 -0
  53. package/dist/components/tabletop-table/TabletopTable.js +165 -0
  54. package/dist/components/tabletop-table/TabletopTable.js.map +1 -0
  55. package/dist/components/tabletop-table/index.d.ts +3 -0
  56. package/dist/components/tabletop-table/index.d.ts.map +1 -0
  57. package/dist/components/tabletop-table/index.js +2 -0
  58. package/dist/components/tabletop-table/index.js.map +1 -0
  59. package/dist/components/tabletop-table/types.d.ts +17 -0
  60. package/dist/components/tabletop-table/types.d.ts.map +1 -0
  61. package/dist/components/tabletop-table/types.js +2 -0
  62. package/dist/components/tabletop-table/types.js.map +1 -0
  63. package/dist/index.d.ts +12 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +6 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/tablePositions.d.ts +11 -0
  68. package/dist/tablePositions.d.ts.map +1 -0
  69. package/dist/tablePositions.js +85 -0
  70. package/dist/tablePositions.js.map +1 -0
  71. package/dist/types.d.ts +22 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. package/package.json +82 -0
  76. package/src/cardLabel.ts +11 -0
  77. package/src/cardSizing.ts +37 -0
  78. package/src/colors.ts +67 -0
  79. package/src/components/card-back/CardBack.tsx +66 -0
  80. package/src/components/card-back/index.ts +2 -0
  81. package/src/components/card-back/types.ts +10 -0
  82. package/src/components/card-hand/CardHand.tsx +55 -0
  83. package/src/components/card-hand/index.ts +2 -0
  84. package/src/components/card-hand/types.ts +12 -0
  85. package/src/components/playing-card/PlayingCard.tsx +99 -0
  86. package/src/components/playing-card/index.ts +2 -0
  87. package/src/components/playing-card/types.ts +12 -0
  88. package/src/components/tabletop-table/TabletopTable.tsx +219 -0
  89. package/src/components/tabletop-table/index.ts +2 -0
  90. package/src/components/tabletop-table/types.ts +24 -0
  91. package/src/index.ts +18 -0
  92. package/src/tablePositions.test.ts +40 -0
  93. package/src/tablePositions.ts +97 -0
  94. 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,2 @@
1
+ export { CardBack } from './CardBack';
2
+ export type { CardBackProps } from './types';
@@ -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,2 @@
1
+ export { CardHand } from './CardHand';
2
+ export type { CardHandProps } from './types';
@@ -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,2 @@
1
+ export { PlayingCard } from './PlayingCard';
2
+ export type { PlayingCardProps } from './types';
@@ -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,2 @@
1
+ export { TabletopTable } from './TabletopTable';
2
+ export type { TabletopTableProps } from './types';
@@ -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
+ }