@dreamboard-games/cli 0.1.30-alpha.1 → 0.1.30-alpha.2

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 (114) hide show
  1. package/README.md +179 -22
  2. package/dist/{chunk-C6UAT6EH.js → chunk-N7XPNNUI.js} +9 -12
  3. package/dist/chunk-N7XPNNUI.js.map +1 -0
  4. package/dist/chunk-SEGVTWSK.js +44 -0
  5. package/dist/{chunk-RS7UXJZV.js → chunk-TAQKH67O.js} +21300 -35881
  6. package/dist/chunk-TAQKH67O.js.map +1 -0
  7. package/dist/{global-config-AGFBDFYD.js → global-config-S4ZIPECE.js} +3 -3
  8. package/dist/index.js +415 -37
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js +3 -4
  11. package/dist/{agent-verifier/keychain-backend-TNOPQV3Z.mjs → keychain-backend-HDF4TZDL.js} +2 -1
  12. package/dist/{agent-verifier/prompt-3BAINGAQ.mjs → prompt-NDV3AE5L.js} +2 -1
  13. package/package.json +6 -6
  14. package/skills/dreamboard/references/building-your-first-game.md +510 -0
  15. package/skills/dreamboard/references/cli.md +104 -0
  16. package/skills/dreamboard/references/game-interface.md +548 -0
  17. package/skills/dreamboard/references/manifest-authoring.md +597 -0
  18. package/skills/dreamboard/references/quickstart.md +66 -0
  19. package/skills/dreamboard/references/reducer.md +864 -0
  20. package/skills/dreamboard/references/rule-authoring.md +147 -0
  21. package/skills/dreamboard/references/testing.md +249 -0
  22. package/skills/dreamboard/scripts/events-extract.mjs +218 -0
  23. package/dist/agent-verifier/agent-workspace-verifier.mjs +0 -227
  24. package/dist/agent-verifier/chunk-2E5P5NWG.mjs +0 -835
  25. package/dist/agent-verifier/chunk-2GBBP27W.mjs +0 -301
  26. package/dist/agent-verifier/chunk-2QMNAVV4.mjs +0 -14522
  27. package/dist/agent-verifier/chunk-2SZHMP6F.mjs +0 -264
  28. package/dist/agent-verifier/chunk-4WD3YU2E.mjs +0 -166
  29. package/dist/agent-verifier/chunk-54TAYXUD.mjs +0 -12
  30. package/dist/agent-verifier/chunk-6A5HRJMQ.mjs +0 -3174
  31. package/dist/agent-verifier/chunk-6UUJEYDV.mjs +0 -213
  32. package/dist/agent-verifier/chunk-7653FPGJ.mjs +0 -381
  33. package/dist/agent-verifier/chunk-7E65UQLY.mjs +0 -38
  34. package/dist/agent-verifier/chunk-BVVNBJM4.mjs +0 -221
  35. package/dist/agent-verifier/chunk-CEDUHGNH.mjs +0 -74
  36. package/dist/agent-verifier/chunk-CEQ2VJWN.mjs +0 -149
  37. package/dist/agent-verifier/chunk-CFU5EWIC.mjs +0 -69
  38. package/dist/agent-verifier/chunk-CJEEA6NJ.mjs +0 -730
  39. package/dist/agent-verifier/chunk-EIQWDQWJ.mjs +0 -186
  40. package/dist/agent-verifier/chunk-EOQIV6PS.mjs +0 -649
  41. package/dist/agent-verifier/chunk-HBNDKQT5.mjs +0 -8381
  42. package/dist/agent-verifier/chunk-HJFQDSTU.mjs +0 -225
  43. package/dist/agent-verifier/chunk-JH22JNYD.mjs +0 -1681
  44. package/dist/agent-verifier/chunk-LI3ZR3BI.mjs +0 -41
  45. package/dist/agent-verifier/chunk-LM3OZLZG.mjs +0 -48
  46. package/dist/agent-verifier/chunk-MINCYHXN.mjs +0 -106
  47. package/dist/agent-verifier/chunk-MRCUP5SW.mjs +0 -128
  48. package/dist/agent-verifier/chunk-RBDDIIPM.mjs +0 -19
  49. package/dist/agent-verifier/chunk-SHUMAVAP.mjs +0 -59
  50. package/dist/agent-verifier/chunk-SYPLYRGB.mjs +0 -2812
  51. package/dist/agent-verifier/chunk-U6OJN7XS.mjs +0 -8092
  52. package/dist/agent-verifier/chunk-VYJTHSYR.mjs +0 -44
  53. package/dist/agent-verifier/chunk-XYDL7GY6.mjs +0 -10
  54. package/dist/agent-verifier/compile-5QSPIOUT.mjs +0 -313
  55. package/dist/agent-verifier/global-config-WX3ZZIVU.mjs +0 -17
  56. package/dist/agent-verifier/local-files-MTPLP62S.mjs +0 -46
  57. package/dist/agent-verifier/local-typecheck-QFYYAZOK.mjs +0 -9
  58. package/dist/agent-verifier/materialize-workspace-FKALAE2T.mjs +0 -90
  59. package/dist/agent-verifier/project-state-7GR6BQTQ.mjs +0 -32
  60. package/dist/agent-verifier/reducer-bundle-preflight-C73LEXI2.mjs +0 -23
  61. package/dist/agent-verifier/reducer-contract-preflight-22X7DSZW.mjs +0 -10
  62. package/dist/agent-verifier/reducer-native-test-harness-GMWBUISX.mjs +0 -53
  63. package/dist/agent-verifier/static-scaffold-AJMZZQWS.mjs +0 -28
  64. package/dist/agent-verifier/sync-3DUQH32H.mjs +0 -594
  65. package/dist/agent-verifier/test-P4U5INTD.mjs +0 -356
  66. package/dist/agent-verifier/testing-5K2BJYF2.mjs +0 -674
  67. package/dist/agent-verifier/workspace-codegen-JDZJRSDV.mjs +0 -11
  68. package/dist/agent-verifier/workspace-dependencies-HZ6VVS4G.mjs +0 -14
  69. package/dist/chunk-2H7UOFLK.js +0 -11
  70. package/dist/chunk-7FOO4AJI.js +0 -50
  71. package/dist/chunk-7FOO4AJI.js.map +0 -1
  72. package/dist/chunk-C6UAT6EH.js.map +0 -1
  73. package/dist/chunk-RS7UXJZV.js.map +0 -1
  74. package/dist/internal.d.ts +0 -311
  75. package/dist/keychain-backend-JHTXAKWC.js +0 -135
  76. package/dist/prompt-GMZABCJC.js +0 -756
  77. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +0 -71
  78. package/dist/runtime-packages/ui-host-runtime/src/browser-interaction.ts +0 -139
  79. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +0 -374
  80. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +0 -266
  81. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +0 -212
  82. package/dist/runtime-packages/ui-host-runtime/src/components/host-primitives.tsx +0 -271
  83. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +0 -135
  84. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +0 -5
  85. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +0 -194
  86. package/dist/runtime-packages/ui-host-runtime/src/gameplay-authority-transport.ts +0 -626
  87. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +0 -1
  88. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +0 -1
  89. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +0 -294
  90. package/dist/runtime-packages/ui-host-runtime/src/index.ts +0 -3
  91. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +0 -11
  92. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +0 -324
  93. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +0 -195
  94. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +0 -138
  95. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +0 -159
  96. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +0 -551
  97. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +0 -13
  98. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +0 -122
  99. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +0 -26
  100. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +0 -583
  101. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +0 -219
  102. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +0 -117
  103. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +0 -431
  104. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +0 -211
  105. package/dist/runtime-packages/ui-host-runtime/src/session-recovery.ts +0 -80
  106. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +0 -1034
  107. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +0 -416
  108. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +0 -184
  109. package/dist/testing-KLSV6CPJ.js +0 -674
  110. package/dist/testing-KLSV6CPJ.js.map +0 -1
  111. /package/dist/{chunk-2H7UOFLK.js.map → chunk-SEGVTWSK.js.map} +0 -0
  112. /package/dist/{global-config-AGFBDFYD.js.map → global-config-S4ZIPECE.js.map} +0 -0
  113. /package/dist/{keychain-backend-JHTXAKWC.js.map → keychain-backend-HDF4TZDL.js.map} +0 -0
  114. /package/dist/{prompt-GMZABCJC.js.map → prompt-NDV3AE5L.js.map} +0 -0
@@ -1,71 +0,0 @@
1
- import type {
2
- SeatAssignment,
3
- SessionActor,
4
- SessionGameSource,
5
- } from "@dreamboard-games/api-client";
6
-
7
- type HexColor = string & { readonly __brand: "HexColor" };
8
-
9
- const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
10
-
11
- function parseHexColor(value: unknown): HexColor | undefined {
12
- return typeof value === "string" && HEX_COLOR_PATTERN.test(value)
13
- ? (value as HexColor)
14
- : undefined;
15
- }
16
-
17
- /** Stable string principal for comparing actors to connection `userId` / demo actor id. */
18
- export function principalKey(actor: SessionActor): string {
19
- if (actor.kind === "AUTH_USER") {
20
- return actor.id;
21
- }
22
- if (actor.kind === "PERF_OPERATOR") {
23
- return `perf:${actor.perfRunId}`;
24
- }
25
- return actor.demoActorSessionId;
26
- }
27
-
28
- export function principalMatchesActor(
29
- principalId: string | null | undefined,
30
- actor: SessionActor,
31
- ): boolean {
32
- if (!principalId) {
33
- return false;
34
- }
35
- return principalKey(actor) === principalId;
36
- }
37
-
38
- export function seatControlledByPrincipal(
39
- seat: SeatAssignment,
40
- principalId: string | null | undefined,
41
- ): boolean {
42
- if (!principalId || !seat.controllerActor) {
43
- return false;
44
- }
45
- return principalMatchesActor(principalId, seat.controllerActor);
46
- }
47
-
48
- export function gameIdFromGameSource(source: SessionGameSource): string {
49
- if (source.kind === "USER_COMPILED") {
50
- return source.gameId;
51
- }
52
- return `demo:${source.slug}:${source.revisionId}`;
53
- }
54
-
55
- export function seatsForPluginSnapshot(seats: SeatAssignment[]): Array<{
56
- playerId: string;
57
- controllerUserId?: string;
58
- displayName: string;
59
- playerColor?: HexColor;
60
- isHost?: boolean;
61
- }> {
62
- return seats.map((seat) => ({
63
- playerId: seat.playerId,
64
- controllerUserId: seat.controllerActor
65
- ? principalKey(seat.controllerActor)
66
- : undefined,
67
- displayName: seat.displayName,
68
- playerColor: parseHexColor(seat.playerColor),
69
- isHost: seat.isHost,
70
- }));
71
- }
@@ -1,139 +0,0 @@
1
- import {
2
- createBrowserInteractionActuatorAttributes,
3
- createBrowserInteractionRootAttributes,
4
- defineBrowserInteractionSurface,
5
- type BrowserInteractionAttributeMap,
6
- } from "@dreamboard-games/sdk/browser-interaction";
7
-
8
- export const HOST_BROWSER_INTERACTION_SURFACE = "host" as const;
9
- export const HOST_BROWSER_INTERACTION_SCOPE = "session" as const;
10
- export const HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_KEY =
11
- "host.switchControlledPlayer" as const;
12
- export const HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_ID =
13
- "switchControlledPlayer" as const;
14
- export const HOST_SWITCH_CONTROLLED_PLAYER_INTENT =
15
- "switchControlledPlayer" as const;
16
- export const HOST_SWITCH_CONTROLLED_PLAYER_EFFECT_KIND =
17
- "switchControlledPlayer" as const;
18
-
19
- export const hostBrowserInteractionSurface = defineBrowserInteractionSurface({
20
- surface: HOST_BROWSER_INTERACTION_SURFACE,
21
- intents: [HOST_SWITCH_CONTROLLED_PLAYER_INTENT],
22
- effectKinds: [HOST_SWITCH_CONTROLLED_PLAYER_EFFECT_KIND],
23
- } as Parameters<typeof defineBrowserInteractionSurface>[0] & {
24
- readonly effectKinds: readonly string[];
25
- });
26
-
27
- export function createHostSwitchControlledPlayerRootAttributes(): BrowserInteractionAttributeMap {
28
- return {
29
- ...createBrowserInteractionRootAttributes({
30
- surface: HOST_BROWSER_INTERACTION_SURFACE,
31
- scopeId: HOST_BROWSER_INTERACTION_SCOPE,
32
- interactionKey: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_KEY,
33
- interactionId: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_ID,
34
- readiness: "ready",
35
- }),
36
- [PROTOCOL_ATTRIBUTE]: "2.0.0",
37
- };
38
- }
39
-
40
- export function createHostSwitchControlledPlayerActuatorAttributes(input: {
41
- readonly playerId: string;
42
- readonly selected: boolean;
43
- readonly enabled: boolean;
44
- }): BrowserInteractionAttributeMap {
45
- return {
46
- ...createBrowserInteractionActuatorAttributes({
47
- surface: HOST_BROWSER_INTERACTION_SURFACE,
48
- scopeId: HOST_BROWSER_INTERACTION_SCOPE,
49
- interactionKey: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_KEY,
50
- interactionId: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_ID,
51
- intent: HOST_SWITCH_CONTROLLED_PLAYER_INTENT,
52
- inputKey: "playerId",
53
- candidateValue: input.playerId,
54
- candidateState: input.selected ? "selected" : "unselected",
55
- enabled: input.enabled,
56
- actuatorKind: "click",
57
- }),
58
- [PROTOCOL_ATTRIBUTE]: "2.0.0",
59
- [SEMANTIC_EFFECTS_ATTRIBUTE]: encodeBrowserEffectList([
60
- {
61
- kind: HOST_SWITCH_CONTROLLED_PLAYER_EFFECT_KIND,
62
- playerId: input.playerId,
63
- beforeSelected: input.selected,
64
- afterSelected: true,
65
- },
66
- ]),
67
- };
68
- }
69
-
70
- export function createHostSwitchControlledPlayerMenuTriggerAttributes(): BrowserInteractionAttributeMap {
71
- return {
72
- ...createBrowserInteractionActuatorAttributes({
73
- surface: HOST_BROWSER_INTERACTION_SURFACE,
74
- scopeId: HOST_BROWSER_INTERACTION_SCOPE,
75
- interactionKey: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_KEY,
76
- interactionId: HOST_SWITCH_CONTROLLED_PLAYER_INTERACTION_ID,
77
- intent: HOST_SWITCH_CONTROLLED_PLAYER_INTENT,
78
- actuatorKind: "click",
79
- prepares: {
80
- intent: HOST_SWITCH_CONTROLLED_PLAYER_INTENT,
81
- inputKey: "playerId",
82
- actuatorKind: "click",
83
- },
84
- }),
85
- [PROTOCOL_ATTRIBUTE]: "2.0.0",
86
- [PREPARATION_PATTERNS_ATTRIBUTE]: encodeBrowserEffectPatternList([
87
- {
88
- kind: "match",
89
- effectKind: HOST_SWITCH_CONTROLLED_PLAYER_EFFECT_KIND,
90
- fields: {},
91
- },
92
- ]),
93
- };
94
- }
95
-
96
- const PROTOCOL_ATTRIBUTE = "data-dreamboard-browser-protocol";
97
- const SEMANTIC_EFFECTS_ATTRIBUTE = "data-dreamboard-semantic-effects";
98
- const PREPARATION_PATTERNS_ATTRIBUTE = "data-dreamboard-preparation-patterns";
99
-
100
- type CanonicalBrowserValue =
101
- | null
102
- | boolean
103
- | number
104
- | string
105
- | readonly CanonicalBrowserValue[]
106
- | { readonly [key: string]: CanonicalBrowserValue };
107
-
108
- function encodeBrowserEffectList(
109
- effects: ReadonlyArray<{ readonly [key: string]: CanonicalBrowserValue }>,
110
- ): string {
111
- return JSON.stringify(effects.map(encodeCanonicalBrowserValue));
112
- }
113
-
114
- function encodeBrowserEffectPatternList(
115
- patterns: ReadonlyArray<{ readonly [key: string]: CanonicalBrowserValue }>,
116
- ): string {
117
- return JSON.stringify(patterns.map(encodeCanonicalBrowserValue));
118
- }
119
-
120
- function encodeCanonicalBrowserValue(value: CanonicalBrowserValue): string {
121
- return JSON.stringify(canonicalizeBrowserValue(value));
122
- }
123
-
124
- function canonicalizeBrowserValue(
125
- value: CanonicalBrowserValue,
126
- ): CanonicalBrowserValue {
127
- if (Array.isArray(value)) {
128
- return value.map(canonicalizeBrowserValue);
129
- }
130
- if (value && typeof value === "object") {
131
- const record = value as { readonly [key: string]: CanonicalBrowserValue };
132
- return Object.fromEntries(
133
- Object.keys(record)
134
- .sort()
135
- .map((key) => [key, canonicalizeBrowserValue(record[key]!)]),
136
- );
137
- }
138
- return value;
139
- }
@@ -1,374 +0,0 @@
1
- import { useMemo, useRef, useState } from "react";
2
- import {
3
- Badge,
4
- Button,
5
- cn,
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuLabel,
9
- DropdownMenuRadioGroup,
10
- DropdownMenuRadioItem,
11
- DropdownMenuSeparator,
12
- DropdownMenuTrigger,
13
- Popover,
14
- PopoverContent,
15
- PopoverTrigger,
16
- ScrollArea,
17
- } from "./host-primitives.js";
18
- import {
19
- Check,
20
- ChevronDown,
21
- Clock3,
22
- History,
23
- RotateCcw,
24
- Sparkles,
25
- Users,
26
- } from "lucide-react";
27
- import type { HistoryState } from "../unified-session-store.js";
28
- import {
29
- createHostSwitchControlledPlayerActuatorAttributes,
30
- createHostSwitchControlledPlayerMenuTriggerAttributes,
31
- createHostSwitchControlledPlayerRootAttributes,
32
- } from "../browser-interaction.js";
33
-
34
- export interface HostControllablePlayer {
35
- playerId: string;
36
- displayName: string;
37
- }
38
-
39
- export interface HostPlayerSwitcherProps {
40
- controllablePlayers: HostControllablePlayer[];
41
- controllingPlayerId: string | null;
42
- onSwitchPlayer: (playerId: string) => void;
43
- className?: string;
44
- }
45
-
46
- export interface HostHistoryNavigatorProps {
47
- isHost: boolean;
48
- history: HistoryState | null;
49
- onRestoreHistory: (entryId: string) => Promise<void> | void;
50
- className?: string;
51
- }
52
-
53
- export interface HostSessionToolbarProps {
54
- children: React.ReactNode;
55
- className?: string;
56
- }
57
-
58
- function formatHistoryTimestamp(timestamp: string): string {
59
- try {
60
- return new Date(timestamp).toLocaleTimeString(undefined, {
61
- hour: "2-digit",
62
- minute: "2-digit",
63
- second: "2-digit",
64
- });
65
- } catch {
66
- return timestamp;
67
- }
68
- }
69
-
70
- export function HostSessionToolbar({
71
- children,
72
- className,
73
- }: HostSessionToolbarProps) {
74
- return (
75
- <div
76
- className={cn("flex flex-wrap items-center justify-end gap-2", className)}
77
- >
78
- {children}
79
- </div>
80
- );
81
- }
82
-
83
- export function HostPlayerSwitcher({
84
- controllablePlayers,
85
- controllingPlayerId,
86
- onSwitchPlayer,
87
- className,
88
- }: HostPlayerSwitcherProps) {
89
- const currentPlayer = useMemo(
90
- () =>
91
- controllablePlayers.find(
92
- (player) => player.playerId === controllingPlayerId,
93
- ) ?? controllablePlayers[0],
94
- [controllablePlayers, controllingPlayerId],
95
- );
96
- const switchRootAttributes = createHostSwitchControlledPlayerRootAttributes();
97
- const switchMenuTriggerAttributes =
98
- createHostSwitchControlledPlayerMenuTriggerAttributes();
99
-
100
- if (controllablePlayers.length <= 1) {
101
- return null;
102
- }
103
-
104
- // Designer's Notebook: the trigger button is the ONE punctuation moment
105
- // for this control (wobbly + hard shadow). The dropdown contents below are
106
- // intentionally calm — plain rows, no rotation, no per-row wobbly borders.
107
- return (
108
- <div {...switchRootAttributes}>
109
- <DropdownMenu>
110
- <DropdownMenuTrigger asChild>
111
- <Button
112
- variant="outline"
113
- className={cn(
114
- "group h-auto min-h-14 min-w-[220px] justify-between gap-3 border-[3px] border-border bg-white px-4 py-3 text-left text-foreground hard-shadow transition-all hover:bg-[#fff9c4] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_#2d2d2d] wobbly-border-md",
115
- className,
116
- )}
117
- {...switchMenuTriggerAttributes}
118
- >
119
- <span className="flex min-w-0 items-center gap-3">
120
- <span className="flex h-10 w-10 shrink-0 items-center justify-center border-2 border-border bg-[#e7eefc] text-[#2d5da1]">
121
- <Users className="h-4 w-4 text-[#2d5da1]" />
122
- </span>
123
- <span className="min-w-0">
124
- <span className="block text-[10px] font-bold uppercase tracking-[0.18em] text-muted-foreground">
125
- Seat Control
126
- </span>
127
- <span className="block truncate font-display text-lg leading-none text-foreground">
128
- {currentPlayer?.displayName ?? "Choose player"}
129
- </span>
130
- <span className="mt-1 block truncate text-xs text-muted-foreground">
131
- {currentPlayer?.playerId ?? "Switch the active seat"}
132
- </span>
133
- </span>
134
- </span>
135
- <span className="flex shrink-0 items-center gap-2">
136
- <Badge
137
- variant="secondary"
138
- className="border border-border/40 bg-[#efe7da] px-2.5 py-0.5 text-[10px] uppercase tracking-[0.14em] text-foreground shadow-none"
139
- >
140
- {controllablePlayers.length} seats
141
- </Badge>
142
- <ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
143
- </span>
144
- </Button>
145
- </DropdownMenuTrigger>
146
- <DropdownMenuContent
147
- align="end"
148
- className="z-[80] w-[22rem] border-2 border-border bg-[#fdfbf7] p-2 font-sans"
149
- >
150
- <DropdownMenuLabel className="px-2 pb-3 pt-1">
151
- <div className="flex items-start justify-between gap-3">
152
- <div>
153
- <p className="text-[10px] font-bold uppercase tracking-[0.18em] text-muted-foreground">
154
- Play As
155
- </p>
156
- <p className="mt-1 font-display text-xl leading-none text-foreground">
157
- Switch the active seat
158
- </p>
159
- </div>
160
- <Sparkles className="h-5 w-5 text-primary shrink-0" />
161
- </div>
162
- </DropdownMenuLabel>
163
- <DropdownMenuSeparator className="mx-1 border-b border-border/40 bg-transparent" />
164
- <DropdownMenuRadioGroup
165
- {...switchRootAttributes}
166
- value={controllingPlayerId ?? ""}
167
- onValueChange={(playerId) => {
168
- if (playerId !== controllingPlayerId) {
169
- onSwitchPlayer(playerId);
170
- }
171
- }}
172
- >
173
- {controllablePlayers.map((player, index) => (
174
- <DropdownMenuRadioItem
175
- key={player.playerId}
176
- value={player.playerId}
177
- {...createHostSwitchControlledPlayerActuatorAttributes({
178
- playerId: player.playerId,
179
- selected: player.playerId === controllingPlayerId,
180
- enabled: true,
181
- })}
182
- className="mb-1 rounded-none border-2 border-transparent bg-white px-3 py-3 transition-colors focus:border-border/40 focus:bg-[#fff9c4] focus:text-foreground focus:outline-none data-[state=checked]:border-border data-[state=checked]:bg-[#fff9c4] data-[state=checked]:text-foreground [&>span]:hidden"
183
- >
184
- <div className="flex min-w-0 items-center gap-3">
185
- <div className="flex h-9 w-9 shrink-0 items-center justify-center border-2 border-border bg-[#efe7da] text-sm font-bold text-foreground">
186
- {index + 1}
187
- </div>
188
- <div className="min-w-0 flex-1">
189
- <span className="block truncate font-display text-lg leading-none">
190
- {player.displayName}
191
- </span>
192
- <span className="mt-1 block truncate text-xs text-muted-foreground">
193
- {player.playerId}
194
- </span>
195
- </div>
196
- {player.playerId === controllingPlayerId ? (
197
- <Check className="ml-auto h-5 w-5 shrink-0 text-primary" />
198
- ) : null}
199
- </div>
200
- </DropdownMenuRadioItem>
201
- ))}
202
- </DropdownMenuRadioGroup>
203
- </DropdownMenuContent>
204
- </DropdownMenu>
205
- </div>
206
- );
207
- }
208
-
209
- export function HostHistoryNavigator({
210
- isHost,
211
- history,
212
- onRestoreHistory,
213
- className,
214
- }: HostHistoryNavigatorProps) {
215
- const triggerContainerRef = useRef<HTMLDivElement | null>(null);
216
- const [open, setOpen] = useState(false);
217
- const [confirmEntryId, setConfirmEntryId] = useState<string | null>(null);
218
- const [restoringEntryId, setRestoringEntryId] = useState<string | null>(null);
219
-
220
- const entries = useMemo(
221
- () => (history ? [...history.entries].reverse() : []),
222
- [history],
223
- );
224
-
225
- if (!isHost || !history || history.entries.length === 0) {
226
- return null;
227
- }
228
-
229
- const handleRestoreClick = async (entryId: string) => {
230
- if (confirmEntryId !== entryId) {
231
- setConfirmEntryId(entryId);
232
- return;
233
- }
234
-
235
- setRestoringEntryId(entryId);
236
- try {
237
- await onRestoreHistory(entryId);
238
- setOpen(false);
239
- setConfirmEntryId(null);
240
- } finally {
241
- setRestoringEntryId(null);
242
- }
243
- };
244
-
245
- const popoverContainer = (() => {
246
- const container = triggerContainerRef.current?.closest(
247
- "[data-slot='drawer-content']",
248
- );
249
- return container instanceof HTMLElement ? container : null;
250
- })();
251
-
252
- return (
253
- <div ref={triggerContainerRef}>
254
- <Popover
255
- open={open}
256
- onOpenChange={(nextOpen) => {
257
- setOpen(nextOpen);
258
- if (!nextOpen) {
259
- setConfirmEntryId(null);
260
- }
261
- }}
262
- >
263
- <PopoverTrigger asChild>
264
- {/* Calm secondary control — the player switcher is the loud one
265
- in this toolbar; history is its quiet sibling. */}
266
- <Button
267
- variant="outline"
268
- size="sm"
269
- className={cn(
270
- "h-10 gap-2 border-2 border-border bg-white text-foreground transition-colors hover:bg-[#e5e0d8]",
271
- className,
272
- )}
273
- >
274
- <History className="h-4 w-4 text-muted-foreground" />
275
- <span className="hidden sm:inline">History</span>
276
- <Badge
277
- variant="secondary"
278
- className="border border-border/40 bg-[#fff9c4] text-foreground"
279
- >
280
- {history.entries.length}
281
- </Badge>
282
- </Button>
283
- </PopoverTrigger>
284
- {/* Designer's Notebook: trigger above is the punctuation moment.
285
- Popover contents are calm — plain bordered rows, no per-row
286
- wobbly. Restore CTA only "presses" via translate, no inflation. */}
287
- <PopoverContent
288
- container={popoverContainer}
289
- side="right"
290
- align="start"
291
- sideOffset={12}
292
- collisionPadding={16}
293
- style={{ zIndex: 200 }}
294
- className="z-[200] w-[26rem] border-2 border-border bg-[#fdfbf7] p-0"
295
- >
296
- <div className="border-b-2 border-border bg-white px-4 py-3">
297
- <div className="flex items-center gap-2 font-display text-base">
298
- <History className="h-4 w-4" />
299
- Session History
300
- </div>
301
- <p className="mt-1 font-sans text-xs text-muted-foreground">
302
- Restore a previous game state. This affects the shared host
303
- session.
304
- </p>
305
- </div>
306
- <ScrollArea className="max-h-80">
307
- <div className="space-y-2 p-3">
308
- {entries.map((entry: HistoryState["entries"][number]) => {
309
- const isConfirming = confirmEntryId === entry.id;
310
- const isRestoring = restoringEntryId === entry.id;
311
-
312
- return (
313
- <div
314
- key={entry.id}
315
- className={cn(
316
- "border-2 border-border px-3 py-3 transition-colors",
317
- entry.isCurrent ? "bg-[#fff9c4]" : "bg-white",
318
- )}
319
- >
320
- <div className="flex items-start justify-between gap-3">
321
- <div className="min-w-0 space-y-1">
322
- <div className="flex items-center gap-2">
323
- <span className="truncate text-sm font-medium">
324
- {entry.description}
325
- </span>
326
- {entry.isCurrent && (
327
- <Badge
328
- variant="secondary"
329
- className="shrink-0 border border-border/40 bg-[#e5e0d8] text-foreground"
330
- >
331
- Current
332
- </Badge>
333
- )}
334
- </div>
335
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
336
- <Clock3 className="h-3.5 w-3.5" />
337
- <span>{formatHistoryTimestamp(entry.timestamp)}</span>
338
- <span>v{entry.version}</span>
339
- </div>
340
- </div>
341
- {!entry.isCurrent && (
342
- <Button
343
- size="sm"
344
- variant={isConfirming ? "default" : "outline"}
345
- className="shrink-0"
346
- disabled={isRestoring}
347
- onClick={() => void handleRestoreClick(entry.id)}
348
- >
349
- {isRestoring ? (
350
- "Restoring..."
351
- ) : isConfirming ? (
352
- <>
353
- <Check className="mr-1 h-4 w-4" />
354
- Confirm
355
- </>
356
- ) : (
357
- <>
358
- <RotateCcw className="mr-1 h-4 w-4" />
359
- Restore
360
- </>
361
- )}
362
- </Button>
363
- )}
364
- </div>
365
- </div>
366
- );
367
- })}
368
- </div>
369
- </ScrollArea>
370
- </PopoverContent>
371
- </Popover>
372
- </div>
373
- );
374
- }