@joystyk/cards-ui 0.0.0-stage → 0.2.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +32 -2
- package/dist/controller.d.ts +110 -0
- package/dist/controller.js +449 -0
- package/dist/controller.js.map +1 -0
- package/dist/face-styles.d.ts +1 -0
- package/dist/face-styles.js +106 -0
- package/dist/face-styles.js.map +1 -0
- package/dist/faces.d.ts +11 -0
- package/dist/faces.js +403 -0
- package/dist/faces.js.map +1 -0
- package/dist/hand-styles.d.ts +1 -0
- package/dist/hand-styles.js +121 -0
- package/dist/hand-styles.js.map +1 -0
- package/dist/hand.d.ts +100 -0
- package/dist/hand.js +480 -0
- package/dist/hand.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/layout.d.ts +122 -0
- package/dist/layout.js +35 -0
- package/dist/layout.js.map +1 -0
- package/dist/solitaire.d.ts +155 -0
- package/dist/solitaire.js +1508 -0
- package/dist/solitaire.js.map +1 -0
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +452 -0
- package/dist/styles.js.map +1 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.js +629 -0
- package/dist/table.js.map +1 -0
- package/dist/tiles.d.ts +3 -0
- package/dist/tiles.js +157 -0
- package/dist/tiles.js.map +1 -0
- package/dist/worker-setup.d.ts +7 -0
- package/dist/worker-setup.js +45 -0
- package/dist/worker-setup.js.map +1 -0
- package/package.json +67 -3
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { css } from "@gyral/core";
|
|
2
|
+
//#region src/hand-styles.ts
|
|
3
|
+
var counts = Array.from({ length: 32 }, (_, k) => `.n-${k + 1} { --n: ${k + 1}; }`).join("\n");
|
|
4
|
+
var handStyles = css`
|
|
5
|
+
@layer components {
|
|
6
|
+
${counts}
|
|
7
|
+
|
|
8
|
+
:host {
|
|
9
|
+
display: block;
|
|
10
|
+
container-type: inline-size;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.hand {
|
|
14
|
+
--card-w: clamp(3.6rem, 16cqi, 6.5rem);
|
|
15
|
+
--card-h: calc(var(--card-w) * 1.4);
|
|
16
|
+
--step: clamp(2.75rem, calc((100cqi - var(--card-w) - 0.5rem) / max(1, var(--n, 1) - 1)), calc(var(--card-w) * 0.66));
|
|
17
|
+
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
row-gap: 0.4rem;
|
|
22
|
+
padding-block-start: calc(var(--card-h) * 0.2);
|
|
23
|
+
padding-inline-end: calc(var(--card-w) - var(--step));
|
|
24
|
+
user-select: none;
|
|
25
|
+
-webkit-user-select: none;
|
|
26
|
+
-webkit-tap-highlight-color: transparent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.hand.tiles { --card-h: calc(var(--card-w) * 2); }
|
|
30
|
+
.hand.tiles .slot { touch-action: none; }
|
|
31
|
+
.hand.tiles .slot:not(.off) .cface { outline: 2px solid var(--hand-select, oklch(40% .06 170)); outline-offset: 1px; translate: 0 -.25rem; }
|
|
32
|
+
|
|
33
|
+
.slot {
|
|
34
|
+
position: relative;
|
|
35
|
+
flex: none;
|
|
36
|
+
inline-size: var(--step);
|
|
37
|
+
block-size: var(--card-h);
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0;
|
|
40
|
+
border: 0;
|
|
41
|
+
background: none;
|
|
42
|
+
color: inherit;
|
|
43
|
+
font: inherit;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
touch-action: manipulation;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.slot .cface {
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset-block-start: 0;
|
|
51
|
+
inset-inline-start: 0;
|
|
52
|
+
inline-size: var(--card-w);
|
|
53
|
+
block-size: var(--card-h);
|
|
54
|
+
transition: translate 140ms ease-out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.slot:focus-visible {
|
|
58
|
+
outline: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.slot:focus-visible .cface {
|
|
62
|
+
outline: 3px solid var(--hand-focus, oklch(55% 0.2 255));
|
|
63
|
+
outline-offset: 2px;
|
|
64
|
+
z-index: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.chosen .cface {
|
|
68
|
+
translate: 0 calc(var(--card-h) * -0.18);
|
|
69
|
+
outline: 3px solid var(--hand-select, oklch(62% 0.19 45));
|
|
70
|
+
outline-offset: 1px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.off .cface {
|
|
74
|
+
translate: 0 calc(var(--card-h) * 0.06);
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.off .cface::after {
|
|
79
|
+
content: '';
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset: 0;
|
|
82
|
+
border-radius: inherit;
|
|
83
|
+
background: repeating-linear-gradient(135deg, oklch(40% 0.02 260 / 0.22) 0 3px, transparent 3px 7px);
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* A noted card (just received) carries a corner tab, besides the word in its name. */
|
|
88
|
+
.noted .cface::before {
|
|
89
|
+
content: '';
|
|
90
|
+
position: absolute;
|
|
91
|
+
inset-block-start: -0.35rem;
|
|
92
|
+
inset-inline-end: 0.3rem;
|
|
93
|
+
inline-size: 0.7rem;
|
|
94
|
+
block-size: 0.7rem;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
background: var(--hand-note, oklch(62% 0.19 45));
|
|
97
|
+
border: 2px solid var(--card-paper, oklch(98.5% 0.008 90));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.calm .slot .cface {
|
|
101
|
+
transition: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (prefers-reduced-motion: reduce) {
|
|
105
|
+
.slot .cface {
|
|
106
|
+
transition: none;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (forced-colors: active) {
|
|
111
|
+
.slot:focus-visible .cface,
|
|
112
|
+
.chosen .cface {
|
|
113
|
+
outline-color: Highlight;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { handStyles };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=hand-styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hand-styles.js","names":[],"sources":["../src/hand-styles.ts"],"sourcesContent":["// The hand widget's layout: a fan of overlapping cards that fits its container. Each card\n// shows at least a 44 px strip (hearts.md, \"Keyboard and pointer controls\"); when the fan\n// can't fit at that, it wraps onto another row. Chosen cards lift with an outline, cards that\n// can't be played sit lower behind a hatch, and the focused card gets a thick ring: none of\n// these rely on color alone. Theme with --hand-focus, --hand-select and --hand-note.\n// The site's Content Security Policy refuses style attributes, so the card count is a class.\nimport { css } from '@gyral/core';\n\nconst counts = Array.from({ length: 32 }, (_, k) => `.n-${k + 1} { --n: ${k + 1}; }`).join('\\n');\n\nexport const handStyles = css`\n @layer components {\n ${counts}\n\n :host {\n display: block;\n container-type: inline-size;\n }\n\n .hand {\n --card-w: clamp(3.6rem, 16cqi, 6.5rem);\n --card-h: calc(var(--card-w) * 1.4);\n --step: clamp(2.75rem, calc((100cqi - var(--card-w) - 0.5rem) / max(1, var(--n, 1) - 1)), calc(var(--card-w) * 0.66));\n\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n row-gap: 0.4rem;\n padding-block-start: calc(var(--card-h) * 0.2);\n padding-inline-end: calc(var(--card-w) - var(--step));\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n }\n\n .hand.tiles { --card-h: calc(var(--card-w) * 2); }\n .hand.tiles .slot { touch-action: none; }\n .hand.tiles .slot:not(.off) .cface { outline: 2px solid var(--hand-select, oklch(40% .06 170)); outline-offset: 1px; translate: 0 -.25rem; }\n\n .slot {\n position: relative;\n flex: none;\n inline-size: var(--step);\n block-size: var(--card-h);\n margin: 0;\n padding: 0;\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n cursor: pointer;\n touch-action: manipulation;\n }\n\n .slot .cface {\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: 0;\n inline-size: var(--card-w);\n block-size: var(--card-h);\n transition: translate 140ms ease-out;\n }\n\n .slot:focus-visible {\n outline: none;\n }\n\n .slot:focus-visible .cface {\n outline: 3px solid var(--hand-focus, oklch(55% 0.2 255));\n outline-offset: 2px;\n z-index: 1;\n }\n\n .chosen .cface {\n translate: 0 calc(var(--card-h) * -0.18);\n outline: 3px solid var(--hand-select, oklch(62% 0.19 45));\n outline-offset: 1px;\n }\n\n .off .cface {\n translate: 0 calc(var(--card-h) * 0.06);\n cursor: not-allowed;\n }\n\n .off .cface::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: repeating-linear-gradient(135deg, oklch(40% 0.02 260 / 0.22) 0 3px, transparent 3px 7px);\n pointer-events: none;\n }\n\n /* A noted card (just received) carries a corner tab, besides the word in its name. */\n .noted .cface::before {\n content: '';\n position: absolute;\n inset-block-start: -0.35rem;\n inset-inline-end: 0.3rem;\n inline-size: 0.7rem;\n block-size: 0.7rem;\n border-radius: 50%;\n background: var(--hand-note, oklch(62% 0.19 45));\n border: 2px solid var(--card-paper, oklch(98.5% 0.008 90));\n }\n\n .calm .slot .cface {\n transition: none;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .slot .cface {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .slot:focus-visible .cface,\n .chosen .cface {\n outline-color: Highlight;\n }\n }\n }\n`;\n"],"mappings":";;AAQA,IAAM,SAAS,MAAM,KAAK,EAAE,QAAQ,GAAG,IAAI,GAAG,MAAM,MAAM,IAAI,EAAE,UAAU,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI;AAE/F,IAAa,aAAa,GAAG;;MAEvB,OAAO"}
|
package/dist/hand.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type Rank, type Suit } from '@joystyk/kit/cards';
|
|
2
|
+
import { type TileFace } from '@joystyk/kit/tiles';
|
|
3
|
+
/** One card in the hand, with what the player may do with it now. */
|
|
4
|
+
export type HandCard = ({
|
|
5
|
+
readonly tile?: never;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly rank: Rank;
|
|
8
|
+
readonly suit: Suit;
|
|
9
|
+
} | {
|
|
10
|
+
readonly tile: TileFace;
|
|
11
|
+
readonly rank?: never;
|
|
12
|
+
readonly suit?: never;
|
|
13
|
+
}) & {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** It may be played (or chosen) now. A card that may not is aria-disabled. */
|
|
16
|
+
readonly playable: boolean;
|
|
17
|
+
/** Chosen for a multi-card choice (aria-pressed), drawn lifted. */
|
|
18
|
+
readonly chosen?: boolean;
|
|
19
|
+
/** Added to the card's name and marked on its face: "just received". */
|
|
20
|
+
readonly note?: string;
|
|
21
|
+
};
|
|
22
|
+
export type HandMode = 'play' | 'choose';
|
|
23
|
+
export type HandLayout = {
|
|
24
|
+
/** The hand's name: "Your hand, 13 cards". */
|
|
25
|
+
readonly label: string;
|
|
26
|
+
/** In display order. */
|
|
27
|
+
readonly cards: readonly HandCard[];
|
|
28
|
+
readonly mode: HandMode;
|
|
29
|
+
};
|
|
30
|
+
export type HandOptions = {
|
|
31
|
+
/** Reduced motion: chosen cards lift without a transition. */
|
|
32
|
+
readonly calm: boolean;
|
|
33
|
+
/** Blue diamonds and green clubs. */
|
|
34
|
+
readonly fourColor: boolean;
|
|
35
|
+
readonly numbers?: boolean;
|
|
36
|
+
readonly draggable?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** What the hand tells the game. */
|
|
39
|
+
export type HandOut = {
|
|
40
|
+
readonly _tag: 'Play';
|
|
41
|
+
readonly id: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly _tag: 'Refused';
|
|
44
|
+
readonly id: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly _tag: 'Toggle';
|
|
47
|
+
readonly id: string;
|
|
48
|
+
} | {
|
|
49
|
+
readonly _tag: 'Commit';
|
|
50
|
+
};
|
|
51
|
+
export type HandState = {
|
|
52
|
+
/** The index of the card the Tab stop is on. */
|
|
53
|
+
readonly focus: number;
|
|
54
|
+
/** The ids last rendered, to keep the focus on the same card when the hand changes. */
|
|
55
|
+
readonly ids: readonly string[];
|
|
56
|
+
/** Focus is inside the hand, so a card that leaves it hands the focus on. */
|
|
57
|
+
readonly inside: boolean;
|
|
58
|
+
};
|
|
59
|
+
export type HandMsg = {
|
|
60
|
+
readonly _tag: 'Key';
|
|
61
|
+
readonly key: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly _tag: 'Press';
|
|
64
|
+
readonly id: string;
|
|
65
|
+
} | {
|
|
66
|
+
readonly _tag: 'Enter';
|
|
67
|
+
readonly id: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly _tag: 'Leave';
|
|
70
|
+
};
|
|
71
|
+
export declare const EMPTY_HAND: HandLayout;
|
|
72
|
+
export declare const DEFAULT_HAND_OPTIONS: HandOptions;
|
|
73
|
+
export declare const isHandLayout: (value: unknown) => value is HandLayout;
|
|
74
|
+
export declare const isHandOptions: (value: unknown) => value is HandOptions;
|
|
75
|
+
/** A card's accessible name: "Queen of spades", "9 of hearts, just received". */
|
|
76
|
+
export declare const handCardLabel: (card: HandCard) => string;
|
|
77
|
+
export declare const initialHand: (layout: HandLayout) => HandState;
|
|
78
|
+
/** One step of the hand's controller: the new state, outputs, and a card to focus after rendering. */
|
|
79
|
+
export type HandStep = {
|
|
80
|
+
readonly state: HandState;
|
|
81
|
+
readonly outs: readonly HandOut[];
|
|
82
|
+
readonly focus: string | null;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Brings the state in line with a new hand. The focus stays on the same card if it is still
|
|
86
|
+
* there, otherwise on the card now at its place; if focus was inside the hand and its card
|
|
87
|
+
* left, the new card takes the focus.
|
|
88
|
+
*/
|
|
89
|
+
export declare const syncHand: (s: HandState, layout: HandLayout) => HandStep;
|
|
90
|
+
/** The hand's reducer, pure, so its behavior is tested without a DOM. */
|
|
91
|
+
export declare const handStep: (before: HandState, layout: HandLayout, m: HandMsg) => HandStep;
|
|
92
|
+
export declare const CardHand: import("@gyral/core").GyralElementClass<HandState, HandMsg, {
|
|
93
|
+
readonly layout: HandLayout;
|
|
94
|
+
readonly options: HandOptions;
|
|
95
|
+
}, HandOut, "Key" | "Enter" | "Press" | "Leave">;
|
|
96
|
+
declare global {
|
|
97
|
+
interface HTMLElementTagNameMap {
|
|
98
|
+
'jy-card-hand': InstanceType<typeof CardHand>;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/hand.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { faceStyles } from "./face-styles.js";
|
|
2
|
+
import { cardFace, suitDefs } from "./faces.js";
|
|
3
|
+
import { handStyles } from "./hand-styles.js";
|
|
4
|
+
import { tileFace } from "./tiles.js";
|
|
5
|
+
import { cardName } from "@joystyk/kit/cards";
|
|
6
|
+
import { define, each, focus, intentsOf, outputs, prop } from "@gyral/core";
|
|
7
|
+
import { compiled } from "@gyral/core/compiled";
|
|
8
|
+
import { tileName } from "@joystyk/kit/tiles";
|
|
9
|
+
//#region src/hand.ts
|
|
10
|
+
var _gyral$t0 = {
|
|
11
|
+
"id": "ak2gvt1npv",
|
|
12
|
+
"html": "<button type=\"button\"><span></span></button>",
|
|
13
|
+
"parts": [
|
|
14
|
+
[
|
|
15
|
+
4,
|
|
16
|
+
[0],
|
|
17
|
+
"class",
|
|
18
|
+
[
|
|
19
|
+
"slot ",
|
|
20
|
+
" ",
|
|
21
|
+
" ",
|
|
22
|
+
""
|
|
23
|
+
]
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
3,
|
|
27
|
+
[0],
|
|
28
|
+
"data-card"
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
3,
|
|
32
|
+
[0],
|
|
33
|
+
"draggable"
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
3,
|
|
37
|
+
[0],
|
|
38
|
+
"tabindex"
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
3,
|
|
42
|
+
[0],
|
|
43
|
+
"aria-label"
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
3,
|
|
47
|
+
[0],
|
|
48
|
+
"aria-disabled"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
3,
|
|
52
|
+
[0],
|
|
53
|
+
"aria-pressed"
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
3,
|
|
57
|
+
[0],
|
|
58
|
+
"data-intent"
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
4,
|
|
62
|
+
[0, 0],
|
|
63
|
+
"class",
|
|
64
|
+
["cface s-", ""]
|
|
65
|
+
],
|
|
66
|
+
[1, [0, 0]]
|
|
67
|
+
],
|
|
68
|
+
"segments": [
|
|
69
|
+
"<button type=\"button\"",
|
|
70
|
+
{
|
|
71
|
+
"k": "attr",
|
|
72
|
+
"name": "class",
|
|
73
|
+
"strings": [
|
|
74
|
+
"slot ",
|
|
75
|
+
" ",
|
|
76
|
+
" ",
|
|
77
|
+
""
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"k": "attr",
|
|
82
|
+
"name": "data-card"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"k": "attr",
|
|
86
|
+
"name": "draggable"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"k": "attr",
|
|
90
|
+
"name": "tabindex"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"k": "attr",
|
|
94
|
+
"name": "aria-label"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"k": "attr",
|
|
98
|
+
"name": "aria-disabled"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"k": "attr",
|
|
102
|
+
"name": "aria-pressed"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"k": "attr",
|
|
106
|
+
"name": "data-intent"
|
|
107
|
+
},
|
|
108
|
+
"><span",
|
|
109
|
+
{
|
|
110
|
+
"k": "attr",
|
|
111
|
+
"name": "class",
|
|
112
|
+
"strings": ["cface s-", ""]
|
|
113
|
+
},
|
|
114
|
+
">",
|
|
115
|
+
{
|
|
116
|
+
"k": "child",
|
|
117
|
+
"in": "span"
|
|
118
|
+
},
|
|
119
|
+
"</span></button>"
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
var _gyral$t1 = {
|
|
123
|
+
"id": "wiy3aoavqm",
|
|
124
|
+
"html": "<div class=\"h-enter\" data-intent-on=\"focusin\"><div class=\"h-leave\" data-intent-on=\"focusout\"><div role=\"toolbar\" data-intent-on=\"keydown\"></div></div></div>",
|
|
125
|
+
"parts": [
|
|
126
|
+
[
|
|
127
|
+
2,
|
|
128
|
+
[],
|
|
129
|
+
0
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
3,
|
|
133
|
+
[0],
|
|
134
|
+
"data-intent"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
3,
|
|
138
|
+
[0, 0],
|
|
139
|
+
"data-intent"
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
4,
|
|
143
|
+
[
|
|
144
|
+
0,
|
|
145
|
+
0,
|
|
146
|
+
0
|
|
147
|
+
],
|
|
148
|
+
"class",
|
|
149
|
+
[
|
|
150
|
+
"hand ",
|
|
151
|
+
" n-",
|
|
152
|
+
" ",
|
|
153
|
+
" ",
|
|
154
|
+
" mode-",
|
|
155
|
+
""
|
|
156
|
+
]
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
3,
|
|
160
|
+
[
|
|
161
|
+
0,
|
|
162
|
+
0,
|
|
163
|
+
0
|
|
164
|
+
],
|
|
165
|
+
"aria-label"
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
3,
|
|
169
|
+
[
|
|
170
|
+
0,
|
|
171
|
+
0,
|
|
172
|
+
0
|
|
173
|
+
],
|
|
174
|
+
"data-intent"
|
|
175
|
+
],
|
|
176
|
+
[1, [
|
|
177
|
+
0,
|
|
178
|
+
0,
|
|
179
|
+
0
|
|
180
|
+
]]
|
|
181
|
+
],
|
|
182
|
+
"segments": [
|
|
183
|
+
{ "k": "child" },
|
|
184
|
+
"<div class=\"h-enter\" data-intent-on=\"focusin\"",
|
|
185
|
+
{
|
|
186
|
+
"k": "attr",
|
|
187
|
+
"name": "data-intent"
|
|
188
|
+
},
|
|
189
|
+
"><div class=\"h-leave\" data-intent-on=\"focusout\"",
|
|
190
|
+
{
|
|
191
|
+
"k": "attr",
|
|
192
|
+
"name": "data-intent"
|
|
193
|
+
},
|
|
194
|
+
"><div role=\"toolbar\" data-intent-on=\"keydown\"",
|
|
195
|
+
{
|
|
196
|
+
"k": "attr",
|
|
197
|
+
"name": "class",
|
|
198
|
+
"strings": [
|
|
199
|
+
"hand ",
|
|
200
|
+
" n-",
|
|
201
|
+
" ",
|
|
202
|
+
" ",
|
|
203
|
+
" mode-",
|
|
204
|
+
""
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"k": "attr",
|
|
209
|
+
"name": "aria-label"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"k": "attr",
|
|
213
|
+
"name": "data-intent"
|
|
214
|
+
},
|
|
215
|
+
">",
|
|
216
|
+
{
|
|
217
|
+
"k": "child",
|
|
218
|
+
"in": "div"
|
|
219
|
+
},
|
|
220
|
+
"</div></div></div>"
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
var EMPTY_HAND = {
|
|
224
|
+
label: "Your hand",
|
|
225
|
+
cards: [],
|
|
226
|
+
mode: "play"
|
|
227
|
+
};
|
|
228
|
+
var DEFAULT_HAND_OPTIONS = {
|
|
229
|
+
calm: false,
|
|
230
|
+
fourColor: false
|
|
231
|
+
};
|
|
232
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
233
|
+
var isHandLayout = (value) => isRecord(value) && typeof value["label"] === "string" && Array.isArray(value["cards"]) && (value["mode"] === "play" || value["mode"] === "choose");
|
|
234
|
+
var isHandOptions = (value) => isRecord(value) && typeof value["calm"] === "boolean" && typeof value["fourColor"] === "boolean";
|
|
235
|
+
/** A card's accessible name: "Queen of spades", "9 of hearts, just received". */
|
|
236
|
+
var handCardLabel = (card) => card.note === void 0 ? card.tile === void 0 ? cardName(card) : tileName(card.tile) : `${card.tile === void 0 ? cardName(card) : tileName(card.tile)}, ${card.note}`;
|
|
237
|
+
var clamp = (n, max) => Math.max(0, Math.min(max, n));
|
|
238
|
+
var initialHand = (layout) => ({
|
|
239
|
+
focus: 0,
|
|
240
|
+
ids: layout.cards.map((c) => c.id),
|
|
241
|
+
inside: false
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* Brings the state in line with a new hand. The focus stays on the same card if it is still
|
|
245
|
+
* there, otherwise on the card now at its place; if focus was inside the hand and its card
|
|
246
|
+
* left, the new card takes the focus.
|
|
247
|
+
*/
|
|
248
|
+
var syncHand = (s, layout) => {
|
|
249
|
+
const ids = layout.cards.map((c) => c.id);
|
|
250
|
+
if (ids.length === s.ids.length && ids.every((id, k) => id === s.ids[k])) return {
|
|
251
|
+
state: s,
|
|
252
|
+
outs: [],
|
|
253
|
+
focus: null
|
|
254
|
+
};
|
|
255
|
+
const was = s.ids[s.focus];
|
|
256
|
+
const kept = was === void 0 ? -1 : ids.indexOf(was);
|
|
257
|
+
const index = kept >= 0 ? kept : clamp(s.focus, ids.length - 1);
|
|
258
|
+
const target = ids[index] ?? null;
|
|
259
|
+
const refocus = s.inside && kept < 0 && target !== null;
|
|
260
|
+
return {
|
|
261
|
+
state: {
|
|
262
|
+
focus: index,
|
|
263
|
+
ids,
|
|
264
|
+
inside: s.inside && target !== null
|
|
265
|
+
},
|
|
266
|
+
outs: [],
|
|
267
|
+
focus: refocus ? target : null
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
var activate = (layout, card, via) => {
|
|
271
|
+
if (layout.mode === "choose") {
|
|
272
|
+
if (via === "enter") return [{ _tag: "Commit" }];
|
|
273
|
+
return card.playable ? [{
|
|
274
|
+
_tag: "Toggle",
|
|
275
|
+
id: card.id
|
|
276
|
+
}] : [{
|
|
277
|
+
_tag: "Refused",
|
|
278
|
+
id: card.id
|
|
279
|
+
}];
|
|
280
|
+
}
|
|
281
|
+
return card.playable ? [{
|
|
282
|
+
_tag: "Play",
|
|
283
|
+
id: card.id
|
|
284
|
+
}] : [{
|
|
285
|
+
_tag: "Refused",
|
|
286
|
+
id: card.id
|
|
287
|
+
}];
|
|
288
|
+
};
|
|
289
|
+
/** The hand's reducer, pure, so its behavior is tested without a DOM. */
|
|
290
|
+
var handStep = (before, layout, m) => {
|
|
291
|
+
const s = syncHand(before, layout).state;
|
|
292
|
+
const cards = layout.cards;
|
|
293
|
+
const last = cards.length - 1;
|
|
294
|
+
switch (m._tag) {
|
|
295
|
+
case "Key": {
|
|
296
|
+
const card = cards[s.focus];
|
|
297
|
+
if (card === void 0) return {
|
|
298
|
+
state: s,
|
|
299
|
+
outs: [],
|
|
300
|
+
focus: null
|
|
301
|
+
};
|
|
302
|
+
const moveTo = (index) => {
|
|
303
|
+
const next = clamp(index, last);
|
|
304
|
+
return {
|
|
305
|
+
state: {
|
|
306
|
+
...s,
|
|
307
|
+
focus: next,
|
|
308
|
+
inside: true
|
|
309
|
+
},
|
|
310
|
+
outs: [],
|
|
311
|
+
focus: cards[next].id
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
switch (m.key) {
|
|
315
|
+
case "ArrowLeft": return moveTo(s.focus - 1);
|
|
316
|
+
case "ArrowRight": return moveTo(s.focus + 1);
|
|
317
|
+
case "Home": return moveTo(0);
|
|
318
|
+
case "End": return moveTo(last);
|
|
319
|
+
case "Enter": return {
|
|
320
|
+
state: s,
|
|
321
|
+
outs: activate(layout, card, "enter"),
|
|
322
|
+
focus: null
|
|
323
|
+
};
|
|
324
|
+
case " ": return {
|
|
325
|
+
state: s,
|
|
326
|
+
outs: activate(layout, card, "space"),
|
|
327
|
+
focus: null
|
|
328
|
+
};
|
|
329
|
+
default: return {
|
|
330
|
+
state: s,
|
|
331
|
+
outs: [],
|
|
332
|
+
focus: null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
case "Press": {
|
|
337
|
+
const index = cards.findIndex((c) => c.id === m.id);
|
|
338
|
+
const card = cards[index];
|
|
339
|
+
if (card === void 0) return {
|
|
340
|
+
state: s,
|
|
341
|
+
outs: [],
|
|
342
|
+
focus: null
|
|
343
|
+
};
|
|
344
|
+
return {
|
|
345
|
+
state: {
|
|
346
|
+
...s,
|
|
347
|
+
focus: index,
|
|
348
|
+
inside: true
|
|
349
|
+
},
|
|
350
|
+
outs: activate(layout, card, "pointer"),
|
|
351
|
+
focus: null
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
case "Enter": {
|
|
355
|
+
const index = cards.findIndex((c) => c.id === m.id);
|
|
356
|
+
return {
|
|
357
|
+
state: {
|
|
358
|
+
...s,
|
|
359
|
+
focus: index >= 0 ? index : s.focus,
|
|
360
|
+
inside: true
|
|
361
|
+
},
|
|
362
|
+
outs: [],
|
|
363
|
+
focus: null
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
case "Leave": return {
|
|
367
|
+
state: {
|
|
368
|
+
...s,
|
|
369
|
+
inside: false
|
|
370
|
+
},
|
|
371
|
+
outs: [],
|
|
372
|
+
focus: null
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
/** Keys the hand handles; their default actions (scrolling, a button's click) are suppressed. */
|
|
377
|
+
var HAND_KEYS = /* @__PURE__ */ new Set([
|
|
378
|
+
"ArrowLeft",
|
|
379
|
+
"ArrowRight",
|
|
380
|
+
"Home",
|
|
381
|
+
"End",
|
|
382
|
+
"Enter",
|
|
383
|
+
" "
|
|
384
|
+
]);
|
|
385
|
+
var emit = outputs();
|
|
386
|
+
var i = intentsOf();
|
|
387
|
+
var CardButton = (card, v) => compiled(_gyral$t0, [
|
|
388
|
+
card.chosen === true ? "chosen" : "",
|
|
389
|
+
card.playable ? "" : "off",
|
|
390
|
+
card.note === void 0 ? "" : "noted",
|
|
391
|
+
card.id,
|
|
392
|
+
v.draggable ? "true" : "false",
|
|
393
|
+
v.focused ? 0 : -1,
|
|
394
|
+
handCardLabel(card),
|
|
395
|
+
card.playable ? null : "true",
|
|
396
|
+
v.mode === "choose" ? String(card.chosen === true) : null,
|
|
397
|
+
i.Press,
|
|
398
|
+
card.suit ?? "tile",
|
|
399
|
+
card.tile === void 0 ? cardFace(card.rank, card.suit) : tileFace(card.tile, v.numbers)
|
|
400
|
+
]);
|
|
401
|
+
var run = (s, layout, m) => {
|
|
402
|
+
const step = handStep(s, layout, m);
|
|
403
|
+
const commands = step.outs.map((out) => emit(out));
|
|
404
|
+
if (step.focus !== null) commands.push(focus(`[data-card="${step.focus}"]`));
|
|
405
|
+
return commands.length === 0 ? step.state : [step.state, commands];
|
|
406
|
+
};
|
|
407
|
+
var CardHand = define()("jy-card-hand", {
|
|
408
|
+
props: {
|
|
409
|
+
layout: prop.value(isHandLayout, { default: EMPTY_HAND }),
|
|
410
|
+
options: prop.value(isHandOptions, { default: DEFAULT_HAND_OPTIONS })
|
|
411
|
+
},
|
|
412
|
+
init: (p) => initialHand(p.layout),
|
|
413
|
+
intent: {
|
|
414
|
+
Key: ({ key, event }) => {
|
|
415
|
+
if (key === void 0 || !(event instanceof KeyboardEvent) || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return void 0;
|
|
416
|
+
if (!HAND_KEYS.has(key)) return void 0;
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
return {
|
|
419
|
+
_tag: "Key",
|
|
420
|
+
key
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
Press: ({ event }) => {
|
|
424
|
+
const id = event.target instanceof Element ? event.target.closest("[data-card]")?.getAttribute("data-card") : null;
|
|
425
|
+
return typeof id === "string" ? {
|
|
426
|
+
_tag: "Press",
|
|
427
|
+
id
|
|
428
|
+
} : void 0;
|
|
429
|
+
},
|
|
430
|
+
Enter: ({ event }) => {
|
|
431
|
+
const id = event.target instanceof Element ? event.target.closest("[data-card]")?.getAttribute("data-card") : null;
|
|
432
|
+
return typeof id === "string" ? {
|
|
433
|
+
_tag: "Enter",
|
|
434
|
+
id
|
|
435
|
+
} : void 0;
|
|
436
|
+
},
|
|
437
|
+
Leave: ({ event, target }) => {
|
|
438
|
+
const next = event instanceof FocusEvent ? event.relatedTarget : null;
|
|
439
|
+
return next instanceof Node && target instanceof Node && target.contains(next) ? void 0 : { _tag: "Leave" };
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
update: {
|
|
443
|
+
Key: (s, m, { props: p }) => run(s, p.layout, m),
|
|
444
|
+
Press: (s, m, { props: p }) => run(s, p.layout, m),
|
|
445
|
+
Enter: (s, m, { props: p }) => run(s, p.layout, m),
|
|
446
|
+
Leave: (s, m, { props: p }) => run(s, p.layout, m),
|
|
447
|
+
PropsChanged: (s, { props: p }) => {
|
|
448
|
+
const step = syncHand(s, p.layout);
|
|
449
|
+
return step.focus === null ? step.state : [step.state, [focus(`[data-card="${step.focus}"]`)]];
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
view: (s, _i, { props: p }) => {
|
|
453
|
+
const { layout, options } = p;
|
|
454
|
+
const focusIndex = clamp(s.focus, layout.cards.length - 1);
|
|
455
|
+
return compiled(_gyral$t1, [
|
|
456
|
+
suitDefs(),
|
|
457
|
+
i.Enter,
|
|
458
|
+
i.Leave,
|
|
459
|
+
layout.cards.some((c) => c.tile !== void 0) ? "tiles" : "",
|
|
460
|
+
Math.min(layout.cards.length, 32),
|
|
461
|
+
options.fourColor ? "four" : "",
|
|
462
|
+
options.calm ? "calm" : "",
|
|
463
|
+
layout.mode,
|
|
464
|
+
layout.label,
|
|
465
|
+
i.Key,
|
|
466
|
+
each(layout.cards, (card) => card.id, CardButton, (card) => ({
|
|
467
|
+
focused: layout.cards[focusIndex]?.id === card.id,
|
|
468
|
+
mode: layout.mode,
|
|
469
|
+
calm: options.calm,
|
|
470
|
+
numbers: options.numbers === true,
|
|
471
|
+
draggable: options.draggable === true
|
|
472
|
+
}))
|
|
473
|
+
]);
|
|
474
|
+
},
|
|
475
|
+
styles: [faceStyles, handStyles]
|
|
476
|
+
});
|
|
477
|
+
//#endregion
|
|
478
|
+
export { CardHand, DEFAULT_HAND_OPTIONS, EMPTY_HAND, handCardLabel, handStep, initialHand, isHandLayout, isHandOptions, syncHand };
|
|
479
|
+
|
|
480
|
+
//# sourceMappingURL=hand.js.map
|